Compare commits

..
3 changed files with 89 additions and 7 deletions
+6 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Business-Rules-and-Glossary
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
wiki_revision: 214ac317abfef23d71362968d5b30f078fa528b6
synchronized_at: 2026-09-29T03:47:42Z
wiki_revision: c483a1f3fff3144dfad1ae24e6ca0b1de643ac12
synchronized_at: 2026-09-29T06:14:25Z
<!-- gitea-wiki-mirror:end -->
<!-- gitea-wiki-mirror:start -->
@@ -740,3 +740,7 @@ SYB 回填与资料完整性独立:正式SYB任务已有单号且仍关联有
- 超时/中断以及启动恢复把未完成 pending/matching 改为明确失败并将旧批次结束;既有 ready/updated/回写证据和范围外记录不修改,用户可手动重试。恢复不调用 SYB 写接口,不自动回写、不删除成功结果。
实现绑定:`fix/350-inner-code-matching`,源码提交 `9c5e20e`;未合并 main、未部署,正式环境仍需发布后启用。
### 档口入库码排版字符兼容(#350 v2)
匹配比较时,双方原始 SKU、variationSku 及档口名称仅去除 CR/LF/TAB 排版控制字符,并沿用首尾空白 trim;普通内部空格、大小写、标点与货号不变。导入原始值和回写计划中的 SYB 原始证据保持原样。归一化后重复候选、SKU/档口冲突仍拒绝,原精确规格、货号、数量与占用校验不放宽。成功匹配只形成可回写计划,不自动执行 SYB 写入。
+15 -5
View File
@@ -417,12 +417,18 @@ func matchSpec(spec string, items []sybclient.DetailItem) []sybclient.DetailItem
}
return result
}
// Compare layout-only controls without changing stored evidence or ordinary spaces.
func normalizeMatchLayout(value string) string {
return strings.TrimSpace(strings.NewReplacer("\r", "", "\n", "", "\t", "").Replace(value))
}
func matchEvidence(stall, sourceSKU string, items []sybclient.DetailItem) ([]sybclient.DetailItem, string) {
sourceSKU = strings.TrimSpace(sourceSKU)
sourceSKU = normalizeMatchLayout(sourceSKU)
if sourceSKU != "" {
skuMatches := []sybclient.DetailItem{}
for _, item := range items {
if strings.TrimSpace(rawText(item.Raw["sku"])) == sourceSKU || strings.TrimSpace(rawText(item.Raw["variationSku"])) == sourceSKU {
if normalizeMatchLayout(rawText(item.Raw["sku"])) == sourceSKU || normalizeMatchLayout(rawText(item.Raw["variationSku"])) == sourceSKU {
skuMatches = append(skuMatches, item)
}
}
@@ -453,7 +459,7 @@ func matchEvidence(stall, sourceSKU string, items []sybclient.DetailItem) ([]syb
return fallback, ""
}
func strictStall(stall string, items []sybclient.DetailItem) []sybclient.DetailItem {
stall = strings.TrimSpace(stall)
stall = normalizeMatchLayout(stall)
if stall == "" {
return nil
}
@@ -471,8 +477,12 @@ func strictStall(stall string, items []sybclient.DetailItem) []sybclient.DetailI
// 候选中同时包含档口名才允许前导零等价(如 "067"≡"67");非数字货号要求精确
// token 匹配;ProductSpec 只在以货号开头时才算命中;货号为空时回退为档口名包含判断。
func stallMatches(stall string, item sybclient.DetailItem) bool {
sku := rawText(item.Raw["sku"])
variation := rawText(item.Raw["variationSku"])
stall = normalizeMatchLayout(stall)
if stall == "" {
return false
}
sku := normalizeMatchLayout(rawText(item.Raw["sku"]))
variation := normalizeMatchLayout(rawText(item.Raw["variationSku"]))
blob := sku + " " + variation + " " + item.ProductSpec
if strings.Contains(blob, stall) {
return true
@@ -0,0 +1,68 @@
package sybinnercode
import (
"context"
"testing"
"go-admin/app/goauto/models"
"go-admin/app/goauto/sybclient"
)
func TestMatchEvidenceNormalizesLayoutOnBothSides(t *testing.T) {
for _, layout := range []string{"\n", "\r", "\r\n", "\t"} {
for _, sourceHasLayout := range []bool{true, false} {
source, candidate := "市场-档口甲#731", "市场-档口甲#731"
if sourceHasLayout {
source = "市场-" + layout + "档口甲#731"
} else {
candidate = "市场-" + layout + "档口甲#731"
}
item := detail(1, "黑色,L", 1, "", candidate, "")
matches, reason := matchEvidence(source, source, []sybclient.DetailItem{item})
if reason != "" || len(matches) != 1 || !stallMatches(source, item) {
t.Fatalf("layout=%q sourceLayout=%v: matches=%v reason=%s", layout, sourceHasLayout, matches, reason)
}
if item.Raw["variationSku"] != candidate {
t.Fatal("comparison modified raw candidate evidence")
}
}
}
item := detail(1, "黑色,L", 1, "SKU-\n1", "", "")
if matches, reason := matchEvidence("", "SKU-1", []sybclient.DetailItem{item}); reason != "" || len(matches) != 1 {
t.Fatalf("raw sku layout did not match: %v %s", matches, reason)
}
}
func TestMatchLayoutPreservesIdentityAndAmbiguityChecks(t *testing.T) {
for _, candidate := range []string{"市场-档口乙#731", "市场-档口甲#732", "市场-档 口甲#731", "市场-档口甲#PDD7319"} {
item := detail(1, "黑色,L", 1, "", candidate, "")
matches, _ := matchEvidence("市场-\n档口甲#731", "市场-\n档口甲#731", []sybclient.DetailItem{item})
if len(matches) != 0 {
t.Fatalf("different identity unexpectedly matched: %q", candidate)
}
}
items := []sybclient.DetailItem{detail(1, "黑色,L", 1, "SKU-1", "", ""), detail(2, "黑色,L", 1, "SKU-\n1", "", "")}
if matches, reason := matchEvidence("", "SKU-1", items); len(matches) != 0 || reason == "" {
t.Fatal("layout-equivalent duplicate SKUs must remain ambiguous")
}
items = []sybclient.DetailItem{detail(1, "黑色,L", 1, "SKU-1", "市场-档口乙#732", ""), detail(2, "黑色,L", 1, "OTHER", "市场-档口甲#731", "")}
if matches, reason := matchEvidence("市场-\n档口甲#731", "SKU-\t1", items); len(matches) != 0 || reason == "" {
t.Fatal("SKU/stall conflict must remain rejected")
}
if normalizeMatchLayout("A B") == normalizeMatchLayout("AB") || normalizeMatchLayout("a") == normalizeMatchLayout("A") {
t.Fatal("ordinary spaces and case must remain significant")
}
}
func TestPlanRecordWithLayoutEvidenceKeepsOriginalValues(t *testing.T) {
record := matchRecord("ORDER-1", "市场-\n档口甲#731", "市场-\n档口甲#731", "IC-1")
item := detail(20, "黑色,L", 1, "", "市场-档口甲#731", "")
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Details: []sybclient.DetailItem{item}}}}
plan, status, reason, err := planRecord(context.Background(), reader, record, map[int64]bool{})
if err != nil || status != models.SYBInnerCodeReady || plan == nil || plan.DetailID != 20 {
t.Fatalf("expected unique ready plan, got %v %s %s %v", plan, status, reason, err)
}
if record.Stall != "市场-\n档口甲#731" || record.SourceSKURaw != record.Stall || plan.SYBVariationSKU != "市场-档口甲#731" {
t.Fatal("original import and plan evidence must remain unchanged")
}
}