fix(import): 修复导入 504 超时 + 添加上传进度显示
Deploy / deploy (push) Successful in 1m15s

nginx:
- 新增 /api/v1/import/ 专属 location,proxy_read_timeout 延长至 300s

后端:
- ImportInventory 预加载商品/仓库/库存(3 次 bulk query 替代 N+1)
- 批量写 inventory_log(CreateInBatches 替代逐行 Create)

前端:
- settings 导入行:显示"上传 XX%"进度条和"导入数据(Ns)"计时
- Dio 超时设置为 send=120s / receive=300s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-05-24 15:12:59 +08:00
parent 5725e84971
commit d5dc499c6b
4 changed files with 401 additions and 98 deletions
+88 -64
View File
@@ -519,23 +519,47 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
}
var res importResult
// 仓库缓存,只查找不创建
warehouseCache := map[string]*uint64{}
// 预加载仓库(1 次查询)
var warehouses []model.Warehouse
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).Find(&warehouses)
warehouseByName := make(map[string]uint64, len(warehouses))
for _, wh := range warehouses {
warehouseByName[wh.Name] = wh.ID
}
findWarehouse := func(name string) *uint64 {
if name == "" {
return nil
}
if idPtr, ok := warehouseCache[name]; ok {
return idPtr
if id, ok := warehouseByName[name]; ok {
return &id
}
var wh model.Warehouse
if h.db.Where("shop_id = ? AND name = ? AND deleted_at IS NULL", shopID, name).First(&wh).Error != nil {
warehouseCache[name] = nil
return nil
return nil
}
// 预加载商品(1 次查询)
var allProducts []model.Product
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).Find(&allProducts)
productByCode := make(map[string]*model.Product, len(allProducts))
productByNSS := make(map[string]*model.Product, len(allProducts))
for i := range allProducts {
p := &allProducts[i]
if p.Code != "" {
productByCode[p.Code] = p
}
id := wh.ID
warehouseCache[name] = &id
return &id
productByNSS[p.Name+"|"+p.Series+"|"+p.Spec] = p
}
// 预加载已有导入库存(1 次查询)
var allInvs []model.Inventory
h.db.Where("shop_id = ? AND stock_in_item_id IS NULL AND deleted_at IS NULL", shopID).Find(&allInvs)
invByKey := make(map[string]*model.Inventory, len(allInvs))
for i := range allInvs {
inv := &allInvs[i]
whID := uint64(0)
if inv.WarehouseID != nil {
whID = *inv.WarehouseID
}
invByKey[fmt.Sprintf("%s|%d", inv.ProductCode, whID)] = inv
}
// Dynamic column detection from header row
@@ -561,6 +585,8 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
}
}
var logsToCreate []model.InventoryLog
for i, row := range rows[1:] {
productName := cell(row, 1)
if productName == "" {
@@ -586,20 +612,30 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
}
price, _ := strconv.ParseFloat(priceStr, 64)
// 只查找商品,不强制创建
var prod model.Product
if h.db.Where("shop_id = ? AND deleted_at IS NULL AND (code = ? OR (name = ? AND series = ? AND spec = ?))",
shopID, productCode, productName, series, spec).First(&prod).Error != nil {
// 若找不到则创建
// 从缓存查商品,找不到才创建
var prod *model.Product
if productCode != "" {
prod = productByCode[productCode]
}
if prod == nil {
prod = productByNSS[productName+"|"+series+"|"+spec]
}
if prod == nil {
newProd, createErr := findOrCreateProductFn(h.db, shopID, productCode, productName, series, spec)
if createErr != nil {
res.errors = append(res.errors, fmt.Sprintf("行%d: 商品创建失败: %s", i+2, createErr.Error()))
continue
}
prod = newProd
allProducts = append(allProducts, newProd)
prod = &allProducts[len(allProducts)-1]
if prod.Code != "" {
productByCode[prod.Code] = prod
}
productByNSS[prod.Name+"|"+prod.Series+"|"+prod.Spec] = prod
}
if unit != "" && prod.Unit == "" {
h.db.Model(&prod).Update("unit", unit)
h.db.Model(prod).Update("unit", unit)
prod.Unit = unit
}
// 解析生产日期
@@ -609,7 +645,6 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
productionDate = &d
}
// 查找仓库(只查,不创建)
whIDPtr := findWarehouse(warehouseName)
var unitPricePtr *float64
@@ -617,30 +652,25 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
unitPricePtr = &price
}
productIDCopy := prod.ID
// Upsert:按商品编号 + 仓库查找已有导入记录,存在则更新,不存在则新建
var existing model.Inventory
q := h.db.Where("shop_id = ? AND product_code = ? AND stock_in_item_id IS NULL AND deleted_at IS NULL",
shopID, prod.Code)
// 从缓存查库存记录
whIDVal := uint64(0)
if whIDPtr != nil {
q = q.Where("warehouse_id = ?", *whIDPtr)
} else {
q = q.Where("warehouse_id IS NULL")
whIDVal = *whIDPtr
}
found := q.First(&existing).Error == nil
invKey := fmt.Sprintf("%s|%d", prod.Code, whIDVal)
existing := invByKey[invKey]
if found {
if existing != nil {
updates := map[string]interface{}{
"quantity": qty,
"product_name": prod.Name,
"series": prod.Series,
"spec": prod.Spec,
"unit": prod.Unit,
"warehouse_name": warehouseName,
"supplier_name": supplierName,
"remark": remark,
"deleted_at": nil,
"quantity": qty,
"product_name": prod.Name,
"series": prod.Series,
"spec": prod.Spec,
"unit": prod.Unit,
"warehouse_name": warehouseName,
"supplier_name": supplierName,
"remark": remark,
"deleted_at": nil,
}
if unitPricePtr != nil {
updates["unit_price"] = *unitPricePtr
@@ -651,11 +681,18 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
if batchNo != "" {
updates["batch_no"] = batchNo
}
if err := h.db.Model(&existing).Updates(updates).Error; err != nil {
if err := h.db.Model(existing).Updates(updates).Error; err != nil {
res.errors = append(res.errors, fmt.Sprintf("行%d: 库存更新失败: %s", i+2, err.Error()))
continue
}
logsToCreate = append(logsToCreate, model.InventoryLog{
ShopID: shopID, WarehouseID: whIDVal, ProductID: prod.ID,
Direction: "in", Quantity: qty, QtyBefore: existing.Quantity, QtyAfter: qty,
RefType: "import", RefID: 0,
})
res.updated++
} else {
productIDCopy := prod.ID
inv := model.Inventory{
ShopID: shopID,
WarehouseID: whIDPtr,
@@ -678,32 +715,19 @@ func (h *ImportHandler) ImportInventory(c *gin.Context) {
res.errors = append(res.errors, fmt.Sprintf("行%d: 库存写入失败: %s", i+2, err.Error()))
continue
}
}
// 写流水
warehouseID := uint64(0)
if whIDPtr != nil {
warehouseID = *whIDPtr
}
qtyBefore := 0.0
if found {
qtyBefore = existing.Quantity
res.updated++
} else {
invByKey[invKey] = &inv
logsToCreate = append(logsToCreate, model.InventoryLog{
ShopID: shopID, WarehouseID: whIDVal, ProductID: prod.ID,
Direction: "in", Quantity: qty, QtyBefore: 0, QtyAfter: qty,
RefType: "import", RefID: 0,
})
res.imported++
}
log := model.InventoryLog{
ShopID: shopID,
WarehouseID: warehouseID,
ProductID: prod.ID,
Direction: "in",
Quantity: qty,
QtyBefore: qtyBefore,
QtyAfter: qty,
RefType: "import",
RefID: 0,
}
h.db.Create(&log)
}
// 批量写流水
if len(logsToCreate) > 0 {
h.db.CreateInBatches(&logsToCreate, 100)
}
c.JSON(http.StatusOK, gin.H{