package service import ( "encoding/json" "fmt" "log" "strconv" "strings" "time" "github.com/xuri/excelize/v2" "gorm.io/datatypes" "gorm.io/gorm" "psi/constant" "psi/database" "psi/models" "psi/models/request" "psi/utils" ) // GoodsImportService 商品导入服务(独立模块,不影响原有代码) type GoodsImportService struct{} // EXCEL_COLUMNS 货号批量修改工具导出的17列(前16列为原表,最后一列"货号[新]"为工具追加) var EXCEL_COLUMNS = []string{ "商品编号", "商品名称", "货号", "ISBN", "作者", "出版社", "商品定价", "商品售价", "商品分类", "本店分类", "品相", "库存", "上书时间", "商品ID", "店铺ID", "商品图片", "货号[新]", } // ParseGoodsImportExcel 解析上传的 Excel 文件为数据行 func (s *GoodsImportService) ParseGoodsImportExcel(fileBytes []byte) ([]request.GoodsImportRow, error) { f, err := excelize.OpenReader(strings.NewReader(string(fileBytes))) if err != nil { return nil, fmt.Errorf("读取Excel失败: %v", err) } defer f.Close() sheet := f.GetSheetName(0) rows, err := f.GetRows(sheet) if err != nil { return nil, fmt.Errorf("获取工作表数据失败: %v", err) } if len(rows) < 2 { return nil, fmt.Errorf("表格至少需要包含表头和数据行") } // 校验表头 header := rows[0] if len(header) < 17 { return nil, fmt.Errorf("表格列数不足,期望17列,实际%d列", len(header)) } for i, expected := range EXCEL_COLUMNS { actual := strings.TrimSpace(header[i]) if actual != expected { return nil, fmt.Errorf("第%d列表头不匹配:期望'%s',实际'%s'", i+1, expected, actual) } } // 补齐数据列数到17列 expectedColCount := len(header) if expectedColCount < 17 { expectedColCount = 17 } // getCol 安全获取指定列的值 getCol := func(rowData []string, idx int) string { if idx < len(rowData) { return strings.TrimSpace(rowData[idx]) } return "" } var result []request.GoodsImportRow for _, row := range rows[1:] { if len(row) == 0 { continue } // 补齐列 for len(row) < expectedColCount { row = append(row, "") } goodsName := strings.TrimSpace(row[1]) huohaoNew := getCol(row, 16) // 货号[新] 在第17列(索引16) if goodsName == "" && huohaoNew == "" { continue // 空行跳过 } result = append(result, request.GoodsImportRow{ GoodsNo: getCol(row, 0), GoodsName: goodsName, Huohao: getCol(row, 2), HuohaoNew: huohaoNew, ISBN: getCol(row, 3), Author: getCol(row, 4), Publisher: getCol(row, 5), PriceListing: getCol(row, 6), PriceSale: getCol(row, 7), Category: getCol(row, 8), ShopCategory: getCol(row, 9), Appearance: getCol(row, 10), Inventory: getCol(row, 11), ProductID: getCol(row, 13), ShopID: getCol(row, 14), LiveImage: getCol(row, 15), }) } return result, nil } // ImportGoodsFromExcel 从解析后的数据导入商品 func (s *GoodsImportService) ImportGoodsFromExcel(userID int64, warehouseID int64, rows []request.GoodsImportRow) (*request.GoodsImportResult, error) { databaseConn, err := database.GetTenantDB(userID) if err != nil { return nil, fmt.Errorf("获取数据库连接失败: %v", err) } result := &request.GoodsImportResult{ SuccessCount: 0, FailCount: 0, FailDetails: make([]string, 0), } if len(rows) == 0 { return result, nil } now := time.Now().Unix() // Step 1: 收集所有货号[新],批量查库位 huohaoNewSet := make(map[string]bool) for _, row := range rows { if h := strings.TrimSpace(row.HuohaoNew); h != "" { huohaoNewSet[h] = true } } codes := make([]string, 0, len(huohaoNewSet)) for code := range huohaoNewSet { codes = append(codes, code) } var existingLocations []models.Location if err := databaseConn.Where("warehouse_id = ? AND code IN ? AND is_del = ?", warehouseID, codes, 0).Find(&existingLocations).Error; err != nil { return nil, fmt.Errorf("查询库位失败: %v", err) } locationMap := make(map[string]models.Location) for _, loc := range existingLocations { locationMap[loc.Code] = loc } // Step 2: 开启事务,逐行处理 tx := databaseConn.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // Step 2.1: 创建入库单(Excel导入直接完成) receivingNo := utils.GenerateReceivingNo() batchNo := utils.GenerateExcelImportBatchNo() receivingOrder := models.ReceivingOrder{ ReceivingNo: receivingNo, PurchaseOrderID: 0, WaveTaskID: 0, WarehouseID: warehouseID, SupplierID: 0, ReceivingDate: now, Status: constant.ReceivingStatusCompleted, Operator: "", OperatorID: userID, Remark: "Excel批量导入", CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&receivingOrder).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建入库单失败: %v", err) } for _, row := range rows { huohaoNew := strings.TrimSpace(row.HuohaoNew) if huohaoNew == "" { result.AddFail(fmt.Sprintf("%s: 货号[新]为空", row.GoodsName)) continue } // 查或建库位 loc, exists := locationMap[huohaoNew] if !exists { // 再查一次(避免 map 中已有 is_del=1 的记录被忽略) var findLoc models.Location findErr := tx.Where("warehouse_id = ? AND code = ? AND is_del = ?", warehouseID, huohaoNew, 0).First(&findLoc).Error if findErr == nil { loc = findLoc locationMap[huohaoNew] = loc } else if findErr == gorm.ErrRecordNotFound { // 不存在则创建 newLoc := models.Location{ WarehouseID: warehouseID, Code: huohaoNew, Type: 1, Capacity: 255, Status: 1, Sort: 0, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if createErr := tx.Create(&newLoc).Error; createErr != nil { tx.Rollback() return nil, fmt.Errorf("创建库位失败(货号=%s): %v", huohaoNew, createErr) } loc = newLoc locationMap[huohaoNew] = loc } else { tx.Rollback() return nil, fmt.Errorf("查询库位异常(货号=%s): %v", huohaoNew, findErr) } } // Step 3: 创建商品 product := s.buildProduct(row, now) if createErr := tx.Create(&product).Error; createErr != nil { tx.Rollback() return nil, fmt.Errorf("创建商品失败(%s): %v", row.GoodsName, createErr) } // Step 3.1: 写入任务库 t_shop_goods_published(先删后插,库存为0时仅删不插) quantity := s.parseIntOrZero(row.Inventory) // 先删除旧记录 if err := database.TaskDB.Exec( `DELETE FROM t_shop_goods_published WHERE erp_shop_id = ? AND product_id = ? AND trilateral_id = ? AND del_flag = 0`, row.ShopID, product.ID, row.ProductID, ).Error; err != nil { log.Printf("删除t_shop_goods_published旧记录失败(%s): %v", row.GoodsName, err) } // 按库存数量插入(库存为0则跳过) for i := int64(0); i < quantity; i++ { if err := database.TaskDB.Exec( `INSERT INTO t_shop_goods_published (erp_shop_id, product_id, trilateral_id, user_id, is_distribution, del_flag, create_time) VALUES (?, ?, ?, ?, ?, ?, ?)`, row.ShopID, product.ID, row.ProductID, userID, 0, 0, now, ).Error; err != nil { log.Printf("写入t_shop_goods_published失败(%s, 第%d条): %v", row.GoodsName, i+1, err) } } // Step 4: 创建库存(quantity 已在 Step 3.1 计算过) if quantity <= 0 { quantity = 1 // 库存≥1条记录 } inventory := models.Inventory{ WarehouseID: warehouseID, ProductID: product.ID, BatchNo: batchNo, ProductionDate: 0, ExpiryDate: 0, Quantity: quantity, LockedQuantity: 0, AvailableQuantity: quantity, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if createErr := tx.Create(&inventory).Error; createErr != nil { tx.Rollback() return nil, fmt.Errorf("创建库存汇总失败(%s): %v", row.GoodsName, createErr) } inventoryDetail := models.InventoryDetail{ WarehouseID: warehouseID, LocationID: loc.ID, ProductID: product.ID, BatchNo: batchNo, ProductionDate: 0, ExpiryDate: 0, Quantity: quantity, LockedQuantity: 0, AvailableQuantity: quantity, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if createErr := tx.Create(&inventoryDetail).Error; createErr != nil { tx.Rollback() return nil, fmt.Errorf("创建库存明细失败(%s): %v", row.GoodsName, createErr) } // Step 4.1: 创建入库单明细 receivingOrderItem := models.ReceivingOrderItem{ ReceivingOrderID: receivingOrder.ID, ProductID: product.ID, LocationID: loc.ID, BatchNo: batchNo, ProductionDate: 0, ExpiryDate: 0, Quantity: quantity, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if err := tx.Create(&receivingOrderItem).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建入库单明细失败(%s): %v", row.GoodsName, err) } // Step 4.2: 创建库存变动日志 inventoryLog := models.InventoryLog{ WarehouseID: warehouseID, LocationID: loc.ID, ProductID: product.ID, BatchNo: batchNo, ChangeType: constant.InventoryChangeInbound, ChangeQuantity: quantity, BeforeQuantity: 0, AfterQuantity: quantity, RelatedOrderType: constant.OrderTypeReceiving, RelatedOrderNo: receivingOrder.ReceivingNo, Operator: "", OperatorID: userID, Remark: "Excel批量导入", CreatedAt: now, IsDel: 0, } if err := tx.Create(&inventoryLog).Error; err != nil { tx.Rollback() return nil, fmt.Errorf("创建库存变动日志失败(%s): %v", row.GoodsName, err) } result.SuccessCount++ } // Step 5: 更新统计表入库次数 var statist models.Statist err = tx.Where("create_by = ?", userID).First(&statist).Error if err == gorm.ErrRecordNotFound { statist = models.Statist{ CreateBy: userID, ReceivingNum: int64(result.SuccessCount), StatDate: now, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if createErr := tx.Create(&statist).Error; createErr != nil { log.Printf("创建统计记录失败: %v", createErr) } } else if err == nil { if updateErr := tx.Model(&models.Statist{}). Where("create_by = ?", userID). Update("receiving_num", gorm.Expr("receiving_num + ?", result.SuccessCount)).Error; updateErr != nil { log.Printf("更新统计记录失败: %v", updateErr) } } // 提交事务 if commitErr := tx.Commit().Error; commitErr != nil { return nil, fmt.Errorf("提交事务失败: %v", commitErr) } result.Message = buildResultMessage(result) return result, nil } // buildProduct 根据 Excel 行数据构造 Product 模型 func (s *GoodsImportService) buildProduct(row request.GoodsImportRow, now int64) models.Product { var liveImage datatypes.JSON liveURL := strings.TrimSpace(row.LiveImage) if liveURL != "" { // 工具导出后图片已是纯URL,包装为JSON数组 arr := []string{liveURL} b, _ := json.Marshal(arr) liveImage = b } else { liveImage = datatypes.JSON("[]") } // 品相:尝试转为数字,不支持则留0 appearance := s.parseAppearance(row.Appearance) // 售价:元转分(如果包含小数点则为元,直接整数则为分) salePrice := s.parsePrice(row.PriceSale) return models.Product{ CategoryID: 1, StandardProductID: 1, Name: row.GoodsName, Appearance: appearance, Barcode: row.ISBN, Price: 0, SalePrice: salePrice, Cost: 0, LiveImage: liveImage, IsBatchManaged: 0, IsShelfLifeManaged: 0, Status: 1, CreatedAt: now, UpdatedAt: now, IsDel: 0, } } // parseAppearance 解析品相字符串为数字 func (s *GoodsImportService) parseAppearance(appearance string) int64 { val, err := strconv.ParseInt(appearance, 10, 64) if err != nil { // 中文品相映射 switch strings.TrimSpace(appearance) { case "全新", "十品": return 100 case "九五品", "九五": return 95 case "九品": return 90 case "八五品", "八五": return 85 case "八品": return 80 case "七五品", "七五": return 75 case "七品": return 70 default: return int64(s.parsePrice(appearance)) } } return val } // parsePrice 解析价格(元→分,表格中价格统一为元,统一乘以100转换为分) func (s *GoodsImportService) parsePrice(price string) int64 { p := strings.TrimSpace(price) if p == "" { return 0 } if f, err := strconv.ParseFloat(p, 64); err == nil { return int64(f * 100) } return 0 } // parseIntOrZero 安全解析整数 func (s *GoodsImportService) parseIntOrZero(val string) int64 { if v, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil { return v } return 0 } func buildResultMessage(result *request.GoodsImportResult) string { msg := fmt.Sprintf("导入完成:成功%d个,失败%d个", result.SuccessCount, result.FailCount) if result.FailCount > 0 { msg += fmt.Sprintf(",失败详情:%s", strings.Join(result.FailDetails, "; ")) } return msg }