diff --git a/controllers/process.go b/controllers/process.go index 1c00f2f..51e33f9 100644 --- a/controllers/process.go +++ b/controllers/process.go @@ -552,6 +552,25 @@ func (r *ProcessApi) CancelSalesOrder(c *gin.Context) { systemRes.OkWithMessage("销售订单取消成功,库存已释放", c) } +// UnlockSalesOrderInventory 解锁销售订单库存 +func (r *ProcessApi) UnlockSalesOrderInventory(c *gin.Context) { + var req systemReq.UnlockSalesOrderInventoryRequest + + if err := c.ShouldBind(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "解锁销售订单库存请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + userInfo := utils.GetUserInfo(c) + resp, err := processService.UnlockSalesOrderInventory(req.SoNo, userInfo.Username, userInfo.ID, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "解锁销售订单库存异常", err, c, req) + return + } + + systemRes.OkWithDetailed(resp, "库存解锁成功", c) +} + // CancelOutboundWave 取消出库波次 func (r *ProcessApi) CancelOutboundWave(c *gin.Context) { var req systemReq.CancelOutboundWaveRequest diff --git a/controllers/product.go b/controllers/product.go index 2245772..39c5811 100644 --- a/controllers/product.go +++ b/controllers/product.go @@ -3,6 +3,7 @@ package controllers import ( "fmt" "github.com/gin-gonic/gin" + "io" "net/http" "net/url" "psi/constant" @@ -11,6 +12,7 @@ import ( systemRes "psi/models/response" "psi/service" "psi/utils" + "strconv" "strings" ) @@ -505,6 +507,38 @@ func parseImageFromForm(c *gin.Context) ([]string, error) { images = append(images, imageStr) } + // 如果解析到了数组格式,直接返回 + if len(images) > 0 { + return images, nil + } + + // 方式2: 尝试解析 liveimage 逗号分隔格式 + liveimageStr := c.PostForm("liveimage") + if liveimageStr != "" { + // 按逗号分割 + parts := strings.Split(liveimageStr, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + images = append(images, part) + } + } + return images, nil + } + + // 方式3: 尝试解析 live_image 逗号分隔格式 + liveImageStr := c.PostForm("live_image") + if liveImageStr != "" { + // 按逗号分割 + parts := strings.Split(liveImageStr, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + images = append(images, part) + } + } + return images, nil + } return images, nil } @@ -550,16 +584,20 @@ func (r *ProductApi) UpdateProductNameAndImages(c *gin.Context) { return } - // 处理live_image参数: 如果表单未绑定,尝试从原始表单解析 - if len(req.LiveImage) == 0 { - image, err := parseImageFromForm(c) - if err != nil { - systemRes.FailWithValidateMessage("参数错误: "+err.Error(), c) - return - } + // 始终从表单解析图片参数,确保能接收到空数组或新图片 + image, err := parseImageFromForm(c) + if err != nil { + systemRes.FailWithValidateMessage("参数错误: "+err.Error(), c) + return + } + // 如果解析到了图片,使用解析的结果;否则保留ShouldBind的结果 + if len(image) > 0 || c.PostForm("live_image[0]") != "" { req.LiveImage = image } + fmt.Printf("【UpdateProductNameAndImages】最终请求参数 - ProductID: %d, Name: %s, LiveImage数量: %d, LiveImage: %+v\n", + req.ProductID, req.Name, len(req.LiveImage), req.LiveImage) + if err := productService.UpdateProductNameAndImages(req, database.GetDB(c)); err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "修改商品信息异常", err, c, req) return @@ -567,3 +605,121 @@ func (r *ProductApi) UpdateProductNameAndImages(c *gin.Context) { systemRes.OkWithMessage("修改成功", c) } + +// ReimportProducts 将导出的Excel修改后重新导入(覆盖更新或新增) +func (r *ProductApi) ReimportProducts(c *gin.Context) { + warehouseIDStr := c.PostForm("warehouse_id") + if warehouseIDStr == "" { + systemRes.FailWithValidateMessage("warehouse_id不能为空", c) + return + } + warehouseID, err := strconv.ParseInt(warehouseIDStr, 10, 64) + if err != nil || warehouseID <= 0 { + systemRes.FailWithValidateMessage("warehouse_id格式错误", c) + return + } + + file, _, err := c.Request.FormFile("file") + if err != nil { + systemRes.FailWithValidateMessage("请上传Excel文件", c) + return + } + defer file.Close() + + fileBytes, err := io.ReadAll(file) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelRequest, "商品回传导入-读取文件失败", err, c, nil) + return + } + + result, err := productService.ReimportProducts(fileBytes, warehouseID, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "商品回传导入异常", err, c, nil) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": "导入完成", + "data": result, + }) +} + +// DestroyProduct 销毁商品 +func (r *ProductApi) DestroyProduct(c *gin.Context) { + var req systemReq.DestroyProductRequest + + if err := c.ShouldBind(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "销毁商品请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + userInfo := utils.GetUserInfo(c) + logID, err := productService.DestroyProduct(req, userInfo.Username, userInfo.ID, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "销毁商品异常", err, c, req) + return + } + + systemRes.OkWithDetailed(gin.H{"destroy_log_id": logID}, "商品已销毁", c) +} + +// RestoreProduct 还原商品 +func (r *ProductApi) RestoreProduct(c *gin.Context) { + var req systemReq.RestoreProductRequest + + if err := c.ShouldBind(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "还原商品请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + userInfo := utils.GetUserInfo(c) + if err := productService.RestoreProduct(req, userInfo.Username, userInfo.ID, database.GetDB(c)); err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "还原商品异常", err, c, req) + return + } + + systemRes.OkWithMessage("商品已还原", c) +} + +// GetDestroyLogList 获取销毁日志列表 +func (r *ProductApi) GetDestroyLogList(c *gin.Context) { + var req systemReq.GetDestroyLogListRequest + + if err := c.ShouldBindQuery(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "销毁日志列表请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + result, err := productService.GetDestroyLogList(req, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "销毁日志列表异常", err, c, req) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": result, + }) +} + +// GetDestroyLogDetail 获取销毁日志详情 +func (r *ProductApi) GetDestroyLogDetail(c *gin.Context) { + var req systemReq.GetDestroyLogDetailRequest + + if err := c.ShouldBindQuery(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "销毁日志详情请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + result, err := productService.GetDestroyLogDetail(req, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "销毁日志详情异常", err, c, req) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": result, + }) +} diff --git a/controllers/split_account_deduction_log.go b/controllers/split_account_deduction_log.go index 1a33aad..9830b7e 100644 --- a/controllers/split_account_deduction_log.go +++ b/controllers/split_account_deduction_log.go @@ -17,6 +17,27 @@ type SplitAccountDeductionLogApi struct{} var splitAccountDeductionLogService = service.SplitAccountDeductionLogService{} +// GetOpenSplitAccountDeductionLogList 公开获取分账扣钱日志列表(无需签名认证) +func (r *SplitAccountDeductionLogApi) GetOpenSplitAccountDeductionLogList(c *gin.Context) { + var req systemReq.GetSplitAccountDeductionLogListRequest + + if err := c.ShouldBindQuery(&req); err != nil { + ValidAndFail(constant.LoggerChannelRequest, "公开分账扣钱日志列表请求参数异常", "参数错误: "+err.Error(), c, err) + return + } + + result, err := splitAccountDeductionLogService.GetSplitAccountDeductionLogList(req, database.GetDB(c)) + if err != nil { + utils.FailWithRequestLog(constant.LoggerChannelWork, "公开分账扣钱日志列表异常", err, c, req) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": result, + }) +} + // GetSplitAccountDeductionLogList 获取分账扣钱日志列表 func (r *SplitAccountDeductionLogApi) GetSplitAccountDeductionLogList(c *gin.Context) { var req systemReq.GetSplitAccountDeductionLogListRequest @@ -79,8 +100,25 @@ func (r *SplitAccountDeductionLogApi) CreateSplitAccountDeductionLog(c *gin.Cont return } + if req.TotalAmount == nil { + systemRes.FailWithValidateMessage("TotalAmount不能为空", c) + return + } + if req.DeductionAmount == nil { + systemRes.FailWithValidateMessage("DeductionAmount不能为空", c) + return + } + if req.RemainingAmount == nil { + systemRes.FailWithValidateMessage("RemainingAmount不能为空", c) + return + } + userInfo := utils.GetUserInfo(c) - id, err := splitAccountDeductionLogService.CreateSplitAccountDeductionLog(req, userInfo.Username, database.GetDB(c)) + createdBy := req.CreatedBy + if createdBy == "" { + createdBy = userInfo.Username + } + id, err := splitAccountDeductionLogService.CreateSplitAccountDeductionLog(req, createdBy, database.GetDB(c)) if err != nil { utils.FailWithRequestLog(constant.LoggerChannelWork, "创建分账扣钱日志异常", err, c, req) return diff --git a/database/mysql.go b/database/mysql.go index 2fc2aa6..76980d1 100644 --- a/database/mysql.go +++ b/database/mysql.go @@ -245,6 +245,11 @@ func Init() { log.Fatal("ProductLog表迁移失败:", err) } + err = DB.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品销毁日志表'").AutoMigrate(&models.ProductDestroyLog{}) + if err != nil { + log.Fatal("ProductDestroyLog表迁移失败:", err) + } + err = DB.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户类型表'").AutoMigrate(&models.UserType{}) if err != nil { log.Fatal("UserType表迁移失败:", err) diff --git a/models/product_destroy_log.go b/models/product_destroy_log.go new file mode 100644 index 0000000..7596826 --- /dev/null +++ b/models/product_destroy_log.go @@ -0,0 +1,32 @@ +package models + +import "gorm.io/datatypes" + +// ProductDestroyLog 商品销毁日志表 +type ProductDestroyLog struct { + ID int64 `json:"id" gorm:"primarykey;comment:日志ID"` + ProductID int64 `json:"product_id" gorm:"not null;default:0;index;comment:商品ID"` + Barcode string `json:"barcode" gorm:"size:100;not null;default:'';index;comment:条码"` + ProductSnapshot datatypes.JSON `json:"product_snapshot" gorm:"type:json;not null;comment:商品快照(完整product表数据)"` + ProductBookSnapshot datatypes.JSON `json:"product_book_snapshot" gorm:"type:json;comment:书籍快照(product_book表数据)"` + InventorySnapshot datatypes.JSON `json:"inventory_snapshot" gorm:"type:json;comment:库存汇总快照"` + InventoryDetailSnapshot datatypes.JSON `json:"inventory_detail_snapshot" gorm:"type:json;comment:库存明细快照"` + DestroyedBy string `json:"destroyed_by" gorm:"size:100;not null;default:'';comment:销毁人"` + DestroyedByID int64 `json:"destroyed_by_id" gorm:"not null;default:0;comment:销毁人ID"` + DestroyedAt int64 `json:"destroyed_at" gorm:"type:bigint;not null;default:0;comment:销毁时间戳"` + RestoredBy string `json:"restored_by" gorm:"size:100;not null;default:'';comment:还原人"` + RestoredByID int64 `json:"restored_by_id" gorm:"not null;default:0;comment:还原人ID"` + RestoredAt int64 `json:"restored_at" gorm:"type:bigint;not null;default:0;comment:还原时间戳"` + Status int8 `json:"status" gorm:"type:tinyint(1);not null;default:0;comment:状态(0:已销毁,1:已还原)"` + CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:创建时间戳"` + UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:更新时间戳"` + IsDel int8 `json:"is_del" gorm:"type:tinyint(1);not null;default:0;comment:逻辑删除"` +} + +func (ProductDestroyLog) TableName() string { + return "product_destroy_log" +} + +func (ProductDestroyLog) TableOptions() string { + return "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品销毁日志表'" +} diff --git a/models/request/outbound.go b/models/request/outbound.go index d87fff6..57f380e 100644 --- a/models/request/outbound.go +++ b/models/request/outbound.go @@ -2,14 +2,16 @@ package request // GetOutboundOrderListRequest 获取出库单列表请求 type GetOutboundOrderListRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` - OutNo string `form:"out_no"` - Status int8 `form:"status"` - CustomerID int64 `form:"customer_id"` - WarehouseID int64 `form:"warehouse_id"` - StartDate int64 `form:"start_date"` - EndDate int64 `form:"end_date"` + Page int `form:"page"` + PageSize int `form:"page_size"` + OutNo string `form:"out_no"` + Status int8 `form:"status"` + CustomerID int64 `form:"customer_id"` + WarehouseID int64 `form:"warehouse_id"` + StartDate int64 `form:"start_date"` + EndDate int64 `form:"end_date"` + AssociationOrderNo string `form:"association_order_no"` + LogisticsNo string `form:"logistics_no"` } // GetOutboundOrderDetailRequest 获取出库单详情请求 diff --git a/models/request/process.go b/models/request/process.go index 5eac127..37d1f0f 100644 --- a/models/request/process.go +++ b/models/request/process.go @@ -133,6 +133,11 @@ type CancelSalesOrderRequest struct { OrderID int64 `form:"order_id" binding:"required"` // 订单ID } +// UnlockSalesOrderInventoryRequest 解锁销售订单库存请求 +type UnlockSalesOrderInventoryRequest struct { + SoNo string `form:"so_no" binding:"required"` // 订单编号 +} + type CancelOutboundWaveRequest struct { WaveID int64 `form:"wave_id" binding:"required"` // 波次ID } diff --git a/models/request/product.go b/models/request/product.go index d79402a..79b9d5d 100644 --- a/models/request/product.go +++ b/models/request/product.go @@ -197,3 +197,28 @@ type UpdateProductNameAndImagesRequest struct { Name string `form:"name"` // 商品名称(可选) LiveImage []string `form:"live_image[]"` // 商品实拍图(可选,支持单图或多图) } + +// 追加到 models/request/product.go 末尾 + +// DestroyProductRequest 销毁商品请求 +type DestroyProductRequest struct { + ProductID int64 `form:"product_id" binding:"required"` // 商品ID +} + +// RestoreProductRequest 还原商品请求 +type RestoreProductRequest struct { + DestroyLogID int64 `form:"destroy_log_id" binding:"required"` // 销毁日志ID +} + +// GetDestroyLogListRequest 获取销毁日志列表请求 +type GetDestroyLogListRequest struct { + Page int `form:"page"` + PageSize int `form:"page_size"` + Keyword string `form:"keyword"` // 搜索条码或商品名称 + Status *int8 `form:"status"` // 0:已销毁 1:已还原 nil:全部 +} + +// GetDestroyLogDetailRequest 获取销毁日志详情请求 +type GetDestroyLogDetailRequest struct { + ID int64 `form:"id" binding:"required"` // 销毁日志ID +} diff --git a/models/request/product_reimport.go b/models/request/product_reimport.go new file mode 100644 index 0000000..cc8b384 --- /dev/null +++ b/models/request/product_reimport.go @@ -0,0 +1,10 @@ +package request + +type ProductReimportResult struct { + UpdatedCount int `json:"updated_count"` + CreatedCount int `json:"created_count"` + SkippedCount int `json:"skipped_count"` + FailCount int `json:"fail_count"` + FailDetails []string `json:"fail_details,omitempty"` + Message string `json:"message"` +} diff --git a/models/request/sales.go b/models/request/sales.go index bb6b7b6..0a9a373 100644 --- a/models/request/sales.go +++ b/models/request/sales.go @@ -2,14 +2,16 @@ package request // GetSalesOrderListRequest 获取销售订单列表请求 type GetSalesOrderListRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` - SoNo string `form:"so_no"` - Status int8 `form:"status"` - CustomerID int64 `form:"customer_id"` - WarehouseID int64 `form:"warehouse_id"` - StartDate int64 `form:"start_date"` - EndDate int64 `form:"end_date"` + Page int `form:"page"` + PageSize int `form:"page_size"` + SoNo string `form:"so_no"` + Status int8 `form:"status"` + CustomerID int64 `form:"customer_id"` + WarehouseID int64 `form:"warehouse_id"` + StartDate int64 `form:"start_date"` + EndDate int64 `form:"end_date"` + AssociationOrderNo string `form:"association_order_no"` + LogisticsNo string `form:"logistics_no"` } // GetSalesOrderDetailRequest 获取销售订单详情请求 @@ -27,4 +29,12 @@ type GetSalesOrderDetailListRequest struct { //WarehouseID int64 `form:"warehouse_id"` //StartDate int64 `form:"start_date"` //EndDate int64 `form:"end_date"` + SoNo string `form:"so_no"` + Status int8 `form:"status"` + CustomerID int64 `form:"customer_id"` + WarehouseID int64 `form:"warehouse_id"` + StartDate int64 `form:"start_date"` + EndDate int64 `form:"end_date"` + AssociationOrderNo string `form:"association_order_no"` + LogisticsNo string `form:"logistics_no"` } diff --git a/models/request/shipping.go b/models/request/shipping.go index 020e4d7..f58b074 100644 --- a/models/request/shipping.go +++ b/models/request/shipping.go @@ -2,13 +2,15 @@ package request // GetShippingOrderListRequest 获取发货单列表请求 type GetShippingOrderListRequest struct { - Page int `form:"page" json:"page" binding:"omitempty,min=1"` - PageSize int `form:"page_size" json:"page_size" binding:"omitempty,min=1,max=100"` - Status int8 `form:"status" json:"status"` - CustomerID int64 `form:"customer_id" json:"customer_id"` - ShippingNo string `form:"shipping_no" json:"shipping_no"` - StartDate int64 `form:"start_date" json:"start_date"` - EndDate int64 `form:"end_date" json:"end_date"` + Page int `form:"page" json:"page" binding:"omitempty,min=1"` + PageSize int `form:"page_size" json:"page_size" binding:"omitempty,min=1,max=100"` + Status int8 `form:"status" json:"status"` + CustomerID int64 `form:"customer_id" json:"customer_id"` + ShippingNo string `form:"shipping_no" json:"shipping_no"` + StartDate int64 `form:"start_date" json:"start_date"` + EndDate int64 `form:"end_date" json:"end_date"` + AssociationOrderNo string `form:"association_order_no" json:"association_order_no"` + LogisticsNo string `form:"logistics_no" json:"logistics_no"` } // GetShippingOrderDetailRequest 获取发货单详情请求 @@ -18,11 +20,13 @@ type GetShippingOrderDetailRequest struct { // GetShippingOrderDetailListRequest 获取发货单详情列表请求 type GetShippingOrderDetailListRequest struct { - Page int `form:"page" json:"page" binding:"omitempty,min=1"` - PageSize int `form:"page_size" json:"page_size" binding:"omitempty,min=1,max=100"` - Status int8 `form:"status" json:"status"` - CustomerID int64 `form:"customer_id" json:"customer_id"` - ShippingNo string `form:"shipping_no" json:"shipping_no"` - StartDate int64 `form:"start_date" json:"start_date"` - EndDate int64 `form:"end_date" json:"end_date"` + Page int `form:"page" json:"page" binding:"omitempty,min=1"` + PageSize int `form:"page_size" json:"page_size" binding:"omitempty,min=1,max=100"` + Status int8 `form:"status" json:"status"` + CustomerID int64 `form:"customer_id" json:"customer_id"` + ShippingNo string `form:"shipping_no" json:"shipping_no"` + StartDate int64 `form:"start_date" json:"start_date"` + EndDate int64 `form:"end_date" json:"end_date"` + AssociationOrderNo string `form:"association_order_no" json:"association_order_no"` + LogisticsNo string `form:"logistics_no" json:"logistics_no"` } diff --git a/models/request/split_account_deduction_log.go b/models/request/split_account_deduction_log.go index ec7bf43..22d80f1 100644 --- a/models/request/split_account_deduction_log.go +++ b/models/request/split_account_deduction_log.go @@ -12,13 +12,14 @@ type GetSplitAccountDeductionLogListRequest struct { // AddSplitAccountDeductionLogRequest 添加分账扣钱日志请求 type AddSplitAccountDeductionLogRequest struct { - BusinessNo string `form:"business_no" binding:"required"` // 业务单号 - ConfigID int64 `form:"config_id" binding:"required"` // 分账配置ID - ConfigName string `form:"config_name" binding:"required"` // 分账配置名称 - DeductionDetails string `form:"deduction_details" binding:"required"` // 扣钱详情 - TotalAmount float64 `form:"total_amount" binding:"required"` // 总金额 - DeductionAmount float64 `form:"deduction_amount" binding:"required"` // 扣钱金额 - RemainingAmount float64 `form:"remaining_amount" binding:"required"` // 剩余金额 + BusinessNo string `form:"business_no" json:"business_no" binding:"required"` // 业务单号 + ConfigID int64 `form:"config_id" json:"config_id" binding:"required"` // 分账配置ID + ConfigName string `form:"config_name" json:"config_name" binding:"required"` // 分账配置名称 + DeductionDetails string `form:"deduction_details" json:"deduction_details" binding:"required"` // 扣钱详情 + TotalAmount *float64 `form:"total_amount" json:"total_amount"` // 总金额(传0为有效值) + DeductionAmount *float64 `form:"deduction_amount" json:"deduction_amount"` // 扣钱金额 + RemainingAmount *float64 `form:"remaining_amount" json:"remaining_amount"` // 剩余金额 + CreatedBy string `form:"created_by" json:"created_by"` // 创建人/系统 } // UpdateSplitAccountDeductionLogRequest 更新分账扣钱日志请求 diff --git a/models/response/outbound.go b/models/response/outbound.go index a59ec78..65867a9 100644 --- a/models/response/outbound.go +++ b/models/response/outbound.go @@ -46,13 +46,17 @@ type OutboundOrderItem struct { WarehouseID int64 `json:"warehouse_id"` WarehouseName string `json:"warehouse_name"` ShopList []OutboundShopInfo `json:"shop_list"` - Status int8 `json:"status"` - StatusText string `json:"status_text"` - Operator string `json:"operator"` - OperatorID int64 `json:"operator_id"` - Remark string `json:"remark"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + + AssociationOrderNo string `json:"association_order_no"` //第三方订单编号 + LogisticsNo string `json:"logistics_no"` //快递单号 + + Status int8 `json:"status"` + StatusText string `json:"status_text"` + Operator string `json:"operator"` + OperatorID int64 `json:"operator_id"` + Remark string `json:"remark"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } // OutboundOrderDetailResponse 发货单详情响应 @@ -99,7 +103,7 @@ type OutboundOrderDetailItem struct { } // ConvertOutboundOrderToItem 将发货单模型转换为响应项 -func ConvertOutboundOrderToItem(order models.OutboundOrder, customerName string, warehouseName string, shopList []OutboundShopInfo) OutboundOrderItem { +func ConvertOutboundOrderToItem(order models.OutboundOrder, customerName string, warehouseName string, shopList []OutboundShopInfo, associationOrderNo string, logisticsNo string) OutboundOrderItem { return OutboundOrderItem{ ID: order.ID, OutboundNo: order.OutNo, @@ -109,13 +113,17 @@ func ConvertOutboundOrderToItem(order models.OutboundOrder, customerName string, WarehouseID: order.WarehouseID, WarehouseName: warehouseName, ShopList: shopList, - Status: order.Status, - StatusText: GetOutboundOrderStatusText(order.Status), - Operator: order.Operator, - OperatorID: order.OperatorID, - Remark: order.Remark, - CreatedAt: order.CreatedAt, - UpdatedAt: order.UpdatedAt, + + AssociationOrderNo: associationOrderNo, + LogisticsNo: logisticsNo, + + Status: order.Status, + StatusText: GetOutboundOrderStatusText(order.Status), + Operator: order.Operator, + OperatorID: order.OperatorID, + Remark: order.Remark, + CreatedAt: order.CreatedAt, + UpdatedAt: order.UpdatedAt, } } diff --git a/models/response/process.go b/models/response/process.go index 86ca28c..2742d60 100644 --- a/models/response/process.go +++ b/models/response/process.go @@ -49,3 +49,15 @@ type ReceivingItemResponse struct { Quantity int64 `json:"quantity"` UnitName string `json:"unit_name"` } +type UnlockInventoryItemResponse struct { + ProductID int64 `json:"product_id"` + ProductName string `json:"product_name"` + ProductCode string `json:"product_code"` + UnlockedQuantity int64 `json:"unlocked_quantity"` +} + +type UnlockInventoryResponse struct { + SoNo string `json:"so_no"` + AssociationOrderNo string `json:"association_order_no"` + Items []UnlockInventoryItemResponse `json:"items"` +} diff --git a/models/response/product.go b/models/response/product.go index 357d836..8016f42 100644 --- a/models/response/product.go +++ b/models/response/product.go @@ -6,10 +6,10 @@ import ( ) type ProductListResponse struct { - List []ProductItem `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"pageSize"` + List []ProductItem `json:"list"` // 商品列表 + Total int64 `json:"total"` // 商品总数 + Page int `json:"page"` // 当前页码 + PageSize int `json:"pageSize"` // 每页数量 LocatedCount int64 `json:"located_count"` // 已落位商品数 UnlocatedCount int64 `json:"unlocated_count"` // 未落位商品数 EnabledCount int64 `json:"enabled_count"` // 启用中商品数 diff --git a/models/response/product_destroy.go b/models/response/product_destroy.go new file mode 100644 index 0000000..666a24f --- /dev/null +++ b/models/response/product_destroy.go @@ -0,0 +1,57 @@ +package response + +// DestroyLogItem 销毁日志列表项 +type DestroyLogItem struct { + ID int64 `json:"id"` //用的这个字段 + ProductID int64 `json:"product_id"` + Barcode string `json:"barcode"` + ProductName string `json:"product_name"` + DestroyedBy string `json:"destroyed_by"` + DestroyedAt int64 `json:"destroyed_at"` + RestoredBy string `json:"restored_by"` + RestoredAt int64 `json:"restored_at"` + Status int8 `json:"status"` + StatusText string `json:"status_text"` + CreatedAt int64 `json:"created_at"` +} + +// DestroyLogListResponse 销毁日志列表响应 +type DestroyLogListResponse struct { + List []DestroyLogItem `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// DestroyLogDetailResponse 销毁日志详情响应 +type DestroyLogDetailResponse struct { + ID int64 `json:"id"` + ProductID int64 `json:"product_id"` + Barcode string `json:"barcode"` + ProductSnapshot string `json:"product_snapshot"` + ProductBookSnapshot string `json:"product_book_snapshot"` + InventorySnapshot string `json:"inventory_snapshot"` + InventoryDetailSnapshot string `json:"inventory_detail_snapshot"` + DestroyedBy string `json:"destroyed_by"` + DestroyedByID int64 `json:"destroyed_by_id"` + DestroyedAt int64 `json:"destroyed_at"` + RestoredBy string `json:"restored_by"` + RestoredByID int64 `json:"restored_by_id"` + RestoredAt int64 `json:"restored_at"` + Status int8 `json:"status"` + StatusText string `json:"status_text"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// GetDestroyLogStatusText 获取销毁日志状态文本 +func GetDestroyLogStatusText(status int8) string { + switch status { + case 0: + return "已销毁" + case 1: + return "已还原" + default: + return "未知" + } +} diff --git a/models/response/sales.go b/models/response/sales.go index 9acddd5..9bab858 100644 --- a/models/response/sales.go +++ b/models/response/sales.go @@ -51,6 +51,7 @@ type SalesOrderItem struct { SalesPerson string `json:"sales_person"` SalesPersonID int64 `json:"sales_person_id"` Remark string `json:"remark"` + LogisticsNo string `json:"logistics_no"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -70,6 +71,8 @@ type SalesOrderDetailResponse struct { StatusText string `json:"status_text"` SalesPerson string `json:"sales_person"` SalesPersonID int64 `json:"sales_person_id"` + AssociationOrderNo string `json:"association_order_no"` + LogisticsNo string `json:"logistics_no"` Remark string `json:"remark"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` @@ -95,13 +98,15 @@ type SalesOrderDetailItem struct { ReceiverName string `json:"receiver_name"` ReceiverPhone string `json:"receiver_phone"` ReceiverAddress string `json:"receiver_address"` + LogisticsCompany string `json:"logistics_company"` + LogisticsNo string `json:"logistics_no"` LocationCode string `json:"location_code"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } // ConvertSalesOrderToItem 将销售订单模型转换为响应项 -func ConvertSalesOrderToItem(order models.SalesOrder, customerName string, warehouseName string) SalesOrderItem { +func ConvertSalesOrderToItem(order models.SalesOrder, customerName string, warehouseName string, logisticsNo string) SalesOrderItem { return SalesOrderItem{ ID: order.ID, SoNo: order.SoNo, @@ -122,6 +127,7 @@ func ConvertSalesOrderToItem(order models.SalesOrder, customerName string, wareh SalesPerson: order.SalesPerson, SalesPersonID: order.SalesPersonID, Remark: order.Remark, + LogisticsNo: logisticsNo, CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, } @@ -143,10 +149,14 @@ func ConvertSalesOrderToDetail(order models.SalesOrder, customerName string, war StatusText: GetSalesOrderStatusText(order.Status), SalesPerson: order.SalesPerson, SalesPersonID: order.SalesPersonID, - Remark: order.Remark, - CreatedAt: order.CreatedAt, - UpdatedAt: order.UpdatedAt, - Items: items, + + AssociationOrderNo: order.AssociationOrderNo, + LogisticsNo: "", + + Remark: order.Remark, + CreatedAt: order.CreatedAt, + UpdatedAt: order.UpdatedAt, + Items: items, } } diff --git a/models/response/shipping.go b/models/response/shipping.go index 3331025..df82d93 100644 --- a/models/response/shipping.go +++ b/models/response/shipping.go @@ -66,6 +66,8 @@ type ShippingOrderItem struct { UpdatedAt *int64 `json:"updated_at"` Remark string `json:"remark"` ShopList []OutboundShopInfo `json:"shop_list"` + AssociationOrderNo string `json:"association_order_no"` + LogisticsNo string `json:"logistics_no"` } // ShippingOrderDetailResponse 发货单详情响应 @@ -124,7 +126,7 @@ type ShippingOrderDetailItem struct { } // ConvertShippingOrderToItem 将发货单模型转换为响应项 -func ConvertShippingOrderToItem(order models.ShippingOrder, customerName string, shopList []OutboundShopInfo) ShippingOrderItem { +func ConvertShippingOrderToItem(order models.ShippingOrder, customerName string, shopList []OutboundShopInfo, associationOrderNo string, logisticsNo string) ShippingOrderItem { return ShippingOrderItem{ ID: order.ID, ShippingNo: order.ShippingNo, @@ -140,6 +142,8 @@ func ConvertShippingOrderToItem(order models.ShippingOrder, customerName string, CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, Remark: order.Remark, + AssociationOrderNo: associationOrderNo, + LogisticsNo: logisticsNo, } } diff --git a/routes/routes.go b/routes/routes.go index b308143..559752c 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -90,9 +90,12 @@ func initRouter() (r *gin.Engine) { public.GET("/split-account-deduction-log/list", splitAccountDeductionLogApi.GetSplitAccountDeductionLogList) // 获取分账扣钱日志列表 public.GET("/split-account-deduction-log/detail/:id", splitAccountDeductionLogApi.GetSplitAccountDeductionLogDetail) // 获取分账扣钱日志详情 - public.POST("/split-account-deduction-log/create", splitAccountDeductionLogApi.CreateSplitAccountDeductionLog) // 创建分账 - public.PUT("/split-account-deduction-log/update", splitAccountDeductionLogApi.UpdateSplitAccountDeductionLog) // 更新分账 - public.DELETE("/split-account-deduction-log/delete", splitAccountDeductionLogApi.DeleteSplitAccountDeductionLog) // 删除分账 + public.GET("/open/split-account-deduction-log/list", splitAccountDeductionLogApi.GetOpenSplitAccountDeductionLogList) + public.POST("/split-account-deduction-log/update", splitAccountDeductionLogApi.UpdateSplitAccountDeductionLog) // 更新分账扣钱日志 + public.POST("/sales-order/unlock-inventory", processApi.UnlockSalesOrderInventory) // 解锁销售订单库存 + /* public.POST("/split-account-deduction-log/create", splitAccountDeductionLogApi.CreateSplitAccountDeductionLog) // 创建分账 + public.PUT("/split-account-deduction-log/update", splitAccountDeductionLogApi.UpdateSplitAccountDeductionLog) // 更新分账 + public.DELETE("/split-account-deduction-log/delete", splitAccountDeductionLogApi.DeleteSplitAccountDeductionLog) // 删除分账*/ } @@ -155,9 +158,17 @@ func initRouter() (r *gin.Engine) { auth.POST("/product/updateNameAndImages", productApi.UpdateProductNameAndImages) // 修改商品名称和实拍图 - auth.POST("/product/delete", productApi.DeleteProduct) // 删除商品 - auth.POST("/product/retry-out-task", productApi.RetryOutTask) // 重新出库 - auth.GET("/product/export", productApi.ExportProducts) // 导出商品 + auth.POST("/product/delete", productApi.DeleteProduct) // 删除商品 + auth.POST("/product/retry-out-task", productApi.RetryOutTask) // 重新出库 + auth.GET("/product/export", productApi.ExportProducts) // 导出商品 + + auth.POST("/product/reimport", productApi.ReimportProducts) // 导出修改后回传导入 + + auth.POST("/product/destroy", productApi.DestroyProduct) // 销毁商品 + auth.POST("/product/restore", productApi.RestoreProduct) // 还原商品 + auth.GET("/product/destroy-log/list", productApi.GetDestroyLogList) // 销毁日志列表 + auth.GET("/product/destroy-log/detail", productApi.GetDestroyLogDetail) // 销毁日志详情 + auth.GET("/product/shop-detail", productApi.GetShopProductDetail) // 获取商品在店铺的详情 // 条形码 auth.POST("/barcode/generate", barcodeApi.GenerateBarcode) // 生成条形码 @@ -181,8 +192,10 @@ func initRouter() (r *gin.Engine) { auth.GET("/outbound/detail/:id", processApi.GetOutboundDetail) // 获取出库单详情 auth.POST("/shipping-order/create", processApi.CreateShippingOrder) // 创建发货单 auth.POST("/shipping-order/update", processApi.UpdateShippingLogistics) // 更新发货单物流信息 - auth.POST("/sales-order/cancel", processApi.CancelSalesOrder) // 取消销售订单 - auth.POST("/wave/outbound/cancel", processApi.CancelOutboundWave) // 取消出库波次 + auth.POST("/sales-order/cancel", processApi.CancelSalesOrder) // 取消销售订 + //auth.POST("/sales-order/unlock-inventory", processApi.UnlockSalesOrderInventory) // 解锁销售订单库存 + + auth.POST("/wave/outbound/cancel", processApi.CancelOutboundWave) // 取消出库波次 // 盘库 auth.POST("/stock_check/adjust", processApi.AdjustInventory) // 盘库 auth.POST("/stock_check/return", processApi.ReturnInventory) // 盘库退货 @@ -251,12 +264,14 @@ func initRouter() (r *gin.Engine) { auth.POST("/split-account-config/create", splitAccountConfigApi.CreateSplitAccountConfig) // 创建分账配置 auth.PUT("/split-account-config/update", splitAccountConfigApi.UpdateSplitAccountConfig) // 更新分账配置 auth.DELETE("/split-account-config/delete", splitAccountConfigApi.DeleteSplitAccountConfig) // 删除分账配置 - // 分账扣钱日志管理 - /*auth.GET("/split-account-deduction-log/list", splitAccountDeductionLogApi.GetSplitAccountDeductionLogList) // 获取分账扣钱日志列表 + /*// 分账扣钱日志管理 + auth.GET("/split-account-deduction-log/list", splitAccountDeductionLogApi.GetSplitAccountDeductionLogList) // 获取分账扣钱日志列表 auth.GET("/split-account-deduction-log/detail/:id", splitAccountDeductionLogApi.GetSplitAccountDeductionLogDetail) // 获取分账扣钱日志详情 - auth.POST("/split-account-deduction-log/create", splitAccountDeductionLogApi.CreateSplitAccountDeductionLog) // 创建分账 - auth.PUT("/split-account-deduction-log/update", splitAccountDeductionLogApi.UpdateSplitAccountDeductionLog) // 更新分账 - auth.DELETE("/split-account-deduction-log/delete", splitAccountDeductionLogApi.DeleteSplitAccountDeductionLog) // 删除分账*/ + //public.GET("/open/split-account-deduction-log/list", splitAccountDeductionLogApi.GetOpenSplitAccountDeductionLogList) // 公开获取分账扣钱日志列表(无需签名认证)*/ + + auth.POST("/split-account-deduction-log/create", splitAccountDeductionLogApi.CreateSplitAccountDeductionLog) // 创建分账 + //auth.PUT("/split-account-deduction-log/update", splitAccountDeductionLogApi.UpdateSplitAccountDeductionLog) // 更新分账 + auth.DELETE("/split-account-deduction-log/delete", splitAccountDeductionLogApi.DeleteSplitAccountDeductionLog) // 删除分账*/ // 产品日志管理 auth.GET("/product_log/list", productApi.GetProductLogList) // 获取产品日志列表 auth.POST("/product_log/save", productApi.SaveProductLog) // 保存产品日志 diff --git a/service/outbound.go b/service/outbound.go index b204646..e30039b 100644 --- a/service/outbound.go +++ b/service/outbound.go @@ -45,6 +45,21 @@ func (s *OutboundService) GetOutboundOrderList(req systemReq.GetOutboundOrderLis query = query.Where("outbound_order.created_at <= ?", req.EndDate) } + if req.AssociationOrderNo != "" { + subQuery := databaseConn.Table("outbound_order_item"). + Select("outbound_order_item.out_order_id"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("outbound_order_item.is_del = 0 AND sales_order.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%") + query = query.Where("outbound_order.id IN (?)", subQuery) + } + if req.LogisticsNo != "" { + subQuery := databaseConn.Table("outbound_order_item"). + Select("outbound_order_item.out_order_id"). + Joins("INNER JOIN sales_order_item ON outbound_order_item.sales_order_id = sales_order_item.sales_order_id AND sales_order_item.is_del = 0"). + Where("outbound_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") + query = query.Where("outbound_order.id IN (?)", subQuery) + } + var total int64 if err := query.Count(&total).Error; err != nil { return nil, utils.NewError("查询总数失败") @@ -94,11 +109,27 @@ func (s *OutboundService) GetOutboundOrderList(req systemReq.GetOutboundOrderLis }) } + var associationOrderNos string + databaseConn.Table("outbound_order_item"). + Select("GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', ')"). + Joins("INNER JOIN sales_order so ON outbound_order_item.sales_order_id = so.id AND so.is_del = 0"). + Where("outbound_order_item.out_order_id = ? AND outbound_order_item.is_del = ? AND so.association_order_no != ''", order.ID, 0). + Scan(&associationOrderNos) + + var logisticsNos string + databaseConn.Table("outbound_order_item"). + Select("GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', ')"). + Joins("INNER JOIN sales_order_item soi ON outbound_order_item.sales_order_id = soi.sales_order_id AND outbound_order_item.product_id = soi.product_id AND soi.is_del = 0"). + Where("outbound_order_item.out_order_id = ? AND outbound_order_item.is_del = ? AND soi.logistics_no != ''", order.ID, 0). + Scan(&logisticsNos) + orderItems = append(orderItems, systemRes.ConvertOutboundOrderToItem( order.OutboundOrder, order.CustomerName, order.WarehouseName, shopList, + associationOrderNos, + logisticsNos, )) } diff --git a/service/process.go b/service/process.go index 6411471..b814d57 100644 --- a/service/process.go +++ b/service/process.go @@ -2062,7 +2062,7 @@ func (s *ProcessService) CreateShippingOrder(req systemReq.CreateShippingOrderRe } } - customerID := outboundOrders[0].CustomerID + /*customerID := outboundOrders[0].CustomerID warehouseID := outboundOrders[0].WarehouseID for i, order := range outboundOrders[1:] { @@ -2072,6 +2072,13 @@ func (s *ProcessService) CreateShippingOrder(req systemReq.CreateShippingOrderRe if order.WarehouseID != warehouseID { return fmt.Errorf("所有出库单必须属于同一个仓库,订单[%s]与第一个订单仓库不一致", outboundOrders[i+1].OutNo) } + }*/ + customerID := outboundOrders[0].CustomerID + + for i, order := range outboundOrders[1:] { + if order.CustomerID != customerID { + return fmt.Errorf("所有出库单必须属于同一个客户,订单[%s]与第一个订单客户不一致", outboundOrders[i+1].OutNo) + } } for _, order := range outboundOrders { @@ -3497,6 +3504,110 @@ func (s *ProcessService) CancelSalesOrder(orderID int64, operator string, operat }) } +// UnlockSalesOrderInventory 解锁销售订单库存(不取消订单) +func (s *ProcessService) UnlockSalesOrderInventory(soNo string, operator string, operatorID int64, db ...*gorm.DB) (*systemRes.UnlockInventoryResponse, error) { + databaseConn := database.OptionalDB(db...) + + now := time.Now().Unix() + var unlockResp *systemRes.UnlockInventoryResponse + + err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { + var salesOrder models.SalesOrder + if err := tx.Where("so_no = ? AND is_del = 0", soNo).First(&salesOrder).Error; err != nil { + return fmt.Errorf("销售订单不存在: %v", err) + } + + if salesOrder.Status == constant.SalesStatusShipped { + return fmt.Errorf("订单已发货,无法解锁库存") + } + if salesOrder.Status == constant.SalesStatusCancelled { + return fmt.Errorf("订单已取消,无法解锁库存") + } + if salesOrder.Status != constant.SalesStatusAllocated && salesOrder.Status != constant.SalesStatusPicking { + return fmt.Errorf("订单状态不允许解锁库存,当前状态: %s", getSalesStatusText(salesOrder.Status)) + } + + var orderItems []models.SalesOrderItem + if err := tx.Where("sales_order_id = ? AND is_del = 0", salesOrder.ID).Find(&orderItems).Error; err != nil { + return fmt.Errorf("查询订单明细失败: %v", err) + } + + // 收集需要解锁的商品ID + var productIDs []int64 + for _, item := range orderItems { + if item.AllocatedQuantity > 0 { + productIDs = append(productIDs, item.ProductID) + } + } + + // 查询商品信息 + var products []models.Product + productMap := make(map[int64]models.Product) + if len(productIDs) > 0 { + if err := tx.Where("id IN ? AND is_del = 0", productIDs).Find(&products).Error; err != nil { + return fmt.Errorf("查询商品信息失败: %v", err) + } + for _, p := range products { + productMap[p.ID] = p + } + } + + // 解锁库存并构建响应 + var responseItems []systemRes.UnlockInventoryItemResponse + for _, item := range orderItems { + if item.AllocatedQuantity > 0 { + if err := s.unlockInventory(tx, salesOrder.WarehouseID, item.ProductID, item.AllocatedQuantity, now); err != nil { + return fmt.Errorf("解锁库存失败[商品ID=%d]: %v", item.ProductID, err) + } + + productName := "" + productCode := "" + if p, ok := productMap[item.ProductID]; ok { + productName = p.Name + productCode = p.Barcode + } + + responseItems = append(responseItems, systemRes.UnlockInventoryItemResponse{ + ProductID: item.ProductID, + ProductName: productName, + ProductCode: productCode, + UnlockedQuantity: item.AllocatedQuantity, + }) + } + } + + if err := tx.Model(&models.SalesOrderItem{}). + Where("sales_order_id = ? AND is_del = 0", salesOrder.ID). + Updates(map[string]interface{}{ + "allocated_quantity": 0, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("重置订单明细已分配数量失败: %v", err) + } + + if err := tx.Model(&salesOrder).Updates(map[string]interface{}{ + "status": constant.SalesStatusConfirmed, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("更新销售订单状态失败: %v", err) + } + + unlockResp = &systemRes.UnlockInventoryResponse{ + SoNo: soNo, + AssociationOrderNo: salesOrder.AssociationOrderNo, + Items: responseItems, + } + + return nil + }) + + if err != nil { + return nil, err + } + + return unlockResp, nil +} + // processInventoryOperationForAdjustment 处理盘库调整的库存汇总操作 func (s *ProcessService) processInventoryOperationForAdjustment(tx *gorm.DB, opKey inventoryKey, locationID int64, quantity int64, orderNo string, operator string, operatorID int64, now int64, remark string) (*models.InventoryLog, error) { @@ -4076,7 +4187,19 @@ func (s *ProcessService) syncProductsToExternal(receivingOrderID, waveTaskID, us "isbn": group.product.Barcode, "book_name": group.product.Name, "image_object": map[string]interface{}{ - "carousel_url_array": group.imgList, + "carousel_url_array": group.imgList, + "white_background_url": []string{}, + "detail_url_object": map[string]interface{}{}, + "introduction_url": []string{}, + "catalogue_url": []string{}, + "live_shooting_url": []string{}, + "other_url": []string{}, + "default_image_url": func() string { + if len(group.imgList) > 0 { + return group.imgList[0] + } + return "" + }(), }, }, "detail": map[string]interface{}{ @@ -4133,7 +4256,19 @@ func (s *ProcessService) syncProductsToExternal(receivingOrderID, waveTaskID, us "isbn": isbn, "book_name": product.Name, "image_object": map[string]interface{}{ - "carousel_url_array": imgList, + "carousel_url_array": imgList, + "white_background_url": []string{}, + "detail_url_object": map[string]interface{}{}, + "introduction_url": []string{}, + "catalogue_url": []string{}, + "live_shooting_url": []string{}, + "other_url": []string{}, + "default_image_url": func() string { + if len(imgList) > 0 { + return imgList[0] + } + return "" + }(), }, }, "detail": map[string]interface{}{ diff --git a/service/product.go b/service/product.go index 0fd15d6..b756cd7 100644 --- a/service/product.go +++ b/service/product.go @@ -29,6 +29,332 @@ type OutTaskInfo struct { ShopList []systemRes.ShopInfo } +// ParseExportedExcel 解析系统导出的Excel文件 +func (s *ProductService) ParseExportedExcel(fileBytes []byte) ([]map[string]string, error) { + f, err := excelize.OpenReader(bytes.NewReader(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("表格至少需要包含表头和1行数据") + } + + var result []map[string]string + for _, row := range rows[1:] { + if len(row) == 0 { + continue + } + + getCol := func(idx int) string { + if idx < len(row) { + return strings.TrimSpace(row[idx]) + } + return "" + } + + isbn := getCol(0) + name := getCol(3) + if isbn == "" && name == "" { + continue + } + + result = append(result, map[string]string{ + "isbn": isbn, + "name": name, + "quantity": getCol(4), + "price": getCol(6), + "location_code": getCol(7), + "image": getCol(9), + }) + } + + return result, nil +} + +// ReimportProducts 将导出的Excel修改后导回,覆盖已有商品或新增 +func (s *ProductService) ReimportProducts(fileBytes []byte, warehouseID int64, db ...*gorm.DB) (*systemReq.ProductReimportResult, error) { + databaseConn := database.OptionalDB(db...) + + rows, err := s.ParseExportedExcel(fileBytes) + if err != nil { + return nil, err + } + + if len(rows) == 0 { + return nil, fmt.Errorf("Excel中没有有效数据行") + } + + result := &systemReq.ProductReimportResult{ + FailDetails: make([]string, 0), + } + + now := time.Now().Unix() + + isbns := make([]string, 0, len(rows)) + locationCodes := make([]string, 0) + for _, row := range rows { + if row["isbn"] != "" { + isbns = append(isbns, row["isbn"]) + } + if code := row["location_code"]; code != "" { + locationCodes = append(locationCodes, code) + } + } + + existingProducts := make(map[string]models.Product) + if len(isbns) > 0 { + var products []models.Product + if err := databaseConn.Where("barcode IN ? AND is_del = ?", isbns, 0).Find(&products).Error; err != nil { + return nil, fmt.Errorf("查询已有商品失败: %v", err) + } + for _, p := range products { + existingProducts[p.Barcode] = p + } + } + + locationMap := make(map[string]models.Location) + if len(locationCodes) > 0 { + var locations []models.Location + if err := databaseConn.Where("code IN ? AND warehouse_id = ? AND is_del = ?", locationCodes, warehouseID, 0).Find(&locations).Error; err != nil { + return nil, fmt.Errorf("查询库位失败: %v", err) + } + for _, loc := range locations { + locationMap[loc.Code] = loc + } + } + + tx := databaseConn.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + for _, row := range rows { + isbn := row["isbn"] + name := row["name"] + + var locationID int64 + if code := row["location_code"]; code != "" { + if loc, exists := locationMap[code]; exists { + locationID = loc.ID + } else { + newLoc := models.Location{ + WarehouseID: warehouseID, + Code: code, + Type: 1, + Capacity: 255, + Status: 1, + Sort: 0, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + } + if err := tx.Create(&newLoc).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("创建库位失败(code=%s): %v", code, err) + } + locationMap[code] = newLoc + locationID = newLoc.ID + } + } + + salePrice := int64(0) + if row["price"] != "" { + if f, err := strconv.ParseFloat(row["price"], 64); err == nil { + salePrice = int64(f * 100) + } + } + + quantity := int64(0) + if row["quantity"] != "" { + if v, err := strconv.ParseInt(row["quantity"], 10, 64); err == nil { + quantity = v + } + } + + var liveImage datatypes.JSON + if row["image"] != "" { + b, _ := json.Marshal([]string{row["image"]}) + liveImage = datatypes.JSON(b) + } else { + liveImage = datatypes.JSON("[]") + } + + if existing, found := existingProducts[isbn]; found { + updates := map[string]interface{}{ + "updated_at": now, + } + if name != "" && name != existing.Name { + updates["name"] = name + } + if salePrice > 0 && salePrice != existing.SalePrice { + updates["sale_price"] = salePrice + } + if liveImage != nil && string(liveImage) != string(existing.LiveImage) { + updates["live_image"] = liveImage + } + if locationID > 0 && locationID != existing.LocationID { + updates["location_id"] = locationID + } + + if err := tx.Model(&existing).Updates(updates).Error; err != nil { + result.FailDetails = append(result.FailDetails, fmt.Sprintf("ISBN=%s 更新商品失败: %v", isbn, err)) + result.FailCount++ + continue + } + + if quantity > 0 { + var invDetail models.InventoryDetail + err := tx.Where("product_id = ? AND warehouse_id = ? AND is_del = ?", existing.ID, warehouseID, 0).First(&invDetail).Error + if err == nil { + oldQty := invDetail.Quantity + tx.Model(&invDetail).Updates(map[string]interface{}{ + "quantity": quantity, + "available_quantity": quantity - invDetail.LockedQuantity, + "updated_at": now, + }) + if quantity != oldQty { + tx.Create(&models.InventoryLog{ + WarehouseID: warehouseID, + LocationID: invDetail.LocationID, + ProductID: existing.ID, + BatchNo: invDetail.BatchNo, + ChangeType: constant.InventoryChangeAdjustment, + + ChangeQuantity: quantity - oldQty, + BeforeQuantity: oldQty, + AfterQuantity: quantity, + RelatedOrderType: "", + RelatedOrderNo: "", + Operator: "", + OperatorID: 0, + Remark: "Excel回传覆盖", + CreatedAt: now, + IsDel: 0, + }) + } + } else if err == gorm.ErrRecordNotFound && quantity > 0 { + locID := locationID + if locID == 0 { + locID = existing.LocationID + } + tx.Create(&models.InventoryDetail{ + WarehouseID: warehouseID, + LocationID: locID, + ProductID: existing.ID, + BatchNo: utils.GenerateExcelImportBatchNo(), + Quantity: quantity, + LockedQuantity: 0, + AvailableQuantity: quantity, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + }) + var inv models.Inventory + invErr := tx.Where("product_id = ? AND warehouse_id = ? AND is_del = ?", existing.ID, warehouseID, 0).First(&inv).Error + if invErr == nil { + tx.Model(&inv).Updates(map[string]interface{}{ + "quantity": quantity, + "available_quantity": quantity - inv.LockedQuantity, + "updated_at": now, + }) + } else if invErr == gorm.ErrRecordNotFound { + tx.Create(&models.Inventory{ + WarehouseID: warehouseID, + ProductID: existing.ID, + BatchNo: utils.GenerateExcelImportBatchNo(), + Quantity: quantity, + LockedQuantity: 0, + AvailableQuantity: quantity, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + }) + } + } + } + + result.UpdatedCount++ + } else { + if isbn == "" { + result.FailDetails = append(result.FailDetails, fmt.Sprintf("商品名称=%s ISBN为空,跳过新增", name)) + result.FailCount++ + continue + } + + product := models.Product{ + CategoryID: 1, + StandardProductID: 1, + Name: name, + Barcode: isbn, + Price: 0, + SalePrice: salePrice, + Cost: 0, + LiveImage: liveImage, + Status: 1, + WarehouseID: warehouseID, + LocationID: locationID, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + } + if err := tx.Create(&product).Error; err != nil { + result.FailDetails = append(result.FailDetails, fmt.Sprintf("ISBN=%s 新增商品失败: %v", isbn, err)) + result.FailCount++ + continue + } + + if quantity > 0 { + batchNo := utils.GenerateExcelImportBatchNo() + tx.Create(&models.Inventory{ + WarehouseID: warehouseID, + ProductID: product.ID, + BatchNo: batchNo, + Quantity: quantity, + LockedQuantity: 0, + AvailableQuantity: quantity, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + }) + tx.Create(&models.InventoryDetail{ + WarehouseID: warehouseID, + LocationID: locationID, + ProductID: product.ID, + BatchNo: batchNo, + Quantity: quantity, + LockedQuantity: 0, + AvailableQuantity: quantity, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + }) + } + + result.CreatedCount++ + } + } + + if err := tx.Commit().Error; err != nil { + return nil, fmt.Errorf("提交事务失败: %v", err) + } + + result.Message = fmt.Sprintf("导入完成:更新%d个,新增%d个,失败%d个", result.UpdatedCount, result.CreatedCount, result.FailCount) + if result.FailCount > 0 && len(result.FailDetails) <= 10 { + result.Message += ",失败详情:" + strings.Join(result.FailDetails, "; ") + } + return result, nil +} + // 获取商品列表 func (s *ProductService) GetProductList(req systemReq.GetProductListRequest, db ...*gorm.DB) (*systemRes.ProductListResponse, error) { databaseConn := database.OptionalDB(db...) @@ -674,18 +1000,24 @@ func (s *ProductService) UpdateProductNameAndImages(req systemReq.UpdateProductN return fmt.Errorf("查询商品失败: %v", err) } + fmt.Printf("【UpdateProductNameAndImages Service】原商品图片: %s\n", string(product.LiveImage)) + updates := make(map[string]interface{}) hasUpdate := false if req.Name != "" { updates["name"] = req.Name hasUpdate = true + fmt.Printf("【UpdateProductNameAndImages Service】更新名称: %s\n", req.Name) } - if len(req.LiveImage) > 0 { + // 只要LiveImage不为nil,就覆盖(包括空数组) + // 注意: Gin的ShouldBind对于未传的数组字段会设为nil,对于传的空数组会设为[]string{} + if req.LiveImage != nil { jsonBytes, _ := json.Marshal(req.LiveImage) updates["live_image"] = datatypes.JSON(jsonBytes) hasUpdate = true + fmt.Printf("【UpdateProductNameAndImages Service】覆盖图片为: %s (数量: %d)\n", string(jsonBytes), len(req.LiveImage)) } if !hasUpdate { @@ -698,6 +1030,7 @@ func (s *ProductService) UpdateProductNameAndImages(req systemReq.UpdateProductN return fmt.Errorf("更新商品失败: %v", err) } + fmt.Printf("【UpdateProductNameAndImages Service】更新成功\n") return nil } @@ -1903,12 +2236,9 @@ func (s *ProductService) batchPushProductBody(db *gorm.DB, outTask models.OutTas } for _, imgStr := range rawImgList { - urls := strings.Split(imgStr, ",") - for _, url := range urls { - url = strings.TrimSpace(url) - if url != "" { - imgList = append(imgList, url) - } + imgStr = strings.TrimSpace(imgStr) + if imgStr != "" { + imgList = append(imgList, imgStr) } } } @@ -1925,7 +2255,19 @@ func (s *ProductService) batchPushProductBody(db *gorm.DB, outTask models.OutTas "isbn": isbn, "book_name": product.Name, "image_object": map[string]interface{}{ - "carousel_url_array": imgList, + "carousel_url_array": imgList, + "white_background_url": []string{}, + "detail_url_object": map[string]interface{}{}, + "introduction_url": []string{}, + "catalogue_url": []string{}, + "live_shooting_url": []string{}, + "other_url": []string{}, + "default_image_url": func() string { + if len(imgList) > 0 { + return imgList[0] + } + return "" + }(), }, }, "detail": map[string]interface{}{ @@ -2043,12 +2385,9 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. } for _, imgStr := range rawImgList { - urls := strings.Split(imgStr, ",") - for _, url := range urls { - url = strings.TrimSpace(url) - if url != "" { - imgList = append(imgList, url) - } + imgStr = strings.TrimSpace(imgStr) + if imgStr != "" { + imgList = append(imgList, imgStr) } } } @@ -2058,13 +2397,10 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. } msgJSON, _ := json.Marshal(msgData) - // 获取库存数量 var stock int64 - // 获取运费模板首费 var cost int64 if hasWaveTask { - // 有波次任务明细:通过波次任务 -> 波次头 -> 仓库 -> 物流模板 var logistics models.Logistics err := db.Table("logistics"). Select("logistics.fir_price"). @@ -2080,7 +2416,6 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. cost = int64(logistics.FirPrice * 100) stock = waveTaskDetail.PlannedQuantity } else { - // 无波次任务明细(同步商品):通过库存明细 -> 仓库 -> 物流模板 var inventoryDetail models.InventoryDetail if err := db.Where("product_id = ? AND is_del = ?", product.ID, 0).First(&inventoryDetail).Error; err != nil { return fmt.Errorf("查询库存明细失败: %v", err) @@ -2108,7 +2443,19 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. "isbn": isbn, "book_name": product.Name, "image_object": map[string]interface{}{ - "carousel_url_array": imgList, + "carousel_url_array": imgList, + "white_background_url": []string{}, + "detail_url_object": map[string]interface{}{}, + "introduction_url": []string{}, + "catalogue_url": []string{}, + "live_shooting_url": []string{}, + "other_url": []string{}, + "default_image_url": func() string { + if len(imgList) > 0 { + return imgList[0] + } + return "" + }(), }, }, "detail": map[string]interface{}{ @@ -2130,7 +2477,7 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. taskID := fmt.Sprintf("%d", outTask.OutTaskID) - allBody := strings.Join(bodyList, "") // 直接无缝拼接(和服务端一致) + allBody := strings.Join(bodyList, "") signParams := map[string]string{ "task_id": taskID, @@ -2139,7 +2486,6 @@ func (s *ProductService) syncPriceToExternalTaskWithOptionalWave(outTask models. sign := utils.SignParams(signParams) - // 发送请求 url := config.AppConfig.ExternalAPI.SyncTaskBodyURL resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign) if err != nil { @@ -2248,12 +2594,9 @@ func (s *ProductService) syncPriceToExternalTask(outTask models.OutTask, product } for _, imgStr := range rawImgList { - urls := strings.Split(imgStr, ",") - for _, url := range urls { - url = strings.TrimSpace(url) - if url != "" { - imgList = append(imgList, url) - } + imgStr = strings.TrimSpace(imgStr) + if imgStr != "" { + imgList = append(imgList, imgStr) } } } @@ -2269,7 +2612,19 @@ func (s *ProductService) syncPriceToExternalTask(outTask models.OutTask, product "isbn": isbn, "book_name": product.Name, "image_object": map[string]interface{}{ - "carousel_url_array": imgList, + "carousel_url_array": imgList, + "white_background_url": []string{}, + "detail_url_object": map[string]interface{}{}, + "introduction_url": []string{}, + "catalogue_url": []string{}, + "live_shooting_url": []string{}, + "other_url": []string{}, + "default_image_url": func() string { + if len(imgList) > 0 { + return imgList[0] + } + return "" + }(), }, }, "detail": map[string]interface{}{ @@ -2291,7 +2646,7 @@ func (s *ProductService) syncPriceToExternalTask(outTask models.OutTask, product taskID := fmt.Sprintf("%d", outTask.OutTaskID) - allBody := strings.Join(bodyList, "") // 直接无缝拼接(和服务端一致) + allBody := strings.Join(bodyList, "") signParams := map[string]string{ "task_id": taskID, @@ -2300,7 +2655,6 @@ func (s *ProductService) syncPriceToExternalTask(outTask models.OutTask, product sign := utils.SignParams(signParams) - // 发送请求 url := config.AppConfig.ExternalAPI.SyncTaskBodyURL resp, err := utils.SubmitMultiBody(url, taskID, bodyList, sign) if err != nil { @@ -3197,3 +3551,399 @@ func (s *ProductService) getShopTypeName(shopType int8) string { mainDB.Create(data) }() }*/ +// 追加到 service/product.go 末尾 + +// DestroyProduct 销毁商品(写入快照日志 + 逻辑删除商品) +func (s *ProductService) DestroyProduct(req systemReq.DestroyProductRequest, operator string, operatorID int64, db ...*gorm.DB) (int64, error) { + databaseConn := database.OptionalDB(db...) + + var product models.Product + if err := databaseConn.Where("id = ? AND is_del = ?", req.ProductID, 0).First(&product).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fmt.Errorf("商品不存在或已被销毁") + } + return 0, fmt.Errorf("查询商品失败: %v", err) + } + + now := time.Now().Unix() + + productSnapshot, _ := json.Marshal(product) + + var productBookSnapshot datatypes.JSON + var book models.ProductBook + if product.Barcode != "" { + tableName := models.ProductBookTableName(product.Barcode) + err := databaseConn.Table(tableName).Where("barcode = ? AND is_del = ?", product.Barcode, 0).First(&book).Error + if err == nil { + productBookSnapshot, _ = json.Marshal(book) + } + } + + var inventorySnapshot datatypes.JSON + var inventory models.Inventory + inventoryIDs := make([]int64, 0) + if err := databaseConn.Model(&models.Inventory{}). + Where("product_id = ? AND is_del = ?", req.ProductID, 0).Find(&inventory).Error; err == nil && inventory.ID > 0 { + inventoryIDs = append(inventoryIDs, inventory.ID) + inventorySnapshot, _ = json.Marshal(inventory) + } + + var inventoryDetailSnapshot datatypes.JSON + var inventoryDetails []models.InventoryDetail + databaseConn.Where("product_id = ? AND is_del = ?", req.ProductID, 0).Find(&inventoryDetails) + if len(inventoryDetails) > 0 { + inventoryDetailSnapshot, _ = json.Marshal(inventoryDetails) + } + + destroyLog := models.ProductDestroyLog{ + ProductID: req.ProductID, + Barcode: product.Barcode, + ProductSnapshot: productSnapshot, + ProductBookSnapshot: productBookSnapshot, + InventorySnapshot: inventorySnapshot, + InventoryDetailSnapshot: inventoryDetailSnapshot, + DestroyedBy: operator, + DestroyedByID: operatorID, + DestroyedAt: now, + Status: 0, + CreatedAt: now, + UpdatedAt: now, + IsDel: 0, + } + + err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { + if err := tx.Create(&destroyLog).Error; err != nil { + return fmt.Errorf("写入销毁日志失败: %v", err) + } + + if err := tx.Model(&models.Product{}). + Where("id = ? AND is_del = ?", req.ProductID, 0). + Updates(map[string]interface{}{ + "is_del": 1, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("删除商品失败: %v", err) + } + + if err := tx.Model(&models.Inventory{}). + Where("product_id = ? AND is_del = ?", req.ProductID, 0). + Updates(map[string]interface{}{ + "is_del": 1, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("删除库存汇总失败: %v", err) + } + + if err := tx.Model(&models.InventoryDetail{}). + Where("product_id = ? AND is_del = ?", req.ProductID, 0). + Updates(map[string]interface{}{ + "is_del": 1, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("删除库存明细失败: %v", err) + } + + if product.Barcode != "" && len(productBookSnapshot) > 0 { + tableName := models.ProductBookTableName(product.Barcode) + if err := tx.Table(tableName). + Where("barcode = ? AND is_del = ?", product.Barcode, 0). + Updates(map[string]interface{}{ + "is_del": 1, + "updated_at": now, + }).Error; err != nil { + return fmt.Errorf("删除书籍反射失败: %v", err) + } + } + + for _, _ = range inventoryIDs { + tx.Create(&models.InventoryLog{ + WarehouseID: inventory.WarehouseID, + LocationID: 0, + ProductID: req.ProductID, + BatchNo: inventory.BatchNo, + ChangeType: constant.InventoryChangeAdjustment, + ChangeQuantity: -inventory.Quantity, + BeforeQuantity: inventory.Quantity, + AfterQuantity: 0, + RelatedOrderType: "", + RelatedOrderNo: "", + Operator: operator, + OperatorID: operatorID, + Remark: "商品销毁", + CreatedAt: now, + IsDel: 0, + }) + } + + return nil + }) + + if err != nil { + return 0, err + } + + return destroyLog.ID, nil +} + +// RestoreProduct 还原商品(从销毁日志中恢复) +func (s *ProductService) RestoreProduct(req systemReq.RestoreProductRequest, operator string, operatorID int64, db ...*gorm.DB) error { + databaseConn := database.OptionalDB(db...) + + var destroyLog models.ProductDestroyLog + if err := databaseConn.Where("id = ? AND is_del = ?", req.DestroyLogID, 0).First(&destroyLog).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("销毁日志不存在") + } + return fmt.Errorf("查询销毁日志失败: %v", err) + } + + if destroyLog.Status == 1 { + return fmt.Errorf("该商品已还原,请勿重复操作") + } + + var product models.Product + if err := json.Unmarshal(destroyLog.ProductSnapshot, &product); err != nil { + return fmt.Errorf("解析商品快照失败: %v", err) + } + + product.IsDel = 0 + product.UpdatedAt = time.Now().Unix() + + err := executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error { + var existing models.Product + checkErr := tx.Where("barcode = ? AND is_del = ?", product.Barcode, 0).First(&existing).Error + if checkErr == nil && existing.ID != product.ID { + return fmt.Errorf("条码[%s]已存在其他商品(ID=%d),无法还原", product.Barcode, existing.ID) + } + + var oldProduct models.Product + if err := tx.Where("id = ?", product.ID).First(&oldProduct).Error; err == nil { + if err := tx.Model(&models.Product{}).Where("id = ?", product.ID).Updates(map[string]interface{}{ + "name": product.Name, + "appearance": product.Appearance, + "barcode": product.Barcode, + "price": product.Price, + "sale_price": product.SalePrice, + "cost": product.Cost, + "live_image": product.LiveImage, + "warehouse_id": product.WarehouseID, + "warehouse_name": product.WarehouseName, + "location_id": product.LocationID, + "location_name": product.LocationName, + "category_id": product.CategoryID, + "standard_product_id": product.StandardProductID, + "status": product.Status, + "is_del": 0, + "updated_at": product.UpdatedAt, + }).Error; err != nil { + return fmt.Errorf("恢复商品失败: %v", err) + } + } else if errors.Is(err, gorm.ErrRecordNotFound) { + product.CreatedAt = time.Now().Unix() + product.UpdatedAt = product.CreatedAt + if err := tx.Create(&product).Error; err != nil { + return fmt.Errorf("创建商品失败: %v", err) + } + } else { + return fmt.Errorf("查询商品失败: %v", err) + } + + if len(destroyLog.ProductBookSnapshot) > 0 { + var book models.ProductBook + if err := json.Unmarshal(destroyLog.ProductBookSnapshot, &book); err == nil { + book.IsDel = 0 + book.UpdatedAt = time.Now().Unix() + tableName := models.ProductBookTableName(book.Barcode) + var oldBook models.ProductBook + if err := tx.Table(tableName).Where("barcode = ?", book.Barcode).First(&oldBook).Error; err == nil { + tx.Table(tableName).Where("barcode = ?", book.Barcode).Updates(map[string]interface{}{ + "is_del": 0, + "updated_at": book.UpdatedAt, + }) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + book.CreatedAt = time.Now().Unix() + book.UpdatedAt = book.CreatedAt + tx.Table(tableName).Create(&book) + } + } + } + + if len(destroyLog.InventorySnapshot) > 0 { + var inventory models.Inventory + if err := json.Unmarshal(destroyLog.InventorySnapshot, &inventory); err == nil { + inventory.IsDel = 0 + inventory.UpdatedAt = time.Now().Unix() + var oldInv models.Inventory + if err := tx.Where("product_id = ? AND warehouse_id = ?", inventory.ProductID, inventory.WarehouseID).First(&oldInv).Error; err == nil { + tx.Model(&oldInv).Updates(map[string]interface{}{ + "is_del": 0, + "quantity": inventory.Quantity, + "updated_at": inventory.UpdatedAt, + }) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + inventory.CreatedAt = time.Now().Unix() + inventory.UpdatedAt = inventory.CreatedAt + tx.Create(&inventory) + } + + tx.Create(&models.InventoryLog{ + WarehouseID: inventory.WarehouseID, + ProductID: inventory.ProductID, + BatchNo: inventory.BatchNo, + ChangeType: constant.InventoryChangeAdjustment, + ChangeQuantity: inventory.Quantity, + BeforeQuantity: 0, + AfterQuantity: inventory.Quantity, + RelatedOrderType: "", + RelatedOrderNo: "", + Operator: operator, + OperatorID: operatorID, + Remark: "商品还原", + CreatedAt: time.Now().Unix(), + IsDel: 0, + }) + } + } + + if len(destroyLog.InventoryDetailSnapshot) > 0 { + var details []models.InventoryDetail + if err := json.Unmarshal(destroyLog.InventoryDetailSnapshot, &details); err == nil { + for _, d := range details { + d.IsDel = 0 + d.UpdatedAt = time.Now().Unix() + var oldDetail models.InventoryDetail + if err := tx.Where("product_id = ? AND location_id = ? AND warehouse_id = ?", d.ProductID, d.LocationID, d.WarehouseID).First(&oldDetail).Error; err == nil { + tx.Model(&oldDetail).Updates(map[string]interface{}{ + "is_del": 0, + "quantity": d.Quantity, + "updated_at": d.UpdatedAt, + }) + } else if errors.Is(err, gorm.ErrRecordNotFound) { + d.CreatedAt = time.Now().Unix() + d.UpdatedAt = d.CreatedAt + tx.Create(&d) + } + } + } + } + + now := time.Now().Unix() + tx.Model(&destroyLog).Updates(map[string]interface{}{ + "status": 1, + "restored_by": operator, + "restored_by_id": operatorID, + "restored_at": now, + "updated_at": now, + }) + + return nil + }) + + return err +} + +// GetDestroyLogList 获取商品销毁日志列表 +func (s *ProductService) GetDestroyLogList(req systemReq.GetDestroyLogListRequest, db ...*gorm.DB) (*systemRes.DestroyLogListResponse, error) { + databaseConn := database.OptionalDB(db...) + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + query := databaseConn.Model(&models.ProductDestroyLog{}).Where("is_del = ?", 0) + + if req.Keyword != "" { + query = query.Where("barcode LIKE ?", "%"+req.Keyword+"%") + } + if req.Status != nil { + query = query.Where("status = ?", *req.Status) + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, utils.NewError("查询总数失败") + } + + if total == 0 { + return &systemRes.DestroyLogListResponse{ + List: []systemRes.DestroyLogItem{}, + Total: 0, + Page: req.Page, + PageSize: req.PageSize, + }, nil + } + + var logs []models.ProductDestroyLog + offset := (req.Page - 1) * req.PageSize + if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&logs).Error; err != nil { + return nil, utils.NewError("查询销毁日志列表失败") + } + + items := make([]systemRes.DestroyLogItem, 0, len(logs)) + for _, log := range logs { + productName := "" + if len(log.ProductSnapshot) > 0 { + var p models.Product + if err := json.Unmarshal(log.ProductSnapshot, &p); err == nil { + productName = p.Name + } + } + + items = append(items, systemRes.DestroyLogItem{ + ID: log.ID, + ProductID: log.ProductID, + Barcode: log.Barcode, + ProductName: productName, + DestroyedBy: log.DestroyedBy, + DestroyedAt: log.DestroyedAt, + RestoredBy: log.RestoredBy, + RestoredAt: log.RestoredAt, + Status: log.Status, + StatusText: systemRes.GetDestroyLogStatusText(log.Status), + CreatedAt: log.CreatedAt, + }) + } + + return &systemRes.DestroyLogListResponse{ + List: items, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// GetDestroyLogDetail 获取商品销毁日志详情 +func (s *ProductService) GetDestroyLogDetail(req systemReq.GetDestroyLogDetailRequest, db ...*gorm.DB) (*systemRes.DestroyLogDetailResponse, error) { + databaseConn := database.OptionalDB(db...) + + var log models.ProductDestroyLog + if err := databaseConn.Where("id = ? AND is_del = ?", req.ID, 0).First(&log).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utils.NewError("销毁日志不存在") + } + return nil, utils.NewError("查询销毁日志失败") + } + + return &systemRes.DestroyLogDetailResponse{ + ID: log.ID, + ProductID: log.ProductID, + Barcode: log.Barcode, + ProductSnapshot: string(log.ProductSnapshot), + ProductBookSnapshot: string(log.ProductBookSnapshot), + InventorySnapshot: string(log.InventorySnapshot), + InventoryDetailSnapshot: string(log.InventoryDetailSnapshot), + DestroyedBy: log.DestroyedBy, + DestroyedByID: log.DestroyedByID, + DestroyedAt: log.DestroyedAt, + RestoredBy: log.RestoredBy, + RestoredByID: log.RestoredByID, + RestoredAt: log.RestoredAt, + Status: log.Status, + StatusText: systemRes.GetDestroyLogStatusText(log.Status), + CreatedAt: log.CreatedAt, + UpdatedAt: log.UpdatedAt, + }, nil +} diff --git a/service/sales.go b/service/sales.go index 5cee761..f41a3a5 100644 --- a/service/sales.go +++ b/service/sales.go @@ -48,6 +48,16 @@ func (s *SalesService) GetSalesOrderList(req systemReq.GetSalesOrderListRequest, query = query.Where("sales_order.created_at <= ?", req.EndDate) } + if req.AssociationOrderNo != "" { + query = query.Where("sales_order.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%") + } + if req.LogisticsNo != "" { + subQuery := databaseConn.Model(&models.SalesOrderItem{}). + Select("sales_order_id"). + Where("logistics_no LIKE ? AND is_del = 0", "%"+req.LogisticsNo+"%") + query = query.Where("sales_order.id IN (?)", subQuery) + } + var total int64 if err := query.Count(&total).Error; err != nil { return nil, utils.NewError("查询总数失败") @@ -76,10 +86,17 @@ func (s *SalesService) GetSalesOrderList(req systemReq.GetSalesOrderListRequest, orderItems := make([]systemRes.SalesOrderItem, 0, len(orders)) for _, order := range orders { + var logisticsNos string + databaseConn.Table("sales_order_item"). + Select("GROUP_CONCAT(DISTINCT logistics_no SEPARATOR ', ')"). + Where("sales_order_id = ? AND is_del = ? AND logistics_no != ''", order.ID, 0). + Scan(&logisticsNos) + orderItems = append(orderItems, systemRes.ConvertSalesOrderToItem( order.SalesOrder, order.CustomerName, order.WarehouseName, + logisticsNos, )) } @@ -124,6 +141,33 @@ func (s *SalesService) GetSalesOrderDetailList(req systemReq.GetSalesOrderDetail //if req.EndDate > 0 { // query = query.Where("sales_order.created_at <= ?", req.EndDate) //} + if req.Status > 0 { + query = query.Where("sales_order.status = ?", req.Status) + } + if req.CustomerID > 0 { + query = query.Where("sales_order.customer_id = ?", req.CustomerID) + } + if req.WarehouseID > 0 { + query = query.Where("sales_order.warehouse_id = ?", req.WarehouseID) + } + if req.SoNo != "" { + query = query.Where("sales_order.so_no LIKE ?", "%"+req.SoNo+"%") + } + if req.StartDate > 0 { + query = query.Where("sales_order.created_at >= ?", req.StartDate) + } + if req.EndDate > 0 { + query = query.Where("sales_order.created_at <= ?", req.EndDate) + } + if req.AssociationOrderNo != "" { + query = query.Where("sales_order.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%") + } + if req.LogisticsNo != "" { + subQuery := databaseConn.Model(&models.SalesOrderItem{}). + Select("sales_order_id"). + Where("logistics_no LIKE ? AND is_del = 0", "%"+req.LogisticsNo+"%") + query = query.Where("sales_order.id IN (?)", subQuery) + } var total int64 if err := query.Count(&total).Error; err != nil { @@ -205,6 +249,8 @@ func (s *SalesService) GetSalesOrderDetailList(req systemReq.GetSalesOrderDetail ReceiverName: item.ReceiverName, ReceiverPhone: item.ReceiverPhone, ReceiverAddress: item.ReceiverAddress, + LogisticsCompany: item.LogisticsCompany, + LogisticsNo: item.LogisticsNo, LocationCode: item.LocationCode, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, @@ -278,6 +324,8 @@ func (s *SalesService) GetSalesOrderDetail(id int64, creatorID int64, role int64 ReceiverName: item.ReceiverName, ReceiverPhone: item.ReceiverPhone, ReceiverAddress: item.ReceiverAddress, + LogisticsCompany: item.LogisticsCompany, + LogisticsNo: item.LogisticsNo, LocationCode: item.LocationCode, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, diff --git a/service/shipping.go b/service/shipping.go index 7bffa90..d6b8582 100644 --- a/service/shipping.go +++ b/service/shipping.go @@ -41,6 +41,23 @@ func (s *ShippingService) GetShippingOrderList(req systemReq.GetShippingOrderLis query = query.Where("shipping_order.created_at <= ?", req.EndDate) } + if req.AssociationOrderNo != "" { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%") + query = query.Where("shipping_order.id IN (?)", subQuery) + } + if req.LogisticsNo != "" { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order_item ON outbound_order_item.sales_order_id = sales_order_item.sales_order_id AND sales_order_item.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") + query = query.Where("shipping_order.id IN (?)", subQuery) + } + var total int64 if err := query.Count(&total).Error; err != nil { return nil, utils.NewError("查询总数失败") @@ -90,10 +107,28 @@ func (s *ShippingService) GetShippingOrderList(req systemReq.GetShippingOrderLis }) } + var associationOrderNos string + databaseConn.Table("shipping_order_item"). + Select("GROUP_CONCAT(DISTINCT so.association_order_no SEPARATOR ', ')"). + Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). + Joins("INNER JOIN sales_order so ON ooi.sales_order_id = so.id AND so.is_del = 0"). + Where("shipping_order_item.shipping_order_id = ? AND shipping_order_item.is_del = ? AND so.association_order_no != ''", order.ID, 0). + Scan(&associationOrderNos) + + var logisticsNos string + databaseConn.Table("shipping_order_item"). + Select("GROUP_CONCAT(DISTINCT soi.logistics_no SEPARATOR ', ')"). + Joins("INNER JOIN outbound_order_item ooi ON shipping_order_item.outbound_order_item_id = ooi.id AND ooi.is_del = 0"). + Joins("INNER JOIN sales_order_item soi ON ooi.sales_order_id = soi.sales_order_id AND ooi.product_id = soi.product_id AND soi.is_del = 0"). + Where("shipping_order_item.shipping_order_id = ? AND shipping_order_item.is_del = ? AND soi.logistics_no != ''", order.ID, 0). + Scan(&logisticsNos) + orderItems = append(orderItems, systemRes.ConvertShippingOrderToItem( order.ShippingOrder, order.CustomerName, shopList, + associationOrderNos, + logisticsNos, )) } @@ -237,6 +272,23 @@ func (s *ShippingService) GetShippingOrderDetailList(req systemReq.GetShippingOr query = query.Where("shipping_order.created_at <= ?", req.EndDate) } + if req.AssociationOrderNo != "" { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order ON outbound_order_item.sales_order_id = sales_order.id AND sales_order.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%") + query = query.Where("shipping_order.id IN (?)", subQuery) + } + if req.LogisticsNo != "" { + subQuery := databaseConn.Table("shipping_order_item"). + Select("shipping_order_item.shipping_order_id"). + Joins("INNER JOIN outbound_order_item ON shipping_order_item.outbound_order_item_id = outbound_order_item.id AND outbound_order_item.is_del = 0"). + Joins("INNER JOIN sales_order_item ON outbound_order_item.sales_order_id = sales_order_item.sales_order_id AND sales_order_item.is_del = 0"). + Where("shipping_order_item.is_del = 0 AND sales_order_item.logistics_no LIKE ?", "%"+req.LogisticsNo+"%") + query = query.Where("shipping_order.id IN (?)", subQuery) + } + var total int64 if err := query.Count(&total).Error; err != nil { return nil, utils.NewError("查询总数失败") diff --git a/service/split_account_deduction_log.go b/service/split_account_deduction_log.go index 0bcd966..7c4a2b0 100644 --- a/service/split_account_deduction_log.go +++ b/service/split_account_deduction_log.go @@ -98,9 +98,9 @@ func (s *SplitAccountDeductionLogService) CreateSplitAccountDeductionLog(req sys ConfigID: req.ConfigID, ConfigName: req.ConfigName, DeductionDetails: deductionDetails, - TotalAmount: req.TotalAmount, - DeductionAmount: req.DeductionAmount, - RemainingAmount: req.RemainingAmount, + TotalAmount: *req.TotalAmount, + DeductionAmount: *req.DeductionAmount, + RemainingAmount: *req.RemainingAmount, CreatedBy: username, CreatedAt: now, UpdatedAt: now, @@ -154,6 +154,8 @@ func (s *SplitAccountDeductionLogService) UpdateSplitAccountDeductionLog(req sys updateData["remaining_amount"] = req.RemainingAmount } + updateData["status"] = req.Status + if err := databaseConn.Model(&log).Updates(updateData).Error; err != nil { return utils.NewError("更新分账扣钱日志失败: " + err.Error()) }