1、优化零售批量导入商品,解决超时问题;

This commit is contained in:
chenlin 2024-11-20 20:33:22 +08:00
parent 19e1647154
commit 429c5a203a
3 changed files with 385 additions and 114 deletions

View File

@ -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 {

View File

@ -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-导出

View File

@ -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