feat: 库存 XLS 导入功能(后端接口 + 前端导入按钮)
This commit is contained in:
@@ -468,6 +468,134 @@ func (h *ImportHandler) ImportStockOut(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"order_no": orderNo, "items": len(items)})
|
||||
}
|
||||
|
||||
// ImportInventory POST /api/v1/import/inventory
|
||||
// 列顺序:商品编号,商品名称,系列,规格,单位,库存数量,单价,金额,生产日期,批次,分类,所在仓库,入库日期,供应商,上次盘点,备注
|
||||
func (h *ImportHandler) ImportInventory(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
|
||||
rows, err := parseUploadedExcel(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
type result struct {
|
||||
imported int
|
||||
skipped int
|
||||
errors []string
|
||||
}
|
||||
var res result
|
||||
|
||||
// 仓库缓存,避免重复查询
|
||||
warehouseCache := map[string]uint64{}
|
||||
findOrCreateWarehouse := func(name string) (uint64, error) {
|
||||
if name == "" {
|
||||
name = "默认仓库"
|
||||
}
|
||||
if id, ok := warehouseCache[name]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var wh model.Warehouse
|
||||
if h.db.Where("shop_id = ? AND name = ? AND deleted_at IS NULL", shopID, name).First(&wh).Error != nil {
|
||||
wh = model.Warehouse{
|
||||
TenantBase: model.TenantBase{ShopID: shopID},
|
||||
Name: name,
|
||||
}
|
||||
if err := h.db.Create(&wh).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
warehouseCache[name] = wh.ID
|
||||
return wh.ID, nil
|
||||
}
|
||||
|
||||
for i, row := range rows[1:] {
|
||||
productName := cell(row, 1)
|
||||
if productName == "" {
|
||||
res.skipped++
|
||||
continue
|
||||
}
|
||||
series := cell(row, 2)
|
||||
spec := cell(row, 3)
|
||||
unit := cell(row, 4)
|
||||
qtyStr := cell(row, 5)
|
||||
priceStr := cell(row, 6)
|
||||
warehouseName := cell(row, 11)
|
||||
|
||||
qty, _ := strconv.ParseFloat(qtyStr, 64)
|
||||
price, _ := strconv.ParseFloat(priceStr, 64)
|
||||
|
||||
// 找或创建商品
|
||||
prod, err := findOrCreateProductFn(h.db, shopID, productName, series, spec)
|
||||
if err != nil {
|
||||
res.errors = append(res.errors, fmt.Sprintf("行%d: 商品创建失败: %s", i+2, err.Error()))
|
||||
continue
|
||||
}
|
||||
if unit != "" && prod.Unit == "" {
|
||||
h.db.Model(&prod).Update("unit", unit)
|
||||
}
|
||||
if price > 0 && prod.PurchasePrice == 0 {
|
||||
h.db.Model(&prod).Update("purchase_price", price)
|
||||
}
|
||||
|
||||
// 找或创建仓库
|
||||
whID, err := findOrCreateWarehouse(warehouseName)
|
||||
if err != nil {
|
||||
res.errors = append(res.errors, fmt.Sprintf("行%d: 仓库创建失败: %s", i+2, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// upsert 库存数量
|
||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||
var inv model.Inventory
|
||||
isNew := false
|
||||
if tx.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?",
|
||||
shopID, whID, prod.ID).First(&inv).Error != nil {
|
||||
inv = model.Inventory{
|
||||
ShopID: shopID,
|
||||
WarehouseID: whID,
|
||||
ProductID: prod.ID,
|
||||
}
|
||||
isNew = true
|
||||
}
|
||||
qtyBefore := inv.Quantity
|
||||
inv.Quantity = qty
|
||||
if isNew {
|
||||
if err := tx.Create(&inv).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Save(&inv).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 写流水
|
||||
log := model.InventoryLog{
|
||||
ShopID: shopID,
|
||||
WarehouseID: whID,
|
||||
ProductID: prod.ID,
|
||||
Direction: "in",
|
||||
Quantity: qty,
|
||||
QtyBefore: qtyBefore,
|
||||
QtyAfter: qty,
|
||||
RefType: "import",
|
||||
}
|
||||
return tx.Create(&log).Error
|
||||
})
|
||||
if err != nil {
|
||||
res.errors = append(res.errors, fmt.Sprintf("行%d: 库存写入失败: %s", i+2, err.Error()))
|
||||
continue
|
||||
}
|
||||
res.imported++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"imported": res.imported,
|
||||
"skipped": res.skipped,
|
||||
"errors": res.errors,
|
||||
})
|
||||
}
|
||||
|
||||
// ── 内部辅助函数 ─────────────────────────────────────────────
|
||||
|
||||
func parseUploadedExcel(c *gin.Context) ([][]string, error) {
|
||||
|
||||
Reference in New Issue
Block a user