fix: 使用 Excel 生成日期导入档口入库码 (#237)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user