feat: 增加档口入库码独立页面 (#233)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"cmautobuy/admin/config"
|
||||
"cmautobuy/admin/service"
|
||||
"cmautobuy/admin/syb"
|
||||
)
|
||||
|
||||
var innerCodeLocation = time.FixedZone("UTC+8", 8*60*60)
|
||||
|
||||
// InnerCodeList 渲染独立的档口入库码工作台。
|
||||
func (h *Handler) InnerCodeList(c *gin.Context) {
|
||||
businessDate := strings.TrimSpace(c.Query("date"))
|
||||
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
|
||||
businessDate = time.Now().In(innerCodeLocation).Format("2006-01-02")
|
||||
}
|
||||
status := strings.TrimSpace(c.Query("status"))
|
||||
keyword := strings.TrimSpace(c.Query("q"))
|
||||
result, err := service.ListInnerCodePage(h.db, businessDate, status, keyword, service.ParsePage(c.Query("page")))
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取档口入库码列表失败,请稍后刷新重试。")
|
||||
return
|
||||
}
|
||||
query := url.Values{"date": {businessDate}}
|
||||
if result.Status != "" {
|
||||
query.Set("status", result.Status)
|
||||
}
|
||||
if keyword != "" {
|
||||
query.Set("q", keyword)
|
||||
}
|
||||
c.HTML(http.StatusOK, "inner_code/list", page(c, "inner-codes", "档口入库码", gin.H{
|
||||
"BusinessDate": businessDate,
|
||||
"StatusFilter": result.Status,
|
||||
"Keyword": keyword,
|
||||
"StatusOptions": service.InnerCodeStatusOptions(),
|
||||
"Rows": result.Rows,
|
||||
"HasAny": result.Counts.Total > 0,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"CurrentPage": result.Page,
|
||||
"Message": strings.TrimSpace(c.Query("message")),
|
||||
"Status": service.InnerCodeStatusMessage(result),
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, query.Encode()),
|
||||
}))
|
||||
}
|
||||
|
||||
// InnerCodeImport 接收一次性 xlsx,导入完成后总会删除临时文件。
|
||||
func (h *Handler) InnerCodeImport(c *gin.Context) {
|
||||
businessDate := strings.TrimSpace(c.PostForm("date"))
|
||||
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
|
||||
fail(c, http.StatusBadRequest, "请选择合法的业务日期;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "没有收到 Excel(也可能超过 10MB);没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "无法打开上传文件;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
head := make([]byte, 8)
|
||||
n, _ := io.ReadFull(src, head)
|
||||
if err := service.ValidateInnerCodeUpload(fileHeader.Filename, fileHeader.Size, head[:n]); err != nil {
|
||||
fail(c, http.StatusBadRequest, err.Error()+";没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取上传文件失败;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
uploadDir, err := config.SubDir("uploads")
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "无法准备临时上传目录;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
tmp, err := os.CreateTemp(uploadDir, "inner-code-*.xlsx")
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "无法保存临时上传文件;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := io.Copy(tmp, src); err != nil {
|
||||
tmp.Close()
|
||||
fail(c, http.StatusInternalServerError, "保存临时上传文件失败;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
fail(c, http.StatusInternalServerError, "保存临时上传文件失败;没有导入任何数据。")
|
||||
return
|
||||
}
|
||||
result, err := service.ImportInnerCodeExcel(h.db, tmpPath, businessDate, currentUser(c).UserID)
|
||||
if err != nil {
|
||||
if service.IsInvalidInnerCodeImport(err) {
|
||||
fail(c, http.StatusBadRequest, err.Error()+";请修正文件后重新导入。")
|
||||
return
|
||||
}
|
||||
fail(c, http.StatusInternalServerError, "档口入库码写库失败,本次导入已整体回滚,请稍后重试。")
|
||||
return
|
||||
}
|
||||
message := fmt.Sprintf("导入完成:读取 %d 行,新增 %d 条,更新 %d 条,文件内重复 %d 行。",
|
||||
result.TotalRows, result.CreatedCount, result.UpdatedCount, result.DuplicateRows)
|
||||
h.innerCodeRedirect(c, businessDate, "", "", 1, message)
|
||||
}
|
||||
|
||||
// InnerCodeMatch 读取远端最新详情并保存只读规划,不执行任何回写。
|
||||
func (h *Handler) InnerCodeMatch(c *gin.Context) {
|
||||
businessDate := strings.TrimSpace(c.PostForm("date"))
|
||||
status, keyword := c.PostForm("status"), c.PostForm("q")
|
||||
pageNumber := service.ParsePage(c.PostForm("page"))
|
||||
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
|
||||
h.innerCodeRedirect(c, time.Now().In(innerCodeLocation).Format("2006-01-02"), status, keyword, 1, "业务日期无效,没有执行匹配。")
|
||||
return
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, "顺运宝配置不可用,没有执行匹配。")
|
||||
return
|
||||
}
|
||||
client, err := syb.New(cfg.Syb.BaseURL)
|
||||
if err != nil {
|
||||
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, "顺运宝地址配置有误,没有执行匹配。")
|
||||
return
|
||||
}
|
||||
if err := service.EnsureSybSession(h.db, client, cfg.Syb.Username, time.Now()); err != nil {
|
||||
message := "校验顺运宝会话失败,没有执行匹配。"
|
||||
if errors.Is(err, service.ErrSybLoginRequired) {
|
||||
message = "顺运宝会话已过期,请先到“顺运宝数据”页面登录后再匹配。"
|
||||
}
|
||||
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
|
||||
return
|
||||
}
|
||||
result, err := service.PlanInnerCodeRecords(c.Request.Context(), h.db, client, businessDate)
|
||||
if err != nil {
|
||||
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, "匹配失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
message := fmt.Sprintf("匹配完成:处理 %d 条,可回写 %d 条,已存在 %d 条,跳过 %d 条,失败 %d 条。",
|
||||
result.Total, result.Ready, result.AlreadyFilled, result.Skipped, result.Failed)
|
||||
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
|
||||
}
|
||||
|
||||
func (h *Handler) innerCodeRedirect(c *gin.Context, businessDate, status, keyword string, pageNumber int, message string) {
|
||||
values := url.Values{"date": {businessDate}, "page": {strconv.Itoa(pageNumber)}, "message": {message}}
|
||||
if strings.TrimSpace(status) != "" {
|
||||
values.Set("status", strings.TrimSpace(status))
|
||||
}
|
||||
if strings.TrimSpace(keyword) != "" {
|
||||
values.Set("q", strings.TrimSpace(keyword))
|
||||
}
|
||||
c.Redirect(http.StatusSeeOther, "/inner-codes?"+values.Encode())
|
||||
}
|
||||
@@ -115,19 +115,25 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration, aiSecret
|
||||
clients.POST("/unassign", h.ClientUnassign)
|
||||
clients.POST("/delete", h.ClientDelete)
|
||||
|
||||
// 6. 用户管理:先经过网页登录,再叠加管理员角色校验。
|
||||
// 6. 档口入库码:独立于顺运宝数据页,导航位置紧跟客户端列表。
|
||||
pages.GET("/inner-codes", h.InnerCodeList)
|
||||
innerCodes := pages.Group("/inner-codes")
|
||||
innerCodes.POST("/import", h.InnerCodeImport)
|
||||
innerCodes.POST("/match", h.InnerCodeMatch)
|
||||
|
||||
// 7. 用户管理:先经过网页登录,再叠加管理员角色校验。
|
||||
users := pages.Group("/users", AdminRequired())
|
||||
users.GET("", h.UserList)
|
||||
users.POST("/create", h.UserCreate)
|
||||
users.POST("/status", h.UserSetStatus)
|
||||
users.POST("/reset-password", h.UserResetPassword)
|
||||
|
||||
// 7. 第三方商品目录导入记录:系统级审计仅管理员可见。
|
||||
// 8. 第三方商品目录导入记录:系统级审计仅管理员可见。
|
||||
integrations := pages.Group("/integrations", AdminRequired())
|
||||
integrations.GET("/catalog", h.CatalogHistory)
|
||||
integrations.GET("/catalog/detail", h.CatalogHistoryDetail)
|
||||
|
||||
// 8. AI 模型配置:密钥和系统级服务商配置仅管理员可见。
|
||||
// 9. AI 模型配置:密钥和系统级服务商配置仅管理员可见。
|
||||
aiSettings := pages.Group("/settings/ai", AdminRequired())
|
||||
aiSettings.GET("", h.AIConfigList)
|
||||
aiSettings.POST("/save", h.AIConfigSave)
|
||||
|
||||
Reference in New Issue
Block a user