feat: 档口入库码后台批量回写 (#246)

This commit is contained in:
chengma
2026-08-15 17:08:17 +08:00
parent 6b30594f4c
commit f8f22a15c7
21 changed files with 708 additions and 134 deletions
+87 -26
View File
@@ -1,9 +1,11 @@
package web
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
@@ -53,21 +55,35 @@ func (h *Handler) InnerCodeList(c *gin.Context) {
if keyword != "" {
query.Set("q", keyword)
}
applyBatchID := strings.TrimSpace(c.Query("apply_batch_id"))
applyProgress, err := service.GetInnerCodeApplyBatchProgress(h.db, applyBatchID)
if err != nil {
fail(c, http.StatusInternalServerError, "读取后台回写进度失败,请稍后刷新重试。")
return
}
if applyProgress != nil {
query.Set("apply_batch_id", applyProgress.BatchID)
}
progressQuery := cloneURLValues(query)
progressQuery.Set("page", strconv.Itoa(result.Page))
progressQuery.Set("page_size", strconv.Itoa(pageSize))
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()),
"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),
"ApplyProgress": applyProgress,
"ApplyProgressURL": "/inner-codes?" + progressQuery.Encode(),
"Pagination": service.NewPaginationView(result.Page, pageSize, result.TotalPages, query.Encode()),
}))
}
@@ -220,30 +236,60 @@ func ensureInnerCodeMatchSession(checkSession func() error, autoLogin func() (st
return false, "顺运宝自动登录未完成:" + reason + ";没有执行匹配,请到“顺运宝数据”页面完成登录。"
}
// InnerCodeApply 在操作员确认后逐条执行安全回写。
// 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()
_, 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)
actorUserID := currentUser(c).UserID
batch, err := service.QueueInnerCodeApplyBatch(h.db, ids, actorUserID)
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"回写未完成:"+err.Error())
"后台回写未提交:"+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)
h.startInnerCodeApplyBatch(batch.ID, actorUserID)
message = fmt.Sprintf("已提交 %d 条后台回写,页面可以继续使用;请刷新进度查看逐条结果。", batch.Count)
h.innerCodeRedirectWithBatch(c, businessDate, status, keyword, pageNumber,
innerCodeFeedbackSuccess, message, batch.ID)
}
func (h *Handler) startInnerCodeApplyBatch(batchID, actorUserID string) {
go func() {
h.innerCodeApplyMu.Lock()
defer h.innerCodeApplyMu.Unlock()
var runErr error
defer func() {
if recovered := recover(); recovered != nil {
runErr = fmt.Errorf("后台执行器异常:%v", recovered)
}
if runErr == nil {
return
}
needsCheck, released, interruptErr := service.InterruptInnerCodeApplyBatch(h.db, batchID)
if interruptErr != nil {
log.Printf("档口入库码后台批次中断且状态收敛失败 batch_id=%s err=%v interrupt_err=%v", batchID, runErr, interruptErr)
return
}
log.Printf("档口入库码后台批次中断 batch_id=%s needs_check=%d released=%d err=%v", batchID, needsCheck, released, runErr)
}()
client, message := h.innerCodeSybClient()
if message != "" {
runErr = errors.New(message)
return
}
result, err := service.RunInnerCodeApplyBatch(context.Background(), h.db, client, batchID, actorUserID)
runErr = err
if err == nil {
log.Printf("档口入库码后台批次完成 batch_id=%s processed=%d updated=%d already_filled=%d skipped=%d failed=%d needs_check=%d",
batchID, result.Requested, result.Updated, result.AlreadyFilled, result.Skipped, result.Failed, result.NeedsCheck)
}
}()
}
func parseInnerCodeIDs(rawIDs []string) []int64 {
@@ -307,6 +353,10 @@ func (h *Handler) innerCodeSybClient() (*syb.Client, string) {
}
func (h *Handler) innerCodeRedirect(c *gin.Context, businessDate, status, keyword string, pageNumber int, feedbackKind, message string) {
h.innerCodeRedirectWithBatch(c, businessDate, status, keyword, pageNumber, feedbackKind, message, "")
}
func (h *Handler) innerCodeRedirectWithBatch(c *gin.Context, businessDate, status, keyword string, pageNumber int, feedbackKind, message, batchID string) {
values := url.Values{"date": {businessDate}, "page": {strconv.Itoa(pageNumber)}}
if message = strings.TrimSpace(message); message != "" {
values.Set("feedback", feedbackKind)
@@ -319,5 +369,16 @@ func (h *Handler) innerCodeRedirect(c *gin.Context, businessDate, status, keywor
if strings.TrimSpace(keyword) != "" {
values.Set("q", strings.TrimSpace(keyword))
}
if strings.TrimSpace(batchID) != "" {
values.Set("apply_batch_id", strings.TrimSpace(batchID))
}
c.Redirect(http.StatusSeeOther, "/inner-codes?"+values.Encode())
}
func cloneURLValues(source url.Values) url.Values {
result := make(url.Values, len(source))
for key, values := range source {
result[key] = append([]string(nil), values...)
}
return result
}