fix: 使用 Excel 生成日期导入档口入库码 (#237)

This commit is contained in:
chengma
2026-08-15 10:38:34 +08:00
parent 3bd2473e8f
commit 74856deff7
4 changed files with 149 additions and 37 deletions
+68 -11
View File
@@ -35,10 +35,13 @@ var (
innerCodeStallToken = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9-]*$`)
innerCodeWeightTail = regexp.MustCompile(`(?i)[\d.]+[-~~到至][\d.]+(?:公斤|kg).*$`)
innerCodeSizeTail = regexp.MustCompile(`(?i)((?:[1-9]\d*)?XL|XXL|XS|S|M|L)$`)
innerCodeFilenameDate = regexp.MustCompile(`(?:^|_)(\d{8})(?:_|\.|$)`)
innerCodeShortDate = regexp.MustCompile(`^(\d{1,2})[-/](\d{1,2})$`)
)
// InnerCodeImportResult 是一次单表幂等导入的统计。
type InnerCodeImportResult struct {
BusinessDate string
TotalRows int
CreatedCount int
UpdatedCount int
@@ -99,10 +102,8 @@ func NormalizeInnerCodeSpecKey(value string) string {
}
// ImportInnerCodeExcel 解析工作表并在一个事务中幂等写入业务表。
func ImportInnerCodeExcel(db *sql.DB, path, businessDate, actorUserID string) (*InnerCodeImportResult, error) {
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
return nil, fmt.Errorf("%w:业务日期格式应为 YYYY-MM-DD", ErrInvalidInnerCodeImport)
}
// 业务日期只取 Excel“生成日期”;originalFilename 仅用于给短日期补充并校验年份。
func ImportInnerCodeExcel(db *sql.DB, path, originalFilename, actorUserID string) (*InnerCodeImportResult, error) {
if strings.TrimSpace(actorUserID) == "" {
return nil, fmt.Errorf("导入账号不能为空")
}
@@ -112,7 +113,7 @@ func ImportInnerCodeExcel(db *sql.DB, path, businessDate, actorUserID string) (*
}
defer book.Close()
result, rows, err := parseInnerCodeWorkbook(book, businessDate, actorUserID)
result, rows, err := parseInnerCodeWorkbook(book, originalFilename, actorUserID)
if err != nil {
return nil, fmt.Errorf("%w:%v", ErrInvalidInnerCodeImport, err)
}
@@ -148,7 +149,7 @@ type innerCodeParsedRow struct {
businessKey string
}
func parseInnerCodeWorkbook(book *excelize.File, businessDate, actorUserID string) (*InnerCodeImportResult, []model.InnerCodeImportRow, error) {
func parseInnerCodeWorkbook(book *excelize.File, originalFilename, actorUserID string) (*InnerCodeImportResult, []model.InnerCodeImportRow, error) {
found := false
for _, name := range book.GetSheetList() {
if name != innerCodeSheetName {
@@ -159,7 +160,7 @@ func parseInnerCodeWorkbook(book *excelize.File, businessDate, actorUserID strin
if err != nil {
return nil, nil, fmt.Errorf("读取工作表 %q 失败: %w", name, err)
}
result, parsed, parseErr := parseInnerCodeRows(rows, businessDate, actorUserID)
result, parsed, parseErr := parseInnerCodeRows(rows, originalFilename, actorUserID)
closeErr := rows.Close()
if parseErr != nil {
return nil, nil, parseErr
@@ -175,9 +176,10 @@ func parseInnerCodeWorkbook(book *excelize.File, businessDate, actorUserID strin
return nil, nil, fmt.Errorf("工作表 %q 无法读取", innerCodeSheetName)
}
func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (*InnerCodeImportResult, []model.InnerCodeImportRow, error) {
func parseInnerCodeRows(rows *excelize.Rows, originalFilename, actorUserID string) (*InnerCodeImportResult, []model.InnerCodeImportRow, error) {
var headers map[string]int
var specColumn int
var businessDate string
rowNumber := 0
parsed := make([]innerCodeParsedRow, 0, 128)
for rows.Next() {
@@ -192,7 +194,8 @@ func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (
}
candidate, candidateSpec := innerCodeHeaderMap(columns)
if _, ok := candidate["内部档口入库码"]; ok {
if _, ok = candidate["Shopee订单编号"]; ok && candidateSpec >= 0 {
_, hasDate := candidate["生成日期"]
if _, ok = candidate["Shopee订单编号"]; ok && candidateSpec >= 0 && hasDate {
headers, specColumn = candidate, candidateSpec
}
}
@@ -209,6 +212,15 @@ func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (
if orderNumber == "" || innerCode == "" {
return nil, nil, fmt.Errorf("第 %d 行缺少 Shopee订单编号或内部档口入库码", rowNumber)
}
rowBusinessDate, err := parseInnerCodeBusinessDate(innerCodeNamedCell(columns, headers, "生成日期"), originalFilename)
if err != nil {
return nil, nil, fmt.Errorf("第 %d 行生成日期无效:%v", rowNumber, err)
}
if businessDate == "" {
businessDate = rowBusinessDate
} else if businessDate != rowBusinessDate {
return nil, nil, fmt.Errorf("同一工作簿包含多个生成日期(%s、%s),请拆分后导入", businessDate, rowBusinessDate)
}
specRaw := strings.ReplaceAll(innerCodeCell(columns, specColumn), "\r", " ")
specRaw = strings.TrimSpace(strings.ReplaceAll(specRaw, "\n", " "))
stall := innerCodeStall(columns, headers)
@@ -235,7 +247,7 @@ func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (
return nil, nil, fmt.Errorf("遍历工作表失败: %w", err)
}
if headers == nil {
return nil, nil, fmt.Errorf("前 10 行未找到包含内部档口入库码、Shopee订单编号和规格列的表头")
return nil, nil, fmt.Errorf("前 10 行未找到包含生成日期、内部档口入库码、Shopee订单编号和规格列的表头")
}
if len(parsed) == 0 {
return nil, nil, fmt.Errorf("工作表只有表头,没有可导入数据")
@@ -250,7 +262,7 @@ func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (
}
codeKeys[row.InnerCode] = row.businessKey
}
result := &InnerCodeImportResult{TotalRows: len(parsed)}
result := &InnerCodeImportResult{BusinessDate: businessDate, TotalRows: len(parsed)}
unique := make([]model.InnerCodeImportRow, 0, len(groups))
seen := make(map[string]bool, len(groups))
for _, original := range parsed {
@@ -272,6 +284,51 @@ func parseInnerCodeRows(rows *excelize.Rows, businessDate, actorUserID string) (
return result, unique, nil
}
// parseInnerCodeBusinessDate 把完整日期或依赖文件名年份的短日期标准化为 YYYY-MM-DD。
func parseInnerCodeBusinessDate(raw, originalFilename string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
return "", fmt.Errorf("不能为空")
}
for _, layout := range []string{"2006-01-02", "2006/01/02", "2006.01.02"} {
if parsed, err := time.ParseInLocation(layout, value, time.Local); err == nil {
return parsed.Format("2006-01-02"), nil
}
}
if serial, err := strconv.ParseFloat(value, 64); err == nil && serial >= 1 {
if parsed, dateErr := excelize.ExcelDateToTime(serial, false); dateErr == nil {
return parsed.Format("2006-01-02"), nil
}
}
match := innerCodeShortDate.FindStringSubmatch(value)
if len(match) != 3 {
return "", fmt.Errorf("%q 不是 YYYY-MM-DD、YYYY/MM/DD、MM-DD 或 MM/DD", value)
}
filenameDate, ok := innerCodeDateFromFilename(originalFilename)
if !ok {
return "", fmt.Errorf("短日期 %q 缺少年份,原始文件名中也没有 _YYYYMMDD_ 日期", value)
}
month, _ := strconv.Atoi(match[1])
day, _ := strconv.Atoi(match[2])
parsed := time.Date(filenameDate.Year(), time.Month(month), day, 0, 0, 0, 0, time.Local)
if parsed.Month() != time.Month(month) || parsed.Day() != day {
return "", fmt.Errorf("短日期 %q 不存在", value)
}
if parsed.Month() != filenameDate.Month() || parsed.Day() != filenameDate.Day() {
return "", fmt.Errorf("短日期 %q 与文件名日期 %s 不一致", value, filenameDate.Format("2006-01-02"))
}
return parsed.Format("2006-01-02"), nil
}
func innerCodeDateFromFilename(filename string) (time.Time, bool) {
match := innerCodeFilenameDate.FindStringSubmatch(filepath.Base(strings.TrimSpace(filename)))
if len(match) != 2 {
return time.Time{}, false
}
parsed, err := time.ParseInLocation("20060102", match[1], time.Local)
return parsed, err == nil
}
func innerCodeHeaderMap(columns []string) (map[string]int, int) {
result := make(map[string]int, len(columns))
specColumn := -1