package controllers import ( "bytes" "errors" "fmt" "gorm.io/gorm" "io" "mime/multipart" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "psi/constant" "psi/database" "psi/models" systemReq "psi/models/request" systemRes "psi/models/response" "psi/service" "psi/utils" "strconv" ) type ProcessApi struct{} var processService = &service.ProcessService{} // CreatePurchaseOrderWithWave 创建采购单并生成入库波次 func (r *ProcessApi) CreatePurchaseOrderWithWave(c *gin.Context) { var req systemReq.PurchaseOrderCreateRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "创建采购单请求参数异常", "参数错误: "+err.Error(), c, err) return } carService := &service.CarService{} capacity, err := carService.GetCarCapacityByID(req.CarID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "查询小车容量异常", err, c, gin.H{"car_id": req.CarID}) return } if len(req.Items) == 0 { items, err := bindPurchaseOrderItems(c, int(capacity)) if err != nil { logAndFail(constant.LoggerChannelRequest, "绑定采购订单项异常", "参数错误: "+err.Error(), c) return } req.Items = items } userInfo := utils.GetUserInfo(c) purchaseOrderID, waveID, err := processService.CreatePurchaseOrderWithWave(req, userInfo.Username, userInfo.ID, int(capacity), database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建采购单及波次异常", err, c, req) return } systemRes.OkWithDetailed(gin.H{"order_id": purchaseOrderID, "wave_id": waveID}, "采购单创建成功,波次已生成", c) } // ReleaseWave 提交波次,生成采购订单明细和波次任务明细 func (r *ProcessApi) ReleaseWave(c *gin.Context) { var req systemReq.WaveRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "提交波次请求参数异常", "参数错误: "+err.Error(), c, err) return } carService := &service.CarService{} capacity, err := carService.GetCarCapacityByID(req.CarID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "查询小车容量异常", err, c, gin.H{"car_id": req.CarID}) return } if len(req.Items) == 0 { items, err := bindWaveItems(c, int(capacity)) if err != nil { logAndFail(constant.LoggerChannelRequest, "绑定波次项异常", "参数错误: "+err.Error(), c) return } req.Items = items } waveID, waveNo, err := processService.ReleaseWave(req, capacity, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "提交波次异常", err, c, req) return } systemRes.OkWithDetailed(systemRes.WaveReleaseResponse{ WaveID: waveID, WaveNo: waveNo, }, "波次提交成功,任务和明细已生成", c) } // BindWave 绑定波次,创建入库单 func (r *ProcessApi) BindWave(c *gin.Context) { var req systemReq.BindWaveRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "绑定波次请求参数异常", "参数错误: "+err.Error(), c, err) return } receivingOrderID, waveTaskID, waveTaskBatchNo, err := processService.BindWave(req, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "绑定波次异常", err, c, req) return } systemRes.OkWithDetailed(map[string]interface{}{ "receiving_order_id": receivingOrderID, // 入库单ID "wave_task_id": waveTaskID, // 波次任务ID "wave_task_batch_no": waveTaskBatchNo, // 波次任务批次号 }, "绑定波次成功,入库单已创建", c) } // GetWaveTaskInfo 获取波次任务信息 func (r *ProcessApi) GetWaveTaskInfo(c *gin.Context) { id, err := parsePathID(c, "id", "获取波次任务信息") if err != nil { return } info, err := processService.GetWaveTaskInfo(id, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "获取波次任务信息异常", err, c, gin.H{"id": id}) return } systemRes.OkWithDetailed(info, "获取成功", c) } // SubmitReceiving 提交入库 func (r *ProcessApi) SubmitReceiving(c *gin.Context) { var req systemReq.ReceivingSubmitRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "提交入库请求参数异常", "参数错误: "+err.Error(), c, err) return } if len(req.Items) == 0 { databaseConn := database.GetDB(c) var waveTask models.WaveTask if err := databaseConn.Where("id = ? AND is_del = 0", req.WaveTaskID).First(&waveTask).Error; err != nil { logAndFail(constant.LoggerChannelWork, "查询波次任务失败", "错误: "+err.Error(), c) return } carCapacity := int(waveTask.CarCapacity) if carCapacity <= 0 { logAndFail(constant.LoggerChannelWork, "波次任务未设置小车容量", fmt.Sprintf("wave_task_id: %d", req.WaveTaskID), c) return } items, err := bindReceivingItems(c, carCapacity) if err != nil { logAndFail(constant.LoggerChannelRequest, "绑定入库项异常", "参数错误: "+err.Error(), c) return } req.Items = items } userInfo := utils.GetUserInfo(c) err := processService.SubmitReceiving(req, userInfo.Username, userInfo.ID, userInfo.AboutID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "提交入库异常", err, c, req) return } // TODO: 添加入库单信息同步到主库的逻辑 // 同步入库单信息到主库 product_book_xx 分表 r.syncProductBookToMainDB(req, userInfo, c) //itemProductIDs := make([]int64, len(req.Items)) //for i, item := range req.Items { // itemProductIDs[i] = item.ProductID //} //fmt.Printf("[DEBUG] itemProductIDs: %v\n", itemProductIDs) // //fmt.Printf("[DEBUG] 获取租户数据库, about_id: %d\n", userInfo.AboutID) //tenantDB, err := database.GetTenantDB(userInfo.AboutID) //if err != nil { // fmt.Printf("[DEBUG] 获取租户数据库失败: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "获取租户数据库连接失败", err, c, gin.H{"about_id": userInfo.AboutID}) // return //} //fmt.Println("[DEBUG] 租户数据库连接成功") // //fmt.Printf("[DEBUG] 查询入库单(租户库), receiving_order_id: %d\n", req.ReceivingOrderID) //var receivingOrder models.ReceivingOrder //if err := tenantDB.Where("id = ? AND is_del = 0", req.ReceivingOrderID).First(&receivingOrder).Error; err != nil { // fmt.Printf("[DEBUG] 查询入库单失败: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "查询入库单失败", err, c, gin.H{"receiving_order_id": req.ReceivingOrderID}) // return //} //fmt.Printf("[DEBUG] 入库单查询成功, warehouse_id: %d\n", receivingOrder.WarehouseID) // //fmt.Printf("[DEBUG] 查询仓库(租户库), warehouse_id: %d\n", receivingOrder.WarehouseID) //var warehouse models.Warehouse //if err := tenantDB.Where("id = ? AND is_del = 0", receivingOrder.WarehouseID).First(&warehouse).Error; err != nil { // fmt.Printf("[DEBUG] 查询仓库失败: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "查询仓库失败", err, c, gin.H{"warehouse_id": receivingOrder.WarehouseID}) // return //} //fmt.Printf("[DEBUG] 仓库查询成功, warehouse_name: %s\n", warehouse.Name) // //fmt.Printf("[DEBUG] 获取租户数据库, about_id: %d\n", userInfo.AboutID) //tenantDB, err = database.GetTenantDB(userInfo.AboutID) //if err != nil { // fmt.Printf("[DEBUG] 获取租户数据库失败: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "获取租户数据库连接失败", err, c, gin.H{"about_id": userInfo.AboutID}) // return //} //fmt.Println("[DEBUG] 租户数据库连接成功") // //var products []models.Product //fmt.Printf("[DEBUG] 查询商品列表, product_ids: %v\n", itemProductIDs) //if err := tenantDB.Where("id IN ? AND is_del = 0", itemProductIDs).Find(&products).Error; err != nil { // fmt.Printf("[DEBUG] 查询商品列表失败: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "查询商品列表失败", err, c, gin.H{"product_ids": itemProductIDs}) // return //} //fmt.Printf("[DEBUG] 商品查询成功, 共 %d 条\n", len(products)) // //type locationInfo struct { // LocationID int64 // Code string //} //locationMap := make(map[int64]locationInfo) //fmt.Println("[DEBUG] 开始查询库位信息") //for _, product := range products { // var inventoryDetail models.InventoryDetail // if err := tenantDB.Where("product_id = ? AND warehouse_id = ? AND is_del = 0", product.ID, receivingOrder.WarehouseID).First(&inventoryDetail).Error; err == nil { // var location models.Location // if err := tenantDB.Where("id = ? AND is_del = 0", inventoryDetail.LocationID).First(&location).Error; err == nil { // locationMap[product.ID] = locationInfo{ // LocationID: location.ID, // Code: location.Code, // } // } // } //} //fmt.Printf("[DEBUG] 库位查询完成, locationMap: %v\n", locationMap) // //bookInfoMap := make(map[int64]models.BookInfo) //fmt.Println("[DEBUG] 开始查询BookInfo") //for _, product := range products { // if product.StandardProductID > 0 { // if _, exists := bookInfoMap[product.StandardProductID]; !exists { // var bookInfo models.BookInfo // if err := database.DB.Where("id = ?", product.StandardProductID).First(&bookInfo).Error; err == nil { // bookInfoMap[product.StandardProductID] = bookInfo // } // } // } //} //fmt.Printf("[DEBUG] BookInfo查询完成, bookInfoMap数量: %d\n", len(bookInfoMap)) // //fmt.Println("[DEBUG] 开始写入product_book分表") //for _, product := range products { // isbn := product.Barcode // if isbn == "" { // fmt.Printf("[DEBUG] 跳过无条码商品, product_id: %d\n", product.ID) // continue // } // // tableName := models.ProductBookTableName(isbn) // fmt.Printf("[DEBUG] 处理商品, product_id: %d, isbn: %s, table: %s\n", product.ID, isbn, tableName) // // var locInfo locationInfo // if li, exists := locationMap[product.ID]; exists { // locInfo = li // } // // var bookInfo models.BookInfo // if bi, exists := bookInfoMap[product.StandardProductID]; exists { // bookInfo = bi // } // // catID := bookInfo.CatID // if catID == nil { // catID = datatypes.JSON("[]") // } // liveImage := product.LiveImage // if liveImage == nil { // liveImage = datatypes.JSON("{}") // } // // now := time.Now().Unix() // // fmt.Printf("[DEBUG] 查重, self_id: %d, about_id: %d\n", product.ID, userInfo.AboutID) // var existingBook models.ProductBook // err := database.DB.Table(tableName).Where("self_id = ? AND about_id = ? AND is_del = 0", product.ID, userInfo.AboutID).First(&existingBook).Error // // if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { // fmt.Println("[DEBUG] 记录不存在,执行新增") // bookRecord := models.ProductBook{ // SelfID: product.ID, // AboutId: userInfo.AboutID, // WarehouseID: warehouse.ID, // WarehouseName: warehouse.Name, // LocationID: locInfo.LocationID, // LocationName: locInfo.Code, // CategoryID: product.CategoryID, // StandardProductID: product.StandardProductID, // Fid: bookInfo.Fid, // Type: bookInfo.Type, // ISBN: isbn, // FISBN: bookInfo.FISBN, // BookName: bookInfo.BookName, // FBookName: bookInfo.FBookName, // Author: bookInfo.Author, // Publishing: bookInfo.Publishing, // PublicationTime: bookInfo.PublicationTime, // Binding: bookInfo.Binding, // PagesCount: bookInfo.PagesCount, // WordsCount: bookInfo.WordsCount, // Format: bookInfo.Format, // CatID: catID, // Name: product.Name, // Appearance: product.Appearance, // Barcode: product.Barcode, // Price: product.Price, // SalePrice: product.SalePrice, // Cost: product.Cost, // LiveImage: liveImage, // IsBatchManaged: product.IsBatchManaged, // IsShelfLifeManaged: product.IsShelfLifeManaged, // Status: product.Status, // CreatedAt: now, // UpdatedAt: now, // IsDel: 0, // } // if createErr := database.DB.Table(tableName).Create(&bookRecord).Error; createErr != nil { // fmt.Printf("[DEBUG] 新增失败: %v\n", createErr) // stmt := database.DB.Session(&gorm.Session{DryRun: true}).Table(tableName).Create(&bookRecord) // utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ // "source": "写入product_book分表失败", // "user_id": userInfo.AboutID, // "self_id": product.ID, // "isbn": isbn, // "table_name": tableName, // "warehouse_id": warehouse.ID, // "sql": stmt.Statement.SQL.String(), // "vars": fmt.Sprintf("%v", stmt.Statement.Vars), // "err_msg": createErr.Error(), // }) // utils.FailWithRequestLog(constant.LoggerChannelWork, "写入product_book分表失败", createErr, c, gin.H{ // "product_id": product.ID, // "isbn": isbn, // "table_name": tableName, // }) // return // } // fmt.Println("[DEBUG] 新增成功") // } else if err == nil { // fmt.Println("[DEBUG] 记录已存在,执行更新") // updateData := map[string]interface{}{ // "warehouse_id": warehouse.ID, // "warehouse_name": warehouse.Name, // "location_id": locInfo.LocationID, // "location_name": locInfo.Code, // "category_id": product.CategoryID, // "standard_product_id": product.StandardProductID, // "name": product.Name, // "book_name": bookInfo.BookName, // "f_isbn": bookInfo.FISBN, // "f_book_name": bookInfo.FBookName, // "author": bookInfo.Author, // "publishing": bookInfo.Publishing, // "publication_time": bookInfo.PublicationTime, // "binding": bookInfo.Binding, // "pages_count": bookInfo.PagesCount, // "words_count": bookInfo.WordsCount, // "format": bookInfo.Format, // "cat_id": catID, // "appearance": product.Appearance, // "barcode": product.Barcode, // "price": product.Price, // "sale_price": product.SalePrice, // "cost": product.Cost, // "live_image": liveImage, // "is_batch_managed": product.IsBatchManaged, // "is_shelf_life_managed": product.IsShelfLifeManaged, // "status": product.Status, // "updated_at": now, // } // if updateErr := database.DB.Table(tableName).Model(&existingBook).Updates(updateData).Error; updateErr != nil { // fmt.Printf("[DEBUG] 更新失败: %v\n", updateErr) // utils.FailWithRequestLog(constant.LoggerChannelWork, "更新product_book分表失败", updateErr, c, gin.H{ // "product_id": product.ID, // "isbn": isbn, // "table_name": tableName, // }) // return // } // fmt.Println("[DEBUG] 更新成功") // } else { // fmt.Printf("[DEBUG] 查询分表异常: %v\n", err) // utils.FailWithRequestLog(constant.LoggerChannelWork, "查询product_book分表失败", err, c, gin.H{ // "product_id": product.ID, // "isbn": isbn, // "table_name": tableName, // }) // return // } //} //fmt.Println("[DEBUG] product_book分表写入全部完成") fmt.Printf("提交入库成功,入库单ID: %d, 波次任务ID: %d, 波次任务批次号: %s\n", req.ReceivingOrderID, req.WaveTaskID) go func(receivingOrderID, aboutID, waveTaskID int64, itemProductIDs []int64) { databaseConn := database.DB var receivingOrder models.ReceivingOrder if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrderID).First(&receivingOrder).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "查询入库单失败", "receiving_order_id": receivingOrderID, "err_msg": err.Error(), }) return } var warehouse models.Warehouse if err := databaseConn.Where("id = ? AND is_del = 0", receivingOrder.WarehouseID).First(&warehouse).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "查询仓库失败", "warehouse_id": receivingOrder.WarehouseID, "err_msg": err.Error(), }) return } // 同步给主库对应的product_book_xx中 // 获取租户数据库连接 tenantDB, err := database.GetTenantDB(aboutID) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "获取租户数据库连接失败", "user_id": aboutID, "err_msg": err.Error(), }) return } // 查询商品详细信息 var products []models.Product if err := tenantDB.Where("id IN ? AND is_del = 0", itemProductIDs).Find(&products).Error; err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "查询商品列表失败", "user_id": aboutID, "product_ids": itemProductIDs, "err_msg": err.Error(), }) return } // 查询库位信息(获取库位ID和Code) type locationInfo struct { LocationID int64 Code string } locationMap := make(map[int64]locationInfo) for _, product := range products { var inventoryDetail models.InventoryDetail if err := tenantDB.Where("product_id = ? AND warehouse_id = ? AND is_del = 0", product.ID, receivingOrder.WarehouseID).First(&inventoryDetail).Error; err == nil { var location models.Location if err := tenantDB.Where("id = ? AND is_del = 0", inventoryDetail.LocationID).First(&location).Error; err == nil { locationMap[product.ID] = locationInfo{ LocationID: location.ID, Code: location.Code, } } } } // 查询BookInfo(按StandardProductID关联) bookInfoMap := make(map[int64]models.BookInfo) for _, product := range products { if product.StandardProductID > 0 { if _, exists := bookInfoMap[product.StandardProductID]; !exists { var bookInfo models.BookInfo if err := database.DB.Where("id = ?", product.StandardProductID).First(&bookInfo).Error; err == nil { bookInfoMap[product.StandardProductID] = bookInfo } } } } // 逐个写入主库 product_book_xx 分表 for _, product := range products { isbn := product.Barcode if isbn == "" { continue } tableName := models.ProductBookTableName(isbn) var locInfo locationInfo if li, exists := locationMap[product.ID]; exists { locInfo = li } var bookInfo models.BookInfo if bi, exists := bookInfoMap[product.StandardProductID]; exists { bookInfo = bi } now := time.Now().Unix() // 按 self_id + about_id 查重,避免不同租户商品ID冲突 var existingBook models.ProductBook err := database.DB.Table(tableName).Where("self_id = ? AND about_id = ? AND is_del = 0", product.ID, aboutID).First(&existingBook).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { bookRecord := models.ProductBook{ SelfID: product.ID, AboutId: aboutID, WarehouseID: warehouse.ID, WarehouseName: warehouse.Name, LocationID: locInfo.LocationID, LocationName: locInfo.Code, CategoryID: product.CategoryID, StandardProductID: product.StandardProductID, Fid: bookInfo.Fid, Type: bookInfo.Type, ISBN: isbn, FISBN: bookInfo.FISBN, BookName: bookInfo.BookName, FBookName: bookInfo.FBookName, Author: bookInfo.Author, Publishing: bookInfo.Publishing, PublicationTime: bookInfo.PublicationTime, Binding: bookInfo.Binding, PagesCount: bookInfo.PagesCount, WordsCount: bookInfo.WordsCount, Format: bookInfo.Format, CatID: bookInfo.CatID, Name: product.Name, Appearance: product.Appearance, Barcode: product.Barcode, Price: product.Price, SalePrice: product.SalePrice, Cost: product.Cost, LiveImage: product.LiveImage, IsBatchManaged: product.IsBatchManaged, IsShelfLifeManaged: product.IsShelfLifeManaged, Status: product.Status, CreatedAt: now, UpdatedAt: now, IsDel: 0, } if createErr := database.DB.Table(tableName).Create(&bookRecord).Error; createErr != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "写入product_book分表失败", "user_id": aboutID, "self_id": product.ID, "isbn": isbn, "table_name": tableName, "warehouse_id": warehouse.ID, "err_msg": createErr.Error(), }) } else { utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ "source": "写入product_book分表成功", "user_id": aboutID, "self_id": product.ID, "isbn": isbn, "table_name": tableName, "warehouse_id": warehouse.ID, "location_id": locInfo.LocationID, }) } } else if err == nil { updateData := map[string]interface{}{ "warehouse_id": warehouse.ID, "warehouse_name": warehouse.Name, "location_id": locInfo.LocationID, "location_name": locInfo.Code, "category_id": product.CategoryID, "standard_product_id": product.StandardProductID, "name": product.Name, "book_name": bookInfo.BookName, "f_isbn": bookInfo.FISBN, "f_book_name": bookInfo.FBookName, "author": bookInfo.Author, "publishing": bookInfo.Publishing, "publication_time": bookInfo.PublicationTime, "binding": bookInfo.Binding, "pages_count": bookInfo.PagesCount, "words_count": bookInfo.WordsCount, "format": bookInfo.Format, "cat_id": bookInfo.CatID, "appearance": product.Appearance, "barcode": product.Barcode, "price": product.Price, "sale_price": product.SalePrice, "cost": product.Cost, "live_image": product.LiveImage, "is_batch_managed": product.IsBatchManaged, "is_shelf_life_managed": product.IsShelfLifeManaged, "status": product.Status, "updated_at": now, } if updateErr := database.DB.Table(tableName).Model(&existingBook).Updates(updateData).Error; updateErr != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "更新product_book分表失败", "user_id": aboutID, "self_id": product.ID, "isbn": isbn, "table_name": tableName, "warehouse_id": warehouse.ID, "err_msg": updateErr.Error(), }) } else { utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ "source": "更新product_book分表成功", "user_id": aboutID, "self_id": product.ID, "isbn": isbn, "table_name": tableName, "warehouse_id": warehouse.ID, "location_id": locInfo.LocationID, }) } } else { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "查询product_book分表失败", "user_id": aboutID, "self_id": product.ID, "isbn": isbn, "table_name": tableName, "err_msg": err.Error(), }) } } warehouseId := warehouse.ID productIds := make([]string, len(itemProductIDs)) for i, pid := range itemProductIDs { productIds[i] = fmt.Sprintf("%d", pid) } productIdsStr := strings.Join(productIds, ",") url := "https://erp.buzhiyushu.cn/zhishu/product/releaseGoodsAuto" method := "POST" payload := &bytes.Buffer{} writer := multipart.NewWriter(payload) _ = writer.WriteField("userId", fmt.Sprintf("%d", aboutID)) _ = writer.WriteField("warehouseId", fmt.Sprintf("%d", warehouseId)) _ = writer.WriteField("productId", productIdsStr) closeErr := writer.Close() if closeErr != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "关闭multipart writer失败", "err_msg": closeErr.Error(), }) return } client := &http.Client{} httpReq, err := http.NewRequest(method, url, payload) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "创建HTTP请求失败", "err_msg": err.Error(), }) return } httpReq.Header.Add("Authorization", "Basic ZWxhc3RpYzo1bVJESVVnNTJWQzBmcDE0bnctRg==") httpReq.Header.Set("Content-Type", writer.FormDataContentType()) res, err := client.Do(httpReq) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "调用外部API失败", "err_msg": err.Error(), }) return } defer func(Body io.ReadCloser) { closeBodyErr := Body.Close() if closeBodyErr != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "调用外部API失败-Body关闭失败", "err_msg": closeBodyErr.Error(), }) } }(res.Body) body, err := io.ReadAll(res.Body) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "读取响应失败", "err_msg": err.Error(), }) return } /*payload := &bytes.Buffer{} writer := multipart.NewWriter(payload) _ = writer.WriteField("userId", fmt.Sprintf("%d", aboutID)) _ = writer.WriteField("warehouseId", fmt.Sprintf("%d", warehouseId)) _ = writer.WriteField("productId", productIdsStr) err := writer.Close() if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "关闭multipart writer失败", "err_msg": err.Error(), }) return } client := &http.Client{} req, err := http.NewRequest(method, url, payload) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "创建HTTP请求失败", "err_msg": err.Error(), }) return } req.Header.Add("Authorization", "Basic ZWxhc3RpYzo1bVJESVVnNTJWQzBmcDE0bnctRg==") req.Header.Set("Content-Type", writer.FormDataContentType()) res, err := client.Do(req) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "调用外部API失败", "err_msg": err.Error(), }) return } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "调用外部API失败-Body关闭失败", "err_msg": err.Error(), }) } }(res.Body) body, err := io.ReadAll(res.Body) if err != nil { utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{ "source": "读取响应失败", "err_msg": err.Error(), }) return }*/ utils.InfoLog(constant.LoggerChannelWork, logrus.Fields{ "source": "外部API调用完成", "user_id": aboutID, "warehouse_id": warehouseId, "product_ids": productIdsStr, "status_code": res.StatusCode, "response": string(body), }) }(req.ReceivingOrderID, userInfo.AboutID, req.WaveTaskID, func() []int64 { productIDs := make([]int64, len(req.Items)) for i, item := range req.Items { productIDs[i] = item.ProductID } return productIDs }()) systemRes.OkWithMessage("入库提交成功", c) } // GetReceivingDetail 获取入库单详情 func (r *ProcessApi) GetReceivingDetail(c *gin.Context) { id, err := parsePathID(c, "id", "获取入库单详情") if err != nil { return } detail, err := processService.GetReceivingDetail(id, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "获取入库单详情异常", err, c, gin.H{"id": id}) return } systemRes.OkWithDetailed(detail, "获取成功", c) } // CreateSalesOrderWithDetail 创建销售订单和明细 func (r *ProcessApi) CreateSalesOrderWithDetail(c *gin.Context) { var req systemReq.SalesOrderCreateRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "创建销售订单请求参数异常", "参数错误: "+err.Error(), c, err) return } if len(req.Items) == 0 { items, err := bindSalesOrderItems(c) if err != nil { logAndFail(constant.LoggerChannelRequest, "绑定销售订单项异常", "参数错误: "+err.Error(), c) return } req.Items = items } salesOrderID, err := processService.CreateSalesOrderWithDetail(req) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建销售订单及波次异常", err, c, req) return } systemRes.OkWithDetailed(gin.H{"order_id": salesOrderID}, "销售订单创建成功", c) } // CreateOutboundOrder 基于多个销售订单创建出库单 func (r *ProcessApi) CreateOutboundOrder(c *gin.Context) { var req systemReq.CreateOutboundOrderRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "创建出库单请求参数异常", "参数错误: "+err.Error(), c, err) return } if len(req.SalesOrderIDs) == 0 { ids, err := parseIdsFrom(c, req.Total) if err != nil { systemRes.FailWithMessage("参数错误: "+err.Error(), c) return } req.SalesOrderIDs = ids } userInfo := utils.GetUserInfo(c) outboundOrderID, outNo, err := processService.CreateOutboundOrder(req, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建出库单异常", err, c, req) return } systemRes.OkWithDetailed(gin.H{ "outbound_order_id": outboundOrderID, "out_no": outNo, }, "出库单创建成功", c) } // CreateOutboundWave 基于出库单创建出库波次 func (r *ProcessApi) CreateOutboundWave(c *gin.Context) { var req systemReq.CreateOutboundWaveRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "创建出库波次请求参数异常", "参数错误: "+err.Error(), c, err) return } if req.OutboundOrderID == 0 { systemRes.FailWithMessage("参数错误: 出库单ID不能为空", c) return } userInfo := utils.GetUserInfo(c) waveID, err := processService.CreateOutboundWave(req, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建出库波次异常", err, c, req) return } systemRes.OkWithDetailed(systemRes.WaveReleaseResponse{ WaveID: waveID, }, "出库波次创建成功", c) } // ReleaseOutboundWave 提交出库波次,生成波次任务明细 func (r *ProcessApi) ReleaseOutboundWave(c *gin.Context) { var req systemReq.WaveRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "提交出库波次请求参数异常", "参数错误: "+err.Error(), c, err) return } waveID, waveNo, err := processService.ReleaseOutboundWave(req, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "提交出库波次异常", err, c, req) return } systemRes.OkWithDetailed(systemRes.WaveReleaseResponse{ WaveID: waveID, WaveNo: waveNo, }, "出库波次提交成功,任务和明细已生成", c) } // BindOutboundWave 绑定出库波次 func (r *ProcessApi) BindOutboundWave(c *gin.Context) { var req systemReq.BindWaveRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "绑定出库波次请求参数异常", "参数错误: "+err.Error(), c, err) return } outboundOrderID, waveTaskID, waveTaskBatchNo, err := processService.BindOutboundWave(req, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "绑定出库波次异常", err, c, req) return } systemRes.OkWithDetailed(map[string]interface{}{ "out_order_id": outboundOrderID, "wave_task_id": waveTaskID, "wave_task_batch_no": waveTaskBatchNo, }, "绑定出库波次成功", c) } // SubmitOutbound 提交出库 func (r *ProcessApi) SubmitOutbound(c *gin.Context) { var req systemReq.OutboundSubmitRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "提交出库请求参数异常", "参数错误: "+err.Error(), c, err) return } if len(req.Items) == 0 { databaseConn := database.GetDB(c) var waveTask models.WaveTask if err := databaseConn.Where("id = ? AND is_del = 0", req.WaveTaskID).First(&waveTask).Error; err != nil { logAndFail(constant.LoggerChannelWork, "查询波次任务失败", "错误: "+err.Error(), c) return } var count int64 if err := databaseConn.Model(&models.WaveTaskDetail{}).Where("wave_task_id = ? AND status = 1 AND is_del = 0", waveTask.ID).Count(&count).Error; err != nil { logAndFail(constant.LoggerChannelWork, "查询波次任务明细数量失败", "错误: "+err.Error(), c) return } items, err := bindOutboundItems(c, int(count)) if err != nil { logAndFail(constant.LoggerChannelRequest, "绑定出库项异常", "参数错误: "+err.Error(), c) return } req.Items = items } userInfo := utils.GetUserInfo(c) err := processService.SubmitOutbound(req, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "提交出库异常", err, c, req) return } systemRes.OkWithMessage("出库提交成功", c) } // GetOutboundDetail 获取发货单详情 func (r *ProcessApi) GetOutboundDetail(c *gin.Context) { id, err := parsePathID(c, "id", "获取发货单详情") if err != nil { return } detail, err := processService.GetOutboundDetail(id, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "获取发货单详情异常", err, c, gin.H{"id": id}) return } systemRes.OkWithDetailed(detail, "获取成功", c) } // CreateShippingOrder 基于多个出库单创建发货单 func (r *ProcessApi) CreateShippingOrder(c *gin.Context) { var req systemReq.CreateShippingOrderRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "创建发货单请求参数异常", "参数错误: "+err.Error(), c, err) return } if len(req.OutboundOrderIDs) == 0 { ids, err := parseIdsFrom(c, req.Total) if err != nil { systemRes.FailWithMessage("参数错误: "+err.Error(), c) return } req.OutboundOrderIDs = ids } userInfo := utils.GetUserInfo(c) shippingOrderID, shippingNo, err := processService.CreateShippingOrder(req, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建发货单异常", err, c, req) return } systemRes.OkWithDetailed(gin.H{ "shipping_order_id": shippingOrderID, "shipping_no": shippingNo, }, "发货单创建成功", c) } // UpdateShippingLogistics 更新发货单物流信息并回填销售订单明细 func (r *ProcessApi) UpdateShippingLogistics(c *gin.Context) { var req systemReq.UpdateShippingLogisticsRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "更新发货单物流信息请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) err := processService.UpdateShippingLogistics(req, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "更新发货单物流信息异常", err, c, req) return } systemRes.OkWithMessage("更新成功", c) } // CancelSalesOrder 取消销售订单 func (r *ProcessApi) CancelSalesOrder(c *gin.Context) { var req systemReq.CancelSalesOrderRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "取消销售订单请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) err := processService.CancelSalesOrder(req.OrderID, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "取消销售订单异常", err, c, req) return } systemRes.OkWithMessage("销售订单取消成功,库存已释放", c) } // CancelOutboundWave 取消出库波次 func (r *ProcessApi) CancelOutboundWave(c *gin.Context) { var req systemReq.CancelOutboundWaveRequest if err := c.ShouldBindJSON(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "取消出库波次请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) err := processService.CancelOutboundWave(req.WaveID, userInfo.Username, userInfo.ID, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "取消出库波次异常", err, c, req) return } systemRes.OkWithMessage("出库波次取消成功,库存已释放", c) } // AdjustInventory 盘库调整(加库存/减库存) func (r *ProcessApi) AdjustInventory(c *gin.Context) { var req systemReq.StockCheckAdjustRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "盘库调整请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) if err := processService.AdjustInventory(req, userInfo.Username, userInfo.ID, database.GetDB(c)); err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "盘库调整异常", err, c, req) return } systemRes.OkWithMessage("盘库调整成功", c) } // ReturnInventory 盘库退货 func (r *ProcessApi) ReturnInventory(c *gin.Context) { var req systemReq.StockCheckReturnRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "盘库退货请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) if err := processService.ReturnInventory(req, userInfo.Username, userInfo.ID, database.GetDB(c)); err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "盘库退货异常", err, c, req) return } systemRes.OkWithMessage("盘库退货成功", c) } // ChangeLocation 出库单切换库位 func (r *ProcessApi) ChangeLocation(c *gin.Context) { var req systemReq.ChangeLocationRequest if err := c.ShouldBind(&req); err != nil { ValidAndFail(constant.LoggerChannelRequest, "出库单切换库位请求参数异常", "参数错误: "+err.Error(), c, err) return } userInfo := utils.GetUserInfo(c) if err := processService.ChangeLocation(req, userInfo.Username, userInfo.ID, database.GetDB(c)); err != nil { if errors.Is(err, utils.ErrNoAvailableLocation) { c.Status(http.StatusNoContent) return } utils.FailWithRequestLog(constant.LoggerChannelWork, "出库单切换库位异常", err, c, req) return } systemRes.OkWithMessage("库位切换成功", c) } // parsePathID 从URL路径参数中获取并验证ID func parsePathID(c *gin.Context, paramName, source string) (int64, error) { idStr := c.Param(paramName) if idStr == "" { utils.ErrorLog(constant.LoggerChannelRequest, logrus.Fields{ "source": source + "请求参数异常", "err_msg": "ID参数不能为空", }) systemRes.FailWithValidateMessage("参数错误: ID不能为空", c) return 0, fmt.Errorf("ID参数不能为空") } var id int64 if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil || id <= 0 { utils.ErrorLog(constant.LoggerChannelRequest, logrus.Fields{ "source": source + "请求参数异常", "err_msg": "ID格式错误: " + idStr, }) systemRes.FailWithValidateMessage("参数错误: ID格式不正确", c) return 0, fmt.Errorf("ID格式错误") } return id, nil } // parseOptionalDate 解析可选日期字段 func parseOptionalDate(c *gin.Context, key string) int64 { if str := c.PostForm(key); str != "" { if val, err := strconv.ParseInt(str, 10, 64); err == nil { return val } } return 0 } // bindSimpleItem 绑定简单项(ProductID + Quantity + UnitPrice) func bindSimpleItem[T any](c *gin.Context, i int, creator func(int64, int64, int64) T) (T, bool, error) { var zero T productID, hasProduct, err := getPostFormInt64(c, fmt.Sprintf("items[%d][product_id]", i)) if err != nil || !hasProduct { return zero, false, err } quantity, hasQuantity, err := getPostFormInt64(c, fmt.Sprintf("items[%d][quantity]", i)) if err != nil || !hasQuantity { return zero, false, err } unitPrice, hasPrice, err := getPostFormInt64(c, fmt.Sprintf("items[%d][unit_price]", i)) if err != nil || !hasPrice { return zero, false, err } return creator(productID, quantity, unitPrice), true, nil } // bindLocationItem 绑定带位置信息的项(ProductID + LocationID + Quantity + 批次信息) func bindLocationItem[T any](c *gin.Context, i int, creator func(int64, int64, string, int64, int64, int64) T) (T, bool, error) { var zero T productID, hasProduct, err := getPostFormInt64(c, fmt.Sprintf("items[%d][product_id]", i)) if err != nil || !hasProduct { return zero, false, err } locationID, hasLocation, err := getPostFormInt64(c, fmt.Sprintf("items[%d][location_id]", i)) if err != nil || !hasLocation { return zero, false, err } quantity, hasQuantity, err := getPostFormInt64(c, fmt.Sprintf("items[%d][quantity]", i)) if err != nil || !hasQuantity { return zero, false, err } batchNo := c.PostForm(fmt.Sprintf("items[%d][batch_no]", i)) productionDate := parseOptionalDate(c, fmt.Sprintf("items[%d][production_date]", i)) expiryDate := parseOptionalDate(c, fmt.Sprintf("items[%d][expiry_date]", i)) return creator(productID, locationID, batchNo, productionDate, expiryDate, quantity), true, nil } // bindPurchaseOrderItems 绑定采购订单项 func bindPurchaseOrderItems(c *gin.Context, capacity int) ([]systemReq.PurchaseOrderItemRequest, error) { return bindItemsWithForm(c, func(i int) (systemReq.PurchaseOrderItemRequest, bool, error) { return bindSimpleItem(c, i, func(productID, quantity, unitPrice int64) systemReq.PurchaseOrderItemRequest { return systemReq.PurchaseOrderItemRequest{ ProductID: productID, Quantity: quantity, UnitPrice: unitPrice, } }) }, capacity) } // bindWaveItems 绑定波次项 func bindWaveItems(c *gin.Context, capacity int) ([]systemReq.WaveItemRequest, error) { return bindItemsWithForm(c, func(i int) (systemReq.WaveItemRequest, bool, error) { return bindSimpleItem(c, i, func(productID, quantity, unitPrice int64) systemReq.WaveItemRequest { return systemReq.WaveItemRequest{ ProductID: productID, Quantity: quantity, UnitPrice: unitPrice, } }) }, capacity) } // bindReceivingItems 绑定入库项 func bindReceivingItems(c *gin.Context, maxItems int) ([]systemReq.ReceivingItemRequest, error) { return bindItemsWithForm(c, func(i int) (systemReq.ReceivingItemRequest, bool, error) { return bindLocationItem(c, i, func(productID, locationID int64, batchNo string, productionDate, expiryDate, quantity int64) systemReq.ReceivingItemRequest { return systemReq.ReceivingItemRequest{ ProductID: productID, LocationID: locationID, BatchNo: batchNo, ProductionDate: productionDate, ExpiryDate: expiryDate, Quantity: quantity, } }) }, maxItems) } // bindSalesOrderItems 绑定销售订单项 func bindSalesOrderItems(c *gin.Context) ([]systemReq.SalesOrderItemRequest, error) { return bindItemsWithForm(c, func(i int) (systemReq.SalesOrderItemRequest, bool, error) { return bindSimpleItem(c, i, func(productID, quantity, unitPrice int64) systemReq.SalesOrderItemRequest { return systemReq.SalesOrderItemRequest{ ProductID: productID, Quantity: quantity, UnitPrice: unitPrice, } }) }, 200) } // bindOutboundItems 绑定出库项 func bindOutboundItems(c *gin.Context, maxItems int) ([]systemReq.OutboundItemRequest, error) { return bindItemsWithForm(c, func(i int) (systemReq.OutboundItemRequest, bool, error) { return bindLocationItem(c, i, func(productID, locationID int64, batchNo string, productionDate, expiryDate, quantity int64) systemReq.OutboundItemRequest { return systemReq.OutboundItemRequest{ ProductID: productID, LocationID: locationID, BatchNo: batchNo, ProductionDate: productionDate, ExpiryDate: expiryDate, Quantity: quantity, } }) }, maxItems) } // bindItemsWithForm 通用表单项绑定函数 func bindItemsWithForm[T any](c *gin.Context, builder func(int) (T, bool, error), maxItems int) ([]T, error) { items := make([]T, 0) for i := 0; i < maxItems; i++ { item, hasData, err := builder(i) if err != nil { return nil, err } if !hasData { continue } items = append(items, item) } return items, nil } func parseIdsFrom(c *gin.Context, total int) ([]int64, error) { var ids []int64 for i := 0; i < total; i++ { idStr := c.PostForm(fmt.Sprintf("order_ids[%d]", i)) if idStr == "" { break } var id int64 if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil { return nil, fmt.Errorf("第%d个ID格式错误", i) } if id <= 0 { return nil, fmt.Errorf("第%d个ID必须大于0", i) } ids = append(ids, id) } if len(ids) == 0 { return nil, fmt.Errorf("至少需要一个ID") } return ids, nil } // getPostFormInt64 获取表单中的int64值 func getPostFormInt64(c *gin.Context, key string) (int64, bool, error) { str := c.PostForm(key) if str == "" { return 0, false, nil } val, err := strconv.ParseInt(str, 10, 64) if err != nil { return 0, true, err } return val, true, nil } // logAndFail 业务统一的错误日志和响应函数 func logAndFail(channel string, source, errMsg string, c *gin.Context) { utils.ErrorLog(channel, logrus.Fields{ "source": source, "err_msg": errMsg, }) systemRes.FailWithMessage(errMsg, c) } // ValidAndFail 参数统一的错误日志和响应函数 func ValidAndFail(channel string, source, errMsg string, c *gin.Context, err error) { utils.ErrorLog(channel, logrus.Fields{ "source": source, "err_msg": errMsg, }) utils.HandleValidationError(c, err) }