fix(backend): 库存导入改用 shakinm/xlsReader,按唯一编号去重,入库日期取自 Excel
extrame/xls 误读真实 .xls(3264 行只读出 455 行、编号丢失、数字被当 日期序列),换用 shakinm/xlsReader 正确读取全部行。去重改为「有商品编号 时仅按编号匹配」,避免不同批次被 NSS 回退合并;Inventory.ProductCode 存行自身唯一编号。入库时间取 Excel 入库日期列写入 created_at。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,13 @@ package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/extrame/xls"
|
||||
"github.com/shakinm/xlsReader/xls"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
@@ -550,10 +548,11 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
|
||||
invByNSS[nssKey] = inv
|
||||
}
|
||||
lookupInv := func(productCode, name, series, spec string, whID uint64) *model.Inventory {
|
||||
// 有商品编号时,仅按「编号|仓库」唯一匹配,绝不回退到名称匹配:
|
||||
// 同一款酒的不同批次/年份是不同的商品编号,名称相同但属于独立库存记录,
|
||||
// 回退名称匹配会把它们错误合并成一条(3264 行被压成 1134 行)。
|
||||
if productCode != "" {
|
||||
if inv, ok := invByCode[fmt.Sprintf("%s|%d", productCode, whID)]; ok {
|
||||
return inv
|
||||
}
|
||||
return invByCode[fmt.Sprintf("%s|%d", productCode, whID)]
|
||||
}
|
||||
return invByNSS[fmt.Sprintf("%s|%s|%s|%d", name, series, spec, whID)]
|
||||
}
|
||||
@@ -782,7 +781,7 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
|
||||
ProductID: &productIDCopy,
|
||||
StockInItemID: nil,
|
||||
Quantity: qty,
|
||||
ProductCode: prod.Code,
|
||||
ProductCode: productCode, // 行自身的商品编号(每条库存记录唯一),而非商品目录编号
|
||||
ProductName: prod.Name,
|
||||
Series: prod.Series,
|
||||
Spec: prod.Spec,
|
||||
@@ -858,65 +857,38 @@ func parseUploadedExcel(c *gin.Context) ([][]string, error) {
|
||||
var rows [][]string
|
||||
|
||||
if isOLE {
|
||||
// 老格式 BIFF — extrame/xls 需要文件路径,写入临时文件
|
||||
tmp, tmpErr := os.CreateTemp("", "import_*.xls")
|
||||
if tmpErr != nil {
|
||||
return nil, fmt.Errorf("cannot create temp file: %s", tmpErr.Error())
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, cpErr := io.Copy(tmp, f); cpErr != nil {
|
||||
tmp.Close()
|
||||
return nil, fmt.Errorf("cannot write temp file: %s", cpErr.Error())
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
wb, xlErr := xls.Open(tmp.Name(), "utf-8")
|
||||
// 老格式 BIFF (.xls)。注意:extrame/xls 对部分真实导出文件会错读单元格
|
||||
// (字符串整列丢失、数字被当成日期序列号),导致 3000 行只解析出几百行。
|
||||
// 改用 shakinm/xlsReader,可正确读取共享字符串表与数值。
|
||||
wb, xlErr := xls.OpenReader(f)
|
||||
if xlErr != nil {
|
||||
return nil, fmt.Errorf("invalid xls file: %s", xlErr.Error())
|
||||
}
|
||||
sheet := wb.GetSheet(0)
|
||||
if sheet == nil {
|
||||
sheet, shErr := wb.GetSheet(0)
|
||||
if shErr != nil || sheet == nil {
|
||||
return nil, fmt.Errorf("no sheet found")
|
||||
}
|
||||
// LastCol() returns 0 for many data rows in extrame/xls; derive column
|
||||
// count from the header row instead.
|
||||
numCols := 0
|
||||
headerRow := sheet.Row(0)
|
||||
for c := 0; c < headerRow.LastCol(); c++ {
|
||||
if strings.TrimSpace(headerRow.Col(c)) != "" {
|
||||
numCols = c + 1
|
||||
}
|
||||
numRows := sheet.GetNumberRows()
|
||||
if numRows < 1 {
|
||||
return nil, fmt.Errorf("empty or invalid sheet")
|
||||
}
|
||||
// 列数以表头行为准
|
||||
header, _ := sheet.GetRow(0)
|
||||
numCols := len(header.GetCols())
|
||||
if numCols == 0 {
|
||||
numCols = 20
|
||||
}
|
||||
// sheet.MaxRow 依赖 DIMENSIONS 记录,旧软件导出的 XLS 该值可能偏小。
|
||||
// 改为读到连续 5 行全空为止,最多 100000 行防止死循环。
|
||||
const maxRows = 100000
|
||||
const maxEmpty = 5
|
||||
emptyStreak := 0
|
||||
for r := 0; r < maxRows; r++ {
|
||||
row := safeXlsRow(sheet, r)
|
||||
for r := 0; r < numRows; r++ {
|
||||
row, _ := sheet.GetRow(r)
|
||||
cells := make([]string, numCols)
|
||||
isEmpty := true
|
||||
if row != nil {
|
||||
for c := 0; c < numCols; c++ {
|
||||
cells[c] = strings.TrimSpace(row.Col(c))
|
||||
if cells[c] != "" {
|
||||
isEmpty = false
|
||||
for col := 0; col < numCols; col++ {
|
||||
if cd, cErr := row.GetCol(col); cErr == nil && cd != nil {
|
||||
cells[col] = strings.TrimSpace(cd.GetString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if isEmpty {
|
||||
emptyStreak++
|
||||
if emptyStreak >= maxEmpty {
|
||||
break
|
||||
}
|
||||
// 保留空行,让调用方自行跳过
|
||||
rows = append(rows, cells)
|
||||
continue
|
||||
}
|
||||
emptyStreak = 0
|
||||
// 保留空行,让调用方自行跳过
|
||||
rows = append(rows, cells)
|
||||
}
|
||||
} else {
|
||||
@@ -1042,9 +1014,3 @@ func cell(row []string, idx int) string {
|
||||
}
|
||||
return strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
// safeXlsRow 安全读取 XLS 行,捕获 extrame/xls 在超出行数时的 panic。
|
||||
func safeXlsRow(sheet *xls.WorkSheet, r int) (row *xls.Row) {
|
||||
defer func() { recover() }() //nolint:errcheck
|
||||
return sheet.Row(r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user