fix: 兼容档口货号括号与前导零 (#273)
This commit is contained in:
@@ -5,11 +5,11 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"cmautobuy/admin/model"
|
"cmautobuy/admin/model"
|
||||||
"cmautobuy/admin/repository"
|
"cmautobuy/admin/repository"
|
||||||
@@ -419,19 +419,103 @@ func innerCodeStallMatches(stall string, item syb.DetailItem) bool {
|
|||||||
if strings.Contains(blob, stall) {
|
if strings.Contains(blob, stall) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
name, article, hasArticle := strings.Cut(stall, "#")
|
name, article, hasArticle := splitInnerCodeStall(stall)
|
||||||
if !hasArticle {
|
if !hasArticle {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
name, article = strings.TrimSpace(name), strings.TrimSpace(article)
|
if article == "" {
|
||||||
if article != "" {
|
return name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
||||||
if strings.Contains(blob, "#"+article) || strings.HasPrefix(item.ProductSpec, article+" ") || strings.HasPrefix(item.ProductSpec, article+",") {
|
}
|
||||||
|
|
||||||
|
// 纯数字货号容易在不同档口重复。只有候选也包含同一档口名称时,
|
||||||
|
// 才允许把 067 与 67 视为同一个货号;不能只凭短数字自动选商品。
|
||||||
|
if isInnerCodeNumericArticle(article) {
|
||||||
|
nameMatches := name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
||||||
|
if !nameMatches {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return innerCodeTextHasNumericArticle(sku, article) ||
|
||||||
|
innerCodeTextHasNumericArticle(variation, article) ||
|
||||||
|
innerCodeTextHasNumericArticle(item.ProductSpec, article)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerCodeTextHasExactArticle(sku, article) ||
|
||||||
|
innerCodeTextHasExactArticle(variation, article) ||
|
||||||
|
innerCodeTextHasExactArticle(item.ProductSpec, article)
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitInnerCodeStall 从“档口名称#货号”取最后一个 #,避免档口名称本身含 # 时截错。
|
||||||
|
func splitInnerCodeStall(stall string) (name, article string, ok bool) {
|
||||||
|
stall = strings.TrimSpace(stall)
|
||||||
|
separator := strings.LastIndex(stall, "#")
|
||||||
|
if separator < 0 {
|
||||||
|
return stall, "", false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stall[:separator]), strings.TrimSpace(stall[separator+1:]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInnerCodeNumericArticle(article string) bool {
|
||||||
|
if article == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, char := range article {
|
||||||
|
if !unicode.IsDigit(char) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func innerCodeTextHasNumericArticle(text, article string) bool {
|
||||||
|
target := normalizeInnerCodeNumericArticle(article)
|
||||||
|
for _, token := range innerCodeArticleTokens(text) {
|
||||||
|
if isInnerCodeNumericArticle(token) && normalizeInnerCodeNumericArticle(token) == target {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
pattern := regexp.MustCompile(`(?:^|[#\-_/\s])` + regexp.QuoteMeta(article) + `(?:$|[#\-_/,\s])`)
|
|
||||||
return pattern.MatchString(sku) || pattern.MatchString(variation)
|
|
||||||
}
|
}
|
||||||
return name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeInnerCodeNumericArticle(article string) string {
|
||||||
|
normalized := strings.TrimLeft(article, "0")
|
||||||
|
if normalized == "" {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func innerCodeTextHasExactArticle(text, article string) bool {
|
||||||
|
for _, token := range innerCodeArticleTokens(text) {
|
||||||
|
if token == article {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// innerCodeArticleTokens 只把连续字母或数字视为货号候选。
|
||||||
|
// `【】`、`#`、横线、空格等标点自然成为边界,因此能识别 `067【档口】`,
|
||||||
|
// 又不会把 `PDD256437` 中间的数字误认为独立货号。
|
||||||
|
func innerCodeArticleTokens(text string) []string {
|
||||||
|
tokens := make([]string, 0)
|
||||||
|
start := -1
|
||||||
|
runes := []rune(text)
|
||||||
|
for index, char := range runes {
|
||||||
|
if unicode.IsLetter(char) || unicode.IsDigit(char) {
|
||||||
|
if start < 0 {
|
||||||
|
start = index
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start >= 0 {
|
||||||
|
tokens = append(tokens, string(runes[start:index]))
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start >= 0 {
|
||||||
|
tokens = append(tokens, string(runes[start:]))
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
func innerCodeRawText(value any) string {
|
func innerCodeRawText(value any) string {
|
||||||
|
|||||||
@@ -223,6 +223,75 @@ func TestMatchInnerCodeItemsWithEvidence_重复和证据冲突均阻断(t *testi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInnerCodeStallMatches_中文括号货号与数字前导零(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stall string
|
||||||
|
item syb.DetailItem
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "067在中文括号前为独立货号",
|
||||||
|
stall: "将客大码网批#067",
|
||||||
|
item: innerCodeTestItem(1, "黑色(有口袋),2XL【建議67.5-82.5公斤】", map[string]any{
|
||||||
|
"sku": "067【将客大码网批】-黑色(有口袋)-2XL",
|
||||||
|
}),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "同档口67与067等价",
|
||||||
|
stall: "将客大码网批#067",
|
||||||
|
item: innerCodeTestItem(2, "黑色,2XL", map[string]any{
|
||||||
|
"variationSku": "汽配-将客大码网批#67#(&19)",
|
||||||
|
}),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "同货号不同档口不匹配",
|
||||||
|
stall: "将客大码网批#067",
|
||||||
|
item: innerCodeTestItem(3, "黑色,2XL", map[string]any{
|
||||||
|
"sku": "067【其他档口】-黑色-2XL", "variationSku": "汽配-其他档口#67",
|
||||||
|
}),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "非数字货号不做类似转换",
|
||||||
|
stall: "A档#A01",
|
||||||
|
item: innerCodeTestItem(4, "黑色,M", map[string]any{"sku": "A档#A1"}),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "非数字货号精确匹配",
|
||||||
|
stall: "A档#A01",
|
||||||
|
item: innerCodeTestItem(5, "黑色,M", map[string]any{"sku": "A档#A01"}),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := innerCodeStallMatches(test.stall, test.item); got != test.want {
|
||||||
|
t.Fatalf("innerCodeStallMatches(%q)=%t,期望 %t", test.stall, got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanInnerCodeRows_将客档口067唯一匹配生产样例(t *testing.T) {
|
||||||
|
record := model.InnerCodeRecord{ID: 255, OrderNumber: "260814A21AAB1G",
|
||||||
|
Stall: "将客大码网批#067", SpecRaw: "黑色(有口袋),2XL【建議67.5-82.5公斤】",
|
||||||
|
InnerCode: "DK-255", SourceDuplicateCount: 1}
|
||||||
|
item := innerCodeTestItem(147048059, record.SpecRaw, map[string]any{
|
||||||
|
"sku": "067【将客大码网批】-黑色(有口袋)-2XL",
|
||||||
|
"variationSku": "汽配-将客大码网批#67#(&19)",
|
||||||
|
})
|
||||||
|
plans := planInnerCodeRows([]model.InnerCodeRecord{record},
|
||||||
|
map[string][]int64{record.OrderNumber: {76159471}},
|
||||||
|
map[int64]syb.StockDetail{76159471: {ID: 76159471, Details: []syb.DetailItem{item}}})
|
||||||
|
if len(plans) != 1 || plans[0].Status != model.InnerCodeReady || plans[0].DetailID != item.ID {
|
||||||
|
t.Fatalf("将客档口记录未生成可回写计划: %+v", plans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlanInnerCodeRows_多件数量一致放行且其他异常继续阻断(t *testing.T) {
|
func TestPlanInnerCodeRows_多件数量一致放行且其他异常继续阻断(t *testing.T) {
|
||||||
records := []model.InnerCodeRecord{
|
records := []model.InnerCodeRecord{
|
||||||
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1,DK2", SourceDuplicateCount: 2},
|
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1,DK2", SourceDuplicateCount: 2},
|
||||||
|
|||||||
Reference in New Issue
Block a user