Files
cmautobuy/admin/handler/web/inner_code.go
T

324 lines
13 KiB
Go

package web
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"cmautobuy/admin/config"
"cmautobuy/admin/model"
"cmautobuy/admin/service"
"cmautobuy/admin/syb"
)
var innerCodeLocation = time.FixedZone("UTC+8", 8*60*60)
const (
innerCodeFeedbackSuccess = "success"
innerCodeFeedbackError = "error"
)
// 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"))
feedbackKind := strings.TrimSpace(c.Query("feedback"))
feedbackMessage := strings.TrimSpace(c.Query("message"))
if feedbackKind != innerCodeFeedbackSuccess && feedbackKind != innerCodeFeedbackError {
feedbackKind, feedbackMessage = "", ""
}
pageSize := service.ParsePageSize(c.Query("page_size"))
result, err := service.ListInnerCodePageWithPageSize(h.db, businessDate, status, keyword,
service.ParsePage(c.Query("page")), pageSize)
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,
"CurrentPageSize": pageSize,
"FeedbackMessage": feedbackMessage,
"FeedbackSuccess": feedbackMessage != "" && feedbackKind == innerCodeFeedbackSuccess,
"FeedbackError": feedbackMessage != "" && feedbackKind == innerCodeFeedbackError,
"Status": service.InnerCodeStatusMessage(result),
"Pagination": service.NewPaginationView(result.Page, pageSize, result.TotalPages, query.Encode()),
}))
}
// InnerCodeImport 接收一次性 xlsx,导入完成后总会删除临时文件。
func (h *Handler) InnerCodeImport(c *gin.Context) {
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, fileHeader.Filename, 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("导入完成:业务日期 %s,读取 %d 行,新增 %d 条,更新 %d 条,恢复 %d 条,同业务键合并 %d 行。",
result.BusinessDate, result.TotalRows, result.CreatedCount, result.UpdatedCount, result.RestoredCount, result.MergedRows)
h.innerCodeRedirect(c, result.BusinessDate, "", "", 1, innerCodeFeedbackSuccess, message)
}
// InnerCodeDelete 软删除当前页已选记录;不撤销任何顺运宝远端操作。
func (h *Handler) InnerCodeDelete(c *gin.Context) {
businessDate, status, keyword := c.PostForm("date"), c.PostForm("status"), c.PostForm("q")
pageNumber := service.ParsePage(c.PostForm("page"))
ids := parseInnerCodeIDs(c.PostFormArray("ids"))
count, err := service.DeleteInnerCodes(h.db, ids, currentUser(c).UserID, time.Now())
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"删除失败:"+err.Error()+";没有部分删除,请刷新后重试。")
return
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackSuccess,
fmt.Sprintf("已删除 %d 条档口入库码记录;顺运宝已写入的快递单号不会撤销。重新导入相同数据可恢复。", count))
}
// 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,
innerCodeFeedbackError, "业务日期无效,没有执行匹配。")
return
}
ids := parseInnerCodeIDs(c.PostFormArray("ids"))
if len(ids) == 0 {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"请先勾选要匹配的记录。")
return
}
cfg, err := config.Load()
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"顺运宝配置不可用,没有执行匹配。")
return
}
client, err := syb.New(cfg.Syb.BaseURL)
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"顺运宝地址配置有误,没有执行匹配。")
return
}
autoLoggedIn, message := ensureInnerCodeMatchSession(
func() error { return service.EnsureSybSession(h.db, client, cfg.Syb.Username, time.Now()) },
func() (string, bool) { return h.attemptSybAutoLogin(c, client, *cfg) },
)
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError, message)
return
}
result, err := service.PlanInnerCodeRecords(c.Request.Context(), h.db, client, businessDate, ids)
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"匹配失败:"+err.Error()+";本次没有保存不完整的匹配结果,请按提示处理后重试。")
return
}
message = fmt.Sprintf("匹配完成:处理 %d 条,可回写 %d 条,已存在 %d 条,跳过 %d 条,失败 %d 条。",
result.Total, result.Ready, result.AlreadyFilled, result.Skipped, result.Failed)
if autoLoggedIn {
message = "顺运宝自动登录成功;" + message
}
feedbackKind := innerCodeFeedbackSuccess
if result.Failed > 0 || result.Skipped > 0 {
feedbackKind = innerCodeFeedbackError
message += " 请查看状态为“已跳过”或“失败”的记录,按逐行原因处理后重新勾选匹配。"
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, feedbackKind, message)
}
// ensureInnerCodeMatchSession 只负责“检查缓存 → 必要时自动登录”的编排。
// 自动登录成功后调用方继续使用同一个 client;这里绝不启动顺运宝同步。
func ensureInnerCodeMatchSession(checkSession func() error, autoLogin func() (string, bool)) (bool, string) {
err := checkSession()
if err == nil {
return false, ""
}
if !errors.Is(err, service.ErrSybLoginRequired) {
return false, "校验顺运宝会话失败,没有执行匹配。"
}
reason, ok := autoLogin()
if ok {
return true, ""
}
reason = strings.TrimSpace(reason)
if reason == "" {
reason = "未配置验证码识别服务,无法自动登录"
}
return false, "顺运宝自动登录未完成:" + reason + ";没有执行匹配,请到“顺运宝数据”页面完成登录。"
}
// InnerCodeApply 在操作员确认后逐条执行安全回写。
func (h *Handler) InnerCodeApply(c *gin.Context) {
businessDate, status, keyword := c.PostForm("date"), c.PostForm("status"), c.PostForm("q")
pageNumber := service.ParsePage(c.PostForm("page"))
ids := parseInnerCodeIDs(c.PostFormArray("ids"))
client, message := h.innerCodeSybClient()
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError, message)
return
}
result, err := service.ApplyInnerCodes(c.Request.Context(), h.db, client, ids, currentUser(c).UserID)
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"回写未完成:"+err.Error())
return
}
message = fmt.Sprintf("回写完成:选择 %d 条,成功 %d 条,已存在 %d 条,跳过 %d 条,失败 %d 条,需核对 %d 条。",
result.Requested, result.Updated, result.AlreadyFilled, result.Skipped, result.Failed, result.NeedsCheck)
feedbackKind := innerCodeFeedbackSuccess
if result.Failed > 0 || result.NeedsCheck > 0 {
feedbackKind = innerCodeFeedbackError
message += " 请查看失败或需核对的记录,确认远端结果后再继续。"
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, feedbackKind, message)
}
func parseInnerCodeIDs(rawIDs []string) []int64 {
ids := make([]int64, 0, len(rawIDs))
seen := make(map[int64]bool, len(rawIDs))
for _, raw := range rawIDs {
id, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || id <= 0 || seen[id] {
continue
}
seen[id] = true
ids = append(ids, id)
}
return ids
}
// InnerCodeRecheck 只重新读取未知结果,绝不再次发送删除或写入。
func (h *Handler) InnerCodeRecheck(c *gin.Context) {
businessDate, status, keyword := c.PostForm("date"), c.PostForm("status"), c.PostForm("q")
pageNumber := service.ParsePage(c.PostForm("page"))
id, err := strconv.ParseInt(strings.TrimSpace(c.PostForm("id")), 10, 64)
if err != nil || id <= 0 {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"记录编号无效,没有执行核对。")
return
}
client, message := h.innerCodeSybClient()
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError, message)
return
}
recheckStatus, message, err := service.RecheckInnerCode(c.Request.Context(), h.db, client, id)
if err != nil {
message = "重新核对失败:" + err.Error()
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError, message)
return
}
feedbackKind := innerCodeFeedbackSuccess
if recheckStatus == model.InnerCodeNeedsCheck {
feedbackKind = innerCodeFeedbackError
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, feedbackKind, message)
}
func (h *Handler) innerCodeSybClient() (*syb.Client, string) {
cfg, err := config.Load()
if err != nil {
return nil, "顺运宝配置不可用,没有发送远端请求。"
}
client, err := syb.New(cfg.Syb.BaseURL)
if err != nil {
return nil, "顺运宝地址配置有误,没有发送远端请求。"
}
if err := service.EnsureSybSession(h.db, client, cfg.Syb.Username, time.Now()); err != nil {
if errors.Is(err, service.ErrSybLoginRequired) {
return nil, "顺运宝会话已过期,请先到“顺运宝数据”页面登录。"
}
return nil, "校验顺运宝会话失败,没有发送远端请求。"
}
return client, ""
}
func (h *Handler) innerCodeRedirect(c *gin.Context, businessDate, status, keyword string, pageNumber int, feedbackKind, message string) {
values := url.Values{"date": {businessDate}, "page": {strconv.Itoa(pageNumber)}}
if message = strings.TrimSpace(message); message != "" {
values.Set("feedback", feedbackKind)
values.Set("message", message)
}
values.Set("page_size", strconv.Itoa(service.ParsePageSize(c.PostForm("page_size"))))
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())
}