diff --git a/app/admin/apis/inventorymanage/Inventory.go b/app/admin/apis/inventorymanage/Inventory.go index 7074f06..2979d61 100644 --- a/app/admin/apis/inventorymanage/Inventory.go +++ b/app/admin/apis/inventorymanage/Inventory.go @@ -122,7 +122,7 @@ func GetInventoryDetail(c *gin.Context) { // @Accept json // @Param request body models.NewErpStockCommodityListReq true "查询库存详情模型" // @Success 200 {object} models.ErpStockCommodityListResp -// @Router /api/v1/inventory/detail [post] +// @Router /api/v1/inventory/detail_new [post] func GetInventoryDetailNew(c *gin.Context) { req := &models.NewErpStockCommodityListReq{} if err := c.ShouldBindJSON(&req); err != nil { diff --git a/app/admin/models/commodity.go b/app/admin/models/commodity.go index 91ca573..fe4e460 100644 --- a/app/admin/models/commodity.go +++ b/app/admin/models/commodity.go @@ -310,7 +310,7 @@ type ErpCommodityListReq struct { IMEI string `json:"imei"` // 串码 ErpBarcode string `json:"erp_barcode"` // 商品条码 ErpSupplierId uint32 `json:"erp_supplier_id"` // 供应商id - PurchaseType uint32 `json:"purchase_type"` // 0-全部,1-正常采购,2-停止采购 + PurchaseType uint32 `json:"purchase_type"` // 1-正常采购,2-停止采购 PageIndex int `json:"pageIndex"` // 页码 PageSize int `json:"pageSize"` // 每页展示数据条数 IsExport uint32 `json:"is_export"` // 1-导出 diff --git a/app/admin/models/file.go b/app/admin/models/file.go index c62adc8..0bfa512 100644 --- a/app/admin/models/file.go +++ b/app/admin/models/file.go @@ -15,6 +15,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "unicode" "unicode/utf8" @@ -612,6 +613,38 @@ func isInputTimeBeforeOrEqualNow(inputTimeString string) (bool, error) { return !isBeforeOrEqual, nil } +// 校验第5列门店名称是否相同 +func hasDuplicateStore(sheetCols [][]string) (string, bool) { + storeMap := make(map[string]struct{}) + var storeName string + + for row := 1; row < len(sheetCols); row++ { + // 获取当前行的门店名称 + currentStore := sheetCols[4][row] // 第5列门店名称 + + if currentStore == "" { + continue // 如果门店名称为空,跳过该行 + } + + // 如果这是第一次遇到门店名称,记录下来 + if storeName == "" { + storeName = currentStore + } + + // 检查当前门店名称是否与之前的名称一致 + if currentStore != storeName { + // 找到不一致的门店名称,返回不一致的门店名称和标记为true + return currentStore, true + } + + // 存储门店名称 + storeMap[currentStore] = struct{}{} + } + + // 如果没有发现不一致的门店名称,返回空字符串和false + return "", false +} + // 校验串码是否相同,有则false func hasDuplicateIMEI(sheetCols []string) (string, bool) { nameMap := make(map[string]struct{}) @@ -1360,139 +1393,346 @@ func IsExistingCommodity(commodityId uint32) bool { // 商品名称和串码是否对应 // 串码商品数量只能为1 // 是否有库存 +//func checkOrderExcel(sheetCols [][]string) error { +// if len(sheetCols) != 8 { +// return errors.New("模版错误,请检查文件") +// } +// +// maxLength := findMaxLength(sheetCols) +// nLow, nMax := determineLowAndMax(sheetCols) +// +// if nMax < maxLength { +// return errors.New("第" + strconv.Itoa(nMax+1) + "行商品名称和商品编号不能同时为空") +// } +// +// // 判断是否有重复串码 +// if duplicateName, nFlag := hasDuplicateIMEI(sheetCols[2]); nFlag { +// return fmt.Errorf("商品串码不允许重复,请检查:[%v]", duplicateName) +// } +// +// //先遍历第1列 +// for i := 1; i < nLow; i++ { +// if sheetCols[0][i] == "" && sheetCols[1][i] == "" { // 商品名称和编号不能都为空 +// return errors.New("第" + strconv.Itoa(i+1) + "行商品名称和商品编号不能同时为空") +// } +// } +// +// for i := 1; i < nMax; i++ { +// if i < len(sheetCols[1]) { +// if !IsExistingProduct(sheetCols[1][i]) { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") +// } +// } +// +// // 商品编号必须为纯数字 +// if i < len(sheetCols[0]) { +// if sheetCols[0][i] != "" { +// if _, err := strconv.Atoi(sheetCols[0][i]); err != nil { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品编号必须为纯数字") +// } +// } +// +// if !isExistingProductCode(sheetCols[0][i]) { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品编号不存在") +// } +// } +// +// // 所属门店不能为空 +// if i < len(sheetCols[4]) { +// if sheetCols[4][i] == "" { +// return errors.New("第" + strconv.Itoa(i+1) + "行所属门店不能为空") +// } +// if !isExistingStore(sheetCols[4][i]) { +// return errors.New("第" + strconv.Itoa(i+1) + "行门店不存在") +// } +// } +// +// // 数量必须为正整数 +// if i < len(sheetCols[3]) { +// if quantity, err := strconv.Atoi(sheetCols[3][i]); err != nil || quantity < 1 { +// return errors.New("第" + strconv.Itoa(i+1) + "行数量必须是大于等于1的整数") +// } +// } +// +// // 串码商品数量只能为1 +// var nCount int +// if i < len(sheetCols[3]) { +// if i < len(sheetCols[3]) { +// nCount, _ = strconv.Atoi(sheetCols[3][i]) +// } +// +// // 串码类商品数量只能为1 +// if sheetCols[2][i] != "" && nCount != 1 { +// return errors.New("第" + strconv.Itoa(i+1) + "行串码类商品数量只能为1") +// } +// } +// +// // 判断商品是否赠送 +// if i < len(sheetCols[6]) { +// if sheetCols[6][i] != "是" && sheetCols[6][i] != "" { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品是否赠送填写有误") +// } +// } +// +// // 查询商品是否有库存 +// if sheetCols[2][i] != "" { // 串码商品 +// var imeiStockCommodity ErpStockCommodity +// err := orm.Eloquent.Table("erp_stock_commodity").Where("imei = ? and state = ?", +// sheetCols[2][i], InStock). +// Find(&imeiStockCommodity).Error +// if err != nil { +// return err +// } +// if imeiStockCommodity.ID == 0 { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") +// } +// // 门店对比 +// if imeiStockCommodity.StoreName != sheetCols[4][i] { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品" + +// "[" + imeiStockCommodity.ErpCommodityName + "]非所选门店库存,请检查") +// } +// // 零售价对比 +// var floatVal float64 +// if i < len(sheetCols[5]) { +// if sheetCols[5][i] != "" { +// floatVal, err = strconv.ParseFloat(sheetCols[5][i], 64) +// if err != nil { +// return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误") +// } +// strMin := strconv.FormatFloat(imeiStockCommodity.MinRetailPrice, 'f', 2, 64) +// if floatVal < imeiStockCommodity.MinRetailPrice { +// return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误,不能低于最低零售价[" + strMin + "]") +// } +// } +// } +// } else { // 非串码商品 +// var count int64 +// var err error +// if sheetCols[0][i] != "" { // 商品编号不为空 +// err = orm.Eloquent.Table("erp_stock_commodity"). +// Where("commodity_serial_number = ? and store_name = ? and state = ? and imei_type = ?", +// sheetCols[0][i], sheetCols[4][i], InStock, NoIMEICommodity). +// Count(&count).Error +// } else { +// err = orm.Eloquent.Table("erp_stock_commodity"). +// Where("erp_commodity_name = ? and store_name = ? and state = ? and imei_type = ?", +// sheetCols[1][i], sheetCols[4][i], InStock, NoIMEICommodity). +// Count(&count).Error +// } +// if err != nil { +// return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") +// } +// +// if count < int64(nCount) { +// // 获取商品名称 +// return errors.New("第" + strconv.Itoa(i+1) + "行商品" + "[" + sheetCols[1][i] + "]库存不足") +// } +// } +// } +// +// return nil +//} + func checkOrderExcel(sheetCols [][]string) error { + // 校验列数是否正确 if len(sheetCols) != 8 { return errors.New("模版错误,请检查文件") } - maxLength := findMaxLength(sheetCols) - nLow, nMax := determineLowAndMax(sheetCols) - - if nMax < maxLength { - return errors.New("第" + strconv.Itoa(nMax+1) + "行商品名称和商品编号不能同时为空") + // 获取最大行数 + nMax := findMaxLength(sheetCols) + if nMax == 0 { + return errors.New("模版内容为空") } - // 判断是否有重复串码 + // 校验商品串码是否重复 if duplicateName, nFlag := hasDuplicateIMEI(sheetCols[2]); nFlag { return fmt.Errorf("商品串码不允许重复,请检查:[%v]", duplicateName) } - //先遍历第1列 - for i := 1; i < nLow; i++ { - if sheetCols[0][i] == "" && sheetCols[1][i] == "" { // 商品名称和编号不能都为空 - return errors.New("第" + strconv.Itoa(i+1) + "行商品名称和商品编号不能同时为空") + // 判断门店名称是否有不同 + if duplicateName, nFlag := hasDuplicateStore(sheetCols); nFlag { + return fmt.Errorf("门店名称不同,请检查:[%v]", duplicateName) + } + + // 缓存商品编号、商品名称和门店查询结果 + productCodeCache := make(map[string]bool) + productNameCache := make(map[string]bool) + storeCache := make(map[string]bool) + + // 预加载所有商品编号、商品名称和门店 + loadAllProductsIntoCache(productCodeCache, productNameCache) + loadAllStoresIntoCache(storeCache) + + // 通道用于并发校验 + var wg sync.WaitGroup + errCh := make(chan error, 10) + + // 限制最大并发数量 + maxConcurrency := 10 + sem := make(chan struct{}, maxConcurrency) + + for i := 1; i < nMax; i++ { + // 校验行是否越界 + if !isRowValid(sheetCols, i) { + continue + } + + // 等待一个空位 + sem <- struct{}{} + wg.Add(1) + go func(row int) { + defer wg.Done() + defer func() { <-sem }() // 完成任务后释放空位 + + if err := validateRow(sheetCols, row, productCodeCache, productNameCache, storeCache); err != nil { + errCh <- fmt.Errorf("第 %d 行: %v", row+1, err) + } + }(i) + } + + wg.Wait() + close(errCh) + + // 返回第一个错误 + for err := range errCh { + return err + } + + validateStock(sheetCols) + + return nil +} + +// 检查第 row 行是否所有列都有有效数据 +func isRowValid(sheetCols [][]string, row int) bool { + for _, col := range sheetCols { + if row >= len(col) { // 行数超出当前列的长度 + return false + } + } + return true +} + +// 批量加载商品信息到缓存 +func loadAllProductsIntoCache(productCodeCache, productNameCache map[string]bool) { + var products []ErpCommodity + orm.Eloquent.Debug().Find(&products) + for _, product := range products { + productCodeCache[product.SerialNumber] = true + productNameCache[product.Name] = true + } +} + +// 批量加载门店信息到缓存 +func loadAllStoresIntoCache(storeCache map[string]bool) { + var stores []Store + orm.Eloquent.Debug().Find(&stores) + for _, store := range stores { + storeCache[store.Name] = true + } +} + +// 校验单行数据 +func validateRow(sheetCols [][]string, row int, productCodeCache, productNameCache, storeCache map[string]bool) error { + fmt.Println("row is:", row) + // 校验商品名称和商品编号 + if sheetCols[0][row] == "" && sheetCols[1][row] == "" { + return errors.New("商品名称和商品编号不能同时为空") + } + + // 校验商品编号是否存在 + if sheetCols[0][row] != "" { + if _, err := strconv.Atoi(sheetCols[0][row]); err != nil { + return errors.New("商品编号必须为纯数字") + } + if !productCodeCache[sheetCols[0][row]] { + return errors.New("商品编号不存在") } } - for i := 1; i < nMax; i++ { - if i < len(sheetCols[1]) { - if !IsExistingProduct(sheetCols[1][i]) { - return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + // 校验商品名称是否存在 + if sheetCols[1][row] != "" && !productNameCache[sheetCols[1][row]] { + return errors.New("商品名称不存在") + } + + // 校验所属门店是否存在 + if sheetCols[4][row] == "" || !storeCache[sheetCols[4][row]] { + return errors.New("所属门店不能为空或不存在") + } + + // 校验数量是否为正整数 + if quantity, err := strconv.Atoi(sheetCols[3][row]); err != nil || quantity < 1 { + return errors.New("数量必须是大于等于1的整数") + } + + // 校验串码商品数量只能为1 + if sheetCols[2][row] != "" && sheetCols[3][row] != "1" { + return errors.New("串码类商品数量只能为1") + } + + // 校验商品是否赠送 + if sheetCols[6][row] != "" && sheetCols[6][row] != "是" { + return errors.New("商品是否赠送填写有误") + } + + return nil +} + +// validateStock 校验库存信息 +func validateStock(sheetCols [][]string) error { + // 预加载所有库存数据,按门店 store_id 过滤 + var imeiStockCommodities []ErpStockCommodity + err := orm.Eloquent.Table("erp_stock_commodity"). + Where("state = ? AND store_name = ?", InStock, sheetCols[4][1]). + Find(&imeiStockCommodities).Error + if err != nil { + return fmt.Errorf("加载库存数据失败: %v", err) + } + + // 构建库存缓存:按 imei 和非串码商品分组 + imeiStockMap := make(map[string]ErpStockCommodity) // 串码商品缓存 + nonIMEIStockMap := make(map[string]map[string]int64) // 非串码商品库存,按 store -> commodity 分组 + for _, stock := range imeiStockCommodities { + if stock.IMEI != "" { + imeiStockMap[stock.IMEI] = stock + } else { + if _, exists := nonIMEIStockMap[stock.StoreName]; !exists { + nonIMEIStockMap[stock.StoreName] = make(map[string]int64) } + key := stock.CommoditySerialNumber + if key == "" { + key = stock.ErpCommodityName + } + nonIMEIStockMap[stock.StoreName][key]++ } + } - // 商品编号必须为纯数字 - if i < len(sheetCols[0]) { - if sheetCols[0][i] != "" { - if _, err := strconv.Atoi(sheetCols[0][i]); err != nil { - return errors.New("第" + strconv.Itoa(i+1) + "行商品编号必须为纯数字") - } + // 校验库存信息 + for row := 1; row < len(sheetCols[0]); row++ { + if sheetCols[2][row] != "" { // 串码商品 + stock, exists := imeiStockMap[sheetCols[2][row]] + if !exists { + return fmt.Errorf("第 %d 行: 串码商品不存在或无库存", row+1) } - - if !isExistingProductCode(sheetCols[0][i]) { - return errors.New("第" + strconv.Itoa(i+1) + "行商品编号不存在") + if stock.StoreName != sheetCols[4][row] { + return fmt.Errorf("第 %d 行: 商品[%s]非所选门店库存,请检查", row+1, stock.ErpCommodityName) } - } - - // 所属门店不能为空 - if i < len(sheetCols[4]) { - if sheetCols[4][i] == "" { - return errors.New("第" + strconv.Itoa(i+1) + "行所属门店不能为空") - } - if !isExistingStore(sheetCols[4][i]) { - return errors.New("第" + strconv.Itoa(i+1) + "行门店不存在") - } - } - - // 数量必须为正整数 - if i < len(sheetCols[3]) { - if quantity, err := strconv.Atoi(sheetCols[3][i]); err != nil || quantity < 1 { - return errors.New("第" + strconv.Itoa(i+1) + "行数量必须是大于等于1的整数") - } - } - - // 串码商品数量只能为1 - var nCount int - if i < len(sheetCols[3]) { - if i < len(sheetCols[3]) { - nCount, _ = strconv.Atoi(sheetCols[3][i]) - } - - // 串码类商品数量只能为1 - if sheetCols[2][i] != "" && nCount != 1 { - return errors.New("第" + strconv.Itoa(i+1) + "行串码类商品数量只能为1") - } - } - - // 判断商品是否赠送 - if i < len(sheetCols[6]) { - if sheetCols[6][i] != "是" && sheetCols[6][i] != "" { - return errors.New("第" + strconv.Itoa(i+1) + "行商品是否赠送填写有误") - } - } - - // 查询商品是否有库存 - if sheetCols[2][i] != "" { // 串码商品 - var imeiStockCommodity ErpStockCommodity - err := orm.Eloquent.Table("erp_stock_commodity").Where("imei = ? and state = ?", - sheetCols[2][i], InStock). - Find(&imeiStockCommodity).Error - if err != nil { - return err - } - if imeiStockCommodity.ID == 0 { - return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") - } - // 门店对比 - if imeiStockCommodity.StoreName != sheetCols[4][i] { - return errors.New("第" + strconv.Itoa(i+1) + "行商品" + - "[" + imeiStockCommodity.ErpCommodityName + "]非所选门店库存,请检查") - } - // 零售价对比 - var floatVal float64 - if i < len(sheetCols[5]) { - if sheetCols[5][i] != "" { - floatVal, err = strconv.ParseFloat(sheetCols[5][i], 64) - if err != nil { - return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误") - } - strMin := strconv.FormatFloat(imeiStockCommodity.MinRetailPrice, 'f', 2, 64) - if floatVal < imeiStockCommodity.MinRetailPrice { - return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误,不能低于最低零售价[" + strMin + "]") - } + if sheetCols[5][row] != "" { + price, err := strconv.ParseFloat(sheetCols[5][row], 64) + if err != nil || price < stock.MinRetailPrice { + return fmt.Errorf("第 %d 行: 零售价有误,不能低于最低零售价[%.2f]", row+1, stock.MinRetailPrice) } } } else { // 非串码商品 - var count int64 - var err error - if sheetCols[0][i] != "" { // 商品编号不为空 - err = orm.Eloquent.Table("erp_stock_commodity"). - Where("commodity_serial_number = ? and store_name = ? and state = ? and imei_type = ?", - sheetCols[0][i], sheetCols[4][i], InStock, NoIMEICommodity). - Count(&count).Error - } else { - err = orm.Eloquent.Table("erp_stock_commodity"). - Where("erp_commodity_name = ? and store_name = ? and state = ? and imei_type = ?", - sheetCols[1][i], sheetCols[4][i], InStock, NoIMEICommodity). - Count(&count).Error + storeName := sheetCols[4][row] + commodityKey := sheetCols[0][row] + if commodityKey == "" { + commodityKey = sheetCols[1][row] } - if err != nil { - return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") - } - - if count < int64(nCount) { - // 获取商品名称 - return errors.New("第" + strconv.Itoa(i+1) + "行商品" + "[" + sheetCols[1][i] + "]库存不足") + storeStock, storeExists := nonIMEIStockMap[storeName] + if !storeExists || storeStock[commodityKey] < 1 { + return fmt.Errorf("第 %d 行: 商品[%s]库存不足", row+1, sheetCols[1][row]) } } } @@ -1500,6 +1740,37 @@ func checkOrderExcel(sheetCols [][]string) error { return nil } +//// 校验库存信息 +//func validateStock(sheetCols [][]string, row int) error { +// if sheetCols[2][row] != "" { // 串码商品 +// var imeiStockCommodity ErpStockCommodity +// err := orm.Eloquent.Table("erp_stock_commodity").Where("imei = ? AND state = ?", +// sheetCols[2][row], InStock).Find(&imeiStockCommodity).Error +// if err != nil || imeiStockCommodity.ID == 0 { +// return errors.New("串码商品不存在或无库存") +// } +// if imeiStockCommodity.StoreName != sheetCols[4][row] { +// return fmt.Errorf("商品[%s]非所选门店库存,请检查", imeiStockCommodity.ErpCommodityName) +// } +// if sheetCols[5][row] != "" { +// if price, err := strconv.ParseFloat(sheetCols[5][row], 64); err != nil || price < imeiStockCommodity.MinRetailPrice { +// return fmt.Errorf("零售价有误,不能低于最低零售价[%.2f]", imeiStockCommodity.MinRetailPrice) +// } +// } +// } else { // 非串码商品 +// var count int64 +// err := orm.Eloquent.Table("erp_stock_commodity"). +// Where("(commodity_serial_number = ? OR erp_commodity_name = ?) AND store_name = ? AND state = ? AND imei_type = ?", +// sheetCols[0][row], sheetCols[1][row], sheetCols[4][row], InStock, NoIMEICommodity). +// Count(&count).Error +// if err != nil || count < 1 { +// return fmt.Errorf("商品[%s]库存不足", sheetCols[1][row]) +// } +// } +// +// return nil +//} + // 将读取的excel数据转换成OrderExcel struct func transOrderData(colsMap []map[string]interface{}) ([]OrderExcel, error) { var stockInfos []OrderExcel @@ -1593,7 +1864,7 @@ func ImportOrderData(colsMap []map[string]interface{}) ([]ErpOrderCommodity, err orderCommodity.RetailPrice = imeiStockCommodity.RetailPrice orderCommodity.SalePrice = floatVal orderCommodity.ReceivedAmount = floatVal - orderCommodity.SaleDiscount = floatVal - imeiStockCommodity.RetailPrice + orderCommodity.SaleDiscount = imeiStockCommodity.RetailPrice - floatVal orderCommodity.WholesalePrice = imeiStockCommodity.WholesalePrice orderCommodity.StaffCostPrice = imeiStockCommodity.StaffCostPrice orderCommodity.MemberDiscount = imeiStockCommodity.MemberDiscount @@ -1671,7 +1942,7 @@ func ImportOrderData(colsMap []map[string]interface{}) ([]ErpOrderCommodity, err orderCommodity.RetailPrice = imeiStockCommodities[0].RetailPrice orderCommodity.SalePrice = floatVal orderCommodity.ReceivedAmount = floatVal - orderCommodity.SaleDiscount = floatVal - imeiStockCommodities[0].RetailPrice + orderCommodity.SaleDiscount = imeiStockCommodities[0].RetailPrice - floatVal orderCommodity.WholesalePrice = imeiStockCommodities[0].WholesalePrice orderCommodity.StaffCostPrice = imeiStockCommodities[0].StaffCostPrice orderCommodity.MemberDiscount = imeiStockCommodities[0].MemberDiscount