feat: 优化批量匹配和分页默认值 (#244)

This commit is contained in:
chengma
2026-08-15 15:56:16 +08:00
parent f315b02d3d
commit ad5bd834d3
17 changed files with 340 additions and 82 deletions
+61 -24
View File
@@ -14,12 +14,18 @@ import (
"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"))
@@ -28,6 +34,11 @@ func (h *Handler) InnerCodeList(c *gin.Context) {
}
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)
@@ -52,7 +63,9 @@ func (h *Handler) InnerCodeList(c *gin.Context) {
"IsFiltered": result.IsFiltered,
"CurrentPage": result.Page,
"CurrentPageSize": pageSize,
"Message": strings.TrimSpace(c.Query("message")),
"FeedbackMessage": feedbackMessage,
"FeedbackSuccess": feedbackMessage != "" && feedbackKind == innerCodeFeedbackSuccess,
"FeedbackError": feedbackMessage != "" && feedbackKind == innerCodeFeedbackError,
"Status": service.InnerCodeStatusMessage(result),
"Pagination": service.NewPaginationView(result.Page, pageSize, result.TotalPages, query.Encode()),
}))
@@ -113,7 +126,7 @@ func (h *Handler) InnerCodeImport(c *gin.Context) {
}
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, message)
h.innerCodeRedirect(c, result.BusinessDate, "", "", 1, innerCodeFeedbackSuccess, message)
}
// InnerCodeDelete 软删除当前页已选记录;不撤销任何顺运宝远端操作。
@@ -123,10 +136,11 @@ func (h *Handler) InnerCodeDelete(c *gin.Context) {
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, "删除失败:"+err.Error()+";没有部分删除,请刷新后重试。")
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"删除失败:"+err.Error()+";没有部分删除,请刷新后重试。")
return
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber,
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackSuccess,
fmt.Sprintf("已删除 %d 条档口入库码记录;顺运宝已写入的快递单号不会撤销。重新导入相同数据可恢复。", count))
}
@@ -136,26 +150,26 @@ func (h *Handler) InnerCodeMatch(c *gin.Context) {
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, "业务日期无效,没有执行匹配。")
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, "请先勾选要匹配的记录。")
return
}
if len(ids) > service.InnerCodeRemoteBatchLimit {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, "一次最多匹配 20 条记录,请减少勾选后重试。")
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"请先勾选要匹配的记录。")
return
}
cfg, err := config.Load()
if err != nil {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, "顺运宝配置不可用,没有执行匹配。")
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, "顺运宝地址配置有误,没有执行匹配。")
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"顺运宝地址配置有误,没有执行匹配。")
return
}
autoLoggedIn, message := ensureInnerCodeMatchSession(
@@ -163,12 +177,13 @@ func (h *Handler) InnerCodeMatch(c *gin.Context) {
func() (string, bool) { return h.attemptSybAutoLogin(c, client, *cfg) },
)
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, 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, "匹配失败:"+err.Error())
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"匹配失败:"+err.Error()+";本次没有保存不完整的匹配结果,请按提示处理后重试。")
return
}
message = fmt.Sprintf("匹配完成:处理 %d 条,可回写 %d 条,已存在 %d 条,跳过 %d 条,失败 %d 条。",
@@ -176,7 +191,12 @@ func (h *Handler) InnerCodeMatch(c *gin.Context) {
if autoLoggedIn {
message = "顺运宝自动登录成功;" + message
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
feedbackKind := innerCodeFeedbackSuccess
if result.Failed > 0 || result.Skipped > 0 {
feedbackKind = innerCodeFeedbackError
message += " 请查看状态为“已跳过”或“失败”的记录,按逐行原因处理后重新勾选匹配。"
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, feedbackKind, message)
}
// ensureInnerCodeMatchSession 只负责“检查缓存 → 必要时自动登录”的编排。
@@ -207,17 +227,23 @@ func (h *Handler) InnerCodeApply(c *gin.Context) {
ids := parseInnerCodeIDs(c.PostFormArray("ids"))
client, message := h.innerCodeSybClient()
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, 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, "回写未完成:"+err.Error())
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)
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
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 {
@@ -240,19 +266,26 @@ func (h *Handler) InnerCodeRecheck(c *gin.Context) {
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, "记录编号无效,没有执行核对。")
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError,
"记录编号无效,没有执行核对。")
return
}
client, message := h.innerCodeSybClient()
if message != "" {
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, innerCodeFeedbackError, message)
return
}
_, message, err = service.RecheckInnerCode(c.Request.Context(), h.db, client, id)
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
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, message)
feedbackKind := innerCodeFeedbackSuccess
if recheckStatus == model.InnerCodeNeedsCheck {
feedbackKind = innerCodeFeedbackError
}
h.innerCodeRedirect(c, businessDate, status, keyword, pageNumber, feedbackKind, message)
}
func (h *Handler) innerCodeSybClient() (*syb.Client, string) {
@@ -273,8 +306,12 @@ func (h *Handler) innerCodeSybClient() (*syb.Client, string) {
return client, ""
}
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}}
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))
+45
View File
@@ -2,9 +2,15 @@ package web
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/gin-gonic/gin"
"cmautobuy/admin/service"
)
@@ -15,6 +21,45 @@ func TestParseInnerCodeIDs_过滤无效并保持首次选择顺序(t *testing.T)
}
}
func TestInnerCodeRedirect_区分错误反馈并使用默认100条(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
form := url.Values{"page_size": {""}}
context.Request = httptest.NewRequest(http.MethodPost, "/inner-codes/match", strings.NewReader(form.Encode()))
context.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
(&Handler{}).innerCodeRedirect(context, "2026-08-15", "pending", "ORDER-1", 2,
innerCodeFeedbackError, "匹配失败:测试错误")
if context.Writer.Status() != http.StatusSeeOther {
t.Fatalf("状态码=%d,期望 303", context.Writer.Status())
}
location, err := url.Parse(recorder.Header().Get("Location"))
if err != nil {
t.Fatal(err)
}
query := location.Query()
if location.Path != "/inner-codes" || query.Get("feedback") != "error" ||
query.Get("message") != "匹配失败:测试错误" || query.Get("page_size") != "100" ||
query.Get("date") != "2026-08-15" || query.Get("status") != "pending" ||
query.Get("q") != "ORDER-1" || query.Get("page") != "2" {
t.Fatalf("反馈跳转参数不完整: %s", recorder.Header().Get("Location"))
}
}
func TestInnerCodeMatch_不再按20条提前拒绝只读匹配(t *testing.T) {
raw, err := os.ReadFile("inner_code.go")
if err != nil {
t.Fatal(err)
}
source := string(raw)
for _, forbidden := range []string{"InnerCodeRemoteBatchLimit", "一次最多匹配 20 条"} {
if strings.Contains(source, forbidden) {
t.Errorf("只读匹配仍包含旧限制 %q", forbidden)
}
}
}
func TestEnsureInnerCodeMatchSession_有效会话不自动登录(t *testing.T) {
loginCalls := 0
autoLoggedIn, message := ensureInnerCodeMatchSession(
+1 -1
View File
@@ -101,7 +101,7 @@ func Test列表写操作跳转保留筛选和页码(t *testing.T) {
func Test列表写操作跳转归一化非法每页条数(t *testing.T) {
context, recorder := listPostContext(t, "/pdd/delete", url.Values{"page_size": {"9999"}})
(&Handler{}).pddRedirect(context, "")
assertRedirectQuery(t, recorder, "/pdd", map[string]string{"page_size": "20"})
assertRedirectQuery(t, recorder, "/pdd", map[string]string{"page_size": "100"})
}
func Test蝦皮分页详情和状态条保留分类搜索(t *testing.T) {