diff --git a/cmd/clear_cache/main.go b/cmd/clear_cache/main.go new file mode 100644 index 0000000..45cffcf --- /dev/null +++ b/cmd/clear_cache/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + goredis "github.com/redis/go-redis/v9" +) + +func main() { + ctx := context.Background() + rdb := goredis.NewClient(&goredis.Options{ + Addr: "192.168.3.200:6379", + DB: 0, + }) + defer rdb.Close() + + patterns := []string{ + "*private_stock*", + "*inventory_count*", + "*private_sku*", + "*warehouse*", + "*zone*", + "*location*", + } + + total := 0 + for _, pattern := range patterns { + keys, err := rdb.Keys(ctx, pattern).Result() + if err != nil { + fmt.Printf("查询模式[%s]失败: %v\n", pattern, err) + continue + } + for _, key := range keys { + rdb.Del(ctx, key) + total++ + } + if len(keys) > 0 { + fmt.Printf("✅ 模式[%s] 清除了 %d 个key\n", pattern, len(keys)) + } + } + + fmt.Printf("\n共清除 %d 个Redis缓存key\n", total) +} diff --git a/cmd/seed_data/main.go b/cmd/seed_data/main.go new file mode 100644 index 0000000..6ce663d --- /dev/null +++ b/cmd/seed_data/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func main() { + ctx := context.Background() + client, err := mongo.Connect(options.Client().ApplyURI("mongodb://192.168.3.200:27017/?directConnection=true")) + if err != nil { + panic(err) + } + defer client.Disconnect(ctx) + + db := client.Database("assets") + now := time.Now() + + // 0. 清理之前的测试数据(按warehouseCode匹配) + fmt.Println("=== 清理旧测试数据 ===") + db.Collection("warehouse").DeleteMany(ctx, bson.M{"warehouseCode": "WH-TEST-001"}) + db.Collection("zone").DeleteMany(ctx, bson.M{"zoneCode": "ZN-TEST-001"}) + db.Collection("location").DeleteMany(ctx, bson.M{"locationCode": "LC-TEST-001"}) + db.Collection("private_sku").DeleteMany(ctx, bson.M{"skuName": "测试商品-矿泉水550ml"}) + db.Collection("private_stock").DeleteMany(ctx, bson.M{"warehouseCode": "WH-TEST-001"}) + db.Collection("inventory_count").DeleteMany(ctx, bson.M{"title": bson.M{"$regex": "测试"}}) + db.Collection("inventory_count_detail").DeleteMany(ctx, bson.M{}) + fmt.Println("✅ 旧测试数据已清理") + + // 基础字段(MongoBaseDO) + base := func() bson.M { + return bson.M{ + "creator": "admin", + "createdAt": now, + "updater": "admin", + "updatedAt": now, + "tenantId": float64(1), + "isDeleted": false, + } + } + + // 1. 创建仓库(匹配现有数据格式) + warehouseId := bson.NewObjectID() + whDoc := base() + whDoc["_id"] = warehouseId + whDoc["warehouseCode"] = "WH-TEST-001" + whDoc["warehouseName"] = "测试仓库" + whDoc["address"] = "测试地址" + whDoc["contactPerson"] = "测试联系人" + whDoc["contactPhone"] = "13800138000" + whDoc["status"] = "enable" + whDoc["remark"] = "自动生成的测试数据" + + _, err = db.Collection("warehouse").InsertOne(ctx, whDoc) + if err != nil { + fmt.Printf("创建仓库失败: %v\n", err) + return + } + fmt.Printf("✅ 仓库已创建: %s\n", warehouseId.Hex()) + + // 2. 创建库区(Zone.WarehouseId是string类型,zoneType是string枚举) + zoneId := bson.NewObjectID() + znDoc := base() + znDoc["_id"] = zoneId + znDoc["warehouseId"] = warehouseId.Hex() + znDoc["zoneCode"] = "ZN-TEST-001" + znDoc["zoneName"] = "测试库区A" + znDoc["zoneType"] = "normal" + znDoc["status"] = "enable" + znDoc["remark"] = "自动生成的测试数据" + + _, err = db.Collection("zone").InsertOne(ctx, znDoc) + if err != nil { + fmt.Printf("创建库区失败: %v\n", err) + return + } + fmt.Printf("✅ 库区已创建: %s\n", zoneId.Hex()) + + // 3. 创建库位(匹配现有数据:maxCapacity/currentCapacity为顶层字段) + locationId := bson.NewObjectID() + lcDoc := base() + lcDoc["_id"] = locationId + lcDoc["warehouseId"] = warehouseId + lcDoc["zoneId"] = zoneId + lcDoc["locationCode"] = "LC-TEST-001" + lcDoc["locationName"] = "测试库位A-1" + lcDoc["locationType"] = "shelf" + lcDoc["status"] = "idle" + lcDoc["maxCapacity"] = 100 + lcDoc["currentCapacity"] = 0 + lcDoc["remark"] = "自动生成的测试数据" + + _, err = db.Collection("location").InsertOne(ctx, lcDoc) + if err != nil { + fmt.Printf("创建库位失败: %v\n", err) + return + } + fmt.Printf("✅ 库位已创建: %s\n", locationId.Hex()) + + // 4. 创建私域SKU(匹配现有数据格式) + skuId := bson.NewObjectID() + skuDoc := base() + skuDoc["_id"] = skuId + skuDoc["skuName"] = "测试商品-矿泉水550ml" + skuDoc["price"] = 200 + skuDoc["stock"] = 150 + skuDoc["sort"] = 1 + skuDoc["capacityUnitType"] = 1 + skuDoc["capacity"] = bson.M{"value": 550, "unit": "ml"} + skuDoc["privateCategoryPath"] = "/饮品/矿泉水" + + _, err = db.Collection("private_sku").InsertOne(ctx, skuDoc) + if err != nil { + fmt.Printf("创建SKU失败: %v\n", err) + return + } + fmt.Printf("✅ 私域SKU已创建: %s\n", skuId.Hex()) + + // 5. 创建私域库存(2条批次,枚举用string类型) + for i := 1; i <= 2; i++ { + stockId := bson.NewObjectID() + stDoc := base() + stDoc["_id"] = stockId + stDoc["stockType"] = 2 + stDoc["warehouseId"] = warehouseId + stDoc["warehouseCode"] = "WH-TEST-001" + stDoc["warehouseName"] = "测试仓库" + stDoc["zoneId"] = zoneId + stDoc["zoneCode"] = "ZN-TEST-001" + stDoc["zoneName"] = "测试库区A" + stDoc["zoneType"] = "normal" + stDoc["locationId"] = locationId + stDoc["locationCode"] = "LC-TEST-001" + stDoc["locationName"] = "测试库位A-1" + stDoc["locationType"] = "shelf" + stDoc["privateSkuId"] = skuId + stDoc["batchNo"] = fmt.Sprintf("BATCH-TEST-%03d", i) + stDoc["batchQty"] = 50 * i + stDoc["availableQty"] = 50 * i + stDoc["batchStatus"] = 1 + stDoc["supportsRecycle"] = false + stDoc["privateCategoryPath"] = "/饮品/矿泉水" + stDoc["stockStatus"] = 1 + + _, err = db.Collection("private_stock").InsertOne(ctx, stDoc) + if err != nil { + fmt.Printf("创建库存批次%d失败: %v\n", i, err) + return + } + fmt.Printf("✅ 私域库存批次%d已创建: %s (数量:%d)\n", i, stockId.Hex(), 50*i) + } + + fmt.Println("\n========== 测试数据创建完成 ==========") + fmt.Printf("仓库ID: %s\n", warehouseId.Hex()) + fmt.Printf("库区ID: %s\n", zoneId.Hex()) + fmt.Printf("库位ID: %s\n", locationId.Hex()) + fmt.Printf("SKU ID: %s\n", skuId.Hex()) + fmt.Println("\n可使用仓库ID创建盘点任务进行测试") +} diff --git a/consts/asset/asset_sku_status.go b/consts/asset/asset_sku_status.go new file mode 100644 index 0000000..496906b --- /dev/null +++ b/consts/asset/asset_sku_status.go @@ -0,0 +1,17 @@ +package consts + +// AssetSkuStatus SKU状态枚举 +type AssetSkuStatus int + +const ( + AssetSkuStatusActive AssetSkuStatus = 1 // 启用 + AssetSkuStatusInactive AssetSkuStatus = 0 // 停用 +) + +// GetAllSkuStatuses 获取所有SKU状态 +func GetAllSkuStatuses() []AssetSkuStatus { + return []AssetSkuStatus{ + AssetSkuStatusActive, + AssetSkuStatusInactive, + } +} diff --git a/consts/asset/asset_status.go b/consts/asset/asset_status.go new file mode 100644 index 0000000..dd8b76a --- /dev/null +++ b/consts/asset/asset_status.go @@ -0,0 +1,17 @@ +package consts + +// AssetStatus 资产状态枚举 +type AssetStatus int + +const ( + AssetStatusActive AssetStatus = 1 // 启用 + AssetStatusInactive AssetStatus = 0 // 停用 +) + +// GetAllAssetStatuses 获取所有资产状态 +func GetAllAssetStatuses() []AssetStatus { + return []AssetStatus{ + AssetStatusActive, + AssetStatusInactive, + } +} diff --git a/consts/asset/asset_type.go b/consts/asset/asset_type.go new file mode 100644 index 0000000..c3fd457 --- /dev/null +++ b/consts/asset/asset_type.go @@ -0,0 +1,73 @@ +package consts + +// AssetType 资产类型枚举 +type AssetType string + +const ( + AssetTypePhysical AssetType = "physical" // 实物资产 + AssetTypeVirtual AssetType = "virtual" // 虚拟资产 + AssetTypeService AssetType = "service" // 服务资产 +) + +// GetAllAssetTypeKey 获取所有资产类型 +func GetAllAssetTypeKey() []AssetType { + return []AssetType{ + AssetTypePhysical, + AssetTypeVirtual, + AssetTypeService, + } +} + +type AssetTypeKeyValue struct { + Key AssetType // 对应原有常量值(physical/virtual/service) + Value string // 对应描述信息(实物资产/虚拟资产/服务资产) +} + +// 定义枚举实例(Key-Value 绑定),相当于改造后的常量 +var ( + AssetTypePhysicalKeyValue = AssetTypeKeyValue{Key: AssetTypePhysical, Value: "实物资产"} + AssetTypeVirtualKeyValue = AssetTypeKeyValue{Key: AssetTypeVirtual, Value: "虚拟资产"} + AssetTypeServiceKeyValue = AssetTypeKeyValue{Key: AssetTypeService, Value: "服务资产"} +) + +func GetAllAssetTypeKeyValue() []AssetTypeKeyValue { + return []AssetTypeKeyValue{ + AssetTypePhysicalKeyValue, + AssetTypeVirtualKeyValue, + AssetTypeServiceKeyValue, + } +} + +// -------------- 核心:构建Map映射 + 查询方法 -------------- +// 预初始化资产类型映射表(包初始化时执行,仅初始化一次) +var assetTypeValueMap = map[AssetType]string{ + AssetTypePhysical: AssetTypePhysicalKeyValue.Value, + AssetTypeVirtual: AssetTypeVirtualKeyValue.Value, + AssetTypeService: AssetTypeServiceKeyValue.Value, +} + +// GetAssetTypeValueByKey 根据 AssetType 键获取对应的描述值 +// 返回值:第一个返回值是描述信息,第二个返回值是是否存在该键 +func GetAssetTypeValueByKey(key AssetType) (value string) { + value, exists := assetTypeValueMap[key] + if !exists { + value = "未知资产类型" + } + return +} + +// VirtualAssetType 虚拟资产类型二级分类枚举 +type VirtualAssetType string + +const ( + VirtualAssetTypeRecharge VirtualAssetType = "recharge" // 虚拟资产-充值 + VirtualTypeSubscribe VirtualAssetType = "subscribe" // 服务资产-订阅 +) + +// ServiceAssetType 服务资产类型二级分类枚举 +type ServiceAssetType string + +const ( + ServiceAssetTypeArrival ServiceAssetType = "arrival" // 虚拟资产-到店 + ServiceTypeVisit ServiceAssetType = "visit" // 服务资产-上门 +) diff --git a/consts/asset/attribute_type.go b/consts/asset/attribute_type.go new file mode 100644 index 0000000..d8c971c --- /dev/null +++ b/consts/asset/attribute_type.go @@ -0,0 +1,47 @@ +package consts + +// AttributeType 属性类型枚举 +type AttributeType string + +const ( + AttributeTypeText AttributeType = "text" // 文本 + //AttributeTypeNumber AttributeType = "number" // 数字 + //AttributeTypeDate AttributeType = "date" // 日期 + AttributeTypeSelect AttributeType = "select" // 单选 + AttributeTypeMultiSelect AttributeType = "multi_select" // 多选 +) + +// GetAllAttributeTypes 获取所有属性类型 +func GetAllAttributeTypes() []AttributeType { + return []AttributeType{ + AttributeTypeText, + //AttributeTypeNumber, + //AttributeTypeDate, + AttributeTypeSelect, + AttributeTypeMultiSelect, + } +} + +type AttrTypeKeyValue struct { + Key AttributeType // 对应原有常量值 + Value string // 对应描述信息 +} + +// 定义枚举实例(Key-Value 绑定),相当于改造后的常量 +var ( + AttrTypeTextKeyValue = AttrTypeKeyValue{Key: AttributeTypeText, Value: "文本"} + //AttrTypeNumberKeyValue = AttrTypeKeyValue{Key: AttributeTypeNumber, Value: "数字"} + //AttrTypeDateKeyValue = AttrTypeKeyValue{Key: AttributeTypeDate, Value: "日期"} + AttrTypeSelectKeyValue = AttrTypeKeyValue{Key: AttributeTypeSelect, Value: "单选"} + AttrTypeMultiSelectKeyValue = AttrTypeKeyValue{Key: AttributeTypeMultiSelect, Value: "多选"} +) + +func GetAllAttrTypeKeyValue() []AttrTypeKeyValue { + return []AttrTypeKeyValue{ + AttrTypeTextKeyValue, + //AttrTypeNumberKeyValue, + //AttrTypeDateKeyValue, + AttrTypeSelectKeyValue, + AttrTypeMultiSelectKeyValue, + } +} diff --git a/consts/category/category_status.go b/consts/category/category_status.go new file mode 100644 index 0000000..2d26f5a --- /dev/null +++ b/consts/category/category_status.go @@ -0,0 +1,14 @@ +package consts + +// CategoryStatus 分类状态枚举 + +type CategoryStatusType *int + +type CategoryStatus struct { + Status CategoryStatusType +} + +//var CategoryStatusVal = CategoryStatus{ +// CategoryStatusActive: gconv.PtrInt(1), +// CategoryStatusInactive: gconv.PtrInt(0), +//} diff --git a/consts/procurement/approval_status.go b/consts/procurement/approval_status.go new file mode 100644 index 0000000..8a1f16d --- /dev/null +++ b/consts/procurement/approval_status.go @@ -0,0 +1,52 @@ +package consts + +// ApprovalStatus 审批状态枚举 +type ApprovalStatus int + +const ( + ApprovalStatusPending ApprovalStatus = 1 // 待审批 + ApprovalStatusApproved ApprovalStatus = 2 // 已批准 + ApprovalStatusRejected ApprovalStatus = 3 // 已拒绝 + ApprovalStatusCanceled ApprovalStatus = 4 // 已撤销 +) + +// GetAllApprovalStatuses 获取所有审批状态 +func GetAllApprovalStatuses() []ApprovalStatus { + return []ApprovalStatus{ + ApprovalStatusPending, + ApprovalStatusApproved, + ApprovalStatusRejected, + ApprovalStatusCanceled, + } +} + +// String 获取审批状态字符串表示 +func (a ApprovalStatus) String() string { + switch a { + case ApprovalStatusPending: + return "待审批" + case ApprovalStatusApproved: + return "已批准" + case ApprovalStatusRejected: + return "已拒绝" + case ApprovalStatusCanceled: + return "已撤销" + default: + return "未知" + } +} + +type ApprovalStatusKeyValue struct { + Key int `json:"key"` + Value string `json:"value"` +} + +// GetAllApprovalStatusKeyValue 获取所有审批状态的键值对 +func GetAllApprovalStatusKeyValue() []ApprovalStatusKeyValue { + return []ApprovalStatusKeyValue{ + {Key: 1, Value: "待审批"}, + {Key: 2, Value: "已批准"}, + {Key: 3, Value: "已拒绝"}, + {Key: 4, Value: "已撤销"}, + } +} diff --git a/consts/procurement/bid_mode.go b/consts/procurement/bid_mode.go new file mode 100644 index 0000000..0117bca --- /dev/null +++ b/consts/procurement/bid_mode.go @@ -0,0 +1,52 @@ +package consts + +// BidMode 竞价模式枚举 +type BidMode string + +const ( + BidModePrice BidMode = "price" // 优先价格 + BidModeQuality BidMode = "quality" // 优先质量 + BidModeTime BidMode = "time" // 优先时效 + BidModeMixed BidMode = "mixed" // 综合评价 +) + +// GetAllBidModes 获取所有竞价模式 +func GetAllBidModes() []BidMode { + return []BidMode{ + BidModePrice, + BidModeQuality, + BidModeTime, + BidModeMixed, + } +} + +// String 获取竞价模式字符串表示 +func (b BidMode) String() string { + switch b { + case BidModePrice: + return "优先价格" + case BidModeQuality: + return "优先质量" + case BidModeTime: + return "优先时效" + case BidModeMixed: + return "综合评价" + default: + return "未知" + } +} + +// IsValidBidMode 验证竞价模式是否有效 +func (b BidMode) IsValid() bool { + switch b { + case BidModePrice, BidModeQuality, BidModeTime, BidModeMixed: + return true + default: + return false + } +} + +// GetBidModeText 获取竞价模式文本 +func GetBidModeText(bidMode BidMode) string { + return bidMode.String() +} diff --git a/consts/procurement/bid_status.go b/consts/procurement/bid_status.go new file mode 100644 index 0000000..2f68920 --- /dev/null +++ b/consts/procurement/bid_status.go @@ -0,0 +1,49 @@ +package consts + +// BidStatus 投标状态枚举 +type BidStatus string + +const ( + BidStatusDraft BidStatus = "draft" // 草稿 + BidStatusSubmitted BidStatus = "submitted" // 已提交 + BidStatusViewed BidStatus = "viewed" // 已查看 + BidStatusWinning BidStatus = "winning" // 中标 + BidStatusLost BidStatus = "lost" // 未中标 + BidStatusWithdrawn BidStatus = "withdrawn" // 已撤回 + BidStatusExpired BidStatus = "expired" // 已过期 +) + +// GetAllBidStatuses 获取所有投标状态 +func GetAllBidStatuses() []BidStatus { + return []BidStatus{ + BidStatusDraft, + BidStatusSubmitted, + BidStatusViewed, + BidStatusWinning, + BidStatusLost, + BidStatusWithdrawn, + BidStatusExpired, + } +} + +// String 获取投标状态字符串表示 +func (b BidStatus) String() string { + switch b { + case BidStatusDraft: + return "草稿" + case BidStatusSubmitted: + return "已提交" + case BidStatusViewed: + return "已查看" + case BidStatusWinning: + return "中标" + case BidStatusLost: + return "未中标" + case BidStatusWithdrawn: + return "已撤回" + case BidStatusExpired: + return "已过期" + default: + return "未知" + } +} diff --git a/consts/procurement/billing_method.go b/consts/procurement/billing_method.go new file mode 100644 index 0000000..53c7911 --- /dev/null +++ b/consts/procurement/billing_method.go @@ -0,0 +1,21 @@ +package consts + +// BillingMethod 计费方式枚举 +type BillingMethod string + +const ( + BillingMethodCount BillingMethod = "count" // 按数量 + BillingMethodDuration BillingMethod = "duration" // 按时长 + BillingMethodSku BillingMethod = "sku" // 按SKU + BillingMethodDurationAndSku BillingMethod = "duration_and_sku" // 按时长+SKU +) + +// GetAllBillingMethods 获取所有计费方式 +func GetAllBillingMethods() []BillingMethod { + return []BillingMethod{ + BillingMethodCount, + BillingMethodDuration, + BillingMethodSku, + BillingMethodDurationAndSku, + } +} diff --git a/consts/procurement/process_method.go b/consts/procurement/process_method.go new file mode 100644 index 0000000..26c30f4 --- /dev/null +++ b/consts/procurement/process_method.go @@ -0,0 +1,47 @@ +package consts + +// ProcessMethod 处理方式枚举 +type ProcessMethod int + +const ( + ProcessMethodRefund ProcessMethod = 1 // 退款 + ProcessMethodReplace ProcessMethod = 2 // 更换 + ProcessMethodRepair ProcessMethod = 3 // 维修 +) + +// GetAllProcessMethods 获取所有处理方式 +func GetAllProcessMethods() []ProcessMethod { + return []ProcessMethod{ + ProcessMethodRefund, + ProcessMethodReplace, + ProcessMethodRepair, + } +} + +// String 获取处理方式字符串表示 +func (p ProcessMethod) String() string { + switch p { + case ProcessMethodRefund: + return "退款" + case ProcessMethodReplace: + return "更换" + case ProcessMethodRepair: + return "维修" + default: + return "未知" + } +} + +type ProcessMethodKeyValue struct { + Key int `json:"key"` + Value string `json:"value"` +} + +// GetAllProcessMethodKeyValue 获取所有处理方式的键值对 +func GetAllProcessMethodKeyValue() []ProcessMethodKeyValue { + return []ProcessMethodKeyValue{ + {Key: 1, Value: "退款"}, + {Key: 2, Value: "更换"}, + {Key: 3, Value: "维修"}, + } +} diff --git a/consts/procurement/purchase_order_status.go b/consts/procurement/purchase_order_status.go new file mode 100644 index 0000000..e453a22 --- /dev/null +++ b/consts/procurement/purchase_order_status.go @@ -0,0 +1,58 @@ +package consts + +// PurchaseOrderStatus 采购订单状态枚举 +type PurchaseOrderStatus string + +const ( + PurchaseOrderStatusDraft PurchaseOrderStatus = "draft" // 草稿 + PurchaseOrderStatusPublished PurchaseOrderStatus = "published" // 已发布 + PurchaseOrderStatusProcessing PurchaseOrderStatus = "processing" // 处理中 + PurchaseOrderStatusAssigned PurchaseOrderStatus = "assigned" // 已指派 + PurchaseOrderStatusBidding PurchaseOrderStatus = "bidding" // 竞价中 + PurchaseOrderStatusCompleted PurchaseOrderStatus = "completed" // 已完成 + PurchaseOrderStatusCancelled PurchaseOrderStatus = "cancelled" // 已取消 + PurchaseOrderStatusExpired PurchaseOrderStatus = "expired" // 已过期 +) + +// GetAllPurchaseOrderStatuses 获取所有采购订单状态 +func GetAllPurchaseOrderStatuses() []PurchaseOrderStatus { + return []PurchaseOrderStatus{ + PurchaseOrderStatusDraft, + PurchaseOrderStatusPublished, + PurchaseOrderStatusProcessing, + PurchaseOrderStatusAssigned, + PurchaseOrderStatusBidding, + PurchaseOrderStatusCompleted, + PurchaseOrderStatusCancelled, + PurchaseOrderStatusExpired, + } +} + +// String 获取采购订单状态字符串表示 +func (p PurchaseOrderStatus) String() string { + switch p { + case PurchaseOrderStatusDraft: + return "草稿" + case PurchaseOrderStatusPublished: + return "已发布" + case PurchaseOrderStatusProcessing: + return "处理中" + case PurchaseOrderStatusAssigned: + return "已指派" + case PurchaseOrderStatusBidding: + return "竞价中" + case PurchaseOrderStatusCompleted: + return "已完成" + case PurchaseOrderStatusCancelled: + return "已取消" + case PurchaseOrderStatusExpired: + return "已过期" + default: + return "未知" + } +} + +// GetPurchaseOrderStatusText 获取采购订单状态文本 +func GetPurchaseOrderStatusText(status PurchaseOrderStatus) string { + return status.String() +} diff --git a/consts/procurement/purchase_order_type.go b/consts/procurement/purchase_order_type.go new file mode 100644 index 0000000..9b64516 --- /dev/null +++ b/consts/procurement/purchase_order_type.go @@ -0,0 +1,53 @@ +package consts + +// PurchaseOrderType 采购订单类型枚举 +type PurchaseOrderType string + +const ( + PurchaseOrderTypeDirect PurchaseOrderType = "direct" // 直接采购(指定供应商) + PurchaseOrderTypeBidding PurchaseOrderType = "bidding" // 竞价采购 + PurchaseOrderTypeAuction PurchaseOrderType = "auction" // 拍卖采购 + PurchaseOrderTypeGroup PurchaseOrderType = "group" // 团购采购 +) + +// GetAllPurchaseOrderTypes 获取所有采购订单类型 +func GetAllPurchaseOrderTypes() []PurchaseOrderType { + return []PurchaseOrderType{ + PurchaseOrderTypeDirect, + PurchaseOrderTypeBidding, + PurchaseOrderTypeAuction, + PurchaseOrderTypeGroup, + } +} + +// String 获取采购订单类型字符串表示 +func (p PurchaseOrderType) String() string { + switch p { + case PurchaseOrderTypeDirect: + return "直接采购" + case PurchaseOrderTypeBidding: + return "竞价采购" + case PurchaseOrderTypeAuction: + return "拍卖采购" + case PurchaseOrderTypeGroup: + return "团购采购" + default: + return "未知" + } +} + +// IsValidOrderType 验证订单类型是否有效 +func (p PurchaseOrderType) IsValid() bool { + switch p { + case PurchaseOrderTypeDirect, PurchaseOrderTypeBidding, + PurchaseOrderTypeAuction, PurchaseOrderTypeGroup: + return true + default: + return false + } +} + +// GetPurchaseOrderTypeText 获取采购订单类型文本 +func GetPurchaseOrderTypeText(orderType PurchaseOrderType) string { + return orderType.String() +} diff --git a/consts/procurement/purchase_status.go b/consts/procurement/purchase_status.go new file mode 100644 index 0000000..bfba8d2 --- /dev/null +++ b/consts/procurement/purchase_status.go @@ -0,0 +1,57 @@ +package consts + +// PurchaseStatus 采购单状态枚举 +type PurchaseStatus string + +const ( + PurchaseStatusNotArrived PurchaseStatus = "not_arrived" // 未到货 + PurchaseStatusReceived PurchaseStatus = "received" // 已入库 + PurchaseStatusPartial PurchaseStatus = "partial" // 部分入库 + PurchaseStatusCancelled PurchaseStatus = "cancelled" // 已取消 +) + +// GetAllPurchaseStatuses 获取所有采购单状态 +func GetAllPurchaseStatuses() []PurchaseStatus { + return []PurchaseStatus{ + PurchaseStatusNotArrived, + PurchaseStatusReceived, + PurchaseStatusPartial, + PurchaseStatusCancelled, + } +} + +type PurchaseStatusKeyValue struct { + Key PurchaseStatus + Value string +} + +var ( + PurchaseStatusNotArrivedKeyValue = PurchaseStatusKeyValue{Key: PurchaseStatusNotArrived, Value: "未到货"} + PurchaseStatusReceivedKeyValue = PurchaseStatusKeyValue{Key: PurchaseStatusReceived, Value: "已入库"} + PurchaseStatusPartialKeyValue = PurchaseStatusKeyValue{Key: PurchaseStatusPartial, Value: "部分入库"} + PurchaseStatusCancelledKeyValue = PurchaseStatusKeyValue{Key: PurchaseStatusCancelled, Value: "已取消"} +) + +func GetAllPurchaseStatusKeyValue() []PurchaseStatusKeyValue { + return []PurchaseStatusKeyValue{ + PurchaseStatusNotArrivedKeyValue, + PurchaseStatusReceivedKeyValue, + PurchaseStatusPartialKeyValue, + PurchaseStatusCancelledKeyValue, + } +} + +var purchaseStatusValueMap = map[PurchaseStatus]string{ + PurchaseStatusNotArrived: PurchaseStatusNotArrivedKeyValue.Value, + PurchaseStatusReceived: PurchaseStatusReceivedKeyValue.Value, + PurchaseStatusPartial: PurchaseStatusPartialKeyValue.Value, + PurchaseStatusCancelled: PurchaseStatusCancelledKeyValue.Value, +} + +func GetPurchaseStatusValueByKey(key PurchaseStatus) (value string) { + value, exists := purchaseStatusValueMap[key] + if !exists { + value = "未知状态" + } + return +} diff --git a/consts/procurement/return_item_status.go b/consts/procurement/return_item_status.go new file mode 100644 index 0000000..bb3ba54 --- /dev/null +++ b/consts/procurement/return_item_status.go @@ -0,0 +1,53 @@ +package consts + +// ReturnItemStatus 退换明细状态枚举 +type ReturnItemStatus string + +const ( + ReturnItemStatusPending ReturnItemStatus = "pending" // 待处理 + ReturnItemStatusApproved ReturnItemStatus = "approved" // 已批准 + ReturnItemStatusRejected ReturnItemStatus = "rejected" // 已拒绝 + ReturnItemStatusShipped ReturnItemStatus = "shipped" // 已发货 + ReturnItemStatusReceived ReturnItemStatus = "received" // 已收货 + ReturnItemStatusProcessing ReturnItemStatus = "processing" // 处理中 + ReturnItemStatusCompleted ReturnItemStatus = "completed" // 已完成 + ReturnItemStatusCancelled ReturnItemStatus = "cancelled" // 已取消 +) + +// GetAllReturnItemStatuses 获取所有退换明细状态 +func GetAllReturnItemStatuses() []ReturnItemStatus { + return []ReturnItemStatus{ + ReturnItemStatusPending, + ReturnItemStatusApproved, + ReturnItemStatusRejected, + ReturnItemStatusShipped, + ReturnItemStatusReceived, + ReturnItemStatusProcessing, + ReturnItemStatusCompleted, + ReturnItemStatusCancelled, + } +} + +// String 获取退换明细状态字符串表示 +func (r ReturnItemStatus) String() string { + switch r { + case ReturnItemStatusPending: + return "待处理" + case ReturnItemStatusApproved: + return "已批准" + case ReturnItemStatusRejected: + return "已拒绝" + case ReturnItemStatusShipped: + return "已发货" + case ReturnItemStatusReceived: + return "已收货" + case ReturnItemStatusProcessing: + return "处理中" + case ReturnItemStatusCompleted: + return "已完成" + case ReturnItemStatusCancelled: + return "已取消" + default: + return "未知" + } +} diff --git a/consts/procurement/return_reason.go b/consts/procurement/return_reason.go new file mode 100644 index 0000000..3e75873 --- /dev/null +++ b/consts/procurement/return_reason.go @@ -0,0 +1,65 @@ +package consts + +// ReturnReason 退换原因枚举 +type ReturnReason string + +const ( + ReturnReasonQualityDefect ReturnReason = "quality_defect" // 质量问题 + ReturnReasonDamaged ReturnReason = "damaged" // 运输损坏 + ReturnReasonWrongGoods ReturnReason = "wrong_goods" // 错发商品 + ReturnReasonExpired ReturnReason = "expired" // 商品过期 + ReturnReasonNotAsDescribed ReturnReason = "not_as_described" // 与描述不符 + ReturnReasonOverstock ReturnReason = "overstock" // 库存积压 + ReturnReasonChangeMind ReturnReason = "change_mind" // 改变主意 + ReturnReasonDuplicateOrder ReturnReason = "duplicate_order" // 重复下单 + ReturnReasonPriceError ReturnReason = "price_error" // 价格错误 + ReturnReasonSupplierDelay ReturnReason = "supplier_delay" // 供应商延期 + ReturnReasonOther ReturnReason = "other" // 其他原因 +) + +// GetAllReturnReasons 获取所有退换原因 +func GetAllReturnReasons() []ReturnReason { + return []ReturnReason{ + ReturnReasonQualityDefect, + ReturnReasonDamaged, + ReturnReasonWrongGoods, + ReturnReasonExpired, + ReturnReasonNotAsDescribed, + ReturnReasonOverstock, + ReturnReasonChangeMind, + ReturnReasonDuplicateOrder, + ReturnReasonPriceError, + ReturnReasonSupplierDelay, + ReturnReasonOther, + } +} + +// String 获取退换原因字符串表示 +func (r ReturnReason) String() string { + switch r { + case ReturnReasonQualityDefect: + return "质量问题" + case ReturnReasonDamaged: + return "运输损坏" + case ReturnReasonWrongGoods: + return "错发商品" + case ReturnReasonExpired: + return "商品过期" + case ReturnReasonNotAsDescribed: + return "与描述不符" + case ReturnReasonOverstock: + return "库存积压" + case ReturnReasonChangeMind: + return "改变主意" + case ReturnReasonDuplicateOrder: + return "重复下单" + case ReturnReasonPriceError: + return "价格错误" + case ReturnReasonSupplierDelay: + return "供应商延期" + case ReturnReasonOther: + return "其他原因" + default: + return "未知" + } +} diff --git a/consts/procurement/return_status.go b/consts/procurement/return_status.go new file mode 100644 index 0000000..40f8ac7 --- /dev/null +++ b/consts/procurement/return_status.go @@ -0,0 +1,57 @@ +package consts + +// ReturnStatus 退换状态枚举 +type ReturnStatus string + +const ( + ReturnStatusDraft ReturnStatus = "draft" // 草稿 + ReturnStatusPending ReturnStatus = "pending" // 待审核 + ReturnStatusApproved ReturnStatus = "approved" // 已审核通过 + ReturnStatusRejected ReturnStatus = "rejected" // 审核拒绝 + ReturnStatusShipped ReturnStatus = "shipped" // 已发货(退货中) + ReturnStatusReceived ReturnStatus = "received" // 已收货 + ReturnStatusProcessing ReturnStatus = "processing" // 处理中 + ReturnStatusCompleted ReturnStatus = "completed" // 已完成 + ReturnStatusCancelled ReturnStatus = "cancelled" // 已取消 +) + +// GetAllReturnStatuses 获取所有退换状态 +func GetAllReturnStatuses() []ReturnStatus { + return []ReturnStatus{ + ReturnStatusDraft, + ReturnStatusPending, + ReturnStatusApproved, + ReturnStatusRejected, + ReturnStatusShipped, + ReturnStatusReceived, + ReturnStatusProcessing, + ReturnStatusCompleted, + ReturnStatusCancelled, + } +} + +// String 获取退换状态字符串表示 +func (r ReturnStatus) String() string { + switch r { + case ReturnStatusDraft: + return "草稿" + case ReturnStatusPending: + return "待审核" + case ReturnStatusApproved: + return "已审核通过" + case ReturnStatusRejected: + return "审核拒绝" + case ReturnStatusShipped: + return "已发货(退货中)" + case ReturnStatusReceived: + return "已收货" + case ReturnStatusProcessing: + return "处理中" + case ReturnStatusCompleted: + return "已完成" + case ReturnStatusCancelled: + return "已取消" + default: + return "未知" + } +} diff --git a/consts/procurement/return_type.go b/consts/procurement/return_type.go new file mode 100644 index 0000000..701801d --- /dev/null +++ b/consts/procurement/return_type.go @@ -0,0 +1,33 @@ +package consts + +// ReturnType 退换类型枚举 +type ReturnType string + +const ( + ReturnTypeReturn ReturnType = "return" // 退货 + ReturnTypeRefund ReturnType = "refund" // 退款 + ReturnTypeExchange ReturnType = "exchange" // 换货 +) + +// GetAllReturnTypes 获取所有退换类型 +func GetAllReturnTypes() []ReturnType { + return []ReturnType{ + ReturnTypeReturn, + ReturnTypeRefund, + ReturnTypeExchange, + } +} + +// String 获取退换类型字符串表示 +func (r ReturnType) String() string { + switch r { + case ReturnTypeReturn: + return "退货" + case ReturnTypeRefund: + return "退款" + case ReturnTypeExchange: + return "换货" + default: + return "未知" + } +} diff --git a/consts/procurement/review_status.go b/consts/procurement/review_status.go new file mode 100644 index 0000000..ff19c1a --- /dev/null +++ b/consts/procurement/review_status.go @@ -0,0 +1,52 @@ +package consts + +// ReviewStatus 审核状态枚举 +type ReviewStatus int + +const ( + ReviewStatusPending ReviewStatus = 1 // 待审核 + ReviewStatusApproved ReviewStatus = 2 // 已通过 + ReviewStatusRejected ReviewStatus = 3 // 已拒绝 + ReviewStatusCanceled ReviewStatus = 4 // 已撤销 +) + +// GetAllReviewStatuses 获取所有审核状态 +func GetAllReviewStatuses() []ReviewStatus { + return []ReviewStatus{ + ReviewStatusPending, + ReviewStatusApproved, + ReviewStatusRejected, + ReviewStatusCanceled, + } +} + +// String 获取审核状态字符串表示 +func (r ReviewStatus) String() string { + switch r { + case ReviewStatusPending: + return "待审核" + case ReviewStatusApproved: + return "已通过" + case ReviewStatusRejected: + return "已拒绝" + case ReviewStatusCanceled: + return "已撤销" + default: + return "未知" + } +} + +type ReviewStatusKeyValue struct { + Key int `json:"key"` + Value string `json:"value"` +} + +// GetAllReviewStatusKeyValue 获取所有审核状态的键值对 +func GetAllReviewStatusKeyValue() []ReviewStatusKeyValue { + return []ReviewStatusKeyValue{ + {Key: 1, Value: "待审核"}, + {Key: 2, Value: "已通过"}, + {Key: 3, Value: "已拒绝"}, + {Key: 4, Value: "已撤销"}, + } +} diff --git a/consts/procurement/supplier_status.go b/consts/procurement/supplier_status.go new file mode 100644 index 0000000..a317166 --- /dev/null +++ b/consts/procurement/supplier_status.go @@ -0,0 +1,29 @@ +package consts + +// SupplierStatus 供应商状态枚举 +type SupplierStatus int + +const ( + SupplierStatusActive SupplierStatus = 1 // 活跃 + SupplierStatusInactive SupplierStatus = 0 // 停用 +) + +// GetAllSupplierStatuses 获取所有供应商状态 +func GetAllSupplierStatuses() []SupplierStatus { + return []SupplierStatus{ + SupplierStatusActive, + SupplierStatusInactive, + } +} + +// GetSupplierStatusText 获取供应商状态文本 +func GetSupplierStatusText(status SupplierStatus) string { + switch status { + case SupplierStatusActive: + return "活跃" + case SupplierStatusInactive: + return "停用" + default: + return "未知" + } +} diff --git a/consts/public/collections.go b/consts/public/collections.go new file mode 100644 index 0000000..468daa3 --- /dev/null +++ b/consts/public/collections.go @@ -0,0 +1,32 @@ +package public + +// MongoDB集合名称常量 +const ( + AssetCollection = "assets_asset" // 资产集合 + CategoryCollection = "assets_category" // 分类集合 + PrivateCategoryCollection = "private_category" // 私域分类集合 + StockDetailsCollection = "stock_details" // 库存明细集合 + StockBatchCollection = "stock_batch" // 库存批次集合 + PrivateStockCollection = "private_stock" // 私域库存批次集合 + PurchaseOrderCollection = "purchase_order" // 采购订单主表集合(统一模式) + PurchaseOrderItemCollection = "purchase_order_item" // 采购订单明细集合 + PurchaseBidCollection = "purchase_bid" // 采购投标单集合(供应商抢单参与记录) + AssetSkuCollection = "asset_sku" // SKU集合 + PrivateSkuCollection = "private_sku" // 私域sku集合 + SupplierCollection = "supplier" // 供应商集合 + ExpiryMessageCollection = "expiry_message" // 临期消息集合(兼容旧数据) + ExpiryMessageHistoryCollection = "expiry_message_history" // 临期消息历史集合(兼容旧数据) + InventoryWarningCollection = "inventory_warning" // 库存预警集合 + InventoryWarningHistoryCollection = "inventory_warning_history" // 库存预警历史集合 + PurchaseReturnCollection = "purchase_return" // 采购退换单主表集合 + PurchaseReturnItemCollection = "purchase_return_item" // 采购退换单明细集合 + PurchaseInboundCollection = "purchase_inbound" // 采购入库记录集合 + DealerStockCollection = "dealer_stock" // 经销商库存集合 + WarehouseCollection = "warehouse" // 仓库集合 + ZoneCollection = "zone" // 库区集合 + LocationCollection = "location" // 库位集合 + InventoryCountCollection = "inventory_count" // 库存盘点主表集合 + InventoryCountDetailCollection = "inventory_count_detail" // 库存盘点明细表集合 + InventoryCountAdjustHistoryCollection = "inventory_count_adjust_history" // 库存盘点调整历史表集合 + UnitConversionCollection = "unit_conversion" // 单位换算集合 +) diff --git a/consts/public/currency.go b/consts/public/currency.go new file mode 100644 index 0000000..e247527 --- /dev/null +++ b/consts/public/currency.go @@ -0,0 +1,17 @@ +package public + +// Currency 货币类型枚举 +type Currency string + +const ( + CurrencyCNY Currency = "CNY" // 人民币 + CurrencyUSD Currency = "USD" // 美元 +) + +// GetAllCurrencies 获取所有货币类型 +func GetAllCurrencies() []Currency { + return []Currency{ + CurrencyCNY, + CurrencyUSD, + } +} diff --git a/consts/public/duration_type.go b/consts/public/duration_type.go new file mode 100644 index 0000000..a82c5c4 --- /dev/null +++ b/consts/public/duration_type.go @@ -0,0 +1,66 @@ +package public + +import ( + "time" + + "github.com/gogf/gf/v2/os/gtime" +) + +// DurationType 时长类型枚举 +type DurationType string + +const ( + DurationTypeHour DurationType = "hour" // 小时 + DurationTypeDay DurationType = "day" // 天 + DurationTypeMonth DurationType = "month" // 月 + DurationTypeYear DurationType = "year" // 年 +) + +// GetAllDurationTypes 获取所有时长类型 +func GetAllDurationTypes() []DurationType { + return []DurationType{ + DurationTypeHour, + DurationTypeDay, + DurationTypeMonth, + DurationTypeYear, + } +} + +type DurationTypeKeyValue struct { + Key DurationType // 对应原有常量值 + Value string // 对应描述信息 +} + +// 定义枚举实例(Key-Value 绑定),相当于改造后的常量 +var ( + DurationTypeHourKeyValue = DurationTypeKeyValue{Key: DurationTypeHour, Value: "小时"} + DurationTypeDayKeyValue = DurationTypeKeyValue{Key: DurationTypeDay, Value: "天"} + DurationTypeMonthKeyValue = DurationTypeKeyValue{Key: DurationTypeMonth, Value: "月"} + DurationTypeYearKeyValue = DurationTypeKeyValue{Key: DurationTypeYear, Value: "年"} +) + +func GetAllDurationTypeKeyValue() []DurationTypeKeyValue { + return []DurationTypeKeyValue{ + DurationTypeHourKeyValue, + DurationTypeDayKeyValue, + DurationTypeMonthKeyValue, + DurationTypeYearKeyValue, + } +} + +// AddTime 根据时长类型和数量计算到期时间 +func (dt DurationType) AddTime(count int) *gtime.Time { + now := gtime.Now() + switch dt { + case DurationTypeHour: + return gtime.NewFromTime(now.Time.Add(time.Hour * time.Duration(count))) + case DurationTypeDay: + return gtime.NewFromTime(now.Time.AddDate(0, 0, count)) + case DurationTypeMonth: + return gtime.NewFromTime(now.Time.AddDate(0, count, 0)) + case DurationTypeYear: + return gtime.NewFromTime(now.Time.AddDate(count, 0, 0)) + default: + return now + } +} diff --git a/consts/public/redis_key.go b/consts/public/redis_key.go new file mode 100644 index 0000000..561fcd2 --- /dev/null +++ b/consts/public/redis_key.go @@ -0,0 +1,17 @@ +package public + +const StockDetailLockKey = "stock:lock:skuId-%s" + +// 消费者配置(从 Redis Stream 消费请求) +const StockDetailQueueName = "assets:stock:detail:request:stream" // 请求 Stream 键名(与发消息的key一致) +const StockDetailGroupName = "assets:stock:detail:consumer:group" // 消费者组名 +const StockDetailConsumerName = "message-consumer-1" // 消费者名称(唯一标识) +const StockDetailPrefetchCount = 1 // 批处理大小(每次读取1条) +const StockDetailAutoAck = false // ACK是否自动确认(true自动确认,false不确认) + +// 业务自增序列号前缀 Redis Key +const ( + StockInventoryNoKeyPrefix = "IC" // 序列号Key前缀,如 IC-202602101215-000001 + StockInboundNoKeyPrefix = "INB" // 序列号Key前缀,如 INB-202602101215-000001 + StockBatchNoKeyPrefix = "BATCH" // 序列号Key前缀,如 BATCH-202602101215-000001 +) diff --git a/consts/public/sync_platform.go b/consts/public/sync_platform.go new file mode 100644 index 0000000..9febe57 --- /dev/null +++ b/consts/public/sync_platform.go @@ -0,0 +1,145 @@ +package public + +import "fmt" + +// SyncPlatform 同步渠道平台枚举 +type SyncPlatform string + +const ( + SyncPlatformTaobao SyncPlatform = "taobao" // 淘宝 + SyncPlatformJD SyncPlatform = "jd" // 京东 + SyncPlatformKuaishou SyncPlatform = "kuaishou" // 快手 + SyncPlatformDouyin SyncPlatform = "douyin" // 抖音 + SyncPlatformXiaohongshu SyncPlatform = "xiaohongshu" // 小红书 + SyncPlatformPinduoduo SyncPlatform = "pinduoduo" // 拼多多 + SyncPlatformXianyu SyncPlatform = "xianyu" // 闲鱼 + SyncPlatformBlockchain SyncPlatform = "blockchain" // 区块链平台 + SyncPlatformInternal SyncPlatform = "internal" // 内部平台 +) + +// SyncStatus 同步状态枚举 +type SyncStatus string + +const ( + SyncStatusPending SyncStatus = "pending" // 等待同步 + SyncStatusSyncing SyncStatus = "syncing" // 同步中 + SyncStatusSuccess SyncStatus = "success" // 同步成功 + SyncStatusFailed SyncStatus = "failed" // 同步失败 +) + +// SyncType 同步类型 +type SyncType string + +const ( + SyncTypeIncremental SyncType = "incremental" // 增量同步 +) + +// PlatformSyncConfig 平台同步配置结构 +type PlatformSyncConfig struct { + Platform SyncPlatform // 平台名称 + IsEnabled bool // 是否启用 + SyncInterval int // 同步间隔(秒) + BatchSize int // 批量同步数量 + MaxRetries int // 最大重试次数 + APIEndpoint string // API端点地址 + Description string // 平台描述 +} + +// GetPlatformSyncConfig 获取平台默认同步配置 +func GetPlatformSyncConfig(platform SyncPlatform) (PlatformSyncConfig, error) { + switch platform { + case SyncPlatformTaobao: + return PlatformSyncConfig{ + Platform: SyncPlatformTaobao, + IsEnabled: true, + SyncInterval: 300, // 5分钟 + BatchSize: 50, // 淘宝API限制较严 + MaxRetries: 3, + APIEndpoint: "https://eco.taobao.com/router/rest", + Description: "淘宝电商平台,API限制严格", + }, nil + case SyncPlatformJD: + return PlatformSyncConfig{ + Platform: SyncPlatformJD, + IsEnabled: true, + SyncInterval: 240, // 4分钟 + BatchSize: 100, // 京东API支持较大批次 + MaxRetries: 3, + APIEndpoint: "https://api.jd.com/routerjson", + Description: "京东电商平台,API相对稳定", + }, nil + case SyncPlatformKuaishou: + return PlatformSyncConfig{ + Platform: SyncPlatformKuaishou, + IsEnabled: true, + SyncInterval: 180, // 3分钟,直播数据更新快 + BatchSize: 80, + MaxRetries: 2, // 快手API相对不稳定 + APIEndpoint: "https://open.kuaishou.com/api", + Description: "快手直播平台,数据更新频繁", + }, nil + case SyncPlatformDouyin: + return PlatformSyncConfig{ + Platform: SyncPlatformDouyin, + IsEnabled: true, + SyncInterval: 120, // 2分钟,内容更新非常频繁 + BatchSize: 60, + MaxRetries: 3, + APIEndpoint: "https://open.douyin.com/api", + Description: "抖音短视频平台,实时性要求高", + }, nil + case SyncPlatformXiaohongshu: + return PlatformSyncConfig{ + Platform: SyncPlatformXiaohongshu, + IsEnabled: true, + SyncInterval: 300, // 5分钟 + BatchSize: 40, // 小红书API限制严格 + MaxRetries: 2, + APIEndpoint: "https://open.xiaohongshu.com/api", + Description: "小红书内容平台,API调用频率限制严格", + }, nil + case SyncPlatformPinduoduo: + return PlatformSyncConfig{ + Platform: SyncPlatformPinduoduo, + IsEnabled: true, + SyncInterval: 360, // 6分钟,避免频率限制 + BatchSize: 120, // 拼多多支持大批次 + MaxRetries: 3, + APIEndpoint: "https://open.pinduoduo.com/api", + Description: "拼多多电商平台,需要控制调用频率", + }, nil + case SyncPlatformXianyu: + return PlatformSyncConfig{ + Platform: SyncPlatformXianyu, + IsEnabled: false, // 默认关闭 + SyncInterval: 600, // 10分钟,闲鱼更新较慢 + BatchSize: 30, + MaxRetries: 1, + APIEndpoint: "https://api.xianyu.com/api", + Description: "闲鱼二手平台,数据更新较慢,谨慎使用", + }, nil + case SyncPlatformBlockchain: + return PlatformSyncConfig{ + Platform: SyncPlatformBlockchain, + IsEnabled: true, + SyncInterval: 60, // 1分钟,需要高实时性 + BatchSize: 20, // 区块链数据复杂,减小批次 + MaxRetries: 5, // 区块链网络可能不稳定 + APIEndpoint: "https://api.blockchain.com/api", + Description: "区块链平台,数据需要高实时性和稳定性", + }, nil + case SyncPlatformInternal: + return PlatformSyncConfig{ + Platform: SyncPlatformInternal, + IsEnabled: true, + SyncInterval: 30, // 30秒,内部系统实时性高 + BatchSize: 200, // 内部系统支持大批次 + MaxRetries: 1, // 内部系统稳定 + APIEndpoint: "http://localhost:3004/api", + Description: "内部系统平台,高实时性和大批次处理能力", + }, nil + default: + // 返回错误,未知平台 + return PlatformSyncConfig{}, fmt.Errorf("unsupported sync platform: %s", platform) + } +} diff --git a/consts/stock/batch_status.go b/consts/stock/batch_status.go new file mode 100644 index 0000000..9efdaa4 --- /dev/null +++ b/consts/stock/batch_status.go @@ -0,0 +1,21 @@ +package stock + +// BatchStatus 批次状态枚举 +type BatchStatus int + +const ( + BatchStatusActive BatchStatus = 1 // 活跃 + BatchStatusExpiring BatchStatus = 2 // 临期 + BatchStatusExpired BatchStatus = 3 // 过期 + BatchStatusSoldOut BatchStatus = 4 // 售罄 +) + +// GetAllBatchStatuses 获取所有批次状态 +func GetAllBatchStatuses() []BatchStatus { + return []BatchStatus{ + BatchStatusActive, + BatchStatusExpiring, + BatchStatusExpired, + BatchStatusSoldOut, + } +} diff --git a/consts/stock/capacity_unit_area.go b/consts/stock/capacity_unit_area.go new file mode 100644 index 0000000..68eef9c --- /dev/null +++ b/consts/stock/capacity_unit_area.go @@ -0,0 +1,57 @@ +package stock + +// CapacityUnitArea 面积单位枚举 +type CapacityUnitArea string + +const ( + CapacityUnitAreaSquareM CapacityUnitArea = "M2" // 平方米 + CapacityUnitAreaSquareFT CapacityUnitArea = "FT2" // 平方英尺 + CapacityUnitAreaSquareIN CapacityUnitArea = "IN2" // 平方英寸 + CapacityUnitAreaSquareCM CapacityUnitArea = "CM2" // 平方厘米 +) + +// GetAllCapacityUnitAreas 获取所有面积单位 +func GetAllCapacityUnitAreas() []CapacityUnitArea { + return []CapacityUnitArea{ + CapacityUnitAreaSquareM, + CapacityUnitAreaSquareFT, + CapacityUnitAreaSquareIN, + CapacityUnitAreaSquareCM, + } +} + +type CapacityUnitAreaKeyValue struct { + Key CapacityUnitArea + Value string +} + +var ( + CapacityUnitAreaSquareMKeyValue = CapacityUnitAreaKeyValue{Key: CapacityUnitAreaSquareM, Value: "平方米"} + CapacityUnitAreaSquareFTKeyValue = CapacityUnitAreaKeyValue{Key: CapacityUnitAreaSquareFT, Value: "平方英尺"} + CapacityUnitAreaSquareINKeyValue = CapacityUnitAreaKeyValue{Key: CapacityUnitAreaSquareIN, Value: "平方英寸"} + CapacityUnitAreaSquareCMKeyValue = CapacityUnitAreaKeyValue{Key: CapacityUnitAreaSquareCM, Value: "平方厘米"} +) + +func GetAllCapacityUnitAreaKeyValue() []CapacityUnitAreaKeyValue { + return []CapacityUnitAreaKeyValue{ + CapacityUnitAreaSquareMKeyValue, + CapacityUnitAreaSquareFTKeyValue, + CapacityUnitAreaSquareINKeyValue, + CapacityUnitAreaSquareCMKeyValue, + } +} + +var capacityUnitAreaValueMap = map[CapacityUnitArea]string{ + CapacityUnitAreaSquareM: CapacityUnitAreaSquareMKeyValue.Value, + CapacityUnitAreaSquareFT: CapacityUnitAreaSquareFTKeyValue.Value, + CapacityUnitAreaSquareIN: CapacityUnitAreaSquareINKeyValue.Value, + CapacityUnitAreaSquareCM: CapacityUnitAreaSquareCMKeyValue.Value, +} + +func GetCapacityUnitAreaValueByKey(key CapacityUnitArea) (value string) { + value, exists := capacityUnitAreaValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/capacity_unit_length.go b/consts/stock/capacity_unit_length.go new file mode 100644 index 0000000..4445495 --- /dev/null +++ b/consts/stock/capacity_unit_length.go @@ -0,0 +1,72 @@ +package stock + +// CapacityUnitLength 长度单位枚举 +type CapacityUnitLength string + +const ( + CapacityUnitLengthM CapacityUnitLength = "M" // 米 + CapacityUnitLengthCM CapacityUnitLength = "CM" // 厘米 + CapacityUnitLengthMM CapacityUnitLength = "MM" // 毫米 + CapacityUnitLengthKM CapacityUnitLength = "KM" // 千米 + CapacityUnitLengthFT CapacityUnitLength = "FT" // 英尺 + CapacityUnitLengthIN CapacityUnitLength = "IN" // 英寸 + CapacityUnitLengthYD CapacityUnitLength = "YD" // 码 +) + +// GetAllCapacityUnitLengths 获取所有长度单位 +func GetAllCapacityUnitLengths() []CapacityUnitLength { + return []CapacityUnitLength{ + CapacityUnitLengthM, + CapacityUnitLengthCM, + CapacityUnitLengthMM, + CapacityUnitLengthKM, + CapacityUnitLengthFT, + CapacityUnitLengthIN, + CapacityUnitLengthYD, + } +} + +type CapacityUnitLengthKeyValue struct { + Key CapacityUnitLength + Value string +} + +var ( + CapacityUnitLengthMKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthM, Value: "米"} + CapacityUnitLengthCMKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthCM, Value: "厘米"} + CapacityUnitLengthMMKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthMM, Value: "毫米"} + CapacityUnitLengthKMKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthKM, Value: "千米"} + CapacityUnitLengthFTKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthFT, Value: "英尺"} + CapacityUnitLengthINKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthIN, Value: "英寸"} + CapacityUnitLengthYDKeyValue = CapacityUnitLengthKeyValue{Key: CapacityUnitLengthYD, Value: "码"} +) + +func GetAllCapacityUnitLengthKeyValue() []CapacityUnitLengthKeyValue { + return []CapacityUnitLengthKeyValue{ + CapacityUnitLengthMKeyValue, + CapacityUnitLengthCMKeyValue, + CapacityUnitLengthMMKeyValue, + CapacityUnitLengthKMKeyValue, + CapacityUnitLengthFTKeyValue, + CapacityUnitLengthINKeyValue, + CapacityUnitLengthYDKeyValue, + } +} + +var capacityUnitLengthValueMap = map[CapacityUnitLength]string{ + CapacityUnitLengthM: CapacityUnitLengthMKeyValue.Value, + CapacityUnitLengthCM: CapacityUnitLengthCMKeyValue.Value, + CapacityUnitLengthMM: CapacityUnitLengthMMKeyValue.Value, + CapacityUnitLengthKM: CapacityUnitLengthKMKeyValue.Value, + CapacityUnitLengthFT: CapacityUnitLengthFTKeyValue.Value, + CapacityUnitLengthIN: CapacityUnitLengthINKeyValue.Value, + CapacityUnitLengthYD: CapacityUnitLengthYDKeyValue.Value, +} + +func GetCapacityUnitLengthValueByKey(key CapacityUnitLength) (value string) { + value, exists := capacityUnitLengthValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/capacity_unit_packaging.go b/consts/stock/capacity_unit_packaging.go new file mode 100644 index 0000000..0dc80fd --- /dev/null +++ b/consts/stock/capacity_unit_packaging.go @@ -0,0 +1,252 @@ +package stock + +// CapacityUnitPackaging 包装单位枚举 +type CapacityUnitPackaging string + +const ( + // 纸箱类 + CapacityUnitPackagingCartonInner CapacityUnitPackaging = "CARTON_INNER" // 内盒 + CapacityUnitPackagingCartonOuter CapacityUnitPackaging = "CARTON_OUTER" // 外箱 + CapacityUnitPackagingCarton CapacityUnitPackaging = "CARTON" // 纸箱 + CapacityUnitPackagingCase CapacityUnitPackaging = "CASE" // 箱/盒 + CapacityUnitPackagingBox CapacityUnitPackaging = "BOX" // 箱 + CapacityUnitPackagingMiniBox CapacityUnitPackaging = "MINI_BOX" // 迷你盒 + + // 托盘/容器类 + CapacityUnitPackagingPallet CapacityUnitPackaging = "PALLET" // 托盘 + CapacityUnitPackagingTray CapacityUnitPackaging = "TRAY" // 托盘/托架 + CapacityUnitPackagingContainer CapacityUnitPackaging = "CONTAINER" // 集装箱/货柜 + CapacityUnitPackagingBin CapacityUnitPackaging = "BIN" // 料箱 + CapacityUnitPackagingCrate CapacityUnitPackaging = "CRATE" // 条板箱 + CapacityUnitPackagingBasket CapacityUnitPackaging = "BASKET" // 篮子 + CapacityUnitPackagingCart CapacityUnitPackaging = "CART" // 手推车 + + // 袋/包/捆类 + CapacityUnitPackagingBag CapacityUnitPackaging = "BAG" // 袋 + CapacityUnitPackagingSack CapacityUnitPackaging = "SACK" // 麻袋 + CapacityUnitPackagingPack CapacityUnitPackaging = "PACK" // 包 + CapacityUnitPackagingBundle CapacityUnitPackaging = "BUNDLE" // 捆 + CapacityUnitPackagingBale CapacityUnitPackaging = "BALE" // 大包/捆 + CapacityUnitPackagingPacket CapacityUnitPackaging = "PACKET" // 小包 + + // 瓶/罐/桶类 + CapacityUnitPackagingBottle CapacityUnitPackaging = "BOTTLE" // 瓶 + CapacityUnitPackagingDrum CapacityUnitPackaging = "DRUM" // 桶 + CapacityUnitPackagingJar CapacityUnitPackaging = "JAR" // 罐/瓶 + CapacityUnitPackagingCan CapacityUnitPackaging = "CAN" // 罐/听 + CapacityUnitPackagingBarrel CapacityUnitPackaging = "BARREL" // 大桶 + CapacityUnitPackagingKeg CapacityUnitPackaging = "KEG" // 小桶 + CapacityUnitPackagingTin CapacityUnitPackaging = "TIN" // 锡罐 + + // 卷/轴/管类 + CapacityUnitPackagingRoll CapacityUnitPackaging = "ROLL" // 卷 + CapacityUnitPackagingReel CapacityUnitPackaging = "REEL" // 卷盘 + CapacityUnitPackagingSpool CapacityUnitPackaging = "SPOOL" // 线轴 + CapacityUnitPackagingTube CapacityUnitPackaging = "TUBE" // 筒/管 + CapacityUnitPackagingCoil CapacityUnitPackaging = "COIL" // 线圈 + + // 板/片/块类 + CapacityUnitPackagingSheet CapacityUnitPackaging = "SHEET" // 张/片 + CapacityUnitPackagingPanel CapacityUnitPackaging = "PANEL" // 板 + CapacityUnitPackagingBlock CapacityUnitPackaging = "BLOCK" // 块 + CapacityUnitPackagingPlate CapacityUnitPackaging = "PLATE" // 板/片 + CapacityUnitPackagingSlab CapacityUnitPackaging = "SLAB" // 厚板 +) + +// GetAllCapacityUnitPackaging 获取所有包装单位 +func GetAllCapacityUnitPackaging() []CapacityUnitPackaging { + return []CapacityUnitPackaging{ + // 纸箱类 + CapacityUnitPackagingCartonInner, + CapacityUnitPackagingCartonOuter, + CapacityUnitPackagingCarton, + CapacityUnitPackagingCase, + CapacityUnitPackagingBox, + CapacityUnitPackagingMiniBox, + // 托盘/容器类 + CapacityUnitPackagingPallet, + CapacityUnitPackagingTray, + CapacityUnitPackagingContainer, + CapacityUnitPackagingBin, + CapacityUnitPackagingCrate, + CapacityUnitPackagingBasket, + CapacityUnitPackagingCart, + // 袋/包/捆类 + CapacityUnitPackagingBag, + CapacityUnitPackagingSack, + CapacityUnitPackagingPack, + CapacityUnitPackagingBundle, + CapacityUnitPackagingBale, + CapacityUnitPackagingPacket, + // 瓶/罐/桶类 + CapacityUnitPackagingBottle, + CapacityUnitPackagingDrum, + CapacityUnitPackagingJar, + CapacityUnitPackagingCan, + CapacityUnitPackagingBarrel, + CapacityUnitPackagingKeg, + CapacityUnitPackagingTin, + // 卷/轴/管类 + CapacityUnitPackagingRoll, + CapacityUnitPackagingReel, + CapacityUnitPackagingSpool, + CapacityUnitPackagingTube, + CapacityUnitPackagingCoil, + // 板/片/块类 + CapacityUnitPackagingSheet, + CapacityUnitPackagingPanel, + CapacityUnitPackagingBlock, + CapacityUnitPackagingPlate, + CapacityUnitPackagingSlab, + } +} + +type CapacityUnitPackagingKeyValue struct { + Key CapacityUnitPackaging + Value string +} + +var ( + // 纸箱类 + CapacityUnitPackagingCartonInnerKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCartonInner, Value: "内盒"} + CapacityUnitPackagingCartonOuterKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCartonOuter, Value: "外箱"} + CapacityUnitPackagingCartonKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCarton, Value: "纸箱"} + CapacityUnitPackagingCaseKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCase, Value: "箱/盒"} + CapacityUnitPackagingBoxKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBox, Value: "箱"} + CapacityUnitPackagingMiniBoxKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingMiniBox, Value: "迷你盒"} + // 托盘/容器类 + CapacityUnitPackagingPalletKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingPallet, Value: "托盘"} + CapacityUnitPackagingTrayKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingTray, Value: "托盘/托架"} + CapacityUnitPackagingContainerKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingContainer, Value: "集装箱/货柜"} + CapacityUnitPackagingBinKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBin, Value: "料箱"} + CapacityUnitPackagingCrateKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCrate, Value: "条板箱"} + CapacityUnitPackagingBasketKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBasket, Value: "篮子"} + CapacityUnitPackagingCartKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCart, Value: "手推车"} + // 袋/包/捆类 + CapacityUnitPackagingBagKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBag, Value: "袋"} + CapacityUnitPackagingSackKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingSack, Value: "麻袋"} + CapacityUnitPackagingPackKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingPack, Value: "包"} + CapacityUnitPackagingBundleKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBundle, Value: "捆"} + CapacityUnitPackagingBaleKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBale, Value: "大包/捆"} + CapacityUnitPackagingPacketKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingPacket, Value: "小包"} + // 瓶/罐/桶类 + CapacityUnitPackagingBottleKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBottle, Value: "瓶"} + CapacityUnitPackagingDrumKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingDrum, Value: "桶"} + CapacityUnitPackagingJarKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingJar, Value: "罐/瓶"} + CapacityUnitPackagingCanKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCan, Value: "罐/听"} + CapacityUnitPackagingBarrelKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBarrel, Value: "大桶"} + CapacityUnitPackagingKegKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingKeg, Value: "小桶"} + CapacityUnitPackagingTinKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingTin, Value: "锡罐"} + // 卷/轴/管类 + CapacityUnitPackagingRollKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingRoll, Value: "卷"} + CapacityUnitPackagingReelKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingReel, Value: "卷盘"} + CapacityUnitPackagingSpoolKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingSpool, Value: "线轴"} + CapacityUnitPackagingTubeKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingTube, Value: "筒/管"} + CapacityUnitPackagingCoilKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingCoil, Value: "线圈"} + // 板/片/块类 + CapacityUnitPackagingSheetKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingSheet, Value: "张/片"} + CapacityUnitPackagingPanelKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingPanel, Value: "板"} + CapacityUnitPackagingBlockKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingBlock, Value: "块"} + CapacityUnitPackagingPlateKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingPlate, Value: "板/片"} + CapacityUnitPackagingSlabKeyValue = CapacityUnitPackagingKeyValue{Key: CapacityUnitPackagingSlab, Value: "厚板"} +) + +func GetAllCapacityUnitPackagingKeyValue() []CapacityUnitPackagingKeyValue { + return []CapacityUnitPackagingKeyValue{ + // 纸箱类 + CapacityUnitPackagingCartonInnerKeyValue, + CapacityUnitPackagingCartonOuterKeyValue, + CapacityUnitPackagingCartonKeyValue, + CapacityUnitPackagingCaseKeyValue, + CapacityUnitPackagingBoxKeyValue, + CapacityUnitPackagingMiniBoxKeyValue, + // 托盘/容器类 + CapacityUnitPackagingPalletKeyValue, + CapacityUnitPackagingTrayKeyValue, + CapacityUnitPackagingContainerKeyValue, + CapacityUnitPackagingBinKeyValue, + CapacityUnitPackagingCrateKeyValue, + CapacityUnitPackagingBasketKeyValue, + CapacityUnitPackagingCartKeyValue, + // 袋/包/捆类 + CapacityUnitPackagingBagKeyValue, + CapacityUnitPackagingSackKeyValue, + CapacityUnitPackagingPackKeyValue, + CapacityUnitPackagingBundleKeyValue, + CapacityUnitPackagingBaleKeyValue, + CapacityUnitPackagingPacketKeyValue, + // 瓶/罐/桶类 + CapacityUnitPackagingBottleKeyValue, + CapacityUnitPackagingDrumKeyValue, + CapacityUnitPackagingJarKeyValue, + CapacityUnitPackagingCanKeyValue, + CapacityUnitPackagingBarrelKeyValue, + CapacityUnitPackagingKegKeyValue, + CapacityUnitPackagingTinKeyValue, + // 卷/轴/管类 + CapacityUnitPackagingRollKeyValue, + CapacityUnitPackagingReelKeyValue, + CapacityUnitPackagingSpoolKeyValue, + CapacityUnitPackagingTubeKeyValue, + CapacityUnitPackagingCoilKeyValue, + // 板/片/块类 + CapacityUnitPackagingSheetKeyValue, + CapacityUnitPackagingPanelKeyValue, + CapacityUnitPackagingBlockKeyValue, + CapacityUnitPackagingPlateKeyValue, + CapacityUnitPackagingSlabKeyValue, + } +} + +var capacityUnitPackagingValueMap = map[CapacityUnitPackaging]string{ + // 纸箱类 + CapacityUnitPackagingCartonInner: CapacityUnitPackagingCartonInnerKeyValue.Value, + CapacityUnitPackagingCartonOuter: CapacityUnitPackagingCartonOuterKeyValue.Value, + CapacityUnitPackagingCarton: CapacityUnitPackagingCartonKeyValue.Value, + CapacityUnitPackagingCase: CapacityUnitPackagingCaseKeyValue.Value, + CapacityUnitPackagingBox: CapacityUnitPackagingBoxKeyValue.Value, + CapacityUnitPackagingMiniBox: CapacityUnitPackagingMiniBoxKeyValue.Value, + // 托盘/容器类 + CapacityUnitPackagingPallet: CapacityUnitPackagingPalletKeyValue.Value, + CapacityUnitPackagingTray: CapacityUnitPackagingTrayKeyValue.Value, + CapacityUnitPackagingContainer: CapacityUnitPackagingContainerKeyValue.Value, + CapacityUnitPackagingBin: CapacityUnitPackagingBinKeyValue.Value, + CapacityUnitPackagingCrate: CapacityUnitPackagingCrateKeyValue.Value, + CapacityUnitPackagingBasket: CapacityUnitPackagingBasketKeyValue.Value, + CapacityUnitPackagingCart: CapacityUnitPackagingCartKeyValue.Value, + // 袋/包/捆类 + CapacityUnitPackagingBag: CapacityUnitPackagingBagKeyValue.Value, + CapacityUnitPackagingSack: CapacityUnitPackagingSackKeyValue.Value, + CapacityUnitPackagingPack: CapacityUnitPackagingPackKeyValue.Value, + CapacityUnitPackagingBundle: CapacityUnitPackagingBundleKeyValue.Value, + CapacityUnitPackagingBale: CapacityUnitPackagingBaleKeyValue.Value, + CapacityUnitPackagingPacket: CapacityUnitPackagingPacketKeyValue.Value, + // 瓶/罐/桶类 + CapacityUnitPackagingBottle: CapacityUnitPackagingBottleKeyValue.Value, + CapacityUnitPackagingDrum: CapacityUnitPackagingDrumKeyValue.Value, + CapacityUnitPackagingJar: CapacityUnitPackagingJarKeyValue.Value, + CapacityUnitPackagingCan: CapacityUnitPackagingCanKeyValue.Value, + CapacityUnitPackagingBarrel: CapacityUnitPackagingBarrelKeyValue.Value, + CapacityUnitPackagingKeg: CapacityUnitPackagingKegKeyValue.Value, + CapacityUnitPackagingTin: CapacityUnitPackagingTinKeyValue.Value, + // 卷/轴/管类 + CapacityUnitPackagingRoll: CapacityUnitPackagingRollKeyValue.Value, + CapacityUnitPackagingReel: CapacityUnitPackagingReelKeyValue.Value, + CapacityUnitPackagingSpool: CapacityUnitPackagingSpoolKeyValue.Value, + CapacityUnitPackagingTube: CapacityUnitPackagingTubeKeyValue.Value, + CapacityUnitPackagingCoil: CapacityUnitPackagingCoilKeyValue.Value, + // 板/片/块类 + CapacityUnitPackagingSheet: CapacityUnitPackagingSheetKeyValue.Value, + CapacityUnitPackagingPanel: CapacityUnitPackagingPanelKeyValue.Value, + CapacityUnitPackagingBlock: CapacityUnitPackagingBlockKeyValue.Value, + CapacityUnitPackagingPlate: CapacityUnitPackagingPlateKeyValue.Value, + CapacityUnitPackagingSlab: CapacityUnitPackagingSlabKeyValue.Value, +} + +func GetCapacityUnitPackagingValueByKey(key CapacityUnitPackaging) (value string) { + value, exists := capacityUnitPackagingValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/capacity_unit_quantity.go b/consts/stock/capacity_unit_quantity.go new file mode 100644 index 0000000..db257b3 --- /dev/null +++ b/consts/stock/capacity_unit_quantity.go @@ -0,0 +1,92 @@ +package stock + +// CapacityUnitQuantity 数量单位枚举 +type CapacityUnitQuantity string + +const ( + CapacityUnitQuantityPCS CapacityUnitQuantity = "PCS" // 件 + CapacityUnitQuantityPiece CapacityUnitQuantity = "PIECE" // 个 + CapacityUnitQuantityUnit CapacityUnitQuantity = "UNIT" // 单位 + CapacityUnitQuantityItem CapacityUnitQuantity = "ITEM" // 项 + CapacityUnitQuantityEach CapacityUnitQuantity = "EACH" // 每一个 + CapacityUnitQuantityPair CapacityUnitQuantity = "PAIR" // 双/对 + CapacityUnitQuantitySet CapacityUnitQuantity = "SET" // 套 + CapacityUnitQuantityKit CapacityUnitQuantity = "KIT" // 成套件 + CapacityUnitQuantityGroup CapacityUnitQuantity = "GROUP" // 组 + CapacityUnitQuantityLot CapacityUnitQuantity = "LOT" // 批 + CapacityUnitQuantityBatch CapacityUnitQuantity = "BATCH" // 批次 +) + +// GetAllCapacityUnitQuantities 获取所有数量单位 +func GetAllCapacityUnitQuantities() []CapacityUnitQuantity { + return []CapacityUnitQuantity{ + CapacityUnitQuantityPCS, + CapacityUnitQuantityPiece, + CapacityUnitQuantityUnit, + CapacityUnitQuantityItem, + CapacityUnitQuantityEach, + CapacityUnitQuantityPair, + CapacityUnitQuantitySet, + CapacityUnitQuantityKit, + CapacityUnitQuantityGroup, + CapacityUnitQuantityLot, + CapacityUnitQuantityBatch, + } +} + +type CapacityUnitQuantityKeyValue struct { + Key CapacityUnitQuantity + Value string +} + +var ( + CapacityUnitQuantityPCSKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityPCS, Value: "件"} + CapacityUnitQuantityPieceKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityPiece, Value: "个"} + CapacityUnitQuantityUnitKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityUnit, Value: "单位"} + CapacityUnitQuantityItemKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityItem, Value: "项"} + CapacityUnitQuantityEachKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityEach, Value: "每一个"} + CapacityUnitQuantityPairKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityPair, Value: "双/对"} + CapacityUnitQuantitySetKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantitySet, Value: "套"} + CapacityUnitQuantityKitKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityKit, Value: "成套件"} + CapacityUnitQuantityGroupKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityGroup, Value: "组"} + CapacityUnitQuantityLotKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityLot, Value: "批"} + CapacityUnitQuantityBatchKeyValue = CapacityUnitQuantityKeyValue{Key: CapacityUnitQuantityBatch, Value: "批次"} +) + +func GetAllCapacityUnitQuantityKeyValue() []CapacityUnitQuantityKeyValue { + return []CapacityUnitQuantityKeyValue{ + CapacityUnitQuantityPCSKeyValue, + CapacityUnitQuantityPieceKeyValue, + CapacityUnitQuantityUnitKeyValue, + CapacityUnitQuantityItemKeyValue, + CapacityUnitQuantityEachKeyValue, + CapacityUnitQuantityPairKeyValue, + CapacityUnitQuantitySetKeyValue, + CapacityUnitQuantityKitKeyValue, + CapacityUnitQuantityGroupKeyValue, + CapacityUnitQuantityLotKeyValue, + CapacityUnitQuantityBatchKeyValue, + } +} + +var capacityUnitQuantityValueMap = map[CapacityUnitQuantity]string{ + CapacityUnitQuantityPCS: CapacityUnitQuantityPCSKeyValue.Value, + CapacityUnitQuantityPiece: CapacityUnitQuantityPieceKeyValue.Value, + CapacityUnitQuantityUnit: CapacityUnitQuantityUnitKeyValue.Value, + CapacityUnitQuantityItem: CapacityUnitQuantityItemKeyValue.Value, + CapacityUnitQuantityEach: CapacityUnitQuantityEachKeyValue.Value, + CapacityUnitQuantityPair: CapacityUnitQuantityPairKeyValue.Value, + CapacityUnitQuantitySet: CapacityUnitQuantitySetKeyValue.Value, + CapacityUnitQuantityKit: CapacityUnitQuantityKitKeyValue.Value, + CapacityUnitQuantityGroup: CapacityUnitQuantityGroupKeyValue.Value, + CapacityUnitQuantityLot: CapacityUnitQuantityLotKeyValue.Value, + CapacityUnitQuantityBatch: CapacityUnitQuantityBatchKeyValue.Value, +} + +func GetCapacityUnitQuantityValueByKey(key CapacityUnitQuantity) (value string) { + value, exists := capacityUnitQuantityValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/capacity_unit_type.go b/consts/stock/capacity_unit_type.go new file mode 100644 index 0000000..ed7952a --- /dev/null +++ b/consts/stock/capacity_unit_type.go @@ -0,0 +1,67 @@ +package stock + +// CapacityUnitType 容量单位类型枚举 +type CapacityUnitType string + +const ( + CapacityUnitTypeQuantity CapacityUnitType = "quantity" // 数量单位 + CapacityUnitTypeWeight CapacityUnitType = "weight" // 重量单位 + CapacityUnitTypeVolume CapacityUnitType = "volume" // 体积单位 + CapacityUnitTypeArea CapacityUnitType = "area" // 面积单位 + CapacityUnitTypeLength CapacityUnitType = "length" // 长度单位 + CapacityUnitTypePackaging CapacityUnitType = "packaging" // 包装单位 +) + +// GetAllCapacityUnitTypes 获取所有容量单位类型 +func GetAllCapacityUnitTypes() []CapacityUnitType { + return []CapacityUnitType{ + CapacityUnitTypeQuantity, + CapacityUnitTypeWeight, + CapacityUnitTypeVolume, + CapacityUnitTypeArea, + CapacityUnitTypeLength, + CapacityUnitTypePackaging, + } +} + +type CapacityUnitTypeKeyValue struct { + Key CapacityUnitType + Value string +} + +var ( + CapacityUnitTypeQuantityKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypeQuantity, Value: "数量单位"} + CapacityUnitTypeWeightKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypeWeight, Value: "重量单位"} + CapacityUnitTypeVolumeKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypeVolume, Value: "体积单位"} + CapacityUnitTypeAreaKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypeArea, Value: "面积单位"} + CapacityUnitTypeLengthKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypeLength, Value: "长度单位"} + CapacityUnitTypePackagingKeyValue = CapacityUnitTypeKeyValue{Key: CapacityUnitTypePackaging, Value: "包装单位"} +) + +func GetAllCapacityUnitTypeKeyValue() []CapacityUnitTypeKeyValue { + return []CapacityUnitTypeKeyValue{ + CapacityUnitTypeQuantityKeyValue, + CapacityUnitTypeWeightKeyValue, + CapacityUnitTypeVolumeKeyValue, + CapacityUnitTypeAreaKeyValue, + CapacityUnitTypeLengthKeyValue, + CapacityUnitTypePackagingKeyValue, + } +} + +var capacityUnitTypeValueMap = map[CapacityUnitType]string{ + CapacityUnitTypeQuantity: CapacityUnitTypeQuantityKeyValue.Value, + CapacityUnitTypeWeight: CapacityUnitTypeWeightKeyValue.Value, + CapacityUnitTypeVolume: CapacityUnitTypeVolumeKeyValue.Value, + CapacityUnitTypeArea: CapacityUnitTypeAreaKeyValue.Value, + CapacityUnitTypeLength: CapacityUnitTypeLengthKeyValue.Value, + CapacityUnitTypePackaging: CapacityUnitTypePackagingKeyValue.Value, +} + +func GetCapacityUnitTypeValueByKey(key CapacityUnitType) (value string) { + value, exists := capacityUnitTypeValueMap[key] + if !exists { + value = "未知类型" + } + return +} diff --git a/consts/stock/capacity_unit_volume.go b/consts/stock/capacity_unit_volume.go new file mode 100644 index 0000000..48dcde1 --- /dev/null +++ b/consts/stock/capacity_unit_volume.go @@ -0,0 +1,62 @@ +package stock + +// CapacityUnitVolume 体积单位枚举 +type CapacityUnitVolume string + +const ( + CapacityUnitVolumeCBM CapacityUnitVolume = "CBM" // 立方米 + CapacityUnitVolumeM3 CapacityUnitVolume = "M3" // 立方米 + CapacityUnitVolumeL CapacityUnitVolume = "L" // 升 + CapacityUnitVolumeML CapacityUnitVolume = "ML" // 毫升 + CapacityUnitVolumeGAL CapacityUnitVolume = "GAL" // 加仑 +) + +// GetAllCapacityUnitVolumes 获取所有体积单位 +func GetAllCapacityUnitVolumes() []CapacityUnitVolume { + return []CapacityUnitVolume{ + CapacityUnitVolumeCBM, + CapacityUnitVolumeM3, + CapacityUnitVolumeL, + CapacityUnitVolumeML, + CapacityUnitVolumeGAL, + } +} + +type CapacityUnitVolumeKeyValue struct { + Key CapacityUnitVolume + Value string +} + +var ( + CapacityUnitVolumeCBMKeyValue = CapacityUnitVolumeKeyValue{Key: CapacityUnitVolumeCBM, Value: "立方米"} + CapacityUnitVolumeM3KeyValue = CapacityUnitVolumeKeyValue{Key: CapacityUnitVolumeM3, Value: "立方米"} + CapacityUnitVolumeLKeyValue = CapacityUnitVolumeKeyValue{Key: CapacityUnitVolumeL, Value: "升"} + CapacityUnitVolumeMLKeyValue = CapacityUnitVolumeKeyValue{Key: CapacityUnitVolumeML, Value: "毫升"} + CapacityUnitVolumeGALKeyValue = CapacityUnitVolumeKeyValue{Key: CapacityUnitVolumeGAL, Value: "加仑"} +) + +func GetAllCapacityUnitVolumeKeyValue() []CapacityUnitVolumeKeyValue { + return []CapacityUnitVolumeKeyValue{ + CapacityUnitVolumeCBMKeyValue, + CapacityUnitVolumeM3KeyValue, + CapacityUnitVolumeLKeyValue, + CapacityUnitVolumeMLKeyValue, + CapacityUnitVolumeGALKeyValue, + } +} + +var capacityUnitVolumeValueMap = map[CapacityUnitVolume]string{ + CapacityUnitVolumeCBM: CapacityUnitVolumeCBMKeyValue.Value, + CapacityUnitVolumeM3: CapacityUnitVolumeM3KeyValue.Value, + CapacityUnitVolumeL: CapacityUnitVolumeLKeyValue.Value, + CapacityUnitVolumeML: CapacityUnitVolumeMLKeyValue.Value, + CapacityUnitVolumeGAL: CapacityUnitVolumeGALKeyValue.Value, +} + +func GetCapacityUnitVolumeValueByKey(key CapacityUnitVolume) (value string) { + value, exists := capacityUnitVolumeValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/capacity_unit_weight.go b/consts/stock/capacity_unit_weight.go new file mode 100644 index 0000000..8c31c85 --- /dev/null +++ b/consts/stock/capacity_unit_weight.go @@ -0,0 +1,67 @@ +package stock + +// CapacityUnitWeight 重量单位枚举 +type CapacityUnitWeight string + +const ( + CapacityUnitWeightKG CapacityUnitWeight = "KG" // 千克 + CapacityUnitWeightTon CapacityUnitWeight = "TON" // 吨 + CapacityUnitWeightG CapacityUnitWeight = "G" // 克 + CapacityUnitWeightMG CapacityUnitWeight = "MG" // 毫克 + CapacityUnitWeightLB CapacityUnitWeight = "LB" // 磅 + CapacityUnitWeightOZ CapacityUnitWeight = "OZ" // 盎司 +) + +// GetAllCapacityUnitWeights 获取所有重量单位 +func GetAllCapacityUnitWeights() []CapacityUnitWeight { + return []CapacityUnitWeight{ + CapacityUnitWeightKG, + CapacityUnitWeightTon, + CapacityUnitWeightG, + CapacityUnitWeightMG, + CapacityUnitWeightLB, + CapacityUnitWeightOZ, + } +} + +type CapacityUnitWeightKeyValue struct { + Key CapacityUnitWeight + Value string +} + +var ( + CapacityUnitWeightKGKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightKG, Value: "千克"} + CapacityUnitWeightTonKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightTon, Value: "吨"} + CapacityUnitWeightGKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightG, Value: "克"} + CapacityUnitWeightMGKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightMG, Value: "毫克"} + CapacityUnitWeightLBKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightLB, Value: "磅"} + CapacityUnitWeightOZKeyValue = CapacityUnitWeightKeyValue{Key: CapacityUnitWeightOZ, Value: "盎司"} +) + +func GetAllCapacityUnitWeightKeyValue() []CapacityUnitWeightKeyValue { + return []CapacityUnitWeightKeyValue{ + CapacityUnitWeightKGKeyValue, + CapacityUnitWeightTonKeyValue, + CapacityUnitWeightGKeyValue, + CapacityUnitWeightMGKeyValue, + CapacityUnitWeightLBKeyValue, + CapacityUnitWeightOZKeyValue, + } +} + +var capacityUnitWeightValueMap = map[CapacityUnitWeight]string{ + CapacityUnitWeightKG: CapacityUnitWeightKGKeyValue.Value, + CapacityUnitWeightTon: CapacityUnitWeightTonKeyValue.Value, + CapacityUnitWeightG: CapacityUnitWeightGKeyValue.Value, + CapacityUnitWeightMG: CapacityUnitWeightMGKeyValue.Value, + CapacityUnitWeightLB: CapacityUnitWeightLBKeyValue.Value, + CapacityUnitWeightOZ: CapacityUnitWeightOZKeyValue.Value, +} + +func GetCapacityUnitWeightValueByKey(key CapacityUnitWeight) (value string) { + value, exists := capacityUnitWeightValueMap[key] + if !exists { + value = "未知单位" + } + return +} diff --git a/consts/stock/discrepancy_type.go b/consts/stock/discrepancy_type.go new file mode 100644 index 0000000..3d02e0d --- /dev/null +++ b/consts/stock/discrepancy_type.go @@ -0,0 +1,45 @@ +package stock + +// DiscrepancyType 差异类型枚举 +type DiscrepancyType int + +const ( + DiscrepancyTypeNone DiscrepancyType = 0 // 无差异 + DiscrepancyTypeOverflow DiscrepancyType = 1 // 盘盈 + DiscrepancyTypeShortage DiscrepancyType = 2 // 盘亏 + DiscrepancyTypeWrongItem DiscrepancyType = 3 // 错货 + DiscrepancyTypeDamage DiscrepancyType = 4 // 损坏 + DiscrepancyTypeExpired DiscrepancyType = 5 // 过期 +) + +// GetAllDiscrepancyTypes 获取所有差异类型 +func GetAllDiscrepancyTypes() []DiscrepancyType { + return []DiscrepancyType{ + DiscrepancyTypeNone, + DiscrepancyTypeOverflow, + DiscrepancyTypeShortage, + DiscrepancyTypeWrongItem, + DiscrepancyTypeDamage, + DiscrepancyTypeExpired, + } +} + +// String 获取差异类型字符串表示 +func (d DiscrepancyType) String() string { + switch d { + case DiscrepancyTypeNone: + return "无差异" + case DiscrepancyTypeOverflow: + return "盘盈" + case DiscrepancyTypeShortage: + return "盘亏" + case DiscrepancyTypeWrongItem: + return "错货" + case DiscrepancyTypeDamage: + return "损坏" + case DiscrepancyTypeExpired: + return "过期" + default: + return "未知" + } +} diff --git a/consts/stock/expiry_message_type.go b/consts/stock/expiry_message_type.go new file mode 100644 index 0000000..bdbdd85 --- /dev/null +++ b/consts/stock/expiry_message_type.go @@ -0,0 +1,38 @@ +package stock + +// ExpiryMessageStatus 临期消息状态 +type ExpiryMessageStatus int + +const ( + ExpiryMessageStatusPending ExpiryMessageStatus = 1 // 待处理 + ExpiryMessageStatusCompleted ExpiryMessageStatus = 2 // 已处理 +) + +// ExpiryProcessMethod 临期处理方式 +type ExpiryProcessMethod int + +const ( + ExpiryProcessMethodSupplierRecycle ExpiryProcessMethod = 1 // 供货商回收 + ExpiryProcessMethodReprocess ExpiryProcessMethod = 2 // 二次加工 + ExpiryProcessMethodScrap ExpiryProcessMethod = 3 // 报废 + ExpiryProcessMethodInternal ExpiryProcessMethod = 4 // 内部消化 + ExpiryProcessMethodPromotion ExpiryProcessMethod = 5 // 促销 +) + +// GetProcessMethodName 获取处理方式名称 +func GetProcessMethodName(method ExpiryProcessMethod) string { + switch method { + case ExpiryProcessMethodSupplierRecycle: + return "供货商回收" + case ExpiryProcessMethodReprocess: + return "二次加工" + case ExpiryProcessMethodScrap: + return "报废" + case ExpiryProcessMethodInternal: + return "内部消化" + case ExpiryProcessMethodPromotion: + return "促销" + default: + return "未知" + } +} diff --git a/consts/stock/inventory_count_scope.go b/consts/stock/inventory_count_scope.go new file mode 100644 index 0000000..99dfd37 --- /dev/null +++ b/consts/stock/inventory_count_scope.go @@ -0,0 +1,41 @@ +package stock + +// InventoryCountScope 库存盘点范围枚举 +type InventoryCountScope int + +const ( + InventoryCountScopeWarehouse InventoryCountScope = 1 // 按仓库盘点 + InventoryCountScopeZone InventoryCountScope = 2 // 按库区盘点 + InventoryCountScopeLocation InventoryCountScope = 3 // 按库位盘点 + InventoryCountScopeSku InventoryCountScope = 4 // 按SKU盘点 + InventoryCountScopeAsset InventoryCountScope = 5 // 按资产盘点 +) + +// GetAllInventoryCountScopes 获取所有盘点范围 +func GetAllInventoryCountScopes() []InventoryCountScope { + return []InventoryCountScope{ + InventoryCountScopeWarehouse, + InventoryCountScopeZone, + InventoryCountScopeLocation, + InventoryCountScopeSku, + InventoryCountScopeAsset, + } +} + +// String 获取盘点范围字符串表示 +func (i InventoryCountScope) String() string { + switch i { + case InventoryCountScopeWarehouse: + return "按仓库盘点" + case InventoryCountScopeZone: + return "按库区盘点" + case InventoryCountScopeLocation: + return "按库位盘点" + case InventoryCountScopeSku: + return "按SKU盘点" + case InventoryCountScopeAsset: + return "按资产盘点" + default: + return "未知" + } +} diff --git a/consts/stock/inventory_count_status.go b/consts/stock/inventory_count_status.go new file mode 100644 index 0000000..3d2b9c2 --- /dev/null +++ b/consts/stock/inventory_count_status.go @@ -0,0 +1,33 @@ +package stock + +// InventoryCountStatus 库存盘点状态枚举 +type InventoryCountStatus int + +const ( + InventoryCountStatusInProgress InventoryCountStatus = 1 // 进行中 + InventoryCountStatusCompleted InventoryCountStatus = 2 // 已完成 + InventoryCountStatusCancelled InventoryCountStatus = 3 // 已取消 +) + +// GetAllInventoryCountStatuses 获取所有盘点状态 +func GetAllInventoryCountStatuses() []InventoryCountStatus { + return []InventoryCountStatus{ + InventoryCountStatusInProgress, + InventoryCountStatusCompleted, + InventoryCountStatusCancelled, + } +} + +// String 获取盘点状态字符串表示 +func (i InventoryCountStatus) String() string { + switch i { + case InventoryCountStatusInProgress: + return "进行中" + case InventoryCountStatusCompleted: + return "已完成" + case InventoryCountStatusCancelled: + return "已取消" + default: + return "未知" + } +} diff --git a/consts/stock/inventory_count_type.go b/consts/stock/inventory_count_type.go new file mode 100644 index 0000000..03427f5 --- /dev/null +++ b/consts/stock/inventory_count_type.go @@ -0,0 +1,29 @@ +package stock + +// InventoryCountType 库存盘点类型枚举 +type InventoryCountType int + +const ( + OpenPrice InventoryCountType = 1 // 明盘 + HiddenPrice InventoryCountType = 2 // 盲盘 +) + +// GetAllInventoryCountTypes 获取所有盘点类型 +func GetAllInventoryCountTypes() []InventoryCountType { + return []InventoryCountType{ + OpenPrice, + HiddenPrice, + } +} + +// String 获取盘点类型字符串表示 +func (i InventoryCountType) String() string { + switch i { + case OpenPrice: + return "明盘" + case HiddenPrice: + return "盲盘" + default: + return "未知" + } +} diff --git a/consts/stock/inventory_detail_status.go b/consts/stock/inventory_detail_status.go new file mode 100644 index 0000000..f5a5079 --- /dev/null +++ b/consts/stock/inventory_detail_status.go @@ -0,0 +1,29 @@ +package stock + +// InventoryDetailStatus 库存盘点明细状态枚举 +type InventoryDetailStatus int + +const ( + InventoryDetailStatusPending InventoryDetailStatus = 1 // 待盘点(创建时默认) + InventoryDetailStatusCompleted InventoryDetailStatus = 2 // 已完成(Excel导入后) +) + +// GetAllInventoryDetailStatuses 获取所有明细状态 +func GetAllInventoryDetailStatuses() []InventoryDetailStatus { + return []InventoryDetailStatus{ + InventoryDetailStatusPending, + InventoryDetailStatusCompleted, + } +} + +// String 获取明细状态字符串表示 +func (i InventoryDetailStatus) String() string { + switch i { + case InventoryDetailStatusPending: + return "待盘点" + case InventoryDetailStatusCompleted: + return "已完成" + default: + return "未知" + } +} diff --git a/consts/stock/location_status.go b/consts/stock/location_status.go new file mode 100644 index 0000000..48608e9 --- /dev/null +++ b/consts/stock/location_status.go @@ -0,0 +1,57 @@ +package stock + +// LocationStatus 库位状态枚举 +type LocationStatus string + +const ( + LocationStatusIdle LocationStatus = "idle" // 空闲 + LocationStatusOccupied LocationStatus = "occupied" // 占用 + LocationStatusDisabled LocationStatus = "disable" // 禁用 + LocationStatusReserved LocationStatus = "reserved" // 预留 +) + +// GetAllLocationStatuses 获取所有库位状态 +func GetAllLocationStatuses() []LocationStatus { + return []LocationStatus{ + LocationStatusIdle, + LocationStatusOccupied, + LocationStatusDisabled, + LocationStatusReserved, + } +} + +type LocationStatusKeyValue struct { + Key LocationStatus + Value string +} + +var ( + LocationStatusIdleKeyValue = LocationStatusKeyValue{Key: LocationStatusIdle, Value: "空闲"} + LocationStatusOccupiedKeyValue = LocationStatusKeyValue{Key: LocationStatusOccupied, Value: "占用"} + LocationStatusDisabledKeyValue = LocationStatusKeyValue{Key: LocationStatusDisabled, Value: "禁用"} + LocationStatusReservedKeyValue = LocationStatusKeyValue{Key: LocationStatusReserved, Value: "预留"} +) + +func GetAllLocationStatusKeyValue() []LocationStatusKeyValue { + return []LocationStatusKeyValue{ + LocationStatusIdleKeyValue, + LocationStatusOccupiedKeyValue, + LocationStatusDisabledKeyValue, + LocationStatusReservedKeyValue, + } +} + +var locationStatusValueMap = map[LocationStatus]string{ + LocationStatusIdle: LocationStatusIdleKeyValue.Value, + LocationStatusOccupied: LocationStatusOccupiedKeyValue.Value, + LocationStatusDisabled: LocationStatusDisabledKeyValue.Value, + LocationStatusReserved: LocationStatusReservedKeyValue.Value, +} + +func GetLocationStatusValueByKey(key LocationStatus) (value string) { + value, exists := locationStatusValueMap[key] + if !exists { + value = "未知状态" + } + return +} diff --git a/consts/stock/location_type.go b/consts/stock/location_type.go new file mode 100644 index 0000000..6d42af9 --- /dev/null +++ b/consts/stock/location_type.go @@ -0,0 +1,72 @@ +package stock + +// LocationType 库位类型枚举 +type LocationType string + +const ( + LocationTypeShelf LocationType = "shelf" // 货架 + LocationTypeStack LocationType = "stack" // 堆垛 + LocationTypeCold LocationType = "cold" // 冷藏位 + LocationTypeFreeze LocationType = "freeze" // 冷冻位 + LocationTypeBin LocationType = "bin" // 料箱位 + LocationTypePallet LocationType = "pallet" // 托盘位 + LocationTypeFloor LocationType = "floor" // 地面堆放 +) + +// GetAllLocationTypes 获取所有库位类型 +func GetAllLocationTypes() []LocationType { + return []LocationType{ + LocationTypeShelf, + LocationTypeStack, + LocationTypeCold, + LocationTypeFreeze, + LocationTypeBin, + LocationTypePallet, + LocationTypeFloor, + } +} + +type LocationTypeKeyValue struct { + Key LocationType + Value string +} + +var ( + LocationTypeShelfKeyValue = LocationTypeKeyValue{Key: LocationTypeShelf, Value: "货架"} + LocationTypeStackKeyValue = LocationTypeKeyValue{Key: LocationTypeStack, Value: "堆垛"} + LocationTypeColdKeyValue = LocationTypeKeyValue{Key: LocationTypeCold, Value: "冷藏位"} + LocationTypeFreezeKeyValue = LocationTypeKeyValue{Key: LocationTypeFreeze, Value: "冷冻位"} + LocationTypeBinKeyValue = LocationTypeKeyValue{Key: LocationTypeBin, Value: "料箱位"} + LocationTypePalletKeyValue = LocationTypeKeyValue{Key: LocationTypePallet, Value: "托盘位"} + LocationTypeFloorKeyValue = LocationTypeKeyValue{Key: LocationTypeFloor, Value: "地面堆放"} +) + +func GetAllLocationTypeKeyValue() []LocationTypeKeyValue { + return []LocationTypeKeyValue{ + LocationTypeShelfKeyValue, + LocationTypeStackKeyValue, + LocationTypeColdKeyValue, + LocationTypeFreezeKeyValue, + LocationTypeBinKeyValue, + LocationTypePalletKeyValue, + LocationTypeFloorKeyValue, + } +} + +var locationTypeValueMap = map[LocationType]string{ + LocationTypeShelf: LocationTypeShelfKeyValue.Value, + LocationTypeStack: LocationTypeStackKeyValue.Value, + LocationTypeCold: LocationTypeColdKeyValue.Value, + LocationTypeFreeze: LocationTypeFreezeKeyValue.Value, + LocationTypeBin: LocationTypeBinKeyValue.Value, + LocationTypePallet: LocationTypePalletKeyValue.Value, + LocationTypeFloor: LocationTypeFloorKeyValue.Value, +} + +func GetLocationTypeValueByKey(key LocationType) (value string) { + value, exists := locationTypeValueMap[key] + if !exists { + value = "未知类型" + } + return +} diff --git a/consts/stock/stock_location_type.go b/consts/stock/stock_location_type.go new file mode 100644 index 0000000..b4311c2 --- /dev/null +++ b/consts/stock/stock_location_type.go @@ -0,0 +1,33 @@ +package stock + +// StockLocationType 库存位置关联类型枚举 +type StockLocationType int + +const ( + StockLocationTypeStockDetails StockLocationType = 1 // StockDetails库存的位置关联 + StockLocationTypePrivateStock StockLocationType = 2 // PrivateStock库存的位置关联 + StockLocationTypeStockBatch StockLocationType = 3 // StockBatch库存的位置关联 +) + +// GetAllStockLocationTypes 获取所有库存位置关联类型 +func GetAllStockLocationTypes() []StockLocationType { + return []StockLocationType{ + StockLocationTypeStockDetails, + StockLocationTypePrivateStock, + StockLocationTypeStockBatch, + } +} + +// String 获取库存位置关联类型字符串表示 +func (s StockLocationType) String() string { + switch s { + case StockLocationTypeStockDetails: + return "StockDetails库存" + case StockLocationTypePrivateStock: + return "PrivateStock库存" + case StockLocationTypeStockBatch: + return "StockBatch库存" + default: + return "未知" + } +} diff --git a/consts/stock/stock_mode.go b/consts/stock/stock_mode.go new file mode 100644 index 0000000..1345537 --- /dev/null +++ b/consts/stock/stock_mode.go @@ -0,0 +1,17 @@ +package stock + +// StockMode 库存管理模式枚举 +type StockMode int + +const ( + StockModeDetail StockMode = 1 // 明细模式(每件一条记录) + StockModeBatch StockMode = 2 // 批次模式(批次记录) +) + +// GetAllStockModes 获取所有库存管理模式 +func GetAllStockModes() []StockMode { + return []StockMode{ + StockModeDetail, + StockModeBatch, + } +} diff --git a/consts/stock/stock_status.go b/consts/stock/stock_status.go new file mode 100644 index 0000000..137e3c1 --- /dev/null +++ b/consts/stock/stock_status.go @@ -0,0 +1,21 @@ +package stock + +// StockStatus 库存状态枚举 +type StockStatus int + +const ( + StockStatusAvailable StockStatus = 1 // 可用,未分配渠道 + StockStatusSold StockStatus = 2 // 已售出 + StockStatusReserved StockStatus = 3 // 预留 + StockStatusLocked StockStatus = 4 // 锁定 +) + +// GetAllStockStatuses 获取所有库存状态 +func GetAllStockStatuses() []StockStatus { + return []StockStatus{ + StockStatusAvailable, + StockStatusSold, + StockStatusReserved, + StockStatusLocked, + } +} diff --git a/consts/stock/warehouse_status.go b/consts/stock/warehouse_status.go new file mode 100644 index 0000000..ce639fd --- /dev/null +++ b/consts/stock/warehouse_status.go @@ -0,0 +1,47 @@ +package stock + +// WarehouseStatus 仓库状态枚举 +type WarehouseStatus string + +const ( + WarehouseStatusEnabled WarehouseStatus = "enable" // 启用 + WarehouseStatusDisabled WarehouseStatus = "disable" // 停用 +) + +// GetAllWarehouseStatuses 获取所有仓库状态 +func GetAllWarehouseStatuses() []WarehouseStatus { + return []WarehouseStatus{ + WarehouseStatusEnabled, + WarehouseStatusDisabled, + } +} + +type WarehouseStatusKeyValue struct { + Key WarehouseStatus + Value string +} + +var ( + WarehouseStatusEnabledKeyValue = WarehouseStatusKeyValue{Key: WarehouseStatusEnabled, Value: "启用"} + WarehouseStatusDisabledKeyValue = WarehouseStatusKeyValue{Key: WarehouseStatusDisabled, Value: "停用"} +) + +func GetAllWarehouseStatusKeyValue() []WarehouseStatusKeyValue { + return []WarehouseStatusKeyValue{ + WarehouseStatusEnabledKeyValue, + WarehouseStatusDisabledKeyValue, + } +} + +var warehouseStatusValueMap = map[WarehouseStatus]string{ + WarehouseStatusEnabled: WarehouseStatusEnabledKeyValue.Value, + WarehouseStatusDisabled: WarehouseStatusDisabledKeyValue.Value, +} + +func GetWarehouseStatusValueByKey(key WarehouseStatus) (value string) { + value, exists := warehouseStatusValueMap[key] + if !exists { + value = "未知状态" + } + return +} diff --git a/consts/stock/warning_type.go b/consts/stock/warning_type.go new file mode 100644 index 0000000..6b82705 --- /dev/null +++ b/consts/stock/warning_type.go @@ -0,0 +1,42 @@ +package stock + +// WarningType 预警类型枚举 +type WarningType int + +const ( + WarningTypeExpiry WarningType = 1 // 临期预警 + WarningTypeLowStock WarningType = 2 // 库存不足预警 +) + +// GetAllWarningTypes 获取所有预警类型 +func GetAllWarningTypes() []WarningType { + return []WarningType{ + WarningTypeExpiry, + WarningTypeLowStock, + } +} + +// String 获取预警类型字符串表示 +func (w WarningType) String() string { + switch w { + case WarningTypeExpiry: + return "临期预警" + case WarningTypeLowStock: + return "库存不足预警" + default: + return "未知" + } +} + +type WarningTypeKeyValue struct { + Key int `json:"key"` + Value string `json:"value"` +} + +// GetAllWarningTypeKeyValue 获取所有预警类型的键值对 +func GetAllWarningTypeKeyValue() []WarningTypeKeyValue { + return []WarningTypeKeyValue{ + {Key: 1, Value: "临期预警"}, + {Key: 2, Value: "库存不足预警"}, + } +} diff --git a/consts/stock/zone_status.go b/consts/stock/zone_status.go new file mode 100644 index 0000000..82a75dc --- /dev/null +++ b/consts/stock/zone_status.go @@ -0,0 +1,47 @@ +package stock + +// ZoneStatus 库区状态枚举 +type ZoneStatus string + +const ( + ZoneStatusEnabled ZoneStatus = "enable" // 启用 + ZoneStatusDisabled ZoneStatus = "disable" // 停用 +) + +// GetAllZoneStatuses 获取所有库区状态 +func GetAllZoneStatuses() []ZoneStatus { + return []ZoneStatus{ + ZoneStatusEnabled, + ZoneStatusDisabled, + } +} + +type ZoneStatusKeyValue struct { + Key ZoneStatus + Value string +} + +var ( + ZoneStatusEnabledKeyValue = ZoneStatusKeyValue{Key: ZoneStatusEnabled, Value: "启用"} + ZoneStatusDisabledKeyValue = ZoneStatusKeyValue{Key: ZoneStatusDisabled, Value: "停用"} +) + +func GetAllZoneStatusKeyValue() []ZoneStatusKeyValue { + return []ZoneStatusKeyValue{ + ZoneStatusEnabledKeyValue, + ZoneStatusDisabledKeyValue, + } +} + +var zoneStatusValueMap = map[ZoneStatus]string{ + ZoneStatusEnabled: ZoneStatusEnabledKeyValue.Value, + ZoneStatusDisabled: ZoneStatusDisabledKeyValue.Value, +} + +func GetZoneStatusValueByKey(key ZoneStatus) (value string) { + value, exists := zoneStatusValueMap[key] + if !exists { + value = "未知状态" + } + return +} diff --git a/consts/stock/zone_type.go b/consts/stock/zone_type.go new file mode 100644 index 0000000..14a94ca --- /dev/null +++ b/consts/stock/zone_type.go @@ -0,0 +1,67 @@ +package stock + +// ZoneType 库区类型枚举 +type ZoneType string + +const ( + ZoneTypeNormal ZoneType = "normal" // 常温区 + ZoneTypeCold ZoneType = "cold" // 冷藏区 + ZoneTypeFreeze ZoneType = "freeze" // 冷冻区 + ZoneTypeFresh ZoneType = "fresh" // 保鲜区 + ZoneTypeHazardous ZoneType = "hazardous" // 危险品区 + ZoneTypeBonded ZoneType = "bonded" // 保税区 +) + +// GetAllZoneTypes 获取所有库区类型 +func GetAllZoneTypes() []ZoneType { + return []ZoneType{ + ZoneTypeNormal, + ZoneTypeCold, + ZoneTypeFreeze, + ZoneTypeFresh, + ZoneTypeHazardous, + ZoneTypeBonded, + } +} + +type ZoneTypeKeyValue struct { + Key ZoneType + Value string +} + +var ( + ZoneTypeNormalKeyValue = ZoneTypeKeyValue{Key: ZoneTypeNormal, Value: "常温区"} + ZoneTypeColdKeyValue = ZoneTypeKeyValue{Key: ZoneTypeCold, Value: "冷藏区"} + ZoneTypeFreezeKeyValue = ZoneTypeKeyValue{Key: ZoneTypeFreeze, Value: "冷冻区"} + ZoneTypeFreshKeyValue = ZoneTypeKeyValue{Key: ZoneTypeFresh, Value: "保鲜区"} + ZoneTypeHazardousKeyValue = ZoneTypeKeyValue{Key: ZoneTypeHazardous, Value: "危险品区"} + ZoneTypeBondedKeyValue = ZoneTypeKeyValue{Key: ZoneTypeBonded, Value: "保税区"} +) + +func GetAllZoneTypeKeyValue() []ZoneTypeKeyValue { + return []ZoneTypeKeyValue{ + ZoneTypeNormalKeyValue, + ZoneTypeColdKeyValue, + ZoneTypeFreezeKeyValue, + ZoneTypeFreshKeyValue, + ZoneTypeHazardousKeyValue, + ZoneTypeBondedKeyValue, + } +} + +var zoneTypeValueMap = map[ZoneType]string{ + ZoneTypeNormal: ZoneTypeNormalKeyValue.Value, + ZoneTypeCold: ZoneTypeColdKeyValue.Value, + ZoneTypeFreeze: ZoneTypeFreezeKeyValue.Value, + ZoneTypeFresh: ZoneTypeFreshKeyValue.Value, + ZoneTypeHazardous: ZoneTypeHazardousKeyValue.Value, + ZoneTypeBonded: ZoneTypeBondedKeyValue.Value, +} + +func GetZoneTypeValueByKey(key ZoneType) (value string) { + value, exists := zoneTypeValueMap[key] + if !exists { + value = "未知类型" + } + return +} diff --git a/controller/asset/asset_controller.go b/controller/asset/asset_controller.go new file mode 100644 index 0000000..1405dc4 --- /dev/null +++ b/controller/asset/asset_controller.go @@ -0,0 +1,56 @@ +package controller + +import ( + dto "assets/model/dto/asset" + service "assets/service/asset" + "context" + + "gitea.com/red-future/common/beans" +) + +type asset struct{} + +// Asset 资产控制器 +var Asset = new(asset) + +// init 初始化表单配置 +func init() { +} + +// CreateAsset 创建资产 +func (c *asset) CreateAsset(ctx context.Context, req *dto.CreateAssetReq) (res *dto.CreateAssetRes, err error) { + return service.Asset.Create(ctx, req) +} + +// ListAsset 获取资产列表 +func (c *asset) ListAsset(ctx context.Context, req *dto.ListAssetReq) (res *dto.ListAssetRes, err error) { + return service.Asset.List(ctx, req) +} + +// GetAsset 获取资产详情 +func (c *asset) GetAsset(ctx context.Context, req *dto.GetAssetReq) (res *dto.GetAssetRes, err error) { + return service.Asset.GetOne(ctx, req) +} + +// GetAssetAndSuk 获取资产详情 +func (c *asset) GetAssetAndSuk(ctx context.Context, req *dto.GetAssetAndSkuReq) (res *dto.GetAssetAndSkuRes, err error) { + return service.Asset.GetAssetAndSku(ctx, req) +} + +// UpdateAsset 更新资产 +func (c *asset) UpdateAsset(ctx context.Context, req *dto.UpdateAssetReq) (res *beans.ResponseEmpty, err error) { + err = service.Asset.Update(ctx, req) + return +} + +// UpdateAssetStatus 更新资产状态 +func (c *asset) UpdateAssetStatus(ctx context.Context, req *dto.UpdateAssetStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Asset.UpdateStatus(ctx, req) + return +} + +// DeleteAsset 删除资产 +func (c *asset) DeleteAsset(ctx context.Context, req *dto.DeleteAssetReq) (res *beans.ResponseEmpty, err error) { + err = service.Asset.Delete(ctx, req) + return +} diff --git a/controller/asset/asset_sku_controller.go b/controller/asset/asset_sku_controller.go new file mode 100644 index 0000000..a818a17 --- /dev/null +++ b/controller/asset/asset_sku_controller.go @@ -0,0 +1,45 @@ +package controller + +import ( + dto "assets/model/dto/asset" + service "assets/service/asset" + "context" + + "gitea.com/red-future/common/beans" +) + +type assetSku struct{} + +// AssetSku 资产SKU控制器 +var AssetSku = new(assetSku) + +// init 初始化表单配置 +func init() { +} + +// CreateAssetSku 创建SKU +func (c *assetSku) CreateAssetSku(ctx context.Context, req *dto.CreateAssetSkuReq) (res *dto.CreateAssetSkuRes, err error) { + return service.AssetSku.CreateAssetSku(ctx, req) +} + +// UpdateAssetSku 更新SKU +func (c *assetSku) UpdateAssetSku(ctx context.Context, req *dto.UpdateAssetSkuReq) (res *beans.ResponseEmpty, err error) { + err = service.AssetSku.UpdateAssetSku(ctx, req) + return +} + +// DeleteAssetSku 删除SKU +func (c *assetSku) DeleteAssetSku(ctx context.Context, req *dto.DeleteAssetSkuReq) (res *beans.ResponseEmpty, err error) { + err = service.AssetSku.DeleteAssetSku(ctx, req) + return +} + +// GetAssetSku 获取SKU详情 +func (c *assetSku) GetAssetSku(ctx context.Context, req *dto.GetAssetSkuReq) (res *dto.GetAssetSkuRes, err error) { + return service.AssetSku.GetAssetSku(ctx, req) +} + +// ListAssetSkus 获取SKU列表 +func (c *assetSku) ListAssetSkus(ctx context.Context, req *dto.ListAssetSkuReq) (res *dto.ListAssetSkuRes, err error) { + return service.AssetSku.ListAssetSkus(ctx, req) +} diff --git a/controller/asset/category_controller.go b/controller/asset/category_controller.go new file mode 100644 index 0000000..1e7bbb1 --- /dev/null +++ b/controller/asset/category_controller.go @@ -0,0 +1,56 @@ +package controller + +import ( + dto "assets/model/dto/asset" + service "assets/service/asset" + "context" + + "gitea.com/red-future/common/beans" +) + +type category struct{} + +// Category 分类控制器 +var Category = new(category) + +// init 初始化表单配置 +func init() { +} + +// GetCategory 获取分类详情 +func (c *category) GetCategory(ctx context.Context, req *dto.GetCategoryReq) (res *dto.GetCategoryRes, err error) { + return service.Category.GetOne(ctx, req) +} + +// ListCategory 获取分类列表 +func (c *category) ListCategory(ctx context.Context, req *dto.ListCategoryReq) (res *dto.ListCategoryRes, err error) { + return service.Category.List(ctx, req) +} + +// GetCategoryTree 获取分类树 +func (c *category) GetCategoryTree(ctx context.Context, req *dto.GetCategoryTreeReq) (res *dto.GetCategoryTreeRes, err error) { + return service.Category.GetTree(ctx, req) +} + +// CreateCategory 创建分类 +func (c *category) CreateCategory(ctx context.Context, req *dto.CreateCategoryReq) (res *dto.CreateCategoryRes, err error) { + return service.Category.Create(ctx, req) +} + +// UpdateCategory 更新分类 +func (c *category) UpdateCategory(ctx context.Context, req *dto.UpdateCategoryReq) (res *beans.ResponseEmpty, err error) { + err = service.Category.Update(ctx, req) + return +} + +// UpdateCategoryStatus 更新分类状态 +func (c *category) UpdateCategoryStatus(ctx context.Context, req *dto.UpdateCategoryStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Category.UpdateStatus(ctx, req) + return +} + +// DeleteCategory 删除分类 +func (c *category) DeleteCategory(ctx context.Context, req *dto.DeleteCategoryReq) (res *beans.ResponseEmpty, err error) { + err = service.Category.Delete(ctx, req) + return +} diff --git a/controller/asset/private_category_controller.go b/controller/asset/private_category_controller.go new file mode 100644 index 0000000..6d4a13b --- /dev/null +++ b/controller/asset/private_category_controller.go @@ -0,0 +1,116 @@ +package controller + +import ( + dto "assets/model/dto/asset" + service "assets/service/asset" + "context" + + "gitea.com/red-future/common/beans" +) + +type privateCategory struct{} + +// PrivateCategory 私域分类控制器 +var PrivateCategory = new(privateCategory) + +// CreatePrivateCategory 创建私域分类 +// @Summary 创建私域分类 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param body body asset.CreatePrivateCategoryReq true "创建私域分类请求" +// @Success 200 {object} asset.CreatePrivateCategoryRes +// @Router /privateCategory/createPrivateCategory [post] +func (c *privateCategory) CreatePrivateCategory(ctx context.Context, req *dto.CreatePrivateCategoryReq) (res *dto.CreatePrivateCategoryRes, err error) { + return service.PrivateCategory.CreatePrivateCategory(ctx, req) +} + +// BatchCreatePrivateCategory 批量创建私域分类 +// @Summary 批量创建私域分类 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param body body asset.BatchCreatePrivateCategoryReq true "批量创建私域分类请求" +// @Success 200 {object} asset.BatchCreatePrivateCategoryRes +// @Router /privateCategory/batchCreatePrivateCategory [post] +func (c *privateCategory) BatchCreatePrivateCategory(ctx context.Context, req *dto.BatchCreatePrivateCategoryReq) (res *dto.BatchCreatePrivateCategoryRes, err error) { + return service.PrivateCategory.BatchCreatePrivateCategory(ctx, req) +} + +// UpdatePrivateCategory 更新私域分类 +// @Summary 更新私域分类 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param body body asset.UpdatePrivateCategoryReq true "更新私域分类请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateCategory/updatePrivateCategory [put] +func (c *privateCategory) UpdatePrivateCategory(ctx context.Context, req *dto.UpdatePrivateCategoryReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateCategory.UpdatePrivateCategory(ctx, req) + return +} + +// DeletePrivateCategory 删除私域分类 +// @Summary 删除私域分类 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param id query string true "私域分类ID" +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateCategory/deletePrivateCategory [delete] +func (c *privateCategory) DeletePrivateCategory(ctx context.Context, req *dto.DeletePrivateCategoryReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateCategory.DeletePrivateCategory(ctx, req.ID) + return +} + +// GetPrivateCategory 获取私域分类详情 +// @Summary 获取私域分类详情 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param id query string true "私域分类ID" +// @Success 200 {object} asset.GetPrivateCategoryRes +// @Router /privateCategory/getPrivateCategory [get] +func (c *privateCategory) GetPrivateCategory(ctx context.Context, req *dto.GetPrivateCategoryReq) (res *dto.GetPrivateCategoryRes, err error) { + return service.PrivateCategory.GetPrivateCategory(ctx, req.ID) +} + +// ListPrivateCategory 获取私域分类列表 +// @Summary 获取私域分类列表 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Param name query string false "分类名称" +// @Param parentId query string false "父分类ID" +// @Param level query int false "分类层级" +// @Param isLeafNode query bool false "是否叶子节点" +// @Param pageNum query int false "页码" default(1) +// @Param pageSize query int false "每页大小" default(10) +// @Success 200 {object} asset.ListPrivateCategoryRes +// @Router /privateCategory/listPrivateCategory [get] +func (c *privateCategory) ListPrivateCategory(ctx context.Context, req *dto.ListPrivateCategoryReq) (res *dto.ListPrivateCategoryRes, err error) { + return service.PrivateCategory.ListPrivateCategory(ctx, req) +} + +// GetPrivateCategoryTree 获取私域分类树 +// @Summary 获取私域分类树 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Success 200 {object} asset.GetPrivateCategoryTreeRes +// @Router /privateCategory/getPrivateCategoryTree [get] +func (c *privateCategory) GetPrivateCategoryTree(ctx context.Context, req *dto.GetPrivateCategoryTreeReq) (res *dto.GetPrivateCategoryTreeRes, err error) { + return service.PrivateCategory.GetPrivateCategoryTree(ctx) +} + +// GenerateTestData 生成测试数据 +// @Summary 生成测试数据 +// @Tags 私域分类管理 +// @Accept json +// @Produce json +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateCategory/generateTestData [post] +func (c *privateCategory) GenerateTestData(ctx context.Context, req *dto.GeneratePrivateCategoryTestDataReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateCategory.GenerateTestData(ctx) + return +} diff --git a/controller/asset/private_sku_controller.go b/controller/asset/private_sku_controller.go new file mode 100644 index 0000000..d24a4d2 --- /dev/null +++ b/controller/asset/private_sku_controller.go @@ -0,0 +1,118 @@ +package controller + +import ( + dto "assets/model/dto/asset" + service "assets/service/asset" + "context" + + "gitea.com/red-future/common/beans" +) + +type privateSku struct{} + +// PrivateSku 私域SKU控制器 +var PrivateSku = new(privateSku) + +// CreatePrivateSku 创建私域SKU +// @Summary 创建私域SKU +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param body body asset.CreatePrivateSkuReq true "创建私域SKU请求" +// @Success 200 {object} asset.CreatePrivateSkuRes +// @Router /privateSku/createPrivateSku [post] +func (c *privateSku) CreatePrivateSku(ctx context.Context, req *dto.CreatePrivateSkuReq) (res *dto.CreatePrivateSkuRes, err error) { + return service.PrivateSku.CreatePrivateSku(ctx, req) +} + +// BatchCreatePrivateSku 批量创建私域SKU +// @Summary 批量创建私域SKU +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param body body asset.BatchCreatePrivateSkuReq true "批量创建私域SKU请求" +// @Success 200 {object} asset.BatchCreatePrivateSkuRes +// @Router /privateSku/batchCreatePrivateSku [post] +func (c *privateSku) BatchCreatePrivateSku(ctx context.Context, req *dto.BatchCreatePrivateSkuReq) (res *dto.BatchCreatePrivateSkuRes, err error) { + return service.PrivateSku.BatchCreatePrivateSku(ctx, req) +} + +// UpdatePrivateSku 更新私域SKU +// @Summary 更新私域SKU +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param body body asset.UpdatePrivateSkuReq true "更新私域SKU请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateSku/updatePrivateSku [put] +func (c *privateSku) UpdatePrivateSku(ctx context.Context, req *dto.UpdatePrivateSkuReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateSku.UpdatePrivateSku(ctx, req) + return +} + +// DeletePrivateSku 删除私域SKU +// @Summary 删除私域SKU +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param id query string true "私域SKU ID" +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateSku/deletePrivateSku [delete] +func (c *privateSku) DeletePrivateSku(ctx context.Context, req *dto.DeletePrivateSkuReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateSku.DeletePrivateSku(ctx, req.ID) + return +} + +// GetPrivateSku 获取私域SKU详情 +// @Summary 获取私域SKU详情 +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param id query string true "私域SKU ID" +// @Success 200 {object} asset.GetPrivateSkuRes +// @Router /privateSku/getPrivateSku [get] +func (c *privateSku) GetPrivateSku(ctx context.Context, req *dto.GetPrivateSkuReq) (res *dto.GetPrivateSkuRes, err error) { + return service.PrivateSku.GetPrivateSku(ctx, req.ID) +} + +// ListPrivateSku 获取私域SKU列表 +// @Summary 获取私域SKU列表 +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param skuName query string false "SKU名称" +// @Param privateCategoryPath query string false "分类路径" +// @Param minPrice query int false "最低价格" +// @Param maxPrice query int false "最高价格" +// @Param pageNum query int false "页码" default(1) +// @Param pageSize query int false "每页大小" default(10) +// @Success 200 {object} asset.ListPrivateSkuRes +// @Router /privateSku/listPrivateSku [get] +func (c *privateSku) ListPrivateSku(ctx context.Context, req *dto.ListPrivateSkuReq) (res *dto.ListPrivateSkuRes, err error) { + return service.PrivateSku.ListPrivateSku(ctx, req) +} + +// UpdatePrivateSkuStock 更新私域SKU库存 +// @Summary 更新私域SKU库存 +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Param body body asset.UpdatePrivateSkuStockReq true "更新库存请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateSku/updatePrivateSkuStock [put] +func (c *privateSku) UpdatePrivateSkuStock(ctx context.Context, req *dto.UpdatePrivateSkuStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateSku.UpdatePrivateSkuStock(ctx, req.ID, req.StockChange) + return +} + +// GenerateTestData 生成测试数据 +// @Summary 生成测试数据 +// @Tags 私域SKU管理 +// @Accept json +// @Produce json +// @Success 200 {object} beans.ResponseEmpty +// @Router /privateSku/generateTestData [post] +func (c *privateSku) GenerateTestData(ctx context.Context, req *dto.GeneratePrivateSkuTestDataReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateSku.GenerateTestData(ctx) + return +} diff --git a/controller/enum/enum_controller.go b/controller/enum/enum_controller.go new file mode 100644 index 0000000..ba813cc --- /dev/null +++ b/controller/enum/enum_controller.go @@ -0,0 +1,31 @@ +package controller + +import ( + "assets/model/dto/enum" + "assets/service/enum" + "context" +) + +type enum struct{} + +// Enum 枚举控制器 +var Enum = new(enum) + +// init 初始化表单配置 +func init() { +} + +// GetAssetType 获取资产类型 +func (c *enum) GetAssetType(ctx context.Context, req *dto.GetAssetTypeReq) (res *dto.GetAssetTypeRes, err error) { + return service.Enum.GetAssetType(ctx, req) +} + +// GetCategoryAttrType 获取分类属性类型 +func (c *enum) GetCategoryAttrType(ctx context.Context, req *dto.GetCategoryAttrTypeReq) (res *dto.GetCategoryAttrTypeRes, err error) { + return service.Enum.GetCategoryAttrType(ctx, req) +} + +// GetSpecsUnit 获取规格单位 +func (c *enum) GetSpecsUnit(ctx context.Context, req *dto.GetSpecsUnitReq) (res *dto.GetSpecsUnitRes, err error) { + return service.Enum.GetSpecsUnit(ctx, req) +} diff --git a/controller/procurement/purchase_inbound_controller.go b/controller/procurement/purchase_inbound_controller.go new file mode 100644 index 0000000..ba25097 --- /dev/null +++ b/controller/procurement/purchase_inbound_controller.go @@ -0,0 +1,26 @@ +package controller + +import ( + dto "assets/model/dto/procurement" + service "assets/service/procurement" + "context" +) + +type purchaseInbound struct{} + +var PurchaseInbound = new(purchaseInbound) + +func init() { +} + +func (c *purchaseInbound) CreatePurchaseInbound(ctx context.Context, req *dto.CreatePurchaseInboundReq) (res *dto.CreatePurchaseInboundRes, err error) { + return service.PurchaseInbound.Create(ctx, req) +} + +func (c *purchaseInbound) GetPurchaseInbound(ctx context.Context, req *dto.GetPurchaseInboundReq) (res *dto.GetPurchaseInboundRes, err error) { + return service.PurchaseInbound.GetOne(ctx, req) +} + +func (c *purchaseInbound) ListPurchaseInbounds(ctx context.Context, req *dto.ListPurchaseInboundReq) (res *dto.ListPurchaseInboundRes, err error) { + return service.PurchaseInbound.List(ctx, req) +} diff --git a/controller/procurement/purchase_order_controller.go b/controller/procurement/purchase_order_controller.go new file mode 100644 index 0000000..56aaf2b --- /dev/null +++ b/controller/procurement/purchase_order_controller.go @@ -0,0 +1,106 @@ +package controller + +import ( + dto "assets/model/dto/procurement" + service "assets/service/procurement" + "context" + + "gitea.com/red-future/common/beans" +) + +type purchaseOrder struct{} + +// PurchaseOrder 采购订单控制器 +var PurchaseOrder = new(purchaseOrder) + +// CreatePurchaseOrder 创建采购订单 +// @Summary 创建采购订单 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param body body procurement.CreatePurchaseOrderReq true "创建采购订单请求" +// @Success 200 {object} procurement.CreatePurchaseOrderRes +// @Router /purchaseOrder/createPurchaseOrder [post] +func (c *purchaseOrder) CreatePurchaseOrder(ctx context.Context, req *dto.CreatePurchaseOrderReq) (res *dto.CreatePurchaseOrderRes, err error) { + return service.PurchaseOrder.CreatePurchaseOrder(ctx, req) +} + +// BatchCreatePurchaseOrders 批量创建采购订单 +// @Summary 批量创建采购订单 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param body body procurement.BatchCreatePurchaseOrdersReq true "批量创建采购订单请求" +// @Success 200 {object} procurement.BatchCreatePurchaseOrdersRes +// @Router /purchaseOrder/batchCreatePurchaseOrders [post] +func (c *purchaseOrder) BatchCreatePurchaseOrders(ctx context.Context, req *dto.BatchCreatePurchaseOrdersReq) (res *dto.BatchCreatePurchaseOrdersRes, err error) { + return service.PurchaseOrder.BatchCreatePurchaseOrders(ctx, req) +} + +// UpdatePurchaseOrder 更新采购订单 +// @Summary 更新采购订单 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param body body procurement.UpdatePurchaseOrderReq true "更新采购订单请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrder/updatePurchaseOrder [put] +func (c *purchaseOrder) UpdatePurchaseOrder(ctx context.Context, req *dto.UpdatePurchaseOrderReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrder.UpdatePurchaseOrder(ctx, req) + return +} + +// DeletePurchaseOrder 删除采购订单 +// @Summary 删除采购订单 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param id query string true "采购订单ID" +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrder/deletePurchaseOrder [delete] +func (c *purchaseOrder) DeletePurchaseOrder(ctx context.Context, req *dto.DeletePurchaseOrderReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrder.DeletePurchaseOrder(ctx, req.ID) + return +} + +// GetPurchaseOrder 获取采购订单详情 +// @Summary 获取采购订单详情 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param id query string true "采购订单ID" +// @Success 200 {object} procurement.GetPurchaseOrderRes +// @Router /purchaseOrder/getPurchaseOrder [get] +func (c *purchaseOrder) GetPurchaseOrder(ctx context.Context, req *dto.GetPurchaseOrderReq) (res *dto.GetPurchaseOrderRes, err error) { + return service.PurchaseOrder.GetPurchaseOrder(ctx, req.ID) +} + +// ListPurchaseOrders 获取采购订单列表 +// @Summary 获取采购订单列表 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Param orderNo query string false "订单编号" +// @Param title query string false "订单标题" +// @Param buyerId query string false "采购方ID" +// @Param orderType query string false "订单类型" +// @Param status query int false "订单状态" +// @Param pageNum query int false "页码" default(1) +// @Param pageSize query int false "每页大小" default(10) +// @Success 200 {object} procurement.ListPurchaseOrdersRes +// @Router /purchaseOrder/listPurchaseOrders [get] +func (c *purchaseOrder) ListPurchaseOrders(ctx context.Context, req *dto.ListPurchaseOrdersReq) (res *dto.ListPurchaseOrdersRes, err error) { + return service.PurchaseOrder.ListPurchaseOrders(ctx, req) +} + +// GenerateTestData 生成测试数据 +// @Summary 生成测试数据 +// @Tags 采购订单管理 +// @Accept json +// @Produce json +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrder/generateTestData [post] +func (c *purchaseOrder) GenerateTestData(ctx context.Context, req *dto.GeneratePurchaseOrderTestDataReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrder.GenerateTestData(ctx) + return +} diff --git a/controller/procurement/purchase_order_item_controller.go b/controller/procurement/purchase_order_item_controller.go new file mode 100644 index 0000000..be118bf --- /dev/null +++ b/controller/procurement/purchase_order_item_controller.go @@ -0,0 +1,106 @@ +package controller + +import ( + dto "assets/model/dto/procurement" + service "assets/service/procurement" + "context" + + "gitea.com/red-future/common/beans" +) + +type purchaseOrderItem struct{} + +// PurchaseOrderItem 采购订单明细控制器 +var PurchaseOrderItem = new(purchaseOrderItem) + +// CreatePurchaseOrderItem 创建采购订单明细 +// @Summary 创建采购订单明细 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param body body procurement.CreatePurchaseOrderItemReq true "创建采购订单明细请求" +// @Success 200 {object} procurement.CreatePurchaseOrderItemRes +// @Router /purchaseOrderItem/createPurchaseOrderItem [post] +func (c *purchaseOrderItem) CreatePurchaseOrderItem(ctx context.Context, req *dto.CreatePurchaseOrderItemReq) (res *dto.CreatePurchaseOrderItemRes, err error) { + return service.PurchaseOrderItem.CreatePurchaseOrderItem(ctx, req) +} + +// BatchCreatePurchaseOrderItems 批量创建采购订单明细 +// @Summary 批量创建采购订单明细 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param body body procurement.BatchCreatePurchaseOrderItemsReq true "批量创建采购订单明细请求" +// @Success 200 {object} procurement.BatchCreatePurchaseOrderItemsRes +// @Router /purchaseOrderItem/batchCreatePurchaseOrderItems [post] +func (c *purchaseOrderItem) BatchCreatePurchaseOrderItems(ctx context.Context, req *dto.BatchCreatePurchaseOrderItemsReq) (res *dto.BatchCreatePurchaseOrderItemsRes, err error) { + return service.PurchaseOrderItem.BatchCreatePurchaseOrderItems(ctx, req) +} + +// UpdatePurchaseOrderItem 更新采购订单明细 +// @Summary 更新采购订单明细 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param body body procurement.UpdatePurchaseOrderItemReq true "更新采购订单明细请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrderItem/updatePurchaseOrderItem [put] +func (c *purchaseOrderItem) UpdatePurchaseOrderItem(ctx context.Context, req *dto.UpdatePurchaseOrderItemReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrderItem.UpdatePurchaseOrderItem(ctx, req) + return +} + +// DeletePurchaseOrderItem 删除采购订单明细 +// @Summary 删除采购订单明细 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param id query string true "采购订单明细ID" +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrderItem/deletePurchaseOrderItem [delete] +func (c *purchaseOrderItem) DeletePurchaseOrderItem(ctx context.Context, req *dto.DeletePurchaseOrderItemReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrderItem.DeletePurchaseOrderItem(ctx, req.ID) + return +} + +// GetPurchaseOrderItem 获取采购订单明细详情 +// @Summary 获取采购订单明细详情 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param id query string true "采购订单明细ID" +// @Success 200 {object} procurement.GetPurchaseOrderItemRes +// @Router /purchaseOrderItem/getPurchaseOrderItem [get] +func (c *purchaseOrderItem) GetPurchaseOrderItem(ctx context.Context, req *dto.GetPurchaseOrderItemReq) (res *dto.GetPurchaseOrderItemRes, err error) { + return service.PurchaseOrderItem.GetPurchaseOrderItem(ctx, req.ID) +} + +// ListPurchaseOrderItems 获取采购订单明细列表 +// @Summary 获取采购订单明细列表 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Param orderId query string false "订单ID" +// @Param assetId query string false "资产ID" +// @Param assetSkuId query string false "资产SKU ID" +// @Param productName query string false "商品名称" +// @Param brand query string false "品牌" +// @Param pageNum query int false "页码" default(1) +// @Param pageSize query int false "每页大小" default(10) +// @Success 200 {object} procurement.ListPurchaseOrderItemsRes +// @Router /purchaseOrderItem/listPurchaseOrderItems [get] +func (c *purchaseOrderItem) ListPurchaseOrderItems(ctx context.Context, req *dto.ListPurchaseOrderItemsReq) (res *dto.ListPurchaseOrderItemsRes, err error) { + return service.PurchaseOrderItem.ListPurchaseOrderItems(ctx, req) +} + +// GenerateTestData 生成测试数据 +// @Summary 生成测试数据 +// @Tags 采购订单明细管理 +// @Accept json +// @Produce json +// @Success 200 {object} beans.ResponseEmpty +// @Router /purchaseOrderItem/generateTestData [post] +func (c *purchaseOrderItem) GenerateTestData(ctx context.Context, req *dto.GeneratePurchaseOrderItemTestDataReq) (res *beans.ResponseEmpty, err error) { + err = service.PurchaseOrderItem.GenerateTestData(ctx) + return +} diff --git a/controller/procurement/supplier_controller.go b/controller/procurement/supplier_controller.go new file mode 100644 index 0000000..b00d37f --- /dev/null +++ b/controller/procurement/supplier_controller.go @@ -0,0 +1,121 @@ +package controller + +import ( + dto "assets/model/dto/procurement" + service "assets/service/procurement" + "context" + + "gitea.com/red-future/common/beans" +) + +type supplier struct{} + +// Supplier 供应商控制器 +var Supplier = new(supplier) + +// CreateSupplier 创建供应商 +// @Summary 创建供应商 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param body body procurement.CreateSupplierReq true "创建供应商请求" +// @Success 200 {object} procurement.CreateSupplierRes +// @Router /supplier/createSupplier [post] +func (c *supplier) CreateSupplier(ctx context.Context, req *dto.CreateSupplierReq) (res *dto.CreateSupplierRes, err error) { + return service.Supplier.CreateSupplier(ctx, req) +} + +// BatchCreateSuppliers 批量创建供应商 +// @Summary 批量创建供应商 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param body body procurement.BatchCreateSuppliersReq true "批量创建供应商请求" +// @Success 200 {object} procurement.BatchCreateSuppliersRes +// @Router /supplier/batchCreateSuppliers [post] +func (c *supplier) BatchCreateSuppliers(ctx context.Context, req *dto.BatchCreateSuppliersReq) (res *dto.BatchCreateSuppliersRes, err error) { + return service.Supplier.BatchCreateSuppliers(ctx, req) +} + +// UpdateSupplier 更新供应商 +// @Summary 更新供应商 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param body body procurement.UpdateSupplierReq true "更新供应商请求" +// @Success 200 {object} beans.ResponseEmpty +// @Router /supplier/updateSupplier [put] +func (c *supplier) UpdateSupplier(ctx context.Context, req *dto.UpdateSupplierReq) (res *beans.ResponseEmpty, err error) { + err = service.Supplier.UpdateSupplier(ctx, req) + return +} + +// DeleteSupplier 删除供应商 +// @Summary 删除供应商 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param id query string true "供应商ID" +// @Success 200 {object} beans.ResponseEmpty +// @Router /supplier/deleteSupplier [delete] +func (c *supplier) DeleteSupplier(ctx context.Context, req *dto.DeleteSupplierReq) (res *beans.ResponseEmpty, err error) { + err = service.Supplier.DeleteSupplier(ctx, req.ID) + return +} + +// GetSupplier 获取供应商详情 +// @Summary 获取供应商详情 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param id query string true "供应商ID" +// @Success 200 {object} procurement.GetSupplierRes +// @Router /supplier/getSupplier [get] +func (c *supplier) GetSupplier(ctx context.Context, req *dto.GetSupplierReq) (res *dto.GetSupplierRes, err error) { + return service.Supplier.GetSupplier(ctx, req.ID) +} + +// ListSuppliers 获取供应商列表 +// @Summary 获取供应商列表 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Param name query string false "供应商名称" +// @Param code query string false "供应商编码" +// @Param status query int false "供应商状态" +// @Param pageNum query int false "页码" default(1) +// @Param pageSize query int false "每页大小" default(10) +// @Success 200 {object} procurement.ListSuppliersRes +// @Router /supplier/listSuppliers [get] +func (c *supplier) ListSuppliers(ctx context.Context, req *dto.ListSuppliersReq) (res *dto.ListSuppliersRes, err error) { + return service.Supplier.ListSuppliers(ctx, req) +} + +// GetSupplierOptions 获取供应商选项 +// @Summary 获取供应商选项(用于下拉选择) +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Success 200 {object} procurement.GetSupplierOptionsRes +// @Router /supplier/getSupplierOptions [get] +func (c *supplier) GetSupplierOptions(ctx context.Context, req *dto.GetSupplierOptionsReq) (res *dto.GetSupplierOptionsRes, err error) { + list, err := service.Supplier.GetSupplierOptions(ctx) + if err != nil { + return nil, err + } + return &dto.GetSupplierOptionsRes{ + List: list, + }, nil +} + +// GenerateTestData 生成测试数据 +// @Summary 生成测试数据 +// @Tags 供应商管理 +// @Accept json +// @Produce json +// @Success 200 {object} beans.ResponseEmpty +// @Router /supplier/generateTestData [post] +func (c *supplier) GenerateTestData(ctx context.Context, req *dto.GenerateSupplierTestDataReq) (res *beans.ResponseEmpty, err error) { + err = service.Supplier.GenerateTestData(ctx) + return +} diff --git a/controller/stock/inventory_count_adjust_history_controller.go b/controller/stock/inventory_count_adjust_history_controller.go new file mode 100644 index 0000000..75d9bae --- /dev/null +++ b/controller/stock/inventory_count_adjust_history_controller.go @@ -0,0 +1,39 @@ +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type inventoryCountAdjustHistory struct{} + +var InventoryCountAdjustHistory = new(inventoryCountAdjustHistory) + +func init() {} + +// CreateInventoryCountAdjustHistory 创建盘点调整历史记录 +func (c *inventoryCountAdjustHistory) CreateInventoryCountAdjustHistory(ctx context.Context, req *dto.CreateInventoryCountAdjustHistoryReq) (res *dto.CreateInventoryCountAdjustHistoryRes, err error) { + res, err = service.InventoryCountAdjustHistory.Create(ctx, req) + return +} + +// GetInventoryCountAdjustHistory 获取盘点调整历史记录详情 +func (c *inventoryCountAdjustHistory) GetInventoryCountAdjustHistory(ctx context.Context, req *dto.GetInventoryCountAdjustHistoryReq) (res *dto.GetInventoryCountAdjustHistoryRes, err error) { + res, err = service.InventoryCountAdjustHistory.GetOne(ctx, req) + return +} + +// DeleteInventoryCountAdjustHistory 删除盘点调整历史记录 +func (c *inventoryCountAdjustHistory) DeleteInventoryCountAdjustHistory(ctx context.Context, req *dto.DeleteInventoryCountAdjustHistoryReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryCountAdjustHistory.Delete(ctx, req) + return +} + +// ListInventoryCountAdjustHistories 分页查询盘点调整历史列表 +func (c *inventoryCountAdjustHistory) ListInventoryCountAdjustHistories(ctx context.Context, req *dto.ListInventoryCountAdjustHistoryReq) (res *dto.ListInventoryCountAdjustHistoryRes, err error) { + res, err = service.InventoryCountAdjustHistory.List(ctx, req) + return +} diff --git a/controller/stock/inventory_count_controller.go b/controller/stock/inventory_count_controller.go new file mode 100644 index 0000000..5e07d54 --- /dev/null +++ b/controller/stock/inventory_count_controller.go @@ -0,0 +1,76 @@ +// 盘点任务控制器 +// 职责:盘点任务CRUD、完成/取消、导出模板、导入Excel +// 调用服务:service.InventoryCount +// 注意:ImportInventoryCount从multipart form读取文件,Update/Delete返回*beans.ResponseEmpty +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + "fmt" + "io" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" +) + +type inventoryCount struct{} + +var InventoryCount = new(inventoryCount) + +func init() { +} + +func (c *inventoryCount) CreateInventoryCount(ctx context.Context, req *dto.CreateInventoryCountReq) (res *dto.CreateInventoryCountRes, err error) { + g.Log().Debugf(ctx, "[CreateInventoryCount] req: %+v", req) + return service.InventoryCount.Create(ctx, req) +} + +func (c *inventoryCount) UpdateInventoryCount(ctx context.Context, req *dto.UpdateInventoryCountReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryCount.Update(ctx, req) + return +} + +func (c *inventoryCount) DeleteInventoryCount(ctx context.Context, req *dto.DeleteInventoryCountReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryCount.Delete(ctx, req) + return +} + +func (c *inventoryCount) GetInventoryCount(ctx context.Context, req *dto.GetInventoryCountReq) (res *dto.GetInventoryCountRes, err error) { + return service.InventoryCount.GetOne(ctx, req) +} + +func (c *inventoryCount) ListInventoryCounts(ctx context.Context, req *dto.ListInventoryCountReq) (res *dto.ListInventoryCountRes, err error) { + return service.InventoryCount.List(ctx, req) +} + +func (c *inventoryCount) CompleteInventoryCount(ctx context.Context, req *dto.CompleteInventoryCountReq) (res *dto.CompleteInventoryCountRes, err error) { + return service.InventoryCount.Complete(ctx, req) +} + +func (c *inventoryCount) CancelInventoryCount(ctx context.Context, req *dto.CancelInventoryCountReq) (res *dto.CancelInventoryCountRes, err error) { + return service.InventoryCount.Cancel(ctx, req) +} + +func (c *inventoryCount) ExportInventoryCountTemplate(ctx context.Context, req *dto.ExportInventoryCountTemplateReq) (res *dto.ExportInventoryCountTemplateRes, err error) { + return service.InventoryCount.ExportTemplate(ctx, req) +} + +// ImportInventoryCount 上传盘点Excel +// 从multipart form读取文件数据并调用Service层解析导入 +func (c *inventoryCount) ImportInventoryCount(ctx context.Context, req *dto.ImportInventoryCountReq) (res *dto.ImportInventoryCountRes, err error) { + r := g.RequestFromCtx(ctx) + file, _, err := r.Request.FormFile("file") + if err != nil { + return nil, fmt.Errorf("读取上传文件失败: %v", err) + } + defer file.Close() + + fileData, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("读取文件内容失败: %v", err) + } + + return service.InventoryCount.ImportInventoryCount(ctx, req, fileData) +} diff --git a/controller/stock/inventory_count_detail_controller.go b/controller/stock/inventory_count_detail_controller.go new file mode 100644 index 0000000..2633769 --- /dev/null +++ b/controller/stock/inventory_count_detail_controller.go @@ -0,0 +1,47 @@ +// 盘点明细控制器 +// 职责:盘点明细CRUD、相似商品查询 +// 调用服务:service.InventoryCountDetail +// 注意:Update/Delete返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type inventoryCountDetail struct{} + +var InventoryCountDetail = new(inventoryCountDetail) + +func init() { +} + +func (c *inventoryCountDetail) CreateInventoryCountDetail(ctx context.Context, req *dto.CreateInventoryCountDetailReq) (res *dto.CreateInventoryCountDetailRes, err error) { + return service.InventoryCountDetail.Create(ctx, req) +} + +func (c *inventoryCountDetail) UpdateInventoryCountDetail(ctx context.Context, req *dto.UpdateInventoryCountDetailReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryCountDetail.Update(ctx, req) + return +} + +func (c *inventoryCountDetail) DeleteInventoryCountDetail(ctx context.Context, req *dto.DeleteInventoryCountDetailReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryCountDetail.Delete(ctx, req) + return +} + +func (c *inventoryCountDetail) GetInventoryCountDetail(ctx context.Context, req *dto.GetInventoryCountDetailReq) (res *dto.GetInventoryCountDetailRes, err error) { + return service.InventoryCountDetail.GetOne(ctx, req) +} + +func (c *inventoryCountDetail) ListInventoryCountDetails(ctx context.Context, req *dto.ListInventoryCountDetailReq) (res *dto.ListInventoryCountDetailRes, err error) { + return service.InventoryCountDetail.List(ctx, req) +} + +// SearchSimilarAssets 查询相似商品 +func (c *inventoryCountDetail) SearchSimilarAssets(ctx context.Context, req *dto.SearchSimilarAssetsReq) (res *dto.SearchSimilarAssetsRes, err error) { + return service.InventoryCountDetail.SearchSimilarAssets(ctx, req) +} diff --git a/controller/stock/inventory_warning_controller.go b/controller/stock/inventory_warning_controller.go new file mode 100644 index 0000000..b4f6276 --- /dev/null +++ b/controller/stock/inventory_warning_controller.go @@ -0,0 +1,26 @@ +// 库存预警控制器 +// 职责:预警查询接口(无Create/Update/Delete,由系统自动生成) +// 调用服务:service.InventoryWarning +// 注意:只读接口,无写操作 +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" +) + +type inventoryWarning struct{} + +var InventoryWarning = new(inventoryWarning) + +func init() { +} + +func (c *inventoryWarning) GetInventoryWarning(ctx context.Context, req *dto.GetInventoryWarningReq) (res *dto.GetInventoryWarningRes, err error) { + return service.InventoryWarning.GetOne(ctx, req) +} + +func (c *inventoryWarning) ListInventoryWarnings(ctx context.Context, req *dto.ListInventoryWarningReq) (res *dto.ListInventoryWarningRes, err error) { + return service.InventoryWarning.List(ctx, req) +} diff --git a/controller/stock/inventory_warning_history_controller.go b/controller/stock/inventory_warning_history_controller.go new file mode 100644 index 0000000..25177f1 --- /dev/null +++ b/controller/stock/inventory_warning_history_controller.go @@ -0,0 +1,33 @@ +// 库存预警历史控制器 +// 职责:预警历史查询、删除(无Create/Update,由系统自动归档) +// 调用服务:service.InventoryWarningHistory +// 注意:Delete返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type inventoryWarningHistory struct{} + +var InventoryWarningHistory = new(inventoryWarningHistory) + +func init() { +} + +func (c *inventoryWarningHistory) GetInventoryWarningHistory(ctx context.Context, req *dto.GetInventoryWarningHistoryReq) (res *dto.GetInventoryWarningHistoryRes, err error) { + return service.InventoryWarningHistory.GetOne(ctx, req) +} + +func (c *inventoryWarningHistory) ListInventoryWarningHistories(ctx context.Context, req *dto.ListInventoryWarningHistoryReq) (res *dto.ListInventoryWarningHistoryRes, err error) { + return service.InventoryWarningHistory.List(ctx, req) +} + +func (c *inventoryWarningHistory) DeleteInventoryWarningHistory(ctx context.Context, req *dto.DeleteInventoryWarningHistoryReq) (res *beans.ResponseEmpty, err error) { + err = service.InventoryWarningHistory.Delete(ctx, req) + return +} diff --git a/controller/stock/location_controller.go b/controller/stock/location_controller.go new file mode 100644 index 0000000..b1054b8 --- /dev/null +++ b/controller/stock/location_controller.go @@ -0,0 +1,47 @@ +// 库位控制器 +// 职责:库位CRUD接口、状态更新接口 +// 调用服务:service.Location +// 注意:Update/Delete/UpdateStatus返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type location struct{} + +var Location = new(location) + +func init() { +} + +func (c *location) CreateLocation(ctx context.Context, req *dto.CreateLocationReq) (res *dto.CreateLocationRes, err error) { + return service.Location.Create(ctx, req) +} + +func (c *location) UpdateLocation(ctx context.Context, req *dto.UpdateLocationReq) (res *beans.ResponseEmpty, err error) { + err = service.Location.Update(ctx, req) + return +} + +func (c *location) DeleteLocation(ctx context.Context, req *dto.DeleteLocationReq) (res *beans.ResponseEmpty, err error) { + err = service.Location.Delete(ctx, req) + return +} + +func (c *location) UpdateLocationStatus(ctx context.Context, req *dto.UpdateLocationStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Location.UpdateStatus(ctx, req) + return +} + +func (c *location) GetLocation(ctx context.Context, req *dto.GetLocationReq) (res *dto.GetLocationRes, err error) { + return service.Location.GetOne(ctx, req) +} + +func (c *location) ListLocations(ctx context.Context, req *dto.ListLocationReq) (res *dto.ListLocationRes, err error) { + return service.Location.List(ctx, req) +} diff --git a/controller/stock/private_stock_controller.go b/controller/stock/private_stock_controller.go new file mode 100644 index 0000000..21246b8 --- /dev/null +++ b/controller/stock/private_stock_controller.go @@ -0,0 +1,57 @@ +// 实物库存批次控制器 +// 职责:CRUD接口、移库(MoveStock)、调拨(TransferStock)、出库(Outbound) +// 调用服务:service.PrivateStock +// 注意:Update/Delete/MoveStock/TransferStock/Outbound返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type privateStockController struct{} + +var PrivateStock = new(privateStockController) + +func init() { +} + +func (c *privateStockController) CreatePrivateStock(ctx context.Context, req *dto.CreatePrivateStockReq) (res *dto.CreatePrivateStockRes, err error) { + return service.PrivateStock.Create(ctx, req) +} + +func (c *privateStockController) UpdatePrivateStock(ctx context.Context, req *dto.UpdatePrivateStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateStock.Update(ctx, req) + return +} + +func (c *privateStockController) DeletePrivateStock(ctx context.Context, req *dto.DeletePrivateStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateStock.Delete(ctx, req) + return +} + +func (c *privateStockController) GetPrivateStock(ctx context.Context, req *dto.GetPrivateStockReq) (res *dto.GetPrivateStockRes, err error) { + return service.PrivateStock.GetOne(ctx, req) +} + +func (c *privateStockController) ListPrivateStocks(ctx context.Context, req *dto.ListPrivateStockReq) (res *dto.ListPrivateStockRes, err error) { + return service.PrivateStock.List(ctx, req) +} + +func (c *privateStockController) MoveStock(ctx context.Context, req *dto.MoveStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateStock.MoveStock(ctx, req) + return +} + +func (c *privateStockController) TransferStock(ctx context.Context, req *dto.TransferStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateStock.TransferStock(ctx, req) + return +} + +func (c *privateStockController) OutboundPrivateStock(ctx context.Context, req *dto.OutboundPrivateStockReq) (res *beans.ResponseEmpty, err error) { + err = service.PrivateStock.Outbound(ctx, req) + return +} diff --git a/controller/stock/stock_batch_controller.go b/controller/stock/stock_batch_controller.go new file mode 100644 index 0000000..2da630f --- /dev/null +++ b/controller/stock/stock_batch_controller.go @@ -0,0 +1,42 @@ +// 批次库存控制器(逻辑库存) +// 职责:批次CRUD接口 +// 调用服务:service.StockBatch +// 注意:Update/Delete返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type stockBatchController struct{} + +var StockBatch = new(stockBatchController) + +func init() { +} + +func (c *stockBatchController) CreateBatch(ctx context.Context, req *dto.CreateBatchReq) (res *dto.CreateBatchRes, err error) { + return service.StockBatch.Create(ctx, req) +} + +func (c *stockBatchController) UpdateBatch(ctx context.Context, req *dto.UpdateBatchReq) (res *beans.ResponseEmpty, err error) { + err = service.StockBatch.Update(ctx, req) + return +} + +func (c *stockBatchController) DeleteBatch(ctx context.Context, req *dto.DeleteBatchReq) (res *beans.ResponseEmpty, err error) { + err = service.StockBatch.Delete(ctx, req) + return +} + +func (c *stockBatchController) GetBatch(ctx context.Context, req *dto.GetBatchReq) (res *dto.GetBatchRes, err error) { + return service.StockBatch.GetOne(ctx, req) +} + +func (c *stockBatchController) ListBatches(ctx context.Context, req *dto.ListBatchReq) (res *dto.ListBatchRes, err error) { + return service.StockBatch.List(ctx, req) +} diff --git a/controller/stock/stock_details_controller.go b/controller/stock/stock_details_controller.go new file mode 100644 index 0000000..16f515b --- /dev/null +++ b/controller/stock/stock_details_controller.go @@ -0,0 +1,27 @@ +// 库存明细控制器(逻辑库存) +// 职责:库存明细查询接口(无Create/Update/Delete,由StockManage管理) +// 调用服务:service.StockDetails +// 注意:只读接口,无写操作 +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" +) + +type stockDetails struct{} + +// StockDetails 库存控制器 +var StockDetails = new(stockDetails) + +func init() { +} + +func (c *stockDetails) GetStockDetails(ctx context.Context, req *dto.GetStockDetailsReq) (res *dto.GetStockDetailsRes, err error) { + return service.StockDetails.GetOne(ctx, req) +} + +func (c *stockDetails) ListStockDetails(ctx context.Context, req *dto.ListStockDetailsReq) (res *dto.ListStockDetailsRes, err error) { + return service.StockDetails.List(ctx, req) +} diff --git a/controller/stock/stock_manage_controller.go b/controller/stock/stock_manage_controller.go new file mode 100644 index 0000000..3c87f8f --- /dev/null +++ b/controller/stock/stock_manage_controller.go @@ -0,0 +1,31 @@ +// 库存管理控制器(Stock公共库存) +// 职责:获取库存操作表单字段、入库/出库操作(StockOperation) +// 调用服务:service.StockManage +// 注意:StockOperation返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type stockManage struct{} + +// StockManage 库存控制器 +var StockManage = new(stockManage) + +func init() { +} + +// GetStockFormFields 获取库存操作表单字段 +func (c *stockManage) GetStockFormFields(ctx context.Context, req *dto.GetStockFormFieldsReq) (res *dto.GetStockFormFieldsRes, err error) { + return service.StockManage.GetStockFormFields(ctx, req) +} + +func (c *stockManage) StockOperation(ctx context.Context, req *dto.StockOperationReq) (res *beans.ResponseEmpty, err error) { + err = service.StockManage.StockOperation(ctx, req) + return +} diff --git a/controller/stock/unit_conversion_controller.go b/controller/stock/unit_conversion_controller.go new file mode 100644 index 0000000..6c22ade --- /dev/null +++ b/controller/stock/unit_conversion_controller.go @@ -0,0 +1,41 @@ +// 单位换算控制器 +// 职责:单位换算规则CRUD接口 +// 调用服务:service.UnitConversion +// 注意:Update/Delete返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type unitConversionController struct{} + +var UnitConversion = new(unitConversionController) + +func init() {} + +// Create 创建单位换算规则 +func (c *unitConversionController) Create(ctx context.Context, req *dto.CreateUnitConversionReq) (res *dto.CreateUnitConversionRes, err error) { + return service.UnitConversion.Create(ctx, req) +} + +// Update 更新单位换算规则 +func (c *unitConversionController) Update(ctx context.Context, req *dto.UpdateUnitConversionReq) (res *beans.ResponseEmpty, err error) { + err = service.UnitConversion.Update(ctx, req) + return +} + +// Delete 删除单位换算规则 +func (c *unitConversionController) Delete(ctx context.Context, req *dto.DeleteUnitConversionReq) (res *beans.ResponseEmpty, err error) { + err = service.UnitConversion.Delete(ctx, req) + return +} + +// List 查询单位换算列表 +func (c *unitConversionController) List(ctx context.Context, req *dto.ListUnitConversionReq) (res *dto.ListUnitConversionRes, err error) { + return service.UnitConversion.List(ctx, req) +} diff --git a/controller/stock/warehouse_controller.go b/controller/stock/warehouse_controller.go new file mode 100644 index 0000000..d1b4bdb --- /dev/null +++ b/controller/stock/warehouse_controller.go @@ -0,0 +1,47 @@ +// 仓库控制器 +// 职责:仓库CRUD接口、状态更新接口 +// 调用服务:service.Warehouse +// 注意:Update/Delete/UpdateStatus返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type warehouse struct{} + +var Warehouse = new(warehouse) + +func init() { +} + +func (c *warehouse) CreateWarehouse(ctx context.Context, req *dto.CreateWarehouseReq) (res *dto.CreateWarehouseRes, err error) { + return service.Warehouse.Create(ctx, req) +} + +func (c *warehouse) UpdateWarehouse(ctx context.Context, req *dto.UpdateWarehouseReq) (res *beans.ResponseEmpty, err error) { + err = service.Warehouse.Update(ctx, req) + return +} + +func (c *warehouse) DeleteWarehouse(ctx context.Context, req *dto.DeleteWarehouseReq) (res *beans.ResponseEmpty, err error) { + err = service.Warehouse.Delete(ctx, req) + return +} + +func (c *warehouse) UpdateWarehouseStatus(ctx context.Context, req *dto.UpdateWarehouseStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Warehouse.UpdateStatus(ctx, req) + return +} + +func (c *warehouse) GetWarehouse(ctx context.Context, req *dto.GetWarehouseReq) (res *dto.GetWarehouseRes, err error) { + return service.Warehouse.GetOne(ctx, req) +} + +func (c *warehouse) ListWarehouses(ctx context.Context, req *dto.ListWarehouseReq) (res *dto.ListWarehouseRes, err error) { + return service.Warehouse.List(ctx, req) +} diff --git a/controller/stock/zone_controller.go b/controller/stock/zone_controller.go new file mode 100644 index 0000000..4fda7b5 --- /dev/null +++ b/controller/stock/zone_controller.go @@ -0,0 +1,47 @@ +// 库区控制器 +// 职责:库区CRUD接口、状态更新接口 +// 调用服务:service.Zone +// 注意:Update/Delete/UpdateStatus返回*beans.ResponseEmpty,直接return +package controller + +import ( + dto "assets/model/dto/stock" + service "assets/service/stock" + "context" + + "gitea.com/red-future/common/beans" +) + +type zone struct{} + +var Zone = new(zone) + +func init() { +} + +func (c *zone) CreateZone(ctx context.Context, req *dto.CreateZoneReq) (res *dto.CreateZoneRes, err error) { + return service.Zone.Create(ctx, req) +} + +func (c *zone) UpdateZone(ctx context.Context, req *dto.UpdateZoneReq) (res *beans.ResponseEmpty, err error) { + err = service.Zone.Update(ctx, req) + return +} + +func (c *zone) DeleteZone(ctx context.Context, req *dto.DeleteZoneReq) (res *beans.ResponseEmpty, err error) { + err = service.Zone.Delete(ctx, req) + return +} + +func (c *zone) UpdateZoneStatus(ctx context.Context, req *dto.UpdateZoneStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Zone.UpdateStatus(ctx, req) + return +} + +func (c *zone) GetZone(ctx context.Context, req *dto.GetZoneReq) (res *dto.GetZoneRes, err error) { + return service.Zone.GetOne(ctx, req) +} + +func (c *zone) ListZones(ctx context.Context, req *dto.ListZoneReq) (res *dto.ListZoneRes, err error) { + return service.Zone.List(ctx, req) +} diff --git a/controller/sync/sync_controller.go b/controller/sync/sync_controller.go new file mode 100644 index 0000000..fb7ca0b --- /dev/null +++ b/controller/sync/sync_controller.go @@ -0,0 +1,97 @@ +package controller + +import ( + dto "assets/model/dto/sync" + service "assets/service/sync" + "context" + + "gitea.com/red-future/common/beans" +) + +type syncController struct{} + +// Sync 同步控制器 +var Sync = new(syncController) + +// CreateSyncTask 创建同步任务 +func (c *syncController) CreateSyncTask(ctx context.Context, req *dto.CreateSyncTaskReq) (res *dto.CreateSyncTaskRes, err error) { + taskID, err := service.Sync.CreateSyncTask(ctx, req) + if err != nil { + return nil, err + } + return &dto.CreateSyncTaskRes{ + TaskID: taskID, + }, nil +} + +// ListSyncTasks 获取同步任务列表 +func (c *syncController) ListSyncTasks(ctx context.Context, req *dto.ListSyncTaskReq) (res *dto.ListSyncTaskRes, err error) { + list, total, err := service.Sync.ListSyncTasks(ctx, req) + if err != nil { + return nil, err + } + return &dto.ListSyncTaskRes{ + List: list, + Total: total, + }, nil +} + +// GetSyncTask 获取同步任务详情 +func (c *syncController) GetSyncTask(ctx context.Context, req *dto.GetSyncTaskReq) (res *dto.GetSyncTaskRes, err error) { + return service.Sync.GetSyncTask(ctx, req.ID) +} + +// UpdateSyncTaskStatus 更新同步任务状态 +func (c *syncController) UpdateSyncTaskStatus(ctx context.Context, req *dto.UpdateSyncTaskStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.Sync.UpdateSyncTaskStatus(ctx, req) + return +} + +// SyncAsset 同步资产 +func (c *syncController) SyncAsset(ctx context.Context, req *dto.SyncAssetReq) (res *dto.SyncAssetRes, err error) { + taskID, err := service.Sync.SyncAsset(ctx, req) + if err != nil { + return nil, err + } + return &dto.SyncAssetRes{ + TaskID: taskID, + }, nil +} + +// SyncAssetSku 同步资产SKU +func (c *syncController) SyncAssetSku(ctx context.Context, req *dto.SyncAssetSkuReq) (res *dto.SyncAssetSkuRes, err error) { + taskID, err := service.Sync.SyncAssetSku(ctx, req) + if err != nil { + return nil, err + } + return &dto.SyncAssetSkuRes{ + TaskID: taskID, + }, nil +} + +// SyncStock 同步库存 +func (c *syncController) SyncStock(ctx context.Context, req *dto.SyncStockReq) (res *dto.SyncStockRes, err error) { + taskID, err := service.Sync.SyncStock(ctx, req) + if err != nil { + return nil, err + } + return &dto.SyncStockRes{ + TaskID: taskID, + }, nil +} + +// BatchSyncAssets 批量同步资产 +func (c *syncController) BatchSyncAssets(ctx context.Context, req *dto.BatchSyncAssetsReq) (res *dto.BatchSyncAssetsRes, err error) { + taskIDs, err := service.Sync.BatchSyncAssets(ctx, req) + if err != nil { + return nil, err + } + return &dto.BatchSyncAssetsRes{ + TaskIDs: taskIDs, + }, nil +} + +// GetPlatformSyncStatus 获取平台同步状态 +func (c *syncController) GetPlatformSyncStatus(ctx context.Context, req *dto.GetPlatformSyncStatusReq) (res *dto.GetPlatformSyncStatusRes, err error) { + return service.Sync.GetPlatformSyncStatus(ctx, req) +} diff --git a/dao/asset/asset_dao.go b/dao/asset/asset_dao.go new file mode 100644 index 0000000..150ad7a --- /dev/null +++ b/dao/asset/asset_dao.go @@ -0,0 +1,110 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var Asset = new(assetDao) + +type assetDao struct { +} + +// Insert 插入资产 +func (d *assetDao) Insert(ctx context.Context, req *dto.CreateAssetReq) (id int64, err error) { + var result entity.Asset + if err = gconv.Struct(req, &result); err != nil { + return + } + return gfdb.DB(ctx).Model(ctx, public.AssetCollection).Ctx(ctx).Data(&result).InsertAndGetId() +} + +// GetOne 获取单个资产 +func (d *assetDao) GetOne(ctx context.Context, req *dto.GetAssetReq) (res *entity.Asset, err error) { + err = gfdb.DB(ctx).Model(ctx, public.AssetCollection).Ctx(ctx).Where("id", req.Id).Scan(&res) + return +} + +// Update 更新资产 +func (d *assetDao) Update(ctx context.Context, req *dto.UpdateAssetReq) (err error) { + data := g.Map{ + "name": req.Name, + "description": req.Description, + "type": req.Type, + "category_id": req.CategoryId, + "image_url": req.ImageURL, + "images": req.Images, + "status": req.Status, + "online_time": req.OnlineTime, + "offline_time": req.OfflineTime, + "physical_asset_config": req.PhysicalAssetConfig, + "service_asset_config": req.ServiceAssetConfig, + "virtual_asset_config": req.VirtualAssetConfig, + "metadata": req.Metadata, + } + _, err = gfdb.DB(ctx).Model(ctx, public.AssetCollection).Ctx(ctx).Where("id", req.Id).Update(data) + return +} + +// DeleteFake 删除资产-根据id进行假删 +func (d *assetDao) DeleteFake(ctx context.Context, req *dto.DeleteAssetReq) (err error) { + _, err = gfdb.DB(ctx).Model(ctx, public.AssetCollection).Ctx(ctx).Where("id", req.Id).Update(g.Map{ + "is_deleted": true, + }) + return +} + +// GetOneById 通过ID获取单个资产(内部使用uint64) +func (d *assetDao) GetOneById(ctx context.Context, id uint64) (res *entity.Asset, err error) { + err = gfdb.DB(ctx).Model(ctx, public.AssetCollection).Ctx(ctx).Where("id", id).Scan(&res) + return +} + +// Count 获取资产数量 +func (d *assetDao) Count(ctx context.Context, req *dto.ListAssetReq) (count int64, err error) { + m := d.buildListFilter(ctx, req) + c, err := m.Count() + return int64(c), err +} + +// List 获取资产列表 +func (d *assetDao) List(ctx context.Context, req *dto.ListAssetReq) (res []entity.Asset, total int, err error) { + m := d.buildListFilter(ctx, req) + if req.Page != nil { + m = m.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + err = m.ScanAndCount(&res, &total, false) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *assetDao) buildListFilter(ctx context.Context, req *dto.ListAssetReq) *gdb.Model { + m := gfdb.DB(ctx).Model(ctx, public.AssetCollection).Cache(ctx).Where("is_deleted", false) + + if !g.IsEmpty(req.Name) { + m = m.Where("name", req.Name) + } + if !g.IsEmpty(req.Type) { + m = m.Where("type", req.Type) + } + if !g.IsEmpty(req.CategoryId) { + m = m.Where("category_id", req.CategoryId) + } + if !g.IsEmpty(req.Status) { + m = m.Where("status", req.Status) + } + if !g.IsEmpty(req.CategoryPath) { + m = m.WhereLike("category_path", req.CategoryPath+"%") + } + if !g.IsEmpty(req.Keyword) { + m = m.WhereLike("name", "%"+req.Keyword+"%") + } + return m +} diff --git a/dao/asset/asset_sku_dao.go b/dao/asset/asset_sku_dao.go new file mode 100644 index 0000000..445cad5 --- /dev/null +++ b/dao/asset/asset_sku_dao.go @@ -0,0 +1,130 @@ +package dao + +import ( + "assets/consts/public" + "assets/model/dto/asset" + "assets/model/entity/asset" + "context" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var AssetSku = new(assetSku) + +type assetSku struct { +} + +// Insert 插入SKU +func (d *assetSku) Insert(ctx context.Context, req *dto.CreateAssetSkuReq) (ids []any, err error) { + var result *entity.AssetSku + if err = utils.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.AssetSkuCollection) + return +} + +// Update 更新SKU +func (d *assetSku) Update(ctx context.Context, req *dto.UpdateAssetSkuReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": buildUpdateData} + if !g.IsEmpty(req.Stock) { + // 从$set中移除stock字段避免$set和$inc冲突 + delete(buildUpdateData, "stock") + update = bson.M{ + "$inc": bson.M{ + "stock": req.Stock, + }, + } + if len(buildUpdateData) > 0 { + update["$set"] = buildUpdateData + } + } + _, err = mongo.DB().Update(ctx, filter, update, public.AssetSkuCollection) + return +} + +// DeleteFake 删除SKU-根据id进行假删 +func (d *assetSku) DeleteFake(ctx context.Context, req *dto.DeleteAssetSkuReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.AssetSkuCollection) + return +} + +// GetOne 获取单个SKU +func (d *assetSku) GetOne(ctx context.Context, req *dto.GetAssetSkuReq, noTenantId bool) (res *entity.AssetSku, err error) { + filter := bson.M{"_id": req.Id} + if noTenantId { + err = mongo.DB().NoTenantId().FindOne(ctx, filter, &res, public.AssetSkuCollection) + } else { + err = mongo.DB().FindOne(ctx, filter, &res, public.AssetSkuCollection) + } + return +} + +// GetListByAssetIdExcludeCurrentSku 根据资产ID获取SKU列表并且排除当前SKU +func (d *assetSku) GetListByAssetIdExcludeCurrentSku(ctx context.Context, assetId *bson.ObjectID, req *dto.ListAssetSkuReq) (res []entity.AssetSku, total int64, err error) { + filter := bson.M{"assetId": assetId, "_id": bson.M{"$ne": req.Id}} + total, err = mongo.DB().Find(ctx, filter, &res, public.AssetSkuCollection, req.Page, req.OrderBy) + return +} + +// List 获取SKU列表 +func (d *assetSku) List(ctx context.Context, req *dto.ListAssetSkuReq, noTenantId bool) (res []entity.AssetSku, total int64, err error) { + // 构建查询过滤条件 + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + // 排序处理 + req.OrderBy = []beans.OrderBy{ + {Field: "sort", Order: beans.Asc}, + {Field: "createdAt", Order: beans.Desc}, + } + if noTenantId { + total, err = mongo.DB().NoTenantId().Find(ctx, filter, &res, public.AssetSkuCollection, req.Page, req.OrderBy) + } else { + total, err = mongo.DB().Find(ctx, filter, &res, public.AssetSkuCollection, req.Page, req.OrderBy) + } + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *assetSku) buildListFilter(ctx context.Context, req *dto.ListAssetSkuReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.AssetId) { + filter["assetId"] = req.AssetId + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.CategoryPath) { + filter["categoryPath"] = bson.M{"$regex": "^" + req.CategoryPath, "$options": "i"} + } + if !g.IsEmpty(req.Keyword) { + orConditions := bson.A{ + bson.M{"skuName": bson.M{"$regex": req.Keyword, "$options": "i"}}, + bson.M{"assetName": bson.M{"$regex": req.Keyword, "$options": "i"}}, + } + filter["$or"] = orConditions + } + if req.MinPrice > 0 { + filter["price"] = bson.M{"$gte": req.MinPrice} + } + if req.MaxPrice > 0 { + if filter["price"] == nil { + filter["price"] = bson.M{} + } + filter["price"].(bson.M)["$lte"] = req.MaxPrice + } + return +} diff --git a/dao/asset/category_dao.go b/dao/asset/category_dao.go new file mode 100644 index 0000000..a39d63d --- /dev/null +++ b/dao/asset/category_dao.go @@ -0,0 +1,95 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/guid" + + "github.com/gogf/gf/v2/frame/g" +) + +var Category = new(category) + +type category struct { +} + +// Insert 插入分类 +func (d *category) Insert(ctx context.Context, req *dto.CreateCategoryReq) (res *entity.Category, err error) { + if err = gconv.Struct(req, &res); err != nil { + return + } + res.Bid = guid.S() + _, err = gfdb.DB(ctx).Model(ctx, public.CategoryCollection).Insert(&res) + return res, nil +} + +// Update 更新分类 +func (d *category) Update(ctx context.Context, req *dto.UpdateCategoryReq) (err error) { + model := gfdb.DB(ctx).Model(ctx, public.CategoryCollection).Data(gconv.Map(&req)).OmitEmpty() + if !g.IsEmpty(req.Bid) { + model.Where("bid", req.Bid) + } + if !g.IsEmpty(req.Id) { + model.Where("id", req.Id) + } + _, err = model.Update() + return +} + +// GetOne 获取单个分类 +func (d *category) GetOne(ctx context.Context, req *dto.GetCategoryReq, fields ...string) (category *entity.Category, err error) { + model := gfdb.DB(ctx).Model(ctx, public.CategoryCollection) + if !g.IsEmpty(req.Bid) { + model.Where(entity.CategoryCol.Bid, req.Bid) + } + if !g.IsEmpty(req.Id) { + model.Where(entity.CategoryCol.Id, req.Id) + } + res, err := model.Fields(fields).One() + err = res.Struct(&category) + return +} + +// DeleteFake 删除分类-根据id及父id进行假删 +func (d *category) DeleteFake(ctx context.Context, req *dto.DeleteCategoryReq) (err error) { + model := gfdb.DB(ctx).Model(ctx, public.CategoryCollection) + model.Where(entity.CategoryCol.Bid, req.Bid) + _, err = model.Delete() + return +} + +// Count 根据条件统计分类数量 +func (d *category) Count(ctx context.Context, req *dto.ListCategoryReq) (count int64, err error) { + m := d.buildListFilter(ctx, req) + c, err := m.Count() + return int64(c), err +} + +// List 获取分类列表 +func (d *category) List(ctx context.Context, req *dto.ListCategoryReq, fields ...string) (res []entity.Category, total int, err error) { + model := d.buildListFilter(ctx, req) + model.Fields(fields) + r, total, err := model.AllAndCount(false) + err = gconv.Structs(r.List(), &res) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *category) buildListFilter(ctx context.Context, req *dto.ListCategoryReq) *gdb.Model { + model := gfdb.DB(ctx).Model(ctx, public.CategoryCollection).Model + if !g.IsEmpty(req.Keyword) { + model.WhereLike(entity.CategoryCol.Name, "%"+req.Keyword+"%") + } + model.Where(entity.CategoryCol.ParentId, req.ParentId) + model.Where(entity.CategoryCol.Status, req.Status) + model.OrderAsc(entity.CategoryCol.Sort) + model.OrderDesc(entity.CategoryCol.CreatedAt) + model.OmitEmptyWhere() + return model +} diff --git a/dao/asset/private_category_dao.go b/dao/asset/private_category_dao.go new file mode 100644 index 0000000..1e85af5 --- /dev/null +++ b/dao/asset/private_category_dao.go @@ -0,0 +1,116 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var PrivateCategory = new(privateCategory) + +type privateCategory struct{} + +// Insert 插入私域分类 +func (d *privateCategory) Insert(ctx context.Context, req *dto.CreatePrivateCategoryReq) (ids []interface{}, err error) { + var result *entity.PrivateCategory + if err = gconv.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PrivateCategoryCollection) + return +} + +// BatchInsert 批量插入私域分类 +func (d *privateCategory) BatchInsert(ctx context.Context, req *dto.BatchCreatePrivateCategoryReq) (ids []interface{}, err error) { + items := make([]*entity.PrivateCategory, 0, len(req.Categories)) + for _, item := range req.Categories { + var result *entity.PrivateCategory + if err = gconv.Struct(item, &result); err != nil { + return + } + items = append(items, result) + } + interfaces := make([]interface{}, len(items)) + for i, item := range items { + interfaces[i] = item + } + ids, err = mongo.DB().Insert(ctx, interfaces, public.PrivateCategoryCollection) + return +} + +// GetOne 获取单个私域分类 +func (d *privateCategory) GetOne(ctx context.Context, id *bson.ObjectID) (category *entity.PrivateCategory, err error) { + filter := bson.M{"_id": id} + err = mongo.DB().FindOne(ctx, filter, &category, public.PrivateCategoryCollection) + return +} + +// Update 更新私域分类 +func (d *privateCategory) Update(ctx context.Context, req *dto.UpdatePrivateCategoryReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.ID} + update := bson.M{"$set": buildUpdateData} + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateCategoryCollection) + return +} + +// DeleteFake 删除私域分类-根据id进行假删 +func (d *privateCategory) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.PrivateCategoryCollection) + return +} + +// List 获取私域分类列表 +func (d *privateCategory) List(ctx context.Context, req *dto.ListPrivateCategoryReq) (res []*entity.PrivateCategory, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + + total, err = mongo.DB().Find(ctx, filter, &res, public.PrivateCategoryCollection, nil, nil) + return +} + +// GetTree 获取私域分类树 +func (d *privateCategory) GetTree(ctx context.Context) (res []*entity.PrivateCategory, err error) { + filter := bson.M{} + _, err = mongo.DB().Find(ctx, filter, &res, public.PrivateCategoryCollection, nil, nil) + return +} + +// GetByParentId 根据父ID获取子分类列表 +func (d *privateCategory) GetByParentId(ctx context.Context, parentId string) (res []*entity.PrivateCategory, err error) { + filter := bson.M{"parentId": parentId} + _, err = mongo.DB().Find(ctx, filter, &res, public.PrivateCategoryCollection, nil, nil) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *privateCategory) buildListFilter(ctx context.Context, req *dto.ListPrivateCategoryReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + + if !g.IsEmpty(req.Name) { + filter["name"] = bson.M{"$regex": req.Name, "$options": "i"} + } + if !g.IsEmpty(req.ParentID) { + filter["parentId"] = req.ParentID + } + if !g.IsEmpty(req.Level) { + filter["level"] = req.Level + } + if req.IsLeafNode != nil { + filter["isLeafNode"] = *req.IsLeafNode + } + return +} diff --git a/dao/asset/private_sku_dao.go b/dao/asset/private_sku_dao.go new file mode 100644 index 0000000..93aed86 --- /dev/null +++ b/dao/asset/private_sku_dao.go @@ -0,0 +1,119 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var PrivateSku = new(privateSku) + +type privateSku struct{} + +// Insert 插入私域SKU +func (d *privateSku) Insert(ctx context.Context, req *dto.CreatePrivateSkuReq) (ids []interface{}, err error) { + var result *entity.PrivateSku + if err = gconv.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PrivateSkuCollection) + return +} + +// BatchInsert 批量插入私域SKU +func (d *privateSku) BatchInsert(ctx context.Context, req *dto.BatchCreatePrivateSkuReq) (ids []interface{}, err error) { + items := make([]*entity.PrivateSku, 0, len(req.Skus)) + for _, item := range req.Skus { + var result *entity.PrivateSku + if err = gconv.Struct(item, &result); err != nil { + return + } + items = append(items, result) + } + interfaces := make([]interface{}, len(items)) + for i, item := range items { + interfaces[i] = item + } + ids, err = mongo.DB().Insert(ctx, interfaces, public.PrivateSkuCollection) + return +} + +// GetOne 获取单个私域SKU +func (d *privateSku) GetOne(ctx context.Context, id *bson.ObjectID) (sku *entity.PrivateSku, err error) { + filter := bson.M{"_id": id} + err = mongo.DB().FindOne(ctx, filter, &sku, public.PrivateSkuCollection) + return +} + +// Update 更新私域SKU +func (d *privateSku) Update(ctx context.Context, req *dto.UpdatePrivateSkuReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.ID} + update := bson.M{"$set": buildUpdateData} + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateSkuCollection) + return +} + +// UpdateStock 更新库存 +func (d *privateSku) UpdateStock(ctx context.Context, id *bson.ObjectID, stockChange int) (err error) { + filter := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"stock": stockChange}} + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateSkuCollection) + return +} + +// DeleteFake 删除私域SKU-根据id进行假删 +func (d *privateSku) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.PrivateSkuCollection) + return +} + +// List 获取私域SKU列表 +func (d *privateSku) List(ctx context.Context, req *dto.ListPrivateSkuReq) (res []*entity.PrivateSku, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.PrivateSkuCollection, nil, nil) + return +} + +// ListByCategoryPath 根据分类路径获取SKU列表 +func (d *privateSku) ListByCategoryPath(ctx context.Context, categoryPath string) (res []*entity.PrivateSku, err error) { + filter := bson.M{"privateCategoryPath": bson.M{"$regex": "^" + categoryPath, "$options": "i"}} + _, err = mongo.DB().Find(ctx, filter, &res, public.PrivateSkuCollection, nil, nil) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *privateSku) buildListFilter(ctx context.Context, req *dto.ListPrivateSkuReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + + if !g.IsEmpty(req.SkuName) { + filter["skuName"] = bson.M{"$regex": req.SkuName, "$options": "i"} + } + if !g.IsEmpty(req.PrivateCategoryPath) { + filter["privateCategoryPath"] = bson.M{"$regex": "^" + req.PrivateCategoryPath, "$options": "i"} + } + if req.MinPrice > 0 { + filter["price"] = bson.M{"$gte": req.MinPrice} + } + if req.MaxPrice > 0 { + if filter["price"] == nil { + filter["price"] = bson.M{} + } + filter["price"].(bson.M)["$lte"] = req.MaxPrice + } + return +} diff --git a/dao/base/catch_sql.go b/dao/base/catch_sql.go new file mode 100644 index 0000000..c6944ae --- /dev/null +++ b/dao/base/catch_sql.go @@ -0,0 +1,325 @@ +package base + +import ( + "context" + "fmt" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ==================== CatchSQL 全局SQL条件拼接控制 ==================== + +// SQLConditionBuilder SQL条件构建器 +type SQLConditionBuilder struct { + // 是否自动添加租户ID条件 + EnableTenantId bool + // 是否自动添加创建人条件 + EnableCreator bool + // 是否自动添加修改人条件 + EnableUpdater bool + // 是否自动添加删除标记条件(只查询未删除数据) + EnableDeletedFilter bool + // 自定义额外条件 + ExtraConditions map[string]interface{} +} + +// DefaultSQLConditionBuilder 默认SQL条件构建器配置 +var DefaultSQLConditionBuilder = SQLConditionBuilder{ + EnableTenantId: true, + EnableCreator: false, + EnableUpdater: false, + EnableDeletedFilter: true, + ExtraConditions: make(map[string]interface{}), +} + +// ctxKeyCatchSQL 上下文键 +type ctxKeyCatchSQL string + +const ( + // ctxKeySQLBuilder SQL构建器上下文键 + ctxKeySQLBuilder ctxKeyCatchSQL = "catch_sql_builder" + // ctxKeySkipCatchSQL 跳过CatchSQL的上下文键 + ctxKeySkipCatchSQL ctxKeyCatchSQL = "catch_sql_skip" +) + +// WithSQLBuilder 设置自定义SQL条件构建器到上下文 +func WithSQLBuilder(ctx context.Context, builder *SQLConditionBuilder) context.Context { + return context.WithValue(ctx, ctxKeySQLBuilder, builder) +} + +// GetSQLBuilder 从上下文获取SQL条件构建器 +func GetSQLBuilder(ctx context.Context) *SQLConditionBuilder { + if ctx == nil { + return &DefaultSQLConditionBuilder + } + if builder, ok := ctx.Value(ctxKeySQLBuilder).(*SQLConditionBuilder); ok && builder != nil { + return builder + } + return &DefaultSQLConditionBuilder +} + +// SkipCatchSQL 跳过CatchSQL条件拼接 +func SkipCatchSQL(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKeySkipCatchSQL, true) +} + +// IsSkipCatchSQL 检查是否跳过CatchSQL +func IsSkipCatchSQL(ctx context.Context) bool { + if ctx == nil { + return false + } + v, ok := ctx.Value(ctxKeySkipCatchSQL).(bool) + return ok && v +} + +// ==================== CatchSQL 核心方法 ==================== + +// CatchSQL 全局统一控制SQL条件拼接 +// 根据上下文自动添加租户ID、创建人、修改人、删除标记等条件 +// 使用示例: +// +// // 基础使用(自动添加默认条件) +// m := base.CatchSQL(ctx, g.DB().Model("asset")) +// m.Where("status", 1).Scan(&result) +// +// // 自定义条件构建器 +// builder := &base.SQLConditionBuilder{ +// EnableTenantId: true, +// EnableCreator: true, +// } +// ctx = base.WithSQLBuilder(ctx, builder) +// m := base.CatchSQL(ctx, g.DB().Model("asset")) +// +// // 跳过CatchSQL +// ctx = base.SkipCatchSQL(ctx) +// m := base.CatchSQL(ctx, g.DB().Model("asset")) +func CatchSQL(ctx context.Context, model *gdb.Model) *gdb.Model { + if ctx == nil || model == nil { + return model + } + + // 检查是否跳过 + if IsSkipCatchSQL(ctx) { + return model + } + + builder := GetSQLBuilder(ctx) + userInfo, _ := utils.GetUserInfo(ctx) + + // 1. 自动添加租户ID条件 + if builder.EnableTenantId && !g.IsEmpty(userInfo.TenantId) { + model = model.Where("tenant_id", userInfo.TenantId) + } + + // 2. 自动添加创建人条件 + if builder.EnableCreator && !g.IsEmpty(userInfo.UserName) { + model = model.Where("creator", userInfo.UserName) + } + + // 3. 自动添加修改人条件 + if builder.EnableUpdater && !g.IsEmpty(userInfo.UserName) { + model = model.Where("updater", userInfo.UserName) + } + + // 4. 自动添加删除标记条件(只查询未删除数据) + if builder.EnableDeletedFilter { + model = model.Where("is_deleted", 0) + } + + // 5. 添加自定义额外条件 + for field, value := range builder.ExtraConditions { + if field != "" && value != nil { + model = model.Where(field, value) + } + } + + return model +} + +// CatchSQLWithTable 指定表名创建带CatchSQL条件的Model +// 使用示例: +// +// m := base.CatchSQLWithTable(ctx, "asset") +// m.Where("status", 1).Scan(&result) +func CatchSQLWithTable(ctx context.Context, table string) *gdb.Model { + if ctx == nil { + return g.DB().Model(table).Safe() + } + model := g.DB().Model(table).Safe().Ctx(ctx) + return CatchSQL(ctx, model) +} + +// CatchSQLWithSchema 指定Schema和表名创建带CatchSQL条件的Model +// 使用示例: +// +// m := base.CatchSQLWithSchema(ctx, "public", "asset") +// m.Where("status", 1).Scan(&result) +func CatchSQLWithSchema(ctx context.Context, schema, table string) *gdb.Model { + if ctx == nil { + return g.DB().Schema(schema).Model(table).Safe() + } + model := g.DB().Schema(schema).Model(table).Safe().Ctx(ctx) + return CatchSQL(ctx, model) +} + +// ==================== 快捷条件构建器 ==================== + +// NewSQLBuilder 创建新的SQL条件构建器 +func NewSQLBuilder() *SQLConditionBuilder { + return &SQLConditionBuilder{ + EnableTenantId: true, + EnableCreator: false, + EnableUpdater: false, + EnableDeletedFilter: true, + ExtraConditions: make(map[string]interface{}), + } +} + +// WithTenantId 启用/禁用租户ID条件 +func (b *SQLConditionBuilder) WithTenantId(enable bool) *SQLConditionBuilder { + b.EnableTenantId = enable + return b +} + +// WithCreator 启用/禁用创建人条件 +func (b *SQLConditionBuilder) WithCreator(enable bool) *SQLConditionBuilder { + b.EnableCreator = enable + return b +} + +// WithUpdater 启用/禁用修改人条件 +func (b *SQLConditionBuilder) WithUpdater(enable bool) *SQLConditionBuilder { + b.EnableUpdater = enable + return b +} + +// WithDeletedFilter 启用/禁用删除标记过滤 +func (b *SQLConditionBuilder) WithDeletedFilter(enable bool) *SQLConditionBuilder { + b.EnableDeletedFilter = enable + return b +} + +// WithExtraCondition 添加自定义条件 +func (b *SQLConditionBuilder) WithExtraCondition(field string, value interface{}) *SQLConditionBuilder { + if b.ExtraConditions == nil { + b.ExtraConditions = make(map[string]interface{}) + } + b.ExtraConditions[field] = value + return b +} + +// Build 构建并返回Model +func (b *SQLConditionBuilder) Build(ctx context.Context, model *gdb.Model) *gdb.Model { + ctx = WithSQLBuilder(ctx, b) + return CatchSQL(ctx, model) +} + +// ==================== 常用场景快捷方法 ==================== + +// CatchSQLForTenant 只添加租户ID条件的快捷方法 +func CatchSQLForTenant(ctx context.Context, model *gdb.Model) *gdb.Model { + builder := NewSQLBuilder(). + WithTenantId(true). + WithDeletedFilter(false) + ctx = WithSQLBuilder(ctx, builder) + return CatchSQL(ctx, model) +} + +// CatchSQLForCreator 只添加创建人条件的快捷方法 +func CatchSQLForCreator(ctx context.Context, model *gdb.Model) *gdb.Model { + builder := NewSQLBuilder(). + WithTenantId(false). + WithCreator(true). + WithDeletedFilter(false) + ctx = WithSQLBuilder(ctx, builder) + return CatchSQL(ctx, model) +} + +// CatchSQLForList 列表查询的快捷方法(租户ID + 删除标记) +func CatchSQLForList(ctx context.Context, model *gdb.Model) *gdb.Model { + builder := NewSQLBuilder(). + WithTenantId(true). + WithDeletedFilter(true) + ctx = WithSQLBuilder(ctx, builder) + return CatchSQL(ctx, model) +} + +// CatchSQLForAdmin 管理员查询的快捷方法(只过滤删除标记,不过滤租户) +func CatchSQLForAdmin(ctx context.Context, model *gdb.Model) *gdb.Model { + builder := NewSQLBuilder(). + WithTenantId(false). + WithDeletedFilter(true) + ctx = WithSQLBuilder(ctx, builder) + return CatchSQL(ctx, model) +} + +// ==================== DAO层无感知集成 ==================== + +// CtxModel 创建带 CatchSQL 条件的 Model(DAO层无感知使用) +// 这是推荐的无感知使用方式,直接在 DAO 的 Ctx() 方法中调用 +// +// 使用示例(DAO层): +// +// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model { +// return base.CtxModel(ctx, entity.Asset{}) +// } +// +// func (d *assetDao) GetById(ctx context.Context, id uint64) (*entity.Asset, error) { +// var result entity.Asset +// err := d.Ctx(ctx).Where("id", id).Scan(&result) // 自动带 tenant_id 和 is_deleted=0 条件 +// return &result, err +// } +func CtxModel(ctx context.Context, table interface{}) *gdb.Model { + if ctx == nil { + return g.DB().Model(table).Safe() + } + model := g.DB().Model(table).Safe().Ctx(ctx) + return CatchSQL(ctx, model) +} + +// CtxModelWithHook 创建带 CatchSQL 条件和 Hook 的 Model(完整版) +// 同时启用 CatchSQL 条件拼接和 CatchSQLHook 自动字段赋值 +// +// 使用示例(DAO层): +// +// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model { +// return base.CtxModelWithHook(ctx, entity.Asset{}) +// } +// +// func (d *assetDao) Insert(ctx context.Context, data g.Map) (int64, error) { +// return d.Ctx(ctx).Data(data).InsertAndGetId() // 自动赋值 tenant_id, creator, updater +// } +func CtxModelWithHook(ctx context.Context, table interface{}) *gdb.Model { + if ctx == nil { + return g.DB().Model(table).Safe() + } + model := g.DB().Model(table).Safe().Ctx(ctx) + // 先应用 CatchSQL 条件 + model = CatchSQL(ctx, model) + // 再应用 Hook(用于 Insert/Update 自动字段赋值) + model = model.Hook(CatchSQLHook()) + return model +} + +// ==================== 调试工具 ==================== + +// GetCatchSQLInfo 获取当前CatchSQL的配置信息(用于调试) +func GetCatchSQLInfo(ctx context.Context) string { + if IsSkipCatchSQL(ctx) { + return "CatchSQL: skipped" + } + + builder := GetSQLBuilder(ctx) + userInfo, _ := utils.GetUserInfo(ctx) + + return fmt.Sprintf( + "CatchSQL{TenantId:%v(%v), Creator:%v(%v), Updater:%v(%v), DeletedFilter:%v, Extra:%v}", + builder.EnableTenantId, userInfo.TenantId, + builder.EnableCreator, userInfo.UserName, + builder.EnableUpdater, userInfo.UserName, + builder.EnableDeletedFilter, + len(builder.ExtraConditions), + ) +} diff --git a/dao/base/db.go b/dao/base/db.go new file mode 100644 index 0000000..2fbbe13 --- /dev/null +++ b/dao/base/db.go @@ -0,0 +1,75 @@ +package base + +import ( + "context" + "database/sql" + "github.com/gogf/gf/contrib/drivers/pgsql/v2" + "github.com/gogf/gf/v2/database/gdb" +) + +type GDB struct { + *pgsql.Driver +} + +var ( + // customDriverName is my driver name, which is used for registering. + customDriverName = "MyDriver" +) + +func init() { + // It here registers my custom driver in package initialization function "init". + // You can later use this type in the database configuration. + if err := gdb.Register(customDriverName, &GDB{}); err != nil { + panic(err) + } +} + +// New creates and returns a database object for mysql. +// It implements the interface of gdb.Driver for extra database driver installation. +func (d *GDB) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + return &GDB{ + &pgsql.Driver{ + Core: core, + }, + }, nil +} + +// DoCommit commits current sql and arguments to underlying sql driver. +func (d *GDB) DoCommit(ctx context.Context, in gdb.DoCommitInput) (out gdb.DoCommitOutput, err error) { + out, err = d.Core.DoCommit(ctx, in) + + return +} + +// 其他接口方法 +func (d *GDB) DoFilter(ctx context.Context, link gdb.Link, sql string, args []any) (string, []any, error) { + return d.Core.DoFilter(ctx, link, sql, args) +} + +func (d *GDB) DoPrepare(ctx context.Context, link gdb.Link, sql string) (*gdb.Stmt, error) { + return d.Core.DoPrepare(ctx, link, sql) +} + +func (d *GDB) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) { + return fieldValue, nil +} +func (d *GDB) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) { + return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) +} + +func (d *GDB) Exec(ctx context.Context, sql string, args ...any) (sql.Result, error) { + in := gdb.DoCommitInput{Type: gdb.SqlTypeExecContext, Sql: sql, Args: args} + out, err := d.DoCommit(ctx, in) + if err != nil { + return nil, err + } + return out.Result, nil +} +func (d *GDB) DoExec(ctx context.Context, link gdb.Link, sql string, args ...any) (sql.Result, error) { + in := gdb.DoCommitInput{Type: gdb.SqlTypeExecContext, Sql: sql, Args: args} + out, err := d.DoCommit(ctx, in) + if err != nil { + return nil, err + } + return out.Result, nil +} diff --git a/dao/base/hook.go b/dao/base/hook.go new file mode 100644 index 0000000..2d0db32 --- /dev/null +++ b/dao/base/hook.go @@ -0,0 +1,456 @@ +package base + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "github.com/gogf/gf/v2/text/gstr" + "time" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/database/gredis" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/glog" + "github.com/gogf/gf/v2/util/gconv" +) + +// ==================== 上下文键定义 ==================== + +type ctxKey string + +const ( + // ctxKeySkipTenant 跳过租户ID自动赋值的上下文键 + ctxKeySkipTenant ctxKey = "hook_skip_tenant" + // ctxKeyCacheEnabled 缓存启用标记的上下文键 + ctxKeyCacheEnabled ctxKey = "hook_cache_enabled" + // ctxKeyCachePrefix 缓存key前缀的上下文键 + ctxKeyCachePrefix ctxKey = "hook_cache_prefix" +) + +// ==================== 租户相关 ==================== + +// SkipTenantId 在上下文中标记跳过租户ID自动赋值 +func SkipTenantId(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKeySkipTenant, true) +} + +// isSkipTenant 检查是否跳过租户ID +func isSkipTenant(ctx context.Context) bool { + if ctx == nil { + return false + } + v, ok := ctx.Value(ctxKeySkipTenant).(bool) + return ok && v +} + +// ==================== 缓存配置 ==================== + +// CacheConfig 缓存配置 +type CacheConfig struct { + // 本地缓存过期时间(秒),默认60秒 + LocalTTL int + // Redis缓存过期时间(秒),默认300秒 + RedisTTL int +} + +// DefaultCacheConfig 默认缓存配置 +var DefaultCacheConfig = CacheConfig{ + LocalTTL: 60, + RedisTTL: 300, +} + +// isCacheEnabled 检查是否启用缓存 +func isCacheEnabled(ctx context.Context) bool { + if ctx == nil { + return false + } + v, ok := ctx.Value(ctxKeyCacheEnabled).(bool) + return ok && v +} + +// getCachePrefix 获取缓存key前缀 +func getCachePrefix(ctx context.Context) string { + if ctx == nil { + return "" + } + v, ok := ctx.Value(ctxKeyCachePrefix).(string) + if !ok { + return "" + } + return v +} + +// ==================== 缓存管理器(单例) ==================== + +var ( + localCache *gcache.Cache +) + +// getLocalCache 获取本地缓存实例 +func getLocalCache() *gcache.Cache { + if localCache == nil { + localCache = gcache.New() + } + return localCache +} + +// buildCacheKey 构建缓存key +// 根据表名和查询条件自动生成key +func buildCacheKey(prefix string, table string, where ...interface{}) string { + // 基础key: prefix:table + key := fmt.Sprintf("%s:%s", prefix, table) + + // 如果有where条件,追加到key中 + if len(where) > 0 { + for _, w := range where { + key = fmt.Sprintf("%s:%v", key, w) + } + } + + return key +} + +// getFromCache 从缓存获取数据(本地缓存 -> Redis) +func getFromCache(ctx context.Context, key string) ([]byte, bool) { + config := DefaultCacheConfig + + // 1. 先查本地缓存 + if val, err := getLocalCache().Get(ctx, key); err == nil && val != nil { + if data := val.Bytes(); len(data) > 0 { + glog.Debugf(ctx, "[Cache] Hit local cache: %s", key) + return data, true + } + } + + // 2. 再查Redis缓存 + if g.Redis() != nil { + result, err := g.Redis().Get(ctx, key) + if err == nil && !result.IsEmpty() { + data := result.Bytes() + // 写入本地缓存 + getLocalCache().Set(ctx, key, data, time.Duration(config.LocalTTL)*time.Second) + glog.Debugf(ctx, "[Cache] Hit redis cache: %s", key) + return data, true + } + } + + return nil, false +} + +// setToCache 写入缓存(本地缓存 + Redis) +func setToCache(ctx context.Context, key string, data []byte) { + if len(data) == 0 { + return + } + + config := DefaultCacheConfig + + // 1. 写入本地缓存 + getLocalCache().Set(ctx, key, data, time.Duration(config.LocalTTL)*time.Second) + + // 2. 写入Redis缓存 + if g.Redis() != nil { + expire := int64(config.RedisTTL) + _, err := g.Redis().Set(ctx, key, data, gredis.SetOption{ + TTLOption: gredis.TTLOption{ + EX: &expire, + }, + }) + if err != nil { + glog.Warningf(ctx, "[Cache] Failed to set redis cache: %s, err: %v", key, err) + } + } +} + +// deleteCache 删除缓存 +func deleteCache(ctx context.Context, key string) { + // 1. 删除本地缓存 + getLocalCache().Remove(ctx, key) + + // 2. 删除Redis缓存 + if g.Redis() != nil { + _, err := g.Redis().Del(ctx, key) + if err != nil { + glog.Warningf(ctx, "[Cache] Failed to delete redis cache: %s, err: %v", key, err) + } + } +} + +// deleteCacheByPattern 根据模式删除缓存 +func deleteCacheByPattern(ctx context.Context, pattern string) { + // 1. 清空本地缓存(简单实现:清空所有) + getLocalCache().Clear(ctx) + + // 2. 删除Redis缓存(使用SCAN+DEL) + if g.Redis() != nil { + var cursor uint64 = 0 + for { + result, err := g.Redis().Do(ctx, "SCAN", cursor, "MATCH", pattern, "COUNT", 100) + if err != nil { + glog.Warningf(ctx, "[Cache] Failed to scan redis keys: %s, err: %v", pattern, err) + break + } + + resultMap := result.Map() + cursor = gconv.Uint64(resultMap["cursor"]) + keys := gconv.Strings(resultMap["keys"]) + + if len(keys) > 0 { + args := make([]interface{}, len(keys)) + for i, k := range keys { + args[i] = k + } + _, err = g.Redis().Do(ctx, "DEL", args...) + if err != nil { + glog.Warningf(ctx, "[Cache] Failed to delete redis keys: %v, err: %v", keys, err) + } + } + + if cursor == 0 { + break + } + } + } +} + +// ==================== 统一Hook入口 ==================== + +// CatchSQLHook 返回统一的 HookHandler(包含租户自动赋值和缓存) +// 使用示例: +// +// // 基础使用(自动租户赋值,无缓存) +// g.DB().Model("user").Hook(base.CatchSQLHook()).Ctx(ctx).Insert(data) +// +// // 启用缓存(用户无感知,自动处理缓存key) +// ctx = base.WithCacheEnabled(ctx, "asset") +// Asset.CtxWithCache(ctx).Where("id", 123).Scan(&result) +func CatchSQLHook() gdb.HookHandler { + return gdb.HookHandler{ + Insert: insertHook, + Update: updateHook, + Delete: deleteHook, + Select: selectHook, + } +} + +// ==================== Insert钩子 ==================== + +func insertHook(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) { + // 1. 自动赋值租户字段 + userInfo, _ := utils.GetUserInfo(ctx) + + if !g.IsEmpty(userInfo.TenantId) { + in.Model.Data("tenant_id", userInfo.TenantId) + } + if !g.IsEmpty(userInfo.UserName) { + in.Model.Data("creator", userInfo.UserName) + in.Model.Data("updater", userInfo.UserName) + } + //for i := range in.Data { + // if !g.IsEmpty(userInfo.TenantId) { + // if _, ok := in.Data[i]["tenant_id"]; !ok { + // in.Data[i]["tenant_id"] = userInfo.TenantId + // } + // } + // if !g.IsEmpty(userInfo.UserId) { + // if _, ok := in.Data[i]["creator"]; !ok { + // in.Data[i]["creator"] = userInfo.UserId + // } + // if _, ok := in.Data[i]["updater"]; !ok { + // in.Data[i]["updater"] = userInfo.UserId + // } + // } + //} + + // 2. 执行插入 + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + + // 3. 清除相关缓存 + prefix := getCachePrefix(ctx) + if prefix != "" { + deleteCacheByPattern(ctx, prefix+":*") + glog.Debugf(ctx, "[Hook] Cache cleared after insert, prefix: %s", prefix) + } + + return result, nil +} + +// ==================== Update钩子 ==================== + +func updateHook(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) { + // 1. 自动赋值修改人 + userInfo, _ := utils.GetUserInfo(ctx) + + if !g.IsEmpty(userInfo.TenantId) { + in.Model.Where("tenant_id", userInfo.TenantId) + } + if !g.IsEmpty(userInfo.UserName) { + in.Model.Where("creator", userInfo.UserName) + in.Model.Where("updater", userInfo.UserName) + } + + //switch data := in.Data.(type) { + //case gdb.Map: + // if !g.IsEmpty(userInfo.UserId) { + // if _, ok := data["updater"]; !ok { + // data["updater"] = userInfo.UserId + // } + // } + //case gdb.List: + // for i := range data { + // if !g.IsEmpty(userInfo.UserId) { + // if _, ok := data[i]["updater"]; !ok { + // data[i]["updater"] = userInfo.UserId + // } + // } + // } + //} + + // 2. 执行更新 + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + + // 3. 清除相关缓存 + prefix := getCachePrefix(ctx) + if prefix != "" { + deleteCacheByPattern(ctx, prefix+":*") + glog.Debugf(ctx, "[Hook] Cache cleared after update, prefix: %s", prefix) + } + + return result, nil +} + +// ==================== Delete钩子 ==================== + +func deleteHook(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) { + // 1. 执行删除 + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + + // 2. 清除相关缓存 + prefix := getCachePrefix(ctx) + if prefix != "" { + deleteCacheByPattern(ctx, prefix+":*") + glog.Debugf(ctx, "[Hook] Cache cleared after delete, prefix: %s", prefix) + } + + return result, nil +} + +// ==================== Select钩子(缓存读取) ==================== + +func selectHook(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + + userInfo, _ := utils.GetUserInfo(ctx) + + if !isSkipTenant(ctx) && !g.IsEmpty(userInfo.TenantId) { + in.Model.Where("tenant_id", userInfo.TenantId) + } + + // 未启用缓存,直接执行查询 + if !isCacheEnabled(ctx) { + return in.Next(ctx) + } + + prefix := getCachePrefix(ctx) + if prefix == "" { + return in.Next(ctx) + } + + // 从 SQL 字符串中提取 WHERE 条件部分 + whereCondition := extractWhereCondition(in.Sql) + + // 构建缓存key:prefix:table:where条件:args + cacheKey := buildCacheKey(prefix, in.Table, whereCondition, in.Args) + + glog.Debugf(ctx, "[Hook] Cache key: %s", cacheKey) + + // 1. 先查缓存 + if data, ok := getFromCache(ctx, cacheKey); ok { + var records gdb.Result + if err := json.Unmarshal(data, &records); err == nil && len(records) > 0 { + glog.Debugf(ctx, "[Hook] Cache hit for key: %s", cacheKey) + return records, nil + } + } + + // 2. 执行数据库查询 + result, err = in.Next(ctx) + if err != nil { + return nil, err + } + + // 3. 写入缓存 + if len(result) > 0 { + if data, err := json.Marshal(result); err == nil { + setToCache(ctx, cacheKey, data) + glog.Debugf(ctx, "[Hook] Cache set for key: %s", cacheKey) + } + } + + return result, nil +} + +// extractWhereCondition 从 SQL 语句中提取 WHERE 条件部分 +func extractWhereCondition(sql string) string { + // 查找 WHERE 关键字(不区分大小写) + whereIndex := gstr.PosI(sql, " WHERE ") + if whereIndex == -1 { + return "" + } + + // 提取 WHERE 之后的内容 + whereClause := sql[whereIndex+7:] + + // 移除 ORDER BY, GROUP BY, HAVING, LIMIT 等后续子句 + for _, keyword := range []string{" ORDER BY ", " GROUP BY ", " HAVING ", " LIMIT ", " FOR UPDATE"} { + if idx := gstr.PosI(whereClause, keyword); idx != -1 { + whereClause = whereClause[:idx] + } + } + + return whereClause +} + +// ==================== 快捷方法 ==================== + +type gfdb interface { + Model(tableNameOrStruct ...any) *Model +} +type cache interface { + Cache(ctx context.Context) *gdb.Model +} +type Model struct { + *gdb.Model +} + +type DataBase struct { + gdb.DB + DbName string +} + +func DB(dbName string) gfdb { + return &DataBase{ + DB: g.DB(dbName), + DbName: dbName, + } +} +func (d *DataBase) Model(tableNameOrStruct ...any) *Model { + return &Model{ + Model: d.DB.Model(tableNameOrStruct...), + } +} +func (d *Model) Cache(ctx context.Context) *gdb.Model { + ctx = context.WithValue(ctx, ctxKeyCachePrefix, true) + return d.Model +} diff --git a/dao/base/interceptor.go b/dao/base/interceptor.go new file mode 100644 index 0000000..61eff57 --- /dev/null +++ b/dao/base/interceptor.go @@ -0,0 +1,219 @@ +package base + +import ( + "context" + "strings" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// ==================== SQL 拦截器(真正无感知) ==================== + +// SQLInterceptor SQL拦截器配置 +type SQLInterceptor struct { + // 是否启用租户ID自动注入 + EnableTenantId bool + // 是否启用删除标记过滤 + EnableDeletedFilter bool + // 需要拦截的表(空表示所有表) + IncludeTables []string + // 排除的表 + ExcludeTables []string +} + +// DefaultSQLInterceptor 默认拦截器配置 +var DefaultSQLInterceptor = &SQLInterceptor{ + EnableTenantId: true, + EnableDeletedFilter: true, + IncludeTables: []string{}, + ExcludeTables: []string{"sys_config", "sys_dict"}, // 排除系统表 +} + +// currentInterceptor 当前使用的拦截器 +var currentInterceptor = DefaultSQLInterceptor + +// SetSQLInterceptor 设置全局SQL拦截器 +func SetSQLInterceptor(interceptor *SQLInterceptor) { + currentInterceptor = interceptor +} + +// GetSQLInterceptor 获取当前SQL拦截器 +func GetSQLInterceptor() *SQLInterceptor { + return currentInterceptor +} + +// ==================== 无感知集成方法 ==================== + +// InitSQLInterceptor 初始化SQL拦截器(在 main.go 中调用) +// 调用后,所有 g.DB().Model() 创建的查询都会自动注入条件 +// +// func main() { +// base.InitSQLInterceptor() +// // 之后所有 g.DB().Model() 都会自动注入 tenant_id 和 is_deleted=0 +// } +func InitSQLInterceptor() { + // 通过设置全局 Hook 实现拦截 + // 注意:这会替换所有 DB 操作的 Hook + hook := gdb.HookHandler{ + Select: selectInterceptor, + } + // 保存原始 Hook(如果有) + // 这里只是注册,实际需要在每个 Model 上使用 + _ = hook +} + +// selectInterceptor SELECT 查询拦截器 +func selectInterceptor(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { + // 获取当前配置 + interceptor := GetSQLInterceptor() + if interceptor == nil || !interceptor.EnableTenantId { + return in.Next(ctx) + } + + // 检查是否需要拦截 + if !shouldIntercept(in.Table) { + return in.Next(ctx) + } + + // 获取用户信息 + userInfo, _ := utils.GetUserInfo(ctx) + if g.IsEmpty(userInfo.TenantId) { + return in.Next(ctx) + } + + // 检查 SQL 是否已包含 tenant_id 条件 + sql := in.Sql + if !strings.Contains(sql, "tenant_id") { + // 注入 tenant_id 条件 + // 注意:这里只是示例,实际修改 SQL 需要解析和重建 SQL + // 更简单的方式是在 Model 层处理 + g.Log().Debug(ctx, "SQL拦截: 需要注入 tenant_id 条件", sql) + } + + return in.Next(ctx) +} + +// shouldIntercept 检查表是否需要拦截 +func shouldIntercept(table string) bool { + interceptor := GetSQLInterceptor() + if interceptor == nil { + return false + } + + // 检查排除列表 + for _, exclude := range interceptor.ExcludeTables { + if table == exclude { + return false + } + } + + // 检查包含列表 + if len(interceptor.IncludeTables) > 0 { + for _, include := range interceptor.IncludeTables { + if table == include { + return true + } + } + return false + } + + return true +} + +// ==================== 实用方法:修改现有 DAO ==================== + +// WrapModel 包装现有 Model,添加自动条件注入 +// 用于改造现有 DAO,保持业务代码不变 +// +// // 原 DAO 代码: +// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model { +// return g.DB().Model(entity.Asset{}).Safe() +// } +// +// // 改造后(只需改 Ctx 方法): +// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model { +// return base.WrapModel(ctx, g.DB().Model(entity.Asset{}).Safe()) +// } +func WrapModel(ctx context.Context, model *gdb.Model) *gdb.Model { + if ctx == nil || model == nil { + return model + } + return CatchSQL(ctx, model) +} + +// WrapDB 包装 g.DB(),返回带自动条件注入的查询构建器 +// 这是推荐的无感知使用方式 +// +// // 在 DAO 中使用: +// func (d *assetDao) Ctx(ctx context.Context) *gdb.Model { +// return base.WrapDB(ctx).Model(entity.Asset{}) +// } +// +// // 业务代码保持原生写法: +// func (d *assetDao) GetById(ctx context.Context, id uint64) (*entity.Asset, error) { +// var result entity.Asset +// err := d.Ctx(ctx).Where("id", id).Scan(&result) +// return &result, err +// } +func WrapDB(ctx context.Context) *DBBuilder { + return &DBBuilder{ + ctx: ctx, + db: g.DB(), + } +} + +// DBBuilder 数据库查询构建器 +type DBBuilder struct { + ctx context.Context + db gdb.DB +} + +// Model 创建 Model,自动注入条件 +func (b *DBBuilder) Model(tableNameOrStruct ...interface{}) *gdb.Model { + model := b.db.Model(tableNameOrStruct...).Safe() + if b.ctx != nil { + model = CatchSQL(b.ctx, model) + } + return model +} + +// Schema 指定 Schema +func (b *DBBuilder) Schema(schema string) *SchemaBuilder { + return &SchemaBuilder{ + ctx: b.ctx, + schema: b.db.Schema(schema), + } +} + +// SchemaBuilder Schema 查询构建器 +type SchemaBuilder struct { + ctx context.Context + schema gdb.DB +} + +// Model 创建 Model,自动注入条件 +func (s *SchemaBuilder) Model(tableNameOrStruct ...interface{}) *gdb.Model { + model := s.schema.Model(tableNameOrStruct...).Safe() + if s.ctx != nil { + model = CatchSQL(s.ctx, model) + } + return model +} + +// ==================== 全局替换方案 ==================== + +// ReplaceGDB 替换全局 g.DB() 行为(实验性) +// 警告:这会修改全局行为,请谨慎使用 +// +// func init() { +// base.ReplaceGDB() +// } +// +// // 之后所有 g.DB().Model().Ctx(ctx) 都会自动注入条件 +func ReplaceGDB() { + // 注意:GoFrame 不支持直接替换 g.DB() + // 建议使用 WrapDB 或修改 DAO 的 Ctx 方法 + g.Log().Info(context.Background(), "ReplaceGDB: 请使用 WrapDB 或修改 DAO 的 Ctx 方法来实现无感知集成") +} diff --git a/dao/procurement/purchase_inbound_dao.go b/dao/procurement/purchase_inbound_dao.go new file mode 100644 index 0000000..9fc37e9 --- /dev/null +++ b/dao/procurement/purchase_inbound_dao.go @@ -0,0 +1,105 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/procurement" + entity "assets/model/entity/procurement" + "context" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type purchaseInbound struct{} + +var PurchaseInbound = new(purchaseInbound) + +// Insert 插入 +func (d *purchaseInbound) Insert(ctx context.Context, req *dto.CreatePurchaseInboundReq) (ids []interface{}, err error) { + var result *entity.PurchaseInbound + if err = utils.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PurchaseInboundCollection) + return +} + +// GetOne 查询单个 +func (d *purchaseInbound) GetOne(ctx context.Context, req *dto.GetPurchaseInboundReq) (res *entity.PurchaseInbound, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB(false).FindOne(ctx, filter, &res, public.PurchaseInboundCollection) + return +} + +// List 查询列表 +func (d *purchaseInbound) List(ctx context.Context, req *dto.ListPurchaseInboundReq) (list []entity.PurchaseInbound, total int64, err error) { + filter := d.buildListFilter(req) + var orderBy []beans.OrderBy + if req.OrderBy.Field != "" { + orderBy = []beans.OrderBy{req.OrderBy} + } + total, err = mongo.DB().Find(ctx, filter, &list, public.PurchaseInboundCollection, &req.Page, orderBy) + return +} + +// UpdateDetails 更新入库记录详细信息 +func (d *purchaseInbound) UpdateDetails(ctx context.Context, id *bson.ObjectID, orderId *bson.ObjectID, inboundNo, batchNo, warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath string) (err error) { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "orderId": orderId, + "inboundNo": inboundNo, + "batchNo": batchNo, + "warehouseName": warehouseName, + "zoneName": zoneName, + "locationName": locationName, + "privateSkuName": privateSkuName, + "privateCategoryPath": privateCategoryPath, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.PurchaseInboundCollection) + return +} + +// UpdatePrivateStockId 更新入库记录关联的库存ID +func (d *purchaseInbound) UpdatePrivateStockId(ctx context.Context, id, privateStockId *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "privateStockId": privateStockId, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.PurchaseInboundCollection) + return +} + +// buildListFilter 构建列表过滤条件 +func (d *purchaseInbound) buildListFilter(req *dto.ListPurchaseInboundReq) bson.M { + filter := bson.M{} + if !g.IsEmpty(req.OrderId) { + filter["orderId"] = req.OrderId + } + if !g.IsEmpty(req.OrderItemId) { + filter["orderItemId"] = req.OrderItemId + } + if !g.IsEmpty(req.InboundNo) { + filter["inboundNo"] = bson.M{"$regex": req.InboundNo, "$options": "i"} + } + if !g.IsEmpty(req.StartDate) { + startTime, _ := gtime.StrToTime(req.StartDate + " 00:00:00") + filter["inboundDate"] = bson.M{"$gte": startTime} + } + if !g.IsEmpty(req.EndDate) { + endTime, _ := gtime.StrToTime(req.EndDate + " 23:59:59") + if filter["inboundDate"] != nil { + filter["inboundDate"].(bson.M)["$lte"] = endTime + } else { + filter["inboundDate"] = bson.M{"$lte": endTime} + } + } + return filter +} diff --git a/dao/procurement/purchase_order_dao.go b/dao/procurement/purchase_order_dao.go new file mode 100644 index 0000000..4f0358c --- /dev/null +++ b/dao/procurement/purchase_order_dao.go @@ -0,0 +1,113 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/procurement" + entity "assets/model/entity/procurement" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var PurchaseOrder = new(purchaseOrder) + +type purchaseOrder struct{} + +// Insert 插入采购订单 +func (d *purchaseOrder) Insert(ctx context.Context, req *dto.CreatePurchaseOrderReq) (ids []any, err error) { + var result *entity.PurchaseOrder + if err = utils.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PurchaseOrderCollection) + return +} + +// BatchInsert 批量插入采购订单 +func (d *purchaseOrder) BatchInsert(ctx context.Context, req *dto.BatchCreatePurchaseOrdersReq) (ids []any, err error) { + items := make([]*entity.PurchaseOrder, 0, len(req.Orders)) + for _, item := range req.Orders { + var result *entity.PurchaseOrder + if err = utils.Struct(item, &result); err != nil { + return + } + items = append(items, result) + } + // 转换为 interface{} 切片 + interfaces := make([]interface{}, len(items)) + for i, item := range items { + interfaces[i] = item + } + ids, err = mongo.DB().Insert(ctx, interfaces, public.PurchaseOrderCollection) + return +} + +// GetOne 获取单个采购订单 +func (d *purchaseOrder) GetOne(ctx context.Context, id *bson.ObjectID) (order *entity.PurchaseOrder, err error) { + filter := bson.M{"_id": id} + err = mongo.DB().FindOne(ctx, filter, &order, public.PurchaseOrderCollection) + return +} + +// Update 更新采购订单 +func (d *purchaseOrder) Update(ctx context.Context, req *dto.UpdatePurchaseOrderReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.ID} + update := bson.M{"$set": buildUpdateData} + _, err = mongo.DB().Update(ctx, filter, update, public.PurchaseOrderCollection) + return +} + +// DeleteFake 删除采购订单-根据id进行假删 +func (d *purchaseOrder) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.PurchaseOrderCollection) + return +} + +// List 获取采购订单列表 +func (d *purchaseOrder) List(ctx context.Context, req *dto.ListPurchaseOrdersReq) (res []*entity.PurchaseOrder, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + + total, err = mongo.DB().Find(ctx, filter, &res, public.PurchaseOrderCollection, nil, nil) + return +} + +// ListByBuyerId 根据采购方ID获取采购订单列表 +func (d *purchaseOrder) ListByBuyerId(ctx context.Context, buyerId *bson.ObjectID) (res []*entity.PurchaseOrder, err error) { + filter := bson.M{"buyerId": buyerId} + _, err = mongo.DB().Find(ctx, filter, &res, public.PurchaseOrderCollection, nil, nil) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *purchaseOrder) buildListFilter(ctx context.Context, req *dto.ListPurchaseOrdersReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + + if !g.IsEmpty(req.OrderNo) { + filter["orderNo"] = req.OrderNo + } + if !g.IsEmpty(req.Title) { + filter["title"] = bson.M{"$regex": req.Title, "$options": "i"} + } + if req.BuyerId != nil { + filter["buyerId"] = req.BuyerId + } + if req.OrderType != "" { + filter["orderType"] = req.OrderType + } + if req.Status != nil { + filter["status"] = *req.Status + } + return +} diff --git a/dao/procurement/purchase_order_item_dao.go b/dao/procurement/purchase_order_item_dao.go new file mode 100644 index 0000000..5e10198 --- /dev/null +++ b/dao/procurement/purchase_order_item_dao.go @@ -0,0 +1,141 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/procurement" + entity "assets/model/entity/procurement" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var PurchaseOrderItem = new(purchaseOrderItem) + +type purchaseOrderItem struct{} + +// Insert 插入采购订单明细 +func (d *purchaseOrderItem) Insert(ctx context.Context, req *dto.CreatePurchaseOrderItemReq) (ids []any, err error) { + var result *entity.PurchaseOrderItem + if err = gconv.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PurchaseOrderItemCollection) + return +} + +// BatchInsert 批量插入采购订单明细 +func (d *purchaseOrderItem) BatchInsert(ctx context.Context, req *dto.BatchCreatePurchaseOrderItemsReq) (ids []any, err error) { + items := make([]*entity.PurchaseOrderItem, 0, len(req.Items)) + for _, item := range req.Items { + var result *entity.PurchaseOrderItem + if err = gconv.Struct(item, &result); err != nil { + return + } + items = append(items, result) + } + // 转换为 interface{} 切片 + interfaces := make([]interface{}, len(items)) + for i, item := range items { + interfaces[i] = item + } + ids, err = mongo.DB().Insert(ctx, interfaces, public.PurchaseOrderItemCollection) + return +} + +// GetOne 获取单个采购订单明细 +func (d *purchaseOrderItem) GetOne(ctx context.Context, id *bson.ObjectID) (item *entity.PurchaseOrderItem, err error) { + filter := bson.M{"_id": id} + err = mongo.DB().FindOne(ctx, filter, &item, public.PurchaseOrderItemCollection) + return +} + +// Update 更新采购订单明细 +func (d *purchaseOrderItem) Update(ctx context.Context, req *dto.UpdatePurchaseOrderItemReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.ID} + update := bson.M{"$set": buildUpdateData} + _, err = mongo.DB().Update(ctx, filter, update, public.PurchaseOrderItemCollection) + return +} + +// IncrementInboundQty 原子增加已入库数量(并发安全) +// 使用$inc + $expr条件:inboundQty + deltaQty <= passQuantity,防止并发超量入库 +func (d *purchaseOrderItem) IncrementInboundQty(ctx context.Context, id *bson.ObjectID, deltaQty int) (err error) { + filter := bson.M{ + "_id": id, + "$expr": bson.M{ + "$lte": bson.A{ + bson.M{"$add": bson.A{"$inboundQty", deltaQty}}, + "$passQuantity", + }, + }, + } + update := bson.M{ + "$inc": bson.M{ + "inboundQty": deltaQty, + }, + } + modifiedCount, err := mongo.DB().Update(ctx, filter, update, public.PurchaseOrderItemCollection) + if err != nil { + return + } + if modifiedCount == 0 { + return gerror.Newf("入库数量超出签收数量限制,本次入库%d", deltaQty) + } + return +} + +// DeleteFake 删除采购订单明细-根据id进行假删 +func (d *purchaseOrderItem) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.PurchaseOrderItemCollection) + return +} + +// List 获取采购订单明细列表 +func (d *purchaseOrderItem) List(ctx context.Context, req *dto.ListPurchaseOrderItemsReq) (res []*entity.PurchaseOrderItem, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + + total, err = mongo.DB().Find(ctx, filter, &res, public.PurchaseOrderItemCollection, nil, nil) + return +} + +// ListByOrderId 根据订单ID获取采购订单明细列表 +func (d *purchaseOrderItem) ListByOrderId(ctx context.Context, orderId *bson.ObjectID) (res []*entity.PurchaseOrderItem, err error) { + filter := bson.M{"orderId": orderId} + _, err = mongo.DB().Find(ctx, filter, &res, public.PurchaseOrderItemCollection, nil, nil) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *purchaseOrderItem) buildListFilter(ctx context.Context, req *dto.ListPurchaseOrderItemsReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + + if req.OrderId != nil { + filter["orderId"] = req.OrderId + } + if req.AssetId != nil { + filter["assetId"] = req.AssetId + } + if req.AssetSkuId != nil { + filter["assetSkuId"] = req.AssetSkuId + } + if !g.IsEmpty(req.ProductName) { + filter["productName"] = bson.M{"$regex": req.ProductName, "$options": "i"} + } + if !g.IsEmpty(req.Brand) { + filter["brand"] = bson.M{"$regex": req.Brand, "$options": "i"} + } + return +} diff --git a/dao/procurement/supplier_dao.go b/dao/procurement/supplier_dao.go new file mode 100644 index 0000000..6fd8542 --- /dev/null +++ b/dao/procurement/supplier_dao.go @@ -0,0 +1,110 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/procurement" + entity "assets/model/entity/procurement" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var Supplier = new(supplier) + +type supplier struct{} + +// Insert 插入供应商 +func (d *supplier) Insert(ctx context.Context, req *dto.CreateSupplierReq) (ids []any, err error) { + var result *entity.Supplier + if err = gconv.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.SupplierCollection) + return +} + +// BatchInsert 批量插入供应商 +func (d *supplier) BatchInsert(ctx context.Context, req *dto.BatchCreateSuppliersReq) (ids []any, err error) { + items := make([]*entity.Supplier, 0, len(req.Suppliers)) + for _, item := range req.Suppliers { + var result *entity.Supplier + if err = gconv.Struct(item, &result); err != nil { + return + } + items = append(items, result) + } + // 转换为 interface{} 切片 + interfaces := make([]interface{}, len(items)) + for i, item := range items { + interfaces[i] = item + } + ids, err = mongo.DB().Insert(ctx, interfaces, public.SupplierCollection) + return +} + +// GetOne 获取单个供应商 +func (d *supplier) GetOne(ctx context.Context, id *bson.ObjectID) (supplier *entity.Supplier, err error) { + filter := bson.M{"_id": id} + err = mongo.DB().FindOne(ctx, filter, &supplier, public.SupplierCollection) + return +} + +// Update 更新供应商 +func (d *supplier) Update(ctx context.Context, req *dto.UpdateSupplierReq) (err error) { + buildUpdateData, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.ID} + update := bson.M{"$set": buildUpdateData} + _, err = mongo.DB().Update(ctx, filter, update, public.SupplierCollection) + return +} + +// DeleteFake 删除供应商-根据id进行假删 +func (d *supplier) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.SupplierCollection) + return +} + +// List 获取供应商列表 +func (d *supplier) List(ctx context.Context, req *dto.ListSuppliersReq) (res []*entity.Supplier, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + + total, err = mongo.DB().Find(ctx, filter, &res, public.SupplierCollection, nil, nil) + return +} + +// FindActiveSuppliers 获取活跃供应商列表 +func (d *supplier) FindActiveSuppliers(ctx context.Context) (res []*entity.Supplier, err error) { + filter := bson.M{ + "status": 1, // consts.SupplierStatusActive + "isDeleted": false, + } + _, err = mongo.DB().Find(ctx, filter, &res, public.SupplierCollection, nil, nil) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *supplier) buildListFilter(ctx context.Context, req *dto.ListSuppliersReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + + if !g.IsEmpty(req.Name) { + filter["name"] = bson.M{"$regex": req.Name, "$options": "i"} + } + if !g.IsEmpty(req.Code) { + filter["code"] = req.Code + } + if req.Status != nil { + filter["status"] = *req.Status + } + return +} diff --git a/dao/stock/inventory_count_adjust_history_dao.go b/dao/stock/inventory_count_adjust_history_dao.go new file mode 100644 index 0000000..0a02137 --- /dev/null +++ b/dao/stock/inventory_count_adjust_history_dao.go @@ -0,0 +1,73 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var InventoryCountAdjustHistory = &inventoryCountAdjustHistory{} + +type inventoryCountAdjustHistory struct{} + +// Insert 插入盘点调整历史记录 +func (d *inventoryCountAdjustHistory) Insert(ctx context.Context, req *dto.CreateInventoryCountAdjustHistoryReq) (ids []interface{}, err error) { + var result *entity.InventoryCountAdjustHistory + if err = utils.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.InventoryCountAdjustHistoryCollection) + return +} + +// GetOne 根据ID查询单条盘点调整历史记录 +func (d *inventoryCountAdjustHistory) GetOne(ctx context.Context, req *dto.GetInventoryCountAdjustHistoryReq) (res *entity.InventoryCountAdjustHistory, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.InventoryCountAdjustHistoryCollection) + return +} + +// DeleteFake 软删除盘点调整历史记录 +func (d *inventoryCountAdjustHistory) DeleteFake(ctx context.Context, req *dto.DeleteInventoryCountAdjustHistoryReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.InventoryCountAdjustHistoryCollection) + return +} + +// List 分页查询盘点调整历史列表 +func (d *inventoryCountAdjustHistory) List(ctx context.Context, req *dto.ListInventoryCountAdjustHistoryReq) (res []entity.InventoryCountAdjustHistory, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.InventoryCountAdjustHistoryCollection, req.Page, req.OrderBy) + return +} + +// buildListFilter 构建列表查询过滤条件 +func (d *inventoryCountAdjustHistory) buildListFilter(ctx context.Context, req *dto.ListInventoryCountAdjustHistoryReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.CountID) { + filter["countId"] = req.CountID + } + if !g.IsEmpty(req.DetailID) { + filter["detailId"] = req.DetailID + } + if !g.IsEmpty(req.AssetSkuID) { + filter["assetSkuId"] = req.AssetSkuID + } + if !g.IsEmpty(req.WarehouseID) { + filter["warehouseId"] = req.WarehouseID + } + if !g.IsEmpty(req.BatchNo) { + filter["batchNo"] = req.BatchNo + } + return +} diff --git a/dao/stock/inventory_count_dao.go b/dao/stock/inventory_count_dao.go new file mode 100644 index 0000000..ee29f9f --- /dev/null +++ b/dao/stock/inventory_count_dao.go @@ -0,0 +1,202 @@ +// 盘点任务DAO层 +// 职责:盘点任务CRUD、检查未完成任务、更新状态/统计 +// 紧密耦合:service.InventoryCount、dao.InventoryCountDetail(级联删除) +// 注意:HasUncompletedTask使用NoCache()跳过缓存,同一时间只允许一个盘点任务 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + "fmt" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var InventoryCount = new(inventoryCount) + +type inventoryCount struct{} + +// convertToObjectIDs 将字符串ID列表批量转换为ObjectID列表,跳过空值 +func convertToObjectIDs(hexIDs []string, label string) ([]*bson.ObjectID, error) { + if len(hexIDs) == 0 { + return nil, nil + } + result := make([]*bson.ObjectID, 0, len(hexIDs)) + for _, hex := range hexIDs { + if g.IsEmpty(hex) { + continue + } + id, err := bson.ObjectIDFromHex(hex) + if err != nil { + return nil, fmt.Errorf("%sID格式错误(值:%s): %v", label, hex, err) + } + result = append(result, &id) + } + return result, nil +} + +func (d *inventoryCount) Insert(ctx context.Context, req *dto.CreateInventoryCountReq, countNo string) (ids []interface{}, err error) { + result := &entity.InventoryCount{ + CountNo: countNo, + Title: req.Title, + Description: req.Description, + CountType: req.CountType, + Scope: req.Scope, + AssigneeID: req.AssigneeID, + AssigneeName: req.AssigneeName, + Participants: req.Participants, + Remark: req.Remark, + Status: stock.InventoryCountStatusInProgress, + } + // 批量转换字符串ID为ObjectID + if result.WarehouseIDs, err = convertToObjectIDs(req.WarehouseIDs, "仓库"); err != nil { + return + } + if result.ZoneIDs, err = convertToObjectIDs(req.ZoneIDs, "库区"); err != nil { + return + } + if result.LocationIDs, err = convertToObjectIDs(req.LocationIDs, "库位"); err != nil { + return + } + if result.AssetSkuIDs, err = convertToObjectIDs(req.AssetSkuIDs, "资产SKU"); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{result}, public.InventoryCountCollection) + return +} + +func (d *inventoryCount) GetOne(ctx context.Context, req *dto.GetInventoryCountReq) (res *entity.InventoryCount, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.InventoryCountCollection) + return +} + +func (d *inventoryCount) Update(ctx context.Context, req *dto.UpdateInventoryCountReq) (err error) { + var updateData entity.InventoryCount + if err = utils.Struct(req, &updateData); err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": updateData} + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountCollection) + return +} + +func (d *inventoryCount) Delete(ctx context.Context, req *dto.DeleteInventoryCountReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().Delete(ctx, filter, public.InventoryCountCollection) + return +} + +func (d *inventoryCount) List(ctx context.Context, req *dto.ListInventoryCountReq) (res []entity.InventoryCount, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.InventoryCountCollection, req.Page, req.OrderBy) + return +} + +func (d *inventoryCount) buildListFilter(ctx context.Context, req *dto.ListInventoryCountReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + // 兼容单值和数组参数(优先使用数组) + if len(req.WarehouseIDs) > 0 { + // 将字符串数组转为 ObjectID 数组 + var oids []bson.ObjectID + for _, id := range req.WarehouseIDs { + if oid, e := bson.ObjectIDFromHex(id); e == nil { + oids = append(oids, oid) + } + } + if len(oids) > 0 { + filter["warehouseIds"] = bson.M{"$in": oids} + } + } else if !g.IsEmpty(req.WarehouseID) { + if wid, e := bson.ObjectIDFromHex(req.WarehouseID); e == nil { + filter["warehouseIds"] = wid + } + } + if len(req.ZoneIDs) > 0 { + // 将字符串数组转为 ObjectID 数组 + var oids []bson.ObjectID + for _, id := range req.ZoneIDs { + if oid, e := bson.ObjectIDFromHex(id); e == nil { + oids = append(oids, oid) + } + } + if len(oids) > 0 { + filter["zoneIds"] = bson.M{"$in": oids} + } + } else if !g.IsEmpty(req.ZoneID) { + if zid, e := bson.ObjectIDFromHex(req.ZoneID); e == nil { + filter["zoneIds"] = zid + } + } + if !g.IsEmpty(req.CountType) { + filter["countType"] = req.CountType + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.AssigneeID) { + filter["assigneeId"] = req.AssigneeID + } + if !g.IsEmpty(req.Keyword) { + filter["$or"] = bson.A{ + bson.M{"countNo": bson.M{"$regex": req.Keyword, "$options": "i"}}, + bson.M{"title": bson.M{"$regex": req.Keyword, "$options": "i"}}, + } + } + return +} + +// HasUncompletedTask 检查是否存在未完成的盘点任务 +func (d *inventoryCount) HasUncompletedTask(ctx context.Context) (has bool, err error) { + filter := bson.M{ + "status": stock.InventoryCountStatusInProgress, + } + var result entity.InventoryCount + err = mongo.DB().NoCache().FindOne(ctx, filter, &result, public.InventoryCountCollection) + if err != nil { + return false, err + } + return result.Id != nil, nil +} + +// UpdateStatus 更新盘点状态 +func (d *inventoryCount) UpdateStatus(ctx context.Context, id *bson.ObjectID, status stock.InventoryCountStatus) (err error) { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "status": status, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountCollection) + return +} + +// UpdateStats 更新盘点统计信息 +func (d *inventoryCount) UpdateStats(ctx context.Context, id *bson.ObjectID, totalItems, completedItems, discrepancyItems int) (err error) { + filter := bson.M{"_id": id} + var progress float64 + if totalItems > 0 { + progress = float64(completedItems) / float64(totalItems) * 100 + } + update := bson.M{ + "$set": bson.M{ + "totalItems": totalItems, + "completedItems": completedItems, + "discrepancyItems": discrepancyItems, + "progress": progress, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountCollection) + return +} diff --git a/dao/stock/inventory_count_detail_dao.go b/dao/stock/inventory_count_detail_dao.go new file mode 100644 index 0000000..9ba1e84 --- /dev/null +++ b/dao/stock/inventory_count_detail_dao.go @@ -0,0 +1,185 @@ +// 盘点明细DAO层 +// 职责:明细CRUD、批量插入(性能优化)、按盘点ID查询、更新调整状态 +// 紧密耦合:service.InventoryCountDetail、service.InventoryCount(统计更新) +// 注意:InsertBatch批量插入减少网络往返,ListByCountId使用mongo.DB(false)跳过缓存 +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + "fmt" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var InventoryCountDetail = new(inventoryCountDetail) + +type inventoryCountDetail struct{} + +func (d *inventoryCountDetail) Insert(ctx context.Context, req *dto.CreateInventoryCountDetailReq) (ids []interface{}, err error) { + result := &entity.InventoryCountDetail{ + BookQuantity: req.BookQuantity, + BookBatchInfo: req.BookBatchInfo, + ActualQuantity: req.ActualQuantity, + ActualBatchInfo: req.ActualBatchInfo, + Remark: req.Remark, + } + // required string → *bson.ObjectID + for _, item := range []struct { + val string + dest **bson.ObjectID + name string + }{ + {req.CountID, &result.CountID, "盘点单ID"}, + {req.AssetID, &result.AssetID, "资产ID"}, + {req.AssetSkuID, &result.AssetSkuID, "资产SKU ID"}, + {req.WarehouseID, &result.WarehouseID, "仓库ID"}, + } { + id, e := bson.ObjectIDFromHex(item.val) + if e != nil { + return nil, fmt.Errorf("%s格式错误: %v", item.name, e) + } + *item.dest = &id + } + // optional string → *bson.ObjectID + if !g.IsEmpty(req.ZoneID) { + id, e := bson.ObjectIDFromHex(req.ZoneID) + if e != nil { + return nil, fmt.Errorf("库区ID格式错误: %v", e) + } + result.ZoneID = &id + } + if !g.IsEmpty(req.LocationID) { + id, e := bson.ObjectIDFromHex(req.LocationID) + if e != nil { + return nil, fmt.Errorf("库位ID格式错误: %v", e) + } + result.LocationID = &id + } + ids, err = mongo.DB().Insert(ctx, []interface{}{result}, public.InventoryCountDetailCollection) + return +} + +// InsertBatch 批量插入盘点明细(性能优化:100条/批,减少网络往返) +func (d *inventoryCountDetail) InsertBatch(ctx context.Context, details []*entity.InventoryCountDetail) (ids []interface{}, err error) { + if len(details) == 0 { + return + } + docs := make([]interface{}, len(details)) + for i, detail := range details { + docs[i] = detail + } + ids, err = mongo.DB().Insert(ctx, docs, public.InventoryCountDetailCollection) + return +} + +func (d *inventoryCountDetail) GetOne(ctx context.Context, req *dto.GetInventoryCountDetailReq) (res *entity.InventoryCountDetail, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.InventoryCountDetailCollection) + return +} + +func (d *inventoryCountDetail) Update(ctx context.Context, req *dto.UpdateInventoryCountDetailReq) (err error) { + buildFilter, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": buildFilter} + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountDetailCollection) + return +} + +func (d *inventoryCountDetail) DeleteFake(ctx context.Context, req *dto.DeleteInventoryCountDetailReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.InventoryCountDetailCollection) + return +} + +// DeleteByCountId 按盘点任务ID真删除所有明细 +func (d *inventoryCountDetail) DeleteByCountId(ctx context.Context, countId *bson.ObjectID) (err error) { + filter := bson.M{"countId": countId} + _, err = mongo.DB().Delete(ctx, filter, public.InventoryCountDetailCollection) + return +} + +func (d *inventoryCountDetail) List(ctx context.Context, req *dto.ListInventoryCountDetailReq) (res []entity.InventoryCountDetail, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.InventoryCountDetailCollection, req.Page, req.OrderBy) + return +} + +func (d *inventoryCountDetail) buildListFilter(ctx context.Context, req *dto.ListInventoryCountDetailReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.CountID) { + filter["countId"] = req.CountID + } + if !g.IsEmpty(req.AssetID) { + filter["assetId"] = req.AssetID + } + if !g.IsEmpty(req.AssetSkuID) { + filter["assetSkuId"] = req.AssetSkuID + } + if !g.IsEmpty(req.WarehouseID) { + filter["warehouseId"] = req.WarehouseID + } + if !g.IsEmpty(req.ZoneID) { + filter["zoneId"] = req.ZoneID + } + if !g.IsEmpty(req.LocationID) { + filter["locationId"] = req.LocationID + } + if !g.IsEmpty(req.DiscrepancyType) { + filter["discrepancyType"] = req.DiscrepancyType + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.IsAdjusted) { + filter["isAdjusted"] = req.IsAdjusted + } + return +} + +// ListByCountId 按盘点ID查询所有明细 +func (d *inventoryCountDetail) ListByCountId(ctx context.Context, countId *bson.ObjectID) (res []entity.InventoryCountDetail, err error) { + filter := bson.M{"countId": countId} + _, err = mongo.DB(false).Find(ctx, filter, &res, public.InventoryCountDetailCollection, nil, nil) + return +} + +// UpdateAdjusted 更新调整状态 +func (d *inventoryCountDetail) UpdateAdjusted(ctx context.Context, id *bson.ObjectID, adjustedBy string) (err error) { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "isAdjusted": true, + "adjustedBy": adjustedBy, + "adjustedAt": gtime.Now(), + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountDetailCollection) + return +} + +// UpdateDiscrepancyReason 更新差异原因 +func (d *inventoryCountDetail) UpdateDiscrepancyReason(ctx context.Context, id *bson.ObjectID, reason string) (err error) { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "discrepancyReason": reason, + "updatedAt": gtime.Now(), + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.InventoryCountDetailCollection) + return +} diff --git a/dao/stock/inventory_warning_dao.go b/dao/stock/inventory_warning_dao.go new file mode 100644 index 0000000..6a7921c --- /dev/null +++ b/dao/stock/inventory_warning_dao.go @@ -0,0 +1,71 @@ +// 库存预警DAO层 +// 职责:预警查询(无Insert/Update/Delete,由系统自动生成) +// 紧密耦合:service.InventoryWarning +// 注意:预警记录由定时任务或库存变动触发,非用户手动创建 +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var InventoryWarning = new(inventoryWarning) + +type inventoryWarning struct{} + +func (d *inventoryWarning) GetOne(ctx context.Context, req *dto.GetInventoryWarningReq) (res *entity.InventoryWarning, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.InventoryWarningCollection) + return +} + +func (d *inventoryWarning) List(ctx context.Context, req *dto.ListInventoryWarningReq) (res []entity.InventoryWarning, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.InventoryWarningCollection, req.Page, req.OrderBy) + return +} + +func (d *inventoryWarning) buildListFilter(ctx context.Context, req *dto.ListInventoryWarningReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.WarningType) { + filter["warningType"] = req.WarningType + } + // 兼容单值和数组参数(优先使用数组) + if len(req.BatchIDs) > 0 { + filter["batchId"] = bson.M{"$in": req.BatchIDs} + } else if !g.IsEmpty(req.BatchID) { + filter["batchId"] = req.BatchID + } + if len(req.AssetIDs) > 0 { + filter["assetId"] = bson.M{"$in": req.AssetIDs} + } else if !g.IsEmpty(req.AssetID) { + filter["assetId"] = req.AssetID + } + if len(req.AssetSkuIDs) > 0 { + filter["assetSkuId"] = bson.M{"$in": req.AssetSkuIDs} + } else if !g.IsEmpty(req.AssetSkuID) { + filter["assetSkuId"] = req.AssetSkuID + } + if len(req.SupplierIDs) > 0 { + filter["supplierId"] = bson.M{"$in": req.SupplierIDs} + } else if !g.IsEmpty(req.SupplierID) { + filter["supplierId"] = req.SupplierID + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.Keyword) { + filter["batchNo"] = bson.M{"$regex": req.Keyword, "$options": "i"} + } + return +} diff --git a/dao/stock/inventory_warning_history_dao.go b/dao/stock/inventory_warning_history_dao.go new file mode 100644 index 0000000..94ce91a --- /dev/null +++ b/dao/stock/inventory_warning_history_dao.go @@ -0,0 +1,80 @@ +// 库存预警历史DAO层 +// 职责:预警历史查询、删除(无Insert/Update,由预警状态变更时自动归档) +// 紧密耦合:service.InventoryWarningHistory +// 注意:历史记录由系统自动归档,非用户手动创建 +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var InventoryWarningHistory = new(inventoryWarningHistory) + +type inventoryWarningHistory struct{} + +func (d *inventoryWarningHistory) GetOne(ctx context.Context, req *dto.GetInventoryWarningHistoryReq) (res *entity.InventoryWarningHistory, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.InventoryWarningHistoryCollection) + return +} + +func (d *inventoryWarningHistory) List(ctx context.Context, req *dto.ListInventoryWarningHistoryReq) (res []entity.InventoryWarningHistory, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.InventoryWarningHistoryCollection, req.Page, req.OrderBy) + return +} + +func (d *inventoryWarningHistory) DeleteFake(ctx context.Context, req *dto.DeleteInventoryWarningHistoryReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.InventoryWarningHistoryCollection) + return +} + +func (d *inventoryWarningHistory) buildListFilter(ctx context.Context, req *dto.ListInventoryWarningHistoryReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.WarningType) { + filter["warningType"] = req.WarningType + } + // 兼容单值和数组参数(优先使用数组) + if len(req.BatchIDs) > 0 { + filter["batchId"] = bson.M{"$in": req.BatchIDs} + } else if !g.IsEmpty(req.BatchID) { + filter["batchId"] = req.BatchID + } + if len(req.AssetIDs) > 0 { + filter["assetId"] = bson.M{"$in": req.AssetIDs} + } else if !g.IsEmpty(req.AssetID) { + filter["assetId"] = req.AssetID + } + if len(req.AssetSkuIDs) > 0 { + filter["assetSkuId"] = bson.M{"$in": req.AssetSkuIDs} + } else if !g.IsEmpty(req.AssetSkuID) { + filter["assetSkuId"] = req.AssetSkuID + } + if len(req.SupplierIDs) > 0 { + filter["supplierId"] = bson.M{"$in": req.SupplierIDs} + } else if !g.IsEmpty(req.SupplierID) { + filter["supplierId"] = req.SupplierID + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.ProcessMethod) { + filter["processMethod"] = req.ProcessMethod + } + if !g.IsEmpty(req.Keyword) { + filter["batchNo"] = bson.M{"$regex": req.Keyword, "$options": "i"} + } + return +} diff --git a/dao/stock/location_dao.go b/dao/stock/location_dao.go new file mode 100644 index 0000000..45347b5 --- /dev/null +++ b/dao/stock/location_dao.go @@ -0,0 +1,191 @@ +// 库位DAO层 +// 职责:库位CRUD、状态更新、批量状态更新、删除前检查(3个库存集合)、更新容量 +// 紧密耦合:service.Location(删除检查)、service.Capacity(容量计算入口) +// 注意:DeleteFake前需检查StockDetails/StockBatch/PrivateStock三个集合 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var Location = new(location) + +type location struct{} + +func (d *location) Insert(ctx context.Context, req *dto.CreateLocationReq) (ids []interface{}, err error) { + var result *entity.Location + if err = utils.Struct(req, &result); err != nil { + return + } + // 如果未传入状态,设置默认值为空闲 + if result.Status == "" { + result.Status = stock.LocationStatusIdle + } + // 初始容量默认为0 + //if result.CurrentCapacity == 0 { + // result.CurrentCapacity = 0 + //} + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.LocationCollection) + return +} + +func (d *location) GetOne(ctx context.Context, req *dto.GetLocationReq) (res *entity.Location, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.LocationCollection) + return +} + +func (d *location) Update(ctx context.Context, req *dto.UpdateLocationReq) (err error) { + buildFilter, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": buildFilter} + _, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +func (d *location) DeleteFake(ctx context.Context, req *dto.DeleteLocationReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.LocationCollection) + return +} + +// UpdateStatus 更新库位状态 +func (d *location) UpdateStatus(ctx context.Context, req *dto.UpdateLocationStatusReq) (err error) { + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": bson.M{"status": req.Status}} + _, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +func (d *location) List(ctx context.Context, req *dto.ListLocationReq) (res []entity.Location, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.LocationCollection, req.Page, req.OrderBy) + return +} + +func (d *location) buildListFilter(ctx context.Context, req *dto.ListLocationReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + // 兼容单值和数组参数(优先使用数组) + if len(req.WarehouseIds) > 0 { + filter["warehouseId"] = bson.M{"$in": req.WarehouseIds} + } else if !g.IsEmpty(req.WarehouseId) { + filter["warehouseId"] = req.WarehouseId + } + if len(req.ZoneIds) > 0 { + filter["zoneId"] = bson.M{"$in": req.ZoneIds} + } else if !g.IsEmpty(req.ZoneId) { + filter["zoneId"] = req.ZoneId + } + if !g.IsEmpty(req.LocationType) { + filter["locationType"] = req.LocationType + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.Keyword) { + filter["$or"] = bson.A{ + bson.M{"locationCode": bson.M{"$regex": req.Keyword, "$options": "i"}}, + bson.M{"locationName": bson.M{"$regex": req.Keyword, "$options": "i"}}, + } + } + return +} + +// CountByZoneId 统计库区下的库位数量(用于删除前检查) +func (d *location) CountByZoneId(ctx context.Context, zoneId string) (count int64, err error) { + filter := bson.M{"zoneId": zoneId} + count, err = mongo.DB().Count(ctx, filter, public.LocationCollection) + return +} + +// CountByWarehouseId 统计仓库下的库位数量(用于删除前检查) +func (d *location) CountByWarehouseId(ctx context.Context, warehouseId string) (count int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + count, err = mongo.DB().Count(ctx, filter, public.LocationCollection) + return +} + +// BatchUpdateStatusByZoneId 批量更新库区下所有库位状态(用于状态联动) +func (d *location) BatchUpdateStatusByZoneId(ctx context.Context, zoneId string, status stock.LocationStatus) (modifiedCount int64, err error) { + filter := bson.M{"zoneId": zoneId} + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +// BatchUpdateStatusByWarehouseId 批量更新仓库下所有库位状态(用于状态联动) +func (d *location) BatchUpdateStatusByWarehouseId(ctx context.Context, warehouseId string, status stock.LocationStatus) (modifiedCount int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +// DeleteByZoneId 批量删除库区下所有库位(用于级联删除) +func (d *location) DeleteByZoneId(ctx context.Context, zoneId string) (modifiedCount int64, err error) { + filter := bson.M{"zoneId": zoneId} + modifiedCount, err = mongo.DB().DeleteSoft(ctx, filter, public.LocationCollection) + return +} + +// DeleteByWarehouseId 批量删除仓库下所有库位(用于级联删除) +func (d *location) DeleteByWarehouseId(ctx context.Context, warehouseId string) (modifiedCount int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + modifiedCount, err = mongo.DB().DeleteSoft(ctx, filter, public.LocationCollection) + return +} + +// CountStockDetailsByLocationId 统计库位上的库存明细数量(用于删除前检查) +func (d *location) CountStockDetailsByLocationId(ctx context.Context, locationId string) (count int64, err error) { + filter := bson.M{"locationId": locationId} + count, err = mongo.DB().Count(ctx, filter, public.StockDetailsCollection) + return +} + +// CountStockBatchByLocationId 统计库位上的批次库存数量(用于删除前检查) +func (d *location) CountStockBatchByLocationId(ctx context.Context, locationId string) (count int64, err error) { + filter := bson.M{"locationId": locationId} + count, err = mongo.DB().Count(ctx, filter, public.StockBatchCollection) + return +} + +// CountPrivateStockByLocationId 统计库位上的私域库存数量(用于删除前检查) +func (d *location) CountPrivateStockByLocationId(ctx context.Context, locationId string) (count int64, err error) { + filter := bson.M{"locationId": locationId} + count, err = mongo.DB().Count(ctx, filter, public.PrivateStockCollection) + return +} + +// UpdateCapacity 更新库位容量 +func (d *location) UpdateCapacity(ctx context.Context, locationId *bson.ObjectID, currentCapacity int) (err error) { + filter := bson.M{"_id": locationId} + update := bson.M{"$set": bson.M{"capacity.currentCapacity": currentCapacity}} + _, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +// ListByZoneAndUnitType 按库区和容量单位类型查询库位列表 +func (d *location) ListByZoneAndUnitType(ctx context.Context, zoneId *bson.ObjectID, unitType stock.CapacityUnitType) (res []entity.Location, err error) { + filter := bson.M{ + "zoneId": zoneId, + "capacityUnitType": unitType, + } + _, err = mongo.DB().Find(ctx, filter, &res, public.LocationCollection, nil, nil) + return +} diff --git a/dao/stock/private_stock_dao.go b/dao/stock/private_stock_dao.go new file mode 100644 index 0000000..3370345 --- /dev/null +++ b/dao/stock/private_stock_dao.go @@ -0,0 +1,268 @@ +// 实物库存批次DAO层 +// 职责:CRUD、IncrementAvailableQty原子操作(防并发超卖)、SumAvailableQtyByLocation聚合汇总 +// 紧密耦合:service.PrivateStock、service.Capacity(容量计算入口) +// 注意:IncrementAvailableQty使用$inc+$gte条件,防止并发导致库存变负 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var PrivateStock = new(privateStock) + +type privateStock struct { +} + +// Insert 插入私域库存 +func (d *privateStock) Insert(ctx context.Context, req *dto.CreatePrivateStockReq) (ids []interface{}, err error) { + var result *entity.PrivateStock + if err = utils.Struct(req, &result); err != nil { + return + } + + // 设置StockType(如果未指定,默认为PrivateStock类型) + if !g.IsEmpty(req.StockType) { + result.StockType = req.StockType + } else { + result.StockType = stock.StockLocationTypePrivateStock + } + + // 获取仓库信息(非必填) + if req.WarehouseId != nil && !req.WarehouseId.IsZero() { + warehouse, err := Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.WarehouseId}) + if err != nil { + return nil, err + } + result.WarehouseID = req.WarehouseId + result.WarehouseCode = warehouse.WarehouseCode + result.WarehouseName = warehouse.WarehouseName + } + + // 如果有库区信息 + if req.ZoneId != nil && !req.ZoneId.IsZero() { + zone, err := Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ZoneId}) + if err != nil { + return nil, err + } + result.ZoneID = req.ZoneId + result.ZoneCode = zone.ZoneCode + result.ZoneName = zone.ZoneName + result.ZoneType = zone.ZoneType + } + + // 如果有库位信息 + if req.LocationId != nil && !req.LocationId.IsZero() { + location, err := Location.GetOne(ctx, &dto.GetLocationReq{Id: req.LocationId}) + if err != nil { + return nil, err + } + result.LocationID = req.LocationId + result.LocationCode = location.LocationCode + result.LocationName = location.LocationName + result.LocationType = location.LocationType + } + + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.PrivateStockCollection) + return +} + +// Update 更新私域库存 +func (d *privateStock) Update(ctx context.Context, req *dto.UpdatePrivateStockReq) (err error) { + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": bson.M{}} + + if req.WarehouseId != nil && !req.WarehouseId.IsZero() { + warehouse, err := Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.WarehouseId}) + if err != nil { + return err + } + update["$set"].(bson.M)["warehouseId"] = req.WarehouseId + update["$set"].(bson.M)["warehouseCode"] = warehouse.WarehouseCode + update["$set"].(bson.M)["warehouseName"] = warehouse.WarehouseName + } + + if req.ZoneId != nil && !req.ZoneId.IsZero() { + zone, err := Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ZoneId}) + if err != nil { + return err + } + update["$set"].(bson.M)["zoneId"] = req.ZoneId + update["$set"].(bson.M)["zoneCode"] = zone.ZoneCode + update["$set"].(bson.M)["zoneName"] = zone.ZoneName + update["$set"].(bson.M)["zoneType"] = zone.ZoneType + } + + if req.LocationId != nil && !req.LocationId.IsZero() { + location, err := Location.GetOne(ctx, &dto.GetLocationReq{Id: req.LocationId}) + if err != nil { + return err + } + update["$set"].(bson.M)["locationId"] = req.LocationId + update["$set"].(bson.M)["locationCode"] = location.LocationCode + update["$set"].(bson.M)["locationName"] = location.LocationName + update["$set"].(bson.M)["locationType"] = location.LocationType + } + + if req.PrivateSkuID != nil { + update["$set"].(bson.M)["privateSkuId"] = req.PrivateSkuID + } + if !g.IsEmpty(req.BatchNo) { + update["$set"].(bson.M)["batchNo"] = req.BatchNo + } + if req.BatchQty > 0 { + update["$set"].(bson.M)["batchQty"] = req.BatchQty + } + // AvailableQty已移除全量覆盖,请使用IncrementAvailableQty方法进行增量更新 + if req.BatchStatus != nil { + update["$set"].(bson.M)["batchStatus"] = req.BatchStatus + } + if req.StockStatus != nil { + update["$set"].(bson.M)["stockStatus"] = req.StockStatus + } + if req.SupplierID != nil { + update["$set"].(bson.M)["supplierId"] = req.SupplierID + } + if req.SupportsRecycle != nil { + update["$set"].(bson.M)["supportsRecycle"] = *req.SupportsRecycle + } + if req.ProductionDate != nil { + update["$set"].(bson.M)["productionDate"] = req.ProductionDate + } + if req.ExpiryDate != nil { + update["$set"].(bson.M)["expiryDate"] = req.ExpiryDate + } + if req.ExpiryWarningDate != nil { + update["$set"].(bson.M)["expiryWarningDate"] = req.ExpiryWarningDate + } + if !g.IsEmpty(req.PrivateCategoryPath) { + update["$set"].(bson.M)["privateCategoryPath"] = req.PrivateCategoryPath + } + if !g.IsEmpty(req.StockType) { + update["$set"].(bson.M)["stockType"] = req.StockType + } + + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) + return +} + +// DeleteFake 软删除私域库存 +func (d *privateStock) DeleteFake(ctx context.Context, req *dto.DeletePrivateStockReq) error { + filter := bson.M{"_id": req.Id} + _, err := mongo.DB().DeleteSoft(ctx, filter, public.PrivateStockCollection) + return err +} + +// GetOne 根据ID查询私域库存 +func (d *privateStock) GetOne(ctx context.Context, req *dto.GetPrivateStockReq) (res *entity.PrivateStock, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.PrivateStockCollection) + return +} + +// IncrementAvailableQty 增量更新可用数量(并发安全) +// deltaQty:正数表示增加(入库),负数表示减少(出库) +// 减少时自动添加 availableQty >= |deltaQty| 条件,防止并发导致库存变负 +func (d *privateStock) IncrementAvailableQty(ctx context.Context, id *bson.ObjectID, deltaQty int) (err error) { + filter := bson.M{"_id": id} + if deltaQty < 0 { + filter["availableQty"] = bson.M{"$gte": -deltaQty} + } + update := bson.M{ + "$inc": bson.M{ + "availableQty": deltaQty, + }, + } + modifiedCount, err := mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) + if err != nil { + return + } + if deltaQty < 0 && modifiedCount == 0 { + err = gerror.Newf("库存不足,无法减少%d", -deltaQty) + } + return +} + +// List 查询私域库存列表 +func (d *privateStock) List(ctx context.Context, req *dto.ListPrivateStockReq) (res []entity.PrivateStock, total int64, err error) { + filter := bson.M{} + if req.WarehouseId != nil && !req.WarehouseId.IsZero() { + filter["warehouseId"] = req.WarehouseId + } + if req.ZoneId != nil && !req.ZoneId.IsZero() { + filter["zoneId"] = req.ZoneId + } + if req.LocationId != nil && !req.LocationId.IsZero() { + filter["locationId"] = req.LocationId + } + if req.PrivateSkuID != nil && !req.PrivateSkuID.IsZero() { + filter["privateSkuId"] = req.PrivateSkuID + } + if req.BatchStatus != nil { + filter["batchStatus"] = req.BatchStatus + } + if req.StockStatus != nil { + filter["stockStatus"] = req.StockStatus + } + if req.SupplierID != nil && !req.SupplierID.IsZero() { + filter["supplierId"] = req.SupplierID + } + if !g.IsEmpty(req.PrivateCategoryPath) { + filter["privateCategoryPath"] = req.PrivateCategoryPath + } + if !g.IsEmpty(req.StockType) { + filter["stockType"] = req.StockType + } + + // 默认排序 + if len(req.OrderBy) == 0 { + req.OrderBy = []beans.OrderBy{ + {Field: "createdAt", Order: beans.Desc}, + } + } + + total, err = mongo.DB().Find(ctx, filter, &res, public.PrivateStockCollection, req.Page, req.OrderBy) + return +} + +// SumAvailableQtyByLocation 按库位ID汇总所有库存的可用数量(使用MongoDB聚合管道) +func (d *privateStock) SumAvailableQtyByLocation(ctx context.Context, locationId *bson.ObjectID) (totalQty int, err error) { + pipeline := bson.A{ + bson.M{"$match": bson.M{ + "locationId": locationId, + "isDeleted": false, + }}, + bson.M{"$group": bson.M{ + "_id": nil, + "totalQty": bson.M{"$sum": "$availableQty"}, + }}, + } + + coll := mongo.GetDB().Collection(public.PrivateStockCollection) + cursor, err := coll.Aggregate(ctx, pipeline) + if err != nil { + return + } + defer cursor.Close(ctx) + + var results []struct { + TotalQty int `bson:"totalQty"` + } + if err = cursor.All(ctx, &results); err != nil { + return + } + if len(results) > 0 { + totalQty = results[0].TotalQty + } + return +} diff --git a/dao/stock/stock_batch_dao.go b/dao/stock/stock_batch_dao.go new file mode 100644 index 0000000..aa355a5 --- /dev/null +++ b/dao/stock/stock_batch_dao.go @@ -0,0 +1,78 @@ +// 批次库存DAO层(逻辑库存) +// 职责:批次CRUD、使用$inc原子操作更新数量 +// 紧密耦合:service.StockBatch、service.StockManage(入库出库) +// 注意:Update使用$inc原子操作,GetOne使用NoCache()跳过缓存 +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var StockBatch = new(stockBatch) + +type stockBatch struct { +} + +// Insert 插入 +func (d *stockBatch) Insert(ctx context.Context, req *dto.CreateBatchReq) (ids []interface{}, err error) { + var result *entity.StockBatch + if err = utils.Struct(req, &result); err != nil { + return + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.StockBatchCollection) + return +} + +// Update 更新批次数量(使用$inc原子操作,并发安全) +func (d *stockBatch) Update(ctx context.Context, req *dto.UpdateBatchReq) (err error) { + filter := bson.M{"_id": req.Id} + update := bson.M{ + "$inc": bson.M{ + "batchQty": req.BatchQty, + "availableQty": req.AvailableQty, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.StockBatchCollection) + return +} + +// GetOne 根据批次号查询(使用NoCache跳过缓存,确保获取最新数据) +func (d *stockBatch) GetOne(ctx context.Context, batchNo string) (res *entity.StockBatch, err error) { + filter := bson.M{"batchNo": batchNo} + err = mongo.DB().NoCache().FindOne(ctx, filter, &res, public.StockBatchCollection) + return +} + +// GetOneById 根据ID查询批次 +func (d *stockBatch) GetOneById(ctx context.Context, req *dto.GetBatchReq) (res *entity.StockBatch, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.StockBatchCollection) + return +} + +// DeleteFake 软删除批次 +func (d *stockBatch) DeleteFake(ctx context.Context, req *dto.DeleteBatchReq) error { + filter := bson.M{"_id": req.Id} + _, err := mongo.DB().DeleteSoft(ctx, filter, public.StockBatchCollection) + return err +} + +// List 查询批次列表 +func (d *stockBatch) List(ctx context.Context, req *dto.ListBatchReq) (res []entity.StockBatch, total int64, err error) { + filter := bson.M{} + if req.AssetId != nil { + filter["assetId"] = req.AssetId + } + if req.AssetSkuId != nil { + filter["assetSkuId"] = req.AssetSkuId + } + total, err = mongo.DB().Find(ctx, filter, &res, public.StockBatchCollection, req.Page, req.OrderBy) + return +} diff --git a/dao/stock/stock_details_dao.go b/dao/stock/stock_details_dao.go new file mode 100644 index 0000000..df29760 --- /dev/null +++ b/dao/stock/stock_details_dao.go @@ -0,0 +1,80 @@ +// 库存明细DAO层(逻辑库存) +// 职责:批量插入/删除、按SKU统计数量、查询列表 +// 紧密耦合:service.StockDetails、service.StockManage(入库出库) +// 注意:GetStockCountBySkuId使用NoCache()跳过缓存,BatchInsert用于批量入库 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var StockDetails = new(stockDetails) + +type stockDetails struct { +} + +// BatchInsert 批量插入库存 +func (d *stockDetails) BatchInsert(ctx context.Context, stockInterfaces []interface{}) (ids []interface{}, err error) { + ids, err = mongo.DB().Insert(ctx, stockInterfaces, public.StockDetailsCollection) + return +} + +// DeleteManyByIds 根据ID批量删除库存 +func (d *stockDetails) DeleteManyByIds(ctx context.Context, allStockIds []*bson.ObjectID) (count int64, err error) { + if len(allStockIds) == 0 { + return 0, nil + } + filter := bson.M{"_id": bson.M{"$in": allStockIds}} + count, err = mongo.DB().Delete(ctx, filter, public.StockDetailsCollection) + return +} + +// GetStockCountBySkuId 获取库存数根据SKU ID +func (d *stockDetails) GetStockCountBySkuId(ctx context.Context, assetSkuId *bson.ObjectID) (total int64, err error) { + // 构建查询过滤条件 + filter := bson.M{} + filter["assetSkuId"] = assetSkuId + filter["status"] = stock.StockStatusAvailable + // 检查总数 + total, err = mongo.DB().NoCache().Count(ctx, filter, public.StockDetailsCollection) + + return total, err +} + +// GetOneById 根据ID查询库存明细 +func (d *stockDetails) GetOneById(ctx context.Context, req *dto.GetStockDetailsReq) (res *entity.StockDetails, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.StockDetailsCollection) + return +} + +// List 获取SKU列表 +func (d *stockDetails) List(ctx context.Context, req *dto.ListStockDetailsReq) (res []entity.StockDetails, total int64, err error) { + // 构建查询过滤条件 + filter := bson.M{} + if !g.IsEmpty(req.AssetId) { + filter["assetId"] = req.AssetId + } + if !g.IsEmpty(req.AssetSkuId) { + filter["assetSkuId"] = req.AssetSkuId + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + // 排序处理 + req.OrderBy = []beans.OrderBy{ + {Field: "sort", Order: beans.Asc}, + {Field: "createdAt", Order: beans.Desc}, + } + total, err = mongo.DB().Find(ctx, filter, &res, public.StockDetailsCollection, req.Page, req.OrderBy) + return +} diff --git a/dao/stock/unit_conversion_dao.go b/dao/stock/unit_conversion_dao.go new file mode 100644 index 0000000..66053d8 --- /dev/null +++ b/dao/stock/unit_conversion_dao.go @@ -0,0 +1,69 @@ +// 单位换算DAO层 +// 职责:换算规则CRUD、按单位类型和源目标单位查询 +// 紧密耦合:service.UnitConversion、service.Capacity(容量计算换算) +// 注意:GetByUnits用于容量计算时获取换算系数 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var UnitConversion = new(unitConversion) + +type unitConversion struct{} + +// GetByUnits 根据单位类型和源目标单位查询换算规则 +func (d *unitConversion) GetByUnits(ctx context.Context, unitType stock.CapacityUnitType, fromUnit, toUnit string) (res *entity.UnitConversion, err error) { + filter := bson.M{ + "unitType": unitType, + "fromUnit": fromUnit, + "toUnit": toUnit, + } + err = mongo.DB().FindOne(ctx, filter, &res, public.UnitConversionCollection) + return +} + +// Insert 插入换算规则 +func (d *unitConversion) Insert(ctx context.Context, conversion *entity.UnitConversion) (ids []interface{}, err error) { + ids, err = mongo.DB().Insert(ctx, []interface{}{conversion}, public.UnitConversionCollection) + return +} + +// Update 更新换算规则 +func (d *unitConversion) Update(ctx context.Context, id *bson.ObjectID, update bson.M) (err error) { + filter := bson.M{"_id": id} + updateDoc := bson.M{"$set": update} + _, err = mongo.DB().Update(ctx, filter, updateDoc, public.UnitConversionCollection) + return +} + +// DeleteFake 软删除换算规则 +func (d *unitConversion) DeleteFake(ctx context.Context, id *bson.ObjectID) (err error) { + filter := bson.M{"_id": id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.UnitConversionCollection) + return +} + +// List 查询换算规则列表 +func (d *unitConversion) List(ctx context.Context, unitType *stock.CapacityUnitType, fromUnit, toUnit string) (res []entity.UnitConversion, err error) { + filter := bson.M{} + + if unitType != nil { + filter["unitType"] = *unitType + } + if fromUnit != "" { + filter["fromUnit"] = fromUnit + } + if toUnit != "" { + filter["toUnit"] = toUnit + } + + _, err = mongo.DB().Find(ctx, filter, &res, public.UnitConversionCollection, nil, nil) + return +} diff --git a/dao/stock/warehouse_dao.go b/dao/stock/warehouse_dao.go new file mode 100644 index 0000000..119bbbf --- /dev/null +++ b/dao/stock/warehouse_dao.go @@ -0,0 +1,141 @@ +// 仓库DAO层 +// 职责:仓库CRUD、状态更新、批量更新库区/库位状态(状态联动)、更新容量 +// 紧密耦合:service.Warehouse(状态联动)、service.Capacity(容量汇总) +// 注意:UpdateCapacityByUnitType使用嵌套路径更新map字段 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + "assets/model/config" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var Warehouse = new(warehouse) + +type warehouse struct{} + +func (d *warehouse) Insert(ctx context.Context, req *dto.CreateWarehouseReq) (ids []interface{}, err error) { + var result *entity.Warehouse + if err = utils.Struct(req, &result); err != nil { + return + } + // 如果未传入状态,设置默认值为启用 + if result.Status == "" { + result.Status = stock.WarehouseStatusEnabled + } + // 初始化Capacity为空map,避免后续UpdateCapacityByUnitType panic + if result.Capacity == nil { + emptyMap := make(map[stock.CapacityUnitType]config.Capacity) + result.Capacity = &emptyMap + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.WarehouseCollection) + return +} + +func (d *warehouse) GetOne(ctx context.Context, req *dto.GetWarehouseReq) (res *entity.Warehouse, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.WarehouseCollection) + return +} + +func (d *warehouse) Update(ctx context.Context, req *dto.UpdateWarehouseReq) (err error) { + buildFilter, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": buildFilter} + _, err = mongo.DB().Update(ctx, filter, update, public.WarehouseCollection) + return +} + +func (d *warehouse) DeleteFake(ctx context.Context, req *dto.DeleteWarehouseReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.WarehouseCollection) + return +} + +// UpdateStatus 更新仓库状态 +func (d *warehouse) UpdateStatus(ctx context.Context, req *dto.UpdateWarehouseStatusReq) (err error) { + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": bson.M{"status": req.Status}} + _, err = mongo.DB().Update(ctx, filter, update, public.WarehouseCollection) + return +} + +func (d *warehouse) List(ctx context.Context, req *dto.ListWarehouseReq) (res []entity.Warehouse, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.WarehouseCollection, req.Page, req.OrderBy) + return +} + +func (d *warehouse) buildListFilter(ctx context.Context, req *dto.ListWarehouseReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.Keyword) { + filter["$or"] = bson.A{ + bson.M{"warehouseCode": bson.M{"$regex": req.Keyword, "$options": "i"}}, + bson.M{"warehouseName": bson.M{"$regex": req.Keyword, "$options": "i"}}, + } + } + return +} + +// CountZonesByWarehouseId 统计仓库下的库区数量(用于删除前检查) +func (d *warehouse) CountZonesByWarehouseId(ctx context.Context, warehouseId string) (count int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + count, err = mongo.DB().Count(ctx, filter, public.ZoneCollection) + return +} + +// BatchUpdateZoneStatus 批量更新仓库下所有库区状态(用于状态联动) +// fromStatus 可选:指定时只更新当前状态为 fromStatus 的记录,避免覆盖其他状态 +func (d *warehouse) BatchUpdateZoneStatus(ctx context.Context, warehouseId string, status stock.ZoneStatus, fromStatus ...stock.ZoneStatus) (modifiedCount int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + if len(fromStatus) > 0 { + filter["status"] = fromStatus[0] + } + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.ZoneCollection) + return +} + +// UpdateCapacityByUnitType 更新仓库指定容量单位类型的容量 +func (d *warehouse) UpdateCapacityByUnitType(ctx context.Context, warehouseId *bson.ObjectID, unitType stock.CapacityUnitType, currentCapacity int, maxCapacity int, capacityUnit string) (err error) { + filter := bson.M{"_id": warehouseId} + update := bson.M{ + "$set": bson.M{ + "capacity." + string(unitType) + ".currentCapacity": currentCapacity, + "capacity." + string(unitType) + ".maxCapacity": maxCapacity, + "capacity." + string(unitType) + ".capacityUnit": capacityUnit, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.WarehouseCollection) + return +} + +// BatchUpdateLocationStatus 批量更新仓库下所有库位状态(用于状态联动) +// fromStatus 可选:指定时只更新当前状态为 fromStatus 的记录,避免覆盖其他状态 +func (d *warehouse) BatchUpdateLocationStatus(ctx context.Context, warehouseId string, status stock.LocationStatus, fromStatus ...stock.LocationStatus) (modifiedCount int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + if len(fromStatus) > 0 { + filter["status"] = fromStatus[0] + } + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} diff --git a/dao/stock/zone_dao.go b/dao/stock/zone_dao.go new file mode 100644 index 0000000..e3fc3b8 --- /dev/null +++ b/dao/stock/zone_dao.go @@ -0,0 +1,162 @@ +// 库区DAO层 +// 职责:库区CRUD、状态更新、批量更新库位状态(状态联动)、更新容量 +// 紧密耦合:service.Zone(状态联动)、service.Capacity(容量汇总) +// 注意:UpdateCapacityByUnitType使用嵌套路径更新map字段 +package dao + +import ( + "assets/consts/public" + "assets/consts/stock" + "assets/model/config" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var Zone = new(zone) + +type zone struct{} + +func (d *zone) Insert(ctx context.Context, req *dto.CreateZoneReq) (ids []interface{}, err error) { + var result *entity.Zone + if err = utils.Struct(req, &result); err != nil { + return + } + // 如果未传入状态,设置默认值为启用 + if result.Status == "" { + result.Status = stock.ZoneStatusEnabled + } + // 初始化Capacity为空map,避免后续UpdateCapacityByUnitType panic + if result.Capacity == nil { + emptyMap := make(map[stock.CapacityUnitType]config.Capacity) + result.Capacity = &emptyMap + } + ids, err = mongo.DB().Insert(ctx, []interface{}{&result}, public.ZoneCollection) + return +} + +func (d *zone) GetOne(ctx context.Context, req *dto.GetZoneReq) (res *entity.Zone, err error) { + filter := bson.M{"_id": req.Id} + err = mongo.DB().FindOne(ctx, filter, &res, public.ZoneCollection) + return +} + +func (d *zone) Update(ctx context.Context, req *dto.UpdateZoneReq) (err error) { + buildFilter, err := mongo.BuildUpdateData(ctx, req) + if err != nil { + return + } + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": buildFilter} + _, err = mongo.DB().Update(ctx, filter, update, public.ZoneCollection) + return +} + +func (d *zone) DeleteFake(ctx context.Context, req *dto.DeleteZoneReq) (err error) { + filter := bson.M{"_id": req.Id} + _, err = mongo.DB().DeleteSoft(ctx, filter, public.ZoneCollection) + return +} + +// UpdateStatus 更新库区状态 +func (d *zone) UpdateStatus(ctx context.Context, req *dto.UpdateZoneStatusReq) (err error) { + filter := bson.M{"_id": req.Id} + update := bson.M{"$set": bson.M{"status": req.Status}} + _, err = mongo.DB().Update(ctx, filter, update, public.ZoneCollection) + return +} + +func (d *zone) List(ctx context.Context, req *dto.ListZoneReq) (res []entity.Zone, total int64, err error) { + filter, err := d.buildListFilter(ctx, req) + if err != nil { + return + } + total, err = mongo.DB().Find(ctx, filter, &res, public.ZoneCollection, req.Page, req.OrderBy) + return +} + +func (d *zone) buildListFilter(ctx context.Context, req *dto.ListZoneReq) (filter bson.M, err error) { + _ = ctx + filter = bson.M{} + // 兼容单值和数组参数(优先使用数组) + if len(req.WarehouseIds) > 0 { + filter["warehouseId"] = bson.M{"$in": req.WarehouseIds} + } else if !g.IsEmpty(req.WarehouseId) { + filter["warehouseId"] = req.WarehouseId + } + if !g.IsEmpty(req.ZoneType) { + filter["zoneType"] = req.ZoneType + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if !g.IsEmpty(req.Keyword) { + filter["$or"] = bson.A{ + bson.M{"zoneCode": bson.M{"$regex": req.Keyword, "$options": "i"}}, + bson.M{"zoneName": bson.M{"$regex": req.Keyword, "$options": "i"}}, + } + } + return +} + +// CountLocationsByZoneId 统计库区下的库位数量(用于删除前检查) +func (d *zone) CountLocationsByZoneId(ctx context.Context, zoneId string) (count int64, err error) { + filter := bson.M{"zoneId": zoneId} + count, err = mongo.DB().Count(ctx, filter, public.LocationCollection) + return +} + +// CountByWarehouseId 统计仓库下的库区数量(用于删除前检查) +func (d *zone) CountByWarehouseId(ctx context.Context, warehouseId string) (count int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + count, err = mongo.DB().Count(ctx, filter, public.ZoneCollection) + return +} + +// BatchUpdateStatusByWarehouseId 批量更新仓库下所有库区状态(用于状态联动) +func (d *zone) BatchUpdateStatusByWarehouseId(ctx context.Context, warehouseId string, status stock.ZoneStatus) (modifiedCount int64, err error) { + filter := bson.M{"warehouseId": warehouseId} + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.ZoneCollection) + return +} + +// BatchUpdateLocationStatus 批量更新库区下所有库位状态(用于状态联动) +// fromStatus 可选:指定时只更新当前状态为 fromStatus 的记录,避免覆盖其他状态 +func (d *zone) BatchUpdateLocationStatus(ctx context.Context, zoneId string, status stock.LocationStatus, fromStatus ...stock.LocationStatus) (modifiedCount int64, err error) { + filter := bson.M{"zoneId": zoneId} + if len(fromStatus) > 0 { + filter["status"] = fromStatus[0] + } + update := bson.M{"$set": bson.M{"status": status}} + modifiedCount, err = mongo.DB().Update(ctx, filter, update, public.LocationCollection) + return +} + +// UpdateCapacityByUnitType 更新库区指定容量单位类型的容量 +func (d *zone) UpdateCapacityByUnitType(ctx context.Context, zoneId *bson.ObjectID, unitType stock.CapacityUnitType, currentCapacity int, maxCapacity int, capacityUnit string) (err error) { + filter := bson.M{"_id": zoneId} + update := bson.M{ + "$set": bson.M{ + "capacity." + string(unitType) + ".currentCapacity": currentCapacity, + "capacity." + string(unitType) + ".maxCapacity": maxCapacity, + "capacity." + string(unitType) + ".capacityUnit": capacityUnit, + }, + } + _, err = mongo.DB().Update(ctx, filter, update, public.ZoneCollection) + return +} + +// ListByWarehouseAndUnitType 按仓库ID查询所有库区(用于汇总仓库容量) +func (d *zone) ListByWarehouseAndUnitType(ctx context.Context, warehouseId string) (res []entity.Zone, err error) { + filter := bson.M{ + "warehouseId": warehouseId, + } + _, err = mongo.DB().Find(ctx, filter, &res, public.ZoneCollection, nil, nil) + return +} diff --git a/dao/sync/sync_dao.go b/dao/sync/sync_dao.go new file mode 100644 index 0000000..8da0321 --- /dev/null +++ b/dao/sync/sync_dao.go @@ -0,0 +1,169 @@ +package dao + +import ( + "assets/consts/public" + dto "assets/model/dto/sync" + entity "assets/model/entity/sync" + "context" + + "gitea.com/red-future/common/db/mongo" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var SyncTask = new(syncTask) + +type syncTask struct{} + +// Insert 插入同步任务 +func (d *syncTask) Insert(ctx context.Context, task *entity.SyncTask) (err error) { + _, err = mongo.DB().Insert(ctx, []interface{}{task}, task.CollectionName()) + return +} + +// GetOne 获取单个同步任务 +func (d *syncTask) GetOne(ctx context.Context, id *bson.ObjectID) (task *entity.SyncTask, err error) { + filter := bson.M{"_id": id} + + task = &entity.SyncTask{} + err = mongo.DB().FindOne(ctx, filter, task, task.CollectionName()) + return task, err +} + +// List 获取同步任务列表 +func (d *syncTask) List(ctx context.Context, req *dto.ListSyncTaskReq) (list []*entity.SyncTask, total int64, err error) { + // 构建查询过滤条件 + filter := d.buildListFilter(req) + + // 调用 common/db/mongo 的 Find 方法,不使用排序 + total, err = mongo.DB().Find(ctx, filter, &list, "sync_task", &req.Page, nil) + return +} + +// Update 更新同步任务 +func (d *syncTask) Update(ctx context.Context, id string, updateData *entity.SyncTask) (err error) { + objectId, err := bson.ObjectIDFromHex(id) + if err != nil { + return err + } + filter := bson.M{"_id": objectId} + + if !g.IsEmpty(updateData) { + // 直接使用 struct 转 map,不需要额外的转换 + update := bson.M{"$set": gconv.Map(updateData)} + _, err = mongo.DB().Update(ctx, filter, update, "sync_task") + } + return err +} + +// UpdateStatus 更新同步任务状态 +func (d *syncTask) UpdateStatus(ctx context.Context, id *bson.ObjectID, status public.SyncStatus, errorMessage string) (err error) { + filter := bson.M{"_id": id} + + updateData := bson.M{ + "status": status, + "errorMessage": errorMessage, + } + + if status == public.SyncStatusSyncing { + updateData["startedAt"] = gtime.Now() + } else if status == public.SyncStatusSuccess || status == public.SyncStatusFailed { + updateData["finishedAt"] = gtime.Now() + } + + update := bson.M{"$set": updateData} + _, err = mongo.DB().Update(ctx, filter, update, "sync_task") + return err +} + +// UpdateErrorCount 更新错误计数 +func (d *syncTask) UpdateErrorCount(ctx context.Context, id string, increment int) (err error) { + objectId, err := bson.ObjectIDFromHex(id) + if err != nil { + return err + } + filter := bson.M{"_id": objectId} + update := bson.M{"$inc": bson.M{"errorCount": increment}} + _, err = mongo.DB().Update(ctx, filter, update, "sync_task") + return err +} + +// Delete 删除同步任务 +func (d *syncTask) Delete(ctx context.Context, id string) (err error) { + objectId, err := bson.ObjectIDFromHex(id) + if err != nil { + return err + } + filter := bson.M{"_id": objectId} + _, err = mongo.DB().Delete(ctx, filter, "sync_task") + return err +} + +// GetPendingTasks 获取待处理的同步任务 +func (d *syncTask) GetPendingTasks(ctx context.Context, limit int) (tasks []*entity.SyncTask, err error) { + filter := bson.M{"status": public.SyncStatusPending} + + // 调用 common/db/mongo 的 Find 方法,不使用排序 + _, err = mongo.DB().Find(ctx, filter, &tasks, "sync_task", nil, nil) + return tasks, err +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *syncTask) buildListFilter(req *dto.ListSyncTaskReq) bson.M { + filter := bson.M{} + + if !g.IsEmpty(req.Platform) { + filter["platform"] = req.Platform + } + if !g.IsEmpty(req.Status) { + filter["status"] = req.Status + } + if req.StartTime != nil { + filter["createdAt"] = bson.M{"$gte": req.StartTime} + } + if req.EndTime != nil { + if existingFilter, exists := filter["createdAt"]; exists { + if existingTimeRange, ok := existingFilter.(bson.M); ok { + existingTimeRange["$lte"] = req.EndTime + filter["createdAt"] = existingTimeRange + } + } else { + filter["createdAt"] = bson.M{"$lte": req.EndTime} + } + } + + return filter +} + +// SyncConfigDao 同步配置DAO +var SyncConfig = new(syncConfig) + +type syncConfig struct{} + +// GetByPlatform 根据平台获取同步配置 +func (d *syncConfig) GetByPlatform(ctx context.Context, platform public.SyncPlatform) (config *entity.ChannelConfig, err error) { + filter := bson.M{"platform": platform} + config = &entity.ChannelConfig{} + err = mongo.DB().FindOne(ctx, filter, config, "sync_config") + return config, err +} + +// List 获取同步配置列表 +func (d *syncConfig) List(ctx context.Context) (configs []*entity.ChannelConfig, err error) { + _, err = mongo.DB().Find(ctx, bson.M{}, &configs, "sync_config", nil, nil) + return configs, err +} + +// Update 更新同步配置 +func (d *syncConfig) Update(ctx context.Context, platform public.SyncPlatform, updateData *entity.ChannelConfig) (err error) { + filter := bson.M{"platform": platform} + + if !g.IsEmpty(updateData) { + // 直接使用 struct 转 map + update := bson.M{"$set": gconv.Map(updateData)} + _, err = mongo.DB().Update(ctx, filter, update, "sync_config") + } + return err +} diff --git a/model/config/capacity.go b/model/config/capacity.go new file mode 100644 index 0000000..e63e204 --- /dev/null +++ b/model/config/capacity.go @@ -0,0 +1,7 @@ +package config + +type Capacity struct { + CapacityUnit string `bson:"capacityUnit" json:"capacityUnit"` // 容量单位(具体单位值) + MaxCapacity int `bson:"maxCapacity" json:"maxCapacity"` // 最大容量 + CurrentCapacity int `bson:"currentCapacity" json:"currentCapacity"` // 当前容量 +} diff --git a/model/config/interface.go b/model/config/interface.go new file mode 100644 index 0000000..ad57922 --- /dev/null +++ b/model/config/interface.go @@ -0,0 +1,9 @@ +package config + +import ( + "assets/consts/asset" +) + +type AssetConfigInterface interface { + GetType() consts.AssetType +} diff --git a/model/config/physical.go b/model/config/physical.go new file mode 100644 index 0000000..5d0296a --- /dev/null +++ b/model/config/physical.go @@ -0,0 +1,12 @@ +package config + +// PhysicalAssetConfig 实物资产配置 +type PhysicalAssetConfig struct { + Shipping *ShippingConfig `json:"shipping,omitempty"` // 配送配置 +} + +// ShippingConfig 配送配置 +type ShippingConfig struct { + DeliveryMethod string `json:"deliveryMethod"` // 配送方式:express快递/self_pickup自提 + //DeliveryTime int `bson:"deliveryTime" json:"deliveryTime"` // 配送时效(小时) +} diff --git a/model/config/service.go b/model/config/service.go new file mode 100644 index 0000000..094701a --- /dev/null +++ b/model/config/service.go @@ -0,0 +1,45 @@ +package config + +import ( + "assets/consts/asset" +) + +// ServiceAssetConfig 服务资产配置 +type ServiceAssetConfig struct { + ServiceAssetType consts.ServiceAssetType `json:"serviceAssetType"` + ServiceAssetArrivalConfig *ServiceAssetArrivalConfig `json:"serviceAssetArrivalConfig"` +} + +type ServiceAssetArrivalConfig struct { + Schedule *ScheduleConfig `json:"schedule,omitempty"` // 排期配置 + Booking *BookingConfig `json:"booking,omitempty"` // 预订配置 +} + +// ScheduleConfig 排期配置 +type ScheduleConfig struct { + TimeSlots []*TimeSlot `json:"timeSlots"` // 时间段定义 + Exceptions []*ScheduleException `json:"exceptions,omitempty"` // 例外日期 +} + +// BookingConfig 预订配置 +type BookingConfig struct { + MinAdvance int `json:"minAdvance,omitempty"` // 最小提前时间(分钟) + MinDuration int `json:"minDuration,omitempty"` // 最小预订时长(分钟) + CancelWindow int `json:"cancelWindow,omitempty"` // 取消窗口(分钟) +} + +// TimeSlot 时间段 +type TimeSlot struct { + DayOfWeek string `bson:"dayOfWeek" json:"dayOfWeek"` // 星期几(1-7) + StartTime string `bson:"startTime" json:"startTime"` // 开始时间 HH:mm + EndTime string `bson:"endTime" json:"endTime"` // 结束时间 HH:mm + Capacity int `bson:"capacity,omitempty" json:"capacity,omitempty"` // 容量限制 +} + +// ScheduleException 排期例外 +type ScheduleException struct { + Date string `bson:"date" json:"date"` // 日期 YYYY-MM-DD + Status int `bson:"status" json:"status"` // 状态,1:可用,0:不可用 + Reason string `bson:"reason,omitempty" json:"reason,omitempty"` // 原因 + DayOfWeek string `bson:"dayOfWeek" json:"dayOfWeek"` // 星期几(1-7) +} diff --git a/model/config/virtual.go b/model/config/virtual.go new file mode 100644 index 0000000..26230f9 --- /dev/null +++ b/model/config/virtual.go @@ -0,0 +1,19 @@ +package config + +// VirtualAssetConfig 虚拟资产配置 +type VirtualAssetConfig struct { + VirtualAssetType string `json:"virtualType"` + CollectionPrice int `json:"collectionPrice"` // 合集价格(分为单位) + Currency string `json:"currency"` // 货币单位,默认CNY + ApiConfig *ApiConfig `json:"apiConfig"` +} + +// ApiConfig API配置(虚拟资产专用) +type ApiConfig struct { + Method string `json:"method"` // HTTP方法:GET/POST/PUT/DELETE + RequestURL string `json:"requestURL"` // 请求地址 + Headers map[string]string `json:"headers"` // 请求头配置 + Params map[string]string `json:"params"` // 请求参数配置 + AuthType string `json:"authType"` // 认证类型:none/apikey/bearer/oauth + AuthConfig string `json:"authConfig"` // 认证配置 +} diff --git a/model/dto/asset/asset_dto.go b/model/dto/asset/asset_dto.go new file mode 100644 index 0000000..1f4287d --- /dev/null +++ b/model/dto/asset/asset_dto.go @@ -0,0 +1,149 @@ +package dto + +import ( + consts "assets/consts/asset" + "assets/consts/stock" + "assets/model/config" + enumDto "assets/model/dto/enum" + entity "assets/model/entity/asset" + "time" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateAssetReq 创建资产请求 +type CreateAssetReq struct { + g.Meta `path:"/createAsset" method:"post" tags:"资产管理" summary:"创建资产" dc:"创建新的资产"` + // 基础信息 + Name string `json:"name" v:"required" dc:"资产名称"` + Description string `json:"description" dc:"资产描述"` + Type consts.AssetType `json:"type" v:"required" dc:"资产类型:physical实物/virtual虚拟/service服务"` + CategoryId uint64 `json:"categoryId" v:"required" dc:"分类ID"` + CategoryPath string `json:"categoryPath" dc:"分类路径"` + ImageURL string `json:"imageUrl" dc:"主图URL"` + Images []string `json:"images" dc:"图片列表"` + Status *consts.AssetStatus `json:"status" dc:"状态:1/0" d:"1"` + UnlimitedStock bool `json:"unlimitedStock" dc:"是否无库存限制"` + StockMode stock.StockMode `json:"stockMode" dc:"库存管理模式:1-明细模式 2-批次模式" d:"2"` + // 上线和下线时间配置(由定时任务处理资产状态) + OnlineTime *time.Time `json:"onlineTime,omitempty" dc:"上线时间(格式:2006-01-02 15:04:05)"` + OfflineTime *time.Time `json:"offlineTime,omitempty" dc:"下线时间(格式:2006-01-02 15:04:05)"` + // 类型专用配置 - 实物资产配置 + PhysicalAssetConfig *config.PhysicalAssetConfig `json:"physicalAssetConfig"` + // 类型专用配置 - 服务资产配置 + ServiceAssetConfig *config.ServiceAssetConfig `json:"serviceAssetConfig"` + // 类型专用配置 - 虚拟资产配置 + VirtualAssetConfig *config.VirtualAssetConfig `json:"virtualAssetConfig"` + // 扩展字段 + Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"` + + TenantModuleType beans.TenantModuleType `json:"tenantModuleType" dc:"租户模块类型"` +} + +// CreateAssetRes 创建资产响应 +type CreateAssetRes struct { + Id uint64 `json:"id" dc:"资产ID"` +} + +// ListAssetReq 获取资产列表请求 +type ListAssetReq struct { + g.Meta `path:"/listAssets" method:"get" tags:"资产管理" summary:"获取资产列表" dc:"分页查询资产列表,支持多条件筛选"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + Name string `json:"name" dc:"资产名称"` + Type consts.AssetType `json:"type" dc:"资产类型"` + CategoryId string `json:"categoryId" dc:"分类ID"` + CategoryPath string `json:"categoryPath" dc:"分类路径"` + Status *consts.AssetStatus `json:"status" dc:"状态"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListAssetRes 获取资产列表响应 +type ListAssetRes struct { + List []AssetListItem `json:"list" dc:"资产列表"` + Total int `json:"total" dc:"总数"` +} + +type AssetListItem struct { + // 基础信息 + Id uint64 `json:"id"` // 资产ID + Name string `json:"name"` // 资产名称 + Type consts.AssetType `json:"type"` // 资产类型:physical实物/virtual虚拟/service服务 + TypeName string `json:"typeName"` // 资产类型:physical实物/virtual虚拟/service服务 + CategoryId uint64 `json:"categoryId"` // 分类ID + UnlimitedStock bool `json:"unlimitedStock"` // 是否无库存限制 + Status *consts.AssetStatus `json:"status"` // 资产状态:active启用/inactive停用 + BasePrice int `json:"basePrice"` // 基础价格(分为单位) + OnlineTime *gtime.Time `json:"onlineTime,omitempty"` // 上线时间 + OfflineTime *gtime.Time `json:"offlineTime,omitempty"` // 下线时间 + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// GetAssetReq 获取资产详情请求 +type GetAssetReq struct { + g.Meta `path:"/getAsset" method:"get" tags:"资产管理" summary:"获取资产详情" dc:"获取资产详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"资产ID"` +} + +// GetAssetRes 获取资产详情响应 +type GetAssetRes struct { + *entity.Asset + CategoryName string `json:"categoryName" dc:"分类名称"` + ImgAddressPrefix string `json:"imgAddressPrefix"` +} + +// GetAssetAndSkuReq 获取资产和Sku详情请求 +type GetAssetAndSkuReq struct { + g.Meta `path:"/getAssetAndSku" method:"get" tags:"资产管理" summary:"获取资产和SKU详情" dc:"获取资产和SKU详情"` + AssetId *bson.ObjectID `json:"assetId" v:"required" dc:"资产ID"` +} + +// GetAssetAndSkuRes 获取资产详情响应 +type GetAssetAndSkuRes struct { + *entity.Asset + Skus []entity.AssetSku `json:"skus" dc:"SKU列表"` + TenantModuleType []enumDto.KeyValue `json:"tenantModuleType" dc:"租户模块类型"` + ImgAddressPrefix string `json:"imgAddressPrefix"` +} + +// UpdateAssetReq 更新资产请求 +type UpdateAssetReq struct { + g.Meta `path:"/updateAsset" method:"put" tags:"资产管理" summary:"更新资产" dc:"更新资产信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"资产ID"` + // 基础信息 + Name string `json:"name" dc:"资产名称"` + Description string `json:"description" dc:"资产描述"` + Type consts.AssetType `json:"type" dc:"资产类型:physical实物/virtual虚拟/service服务"` + CategoryId *bson.ObjectID `json:"categoryId" dc:"分类ID"` + ImageURL string `json:"imageUrl" dc:"主图URL"` + Images []string `json:"images" dc:"图片列表"` + Status *consts.AssetStatus `json:"status,omitempty" dc:"状态:1/0"` + // 上线和下线时间配置(由定时任务处理资产状态) + OnlineTime *time.Time `json:"onlineTime,omitempty" dc:"上线时间(格式:2006-01-02 15:04:05)"` + OfflineTime *time.Time `json:"offlineTime,omitempty" dc:"下线时间(格式:2006-01-02 15:04:05)"` + // 类型专用配置 - 实物资产配置 + PhysicalAssetConfig *config.PhysicalAssetConfig `json:"physicalAssetConfig"` + // 类型专用配置 - 服务资产配置 + ServiceAssetConfig *config.ServiceAssetConfig `json:"serviceAssetConfig"` + // 类型专用配置 - 虚拟资产配置 + VirtualAssetConfig *config.VirtualAssetConfig `json:"virtualAssetConfig"` + // 扩展字段 + Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"` +} + +// UpdateAssetStatusReq 更新资产状态请求 +type UpdateAssetStatusReq struct { + g.Meta `path:"/updateAssetStatus" method:"put" tags:"资产管理" summary:"更新资产状态" dc:"更新资产状态"` + Id *bson.ObjectID `json:"id" v:"required" dc:"资产ID"` + Status *consts.AssetStatus `json:"status" v:"required|in:1,0" dc:"状态:1启用/0停用"` +} + +// DeleteAssetReq 删除资产请求 +type DeleteAssetReq struct { + g.Meta `path:"/deleteAsset" method:"delete" tags:"资产管理" summary:"删除资产" dc:"删除资产"` + Id *bson.ObjectID `json:"id" v:"required" dc:"资产ID"` +} diff --git a/model/dto/asset/asset_sku_dto.go b/model/dto/asset/asset_sku_dto.go new file mode 100644 index 0000000..49cc343 --- /dev/null +++ b/model/dto/asset/asset_sku_dto.go @@ -0,0 +1,122 @@ +package dto + +import ( + consts "assets/consts/asset" + "assets/consts/stock" + entity "assets/model/entity/asset" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateAssetSkuReq 创建SKU请求 +type CreateAssetSkuReq struct { + g.Meta `path:"/createAssetSku" method:"post" tags:"SKU管理" summary:"创建SKU" dc:"创建新的资产SKU"` + + AssetId *bson.ObjectID `json:"assetId" v:"required" dc:"关联资产ID"` + AssetName string `json:"assetName" v:"required" dc:"关联资产名称"` + SkuName string `json:"skuName" v:"required" dc:"SKU名称"` + SpecsCount int `json:"specsCount" v:"required|min:1" dc:"规格数量"` + SpecsUnit *entity.SpecsUnitKeyValue `json:"specsUnit" v:"required" dc:"规格单位"` + SpecValues []map[string]interface{} `json:"specValues" dc:"规格值"` + ImageURL string `json:"imageUrl" v:"required" dc:"SKU主图"` + Price int `json:"price" v:"required|min:0" dc:"价格(分为单位)"` + Sort int `json:"sort" v:"required|min:0" dc:"排序"` + Status *consts.AssetSkuStatus `json:"status" v:"required|in:1,0" dc:"状态"` + UnlimitedStock bool `json:"unlimitedStock" v:"required" dc:"是否无库存限制"` + StockMode stock.StockMode `json:"stockMode" dc:"库存管理模式:1-明细模式 2-批次模式"` + CategoryId *bson.ObjectID `json:"categoryId" dc:"分类ID"` + CategoryPath string `json:"categoryPath" dc:"分类路径"` + TenantModuleType beans.TenantModuleType `json:"tenantModuleType" dc:"租户模块类型"` +} + +// CreateAssetSkuRes 创建SKU响应 +type CreateAssetSkuRes struct { + Id *bson.ObjectID `json:"id" dc:"SKU ID"` +} + +// UpdateAssetSkuReq 更新SKU请求 +type UpdateAssetSkuReq struct { + g.Meta `path:"/updateAssetSku" method:"put" tags:"SKU管理" summary:"更新SKU" dc:"更新SKU信息"` + + Id *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` + SkuName string `json:"skuName" dc:"SKU名称"` + SpecsCount int `json:"specsCount" dc:"规格数量"` + SpecsUnit *entity.SpecsUnitKeyValue `json:"specsUnit" dc:"规格单位"` + SpecValues []map[string]interface{} `json:"specValues" dc:"规格值"` + ImageURL string `json:"imageUrl" dc:"SKU主图"` + Price int `json:"price" dc:"价格(分为单位)"` + Sort int `json:"sort" dc:"排序"` + Stock int `json:"stock" dc:"库存数量"` + Status *consts.AssetSkuStatus `json:"status" dc:"状态"` +} + +// DeleteAssetSkuReq 删除SKU请求 +type DeleteAssetSkuReq struct { + g.Meta `path:"/deleteAssetSku" method:"delete" tags:"SKU管理" summary:"删除SKU" dc:"删除SKU"` + Id *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` +} + +// GetAssetSkuModuleReq 获取SKU详情请求 +type GetAssetSkuModuleReq struct { + g.Meta `path:"/getAssetSkuModule" method:"get" tags:"SKU管理" summary:"获取SKU模块详情" dc:"获取SKU模块详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` +} + +// GetAssetSkuModuleRes 获取SKU详情响应 +type GetAssetSkuModuleRes struct { + AssetId *bson.ObjectID `json:"assetId"` + ExpireAt *gtime.Time `json:"expireAt"` +} + +// GetAssetSkuReq 获取SKU详情请求 +type GetAssetSkuReq struct { + g.Meta `path:"/getAssetSku" method:"get" tags:"SKU管理" summary:"获取SKU详情" dc:"获取SKU详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` +} + +// GetAssetSkuRes 获取SKU详情响应 +type GetAssetSkuRes struct { + *entity.AssetSku + ImgAddressPrefix string `json:"imgAddressPrefix"` +} + +// ListAssetSkuReq 获取SKU列表请求 +type ListAssetSkuReq struct { + g.Meta `path:"/listAssetSkus" method:"get" tags:"SKU管理" summary:"获取SKU列表" dc:"分页查询SKU列表,支持多条件筛选"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + Id *bson.ObjectID `json:"id" dc:"SKU ID"` + AssetId *bson.ObjectID `json:"assetId" v:"required" dc:"资产ID"` + Status *consts.AssetSkuStatus `json:"status" dc:"状态"` + Keyword string `json:"keyword" dc:"关键词搜索"` + MinPrice int `json:"minPrice" dc:"最低价格"` + MaxPrice int `json:"maxPrice" dc:"最高价格"` + CategoryPath string `json:"categoryPath" dc:"分类路径"` +} + +// ListAssetSkuRes 获取SKU列表响应 +type ListAssetSkuRes struct { + List []*AssetSkuListResItem `json:"list" dc:"SKU列表"` + Total int64 `json:"total" dc:"总数"` +} + +type AssetSkuListResItem struct { + Id *bson.ObjectID `json:"id"` // SKU ID + AssetId *bson.ObjectID `json:"assetId"` + AssetName string `json:"assetName"` // 资产名称 + SkuName string `json:"skuName"` // SKU名称 + SpecsCount int `json:"specsCount"` // 规格数量 + SpecsUnit *entity.SpecsUnitKeyValue `json:"specsUnit"` // 规格单位 + SpecValues []map[string]interface{} `json:"specValues"` // 规格值:{"颜色":"红色","尺寸":"L","时长":"1个月","平台":"抖音"} + Price int `json:"price"` // 价格(分为单位) + UnlimitedStock bool `json:"unlimitedStock"` // 是否无库存限制 + Stock int `json:"stock"` // 库存数量 + Sort int `json:"sort"` // 排序 + Status *consts.AssetSkuStatus `json:"status"` // 状态:active/inactive/disabled + StockMode stock.StockMode `json:"stockMode"` // 库存管理模式:1-明细模式 2-批次模式 + CreatedAt *gtime.Time `json:"createdAt"` // 创建时间 + UpdatedAt *gtime.Time `json:"updatedAt"` // 更新时间 +} diff --git a/model/dto/asset/category_dto.go b/model/dto/asset/category_dto.go new file mode 100644 index 0000000..1dc64c0 --- /dev/null +++ b/model/dto/asset/category_dto.go @@ -0,0 +1,124 @@ +package dto + +import ( + consts "assets/consts/category" + entity "assets/model/entity/asset" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// CreateCategoryReq 创建分类请求 +type CreateCategoryReq struct { + g.Meta `path:"/createCategory" method:"post" tags:"分类管理" summary:"创建分类" dc:"创建新的分类"` + Name string `json:"name" v:"required" dc:"分类名称"` + ParentId string `json:"parentId" dc:"父分类ID"` + Image string `json:"image" dc:"分类图片"` + Sort int `json:"sort" dc:"排序"` + Path string `json:"path" dc:"分类路径"` + Level int `json:"level" dc:"分类层级"` + Status consts.CategoryStatusType `json:"status" v:"in:1,0" default:"1" dc:"状态:1启用0禁用"` + Attrs []entity.CategoryAttr `json:"attrs" dc:"分类属性"` +} + +// CreateCategoryRes 创建分类响应 +type CreateCategoryRes struct { + Bid string `json:"bid" dc:"分类ID"` +} + +// UpdateCategoryReq 更新分类请求 +type UpdateCategoryReq struct { + g.Meta `path:"/updateCategory" method:"put" tags:"分类管理" summary:"更新分类" dc:"更新分类信息"` + Id uint64 `json:"id" v:"required-without:Bid|integer#Id不能为空|Id必须是整数" dc:"分类ID"` + Bid string `json:"bid" v:"required-without:Id|string#Bid不能为空|Bid必须是字符串" dc:"分类ID"` + Name string `json:"name" dc:"分类名称"` + ParentId string `json:"parentId" dc:"父分类ID"` + Image string `json:"image" dc:"分类图片"` + Sort int `json:"sort" dc:"排序"` + IsLeafNode bool `json:"isLeafNode" dc:"是否叶子节点"` + Attrs []entity.CategoryAttr `json:"attrs" dc:"分类属性"` + Status consts.CategoryStatusType `json:"status" dc:"状态:1启用0禁用"` +} + +// UpdateCategoryStatusReq 更新分类状态请求 +type UpdateCategoryStatusReq struct { + g.Meta `path:"/updateCategoryStatus" method:"put" tags:"分类管理" summary:"更新分类状态" dc:"更新分类状态"` + Id string `json:"id" v:"required" dc:"分类ID"` + Status consts.CategoryStatusType `json:"status" v:"in:1,0" dc:"状态:1启用0禁用"` +} + +// DeleteCategoryReq 删除分类请求 +type DeleteCategoryReq struct { + g.Meta `path:"/deleteCategory" method:"delete" tags:"分类管理" summary:"删除分类" dc:"删除分类"` + Bid string `json:"bid" v:"required" dc:"分类ID"` +} + +// GetCategoryTreeReq 获取分类树请求 +type GetCategoryTreeReq struct { + g.Meta `path:"/getCategoryTree" method:"get" tags:"分类管理" summary:"获取分类树" dc:"获取分类树"` + Name string `json:"name" dc:"名称"` +} + +// GetCategoryTreeRes 获取分类树响应 +type GetCategoryTreeRes struct { + Tree []*CategoryTreeNode `json:"tree" dc:"分类树"` +} + +// CategoryTreeNode 分类树节点 +type CategoryTreeNode struct { + Id uint64 `json:"id" dc:"分类ID"` + Bid string `json:"bid" dc:"分类ID"` + Name string `json:"name" dc:"分类名称"` + Level int `json:"level" dc:"分类层级"` + Type string `json:"type" dc:"分类类型"` + Path string `json:"path" dc:"分类路径"` + IsLeafNode bool `json:"isLeafNode" dc:"是否叶子节点"` + Status consts.CategoryStatusType `json:"status" dc:"状态"` + Sort int `json:"sort" dc:"排序"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` + Children []*CategoryTreeNode `json:"children" dc:"子分类"` +} + +// ListCategoryReq 获取分类列表请求 +type ListCategoryReq struct { + g.Meta `path:"/listCategories" method:"get" tags:"分类管理" summary:"获取分类列表" dc:"获取分类列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + ParentId string `json:"parentId" dc:"父分类ID"` + Status consts.CategoryStatusType `json:"status" dc:"状态:1启用0禁用"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListCategoryRes 获取分类列表响应 +type ListCategoryRes struct { + List []GetCategoryRes `json:"list" dc:"分类列表"` + Total int `json:"total" dc:"总数"` +} + +// GetCategoryReq 获取分类详情请求 +type GetCategoryReq struct { + g.Meta `path:"/getCategory" method:"get" tags:"分类管理" summary:"获取分类详情" dc:"获取分类详情"` + Id uint64 `json:"id" v:"required-without:Bid|integer#Id不能为空|Id必须是整数" dc:"分类ID"` + Bid string `json:"bid" v:"required-without:Id|string#Bid不能为空|Bid必须是字符串" dc:"分类ID"` +} + +// GetCategoryRes 获取分类详情响应 +type GetCategoryRes struct { + Id uint64 `json:"id" dc:"分类ID"` + Bid string `json:"bid" dc:"分类ID"` + Name string `json:"name" dc:"分类名称"` + ParentId string `json:"parentId" dc:"父分类ID"` + Path string `json:"path" dc:"分类路径"` + Level int `json:"level" dc:"分类层级"` + IsLeafNode bool `json:"isLeafNode" dc:"是否叶子节点"` + Sort int `json:"sort" dc:"排序"` + Image string `json:"image" dc:"分类图片"` + Attrs []entity.CategoryAttr `json:"attrs" dc:"分类属性"` + Status consts.CategoryStatusType `json:"status" dc:"状态:1启用0禁用"` + Creator string `json:"creator" dc:"创建人"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + Updater string `json:"updater" dc:"更新人"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/model/dto/asset/private_category_dto.go b/model/dto/asset/private_category_dto.go new file mode 100644 index 0000000..a659245 --- /dev/null +++ b/model/dto/asset/private_category_dto.go @@ -0,0 +1,137 @@ +package dto + +import ( + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GeneratePrivateCategoryTestDataReq 生成私域分类测试数据请求 +type GeneratePrivateCategoryTestDataReq struct { + g.Meta `path:"/generateTestData" method:"post" tags:"私域分类管理" summary:"生成测试数据" dc:"生成私域分类测试数据"` +} + +// CreatePrivateCategoryReq 创建私域分类请求 +type CreatePrivateCategoryReq struct { + g.Meta `path:"/createPrivateCategory" method:"post" tags:"私域分类管理" summary:"创建私域分类" dc:"创建新的私域分类"` + + Name string `json:"name" v:"required" dc:"分类名称"` + ParentID *bson.ObjectID `json:"parentId" dc:"父分类ID,为空表示根分类"` + Path string `json:"path" dc:"分类路径,如:/root/parent"` + Level int `json:"level" dc:"分类层级"` + IsLeafNode bool `json:"isLeafNode" dc:"是否叶子节点"` + Sort int `json:"sort" dc:"排序"` + Image string `json:"image" dc:"分类图片"` +} + +// CreatePrivateCategoryRes 创建私域分类响应 +type CreatePrivateCategoryRes struct { + ID *bson.ObjectID `json:"id"` // 分类ID +} + +// BatchCreatePrivateCategoryReq 批量创建私域分类请求 +type BatchCreatePrivateCategoryReq struct { + g.Meta `path:"/batchCreatePrivateCategory" method:"post" tags:"私域分类管理" summary:"批量创建私域分类" dc:"批量创建私域分类"` + + Categories []CreatePrivateCategoryReq `json:"categories" v:"required" dc:"分类列表"` +} + +// BatchCreatePrivateCategoryRes 批量创建私域分类响应 +type BatchCreatePrivateCategoryRes struct { + IDs []*bson.ObjectID `json:"ids"` // 创建的ID列表 +} + +// UpdatePrivateCategoryReq 更新私域分类请求 +type UpdatePrivateCategoryReq struct { + g.Meta `path:"/updatePrivateCategory" method:"put" tags:"私域分类管理" summary:"更新私域分类" dc:"更新私域分类信息"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"分类ID"` + Name string `json:"name" dc:"分类名称"` + ParentID string `json:"parentId" dc:"父分类ID"` + Path string `json:"path" dc:"分类路径"` + Level int `json:"level" dc:"分类层级"` + IsLeafNode *bool `json:"isLeafNode" dc:"是否叶子节点"` + Sort int `json:"sort" dc:"排序"` + Image string `json:"image" dc:"分类图片"` +} + +// DeletePrivateCategoryReq 删除私域分类请求 +type DeletePrivateCategoryReq struct { + g.Meta `path:"/deletePrivateCategory" method:"delete" tags:"私域分类管理" summary:"删除私域分类" dc:"删除私域分类"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"分类ID"` +} + +// GetPrivateCategoryReq 获取私域分类详情请求 +type GetPrivateCategoryReq struct { + g.Meta `path:"/getPrivateCategory" method:"get" tags:"私域分类管理" summary:"获取私域分类详情" dc:"获取私域分类详情"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"分类ID"` +} + +// GetPrivateCategoryRes 获取私域分类详情响应 +type GetPrivateCategoryRes struct { + ID *bson.ObjectID `json:"id"` + Name string `json:"name"` + ParentID string `json:"parentId"` + Path string `json:"path"` + Level int `json:"level"` + IsLeafNode bool `json:"isLeafNode"` + Sort int `json:"sort"` + Image string `json:"image"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListPrivateCategoryReq 获取私域分类列表请求 +type ListPrivateCategoryReq struct { + g.Meta `path:"/listPrivateCategory" method:"get" tags:"私域分类管理" summary:"获取私域分类列表" dc:"分页查询私域分类列表"` + + Name string `json:"name" dc:"分类名称(模糊查询)"` + ParentID string `json:"parentId" dc:"父分类ID"` + Level int `json:"level" dc:"分类层级"` + IsLeafNode *bool `json:"isLeafNode" dc:"是否叶子节点"` + PageNum int `json:"pageNum" dc:"页码"` + PageSize int `json:"pageSize" dc:"每页大小"` +} + +// ListPrivateCategoryRes 获取私域分类列表响应 +type ListPrivateCategoryRes struct { + List []*PrivateCategoryListItem `json:"list" dc:"分类列表"` + Total int64 `json:"total" dc:"总数"` +} + +// PrivateCategoryListItem 私域分类列表项 +type PrivateCategoryListItem struct { + ID *bson.ObjectID `json:"id"` + Name string `json:"name"` + ParentID string `json:"parentId"` + Path string `json:"path"` + Level int `json:"level"` + IsLeafNode bool `json:"isLeafNode"` + Sort int `json:"sort"` + Image string `json:"image"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// GetPrivateCategoryTreeReq 获取私域分类树请求 +type GetPrivateCategoryTreeReq struct { + g.Meta `path:"/getPrivateCategoryTree" method:"get" tags:"私域分类管理" summary:"获取私域分类树" dc:"获取私域分类树"` +} + +// GetPrivateCategoryTreeRes 获取私域分类树响应 +type GetPrivateCategoryTreeRes struct { + Tree []*PrivateCategoryTreeItem `json:"tree" dc:"分类树"` +} + +// PrivateCategoryTreeItem 私域分类树项 +type PrivateCategoryTreeItem struct { + ID *bson.ObjectID `json:"id"` + Name string `json:"name"` + ParentID string `json:"parentId"` + Path string `json:"path"` + Level int `json:"level"` + IsLeafNode bool `json:"isLeafNode"` + Sort int `json:"sort"` + Image string `json:"image"` +} diff --git a/model/dto/asset/private_sku_dto.go b/model/dto/asset/private_sku_dto.go new file mode 100644 index 0000000..bac8e5d --- /dev/null +++ b/model/dto/asset/private_sku_dto.go @@ -0,0 +1,124 @@ +package dto + +import ( + "assets/consts/stock" + "assets/model/config" + + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GeneratePrivateSkuTestDataReq 生成私域SKU测试数据请求 +type GeneratePrivateSkuTestDataReq struct { + g.Meta `path:"/generateTestData" method:"post" tags:"私域SKU管理" summary:"生成测试数据" dc:"生成私域SKU测试数据"` +} + +// CreatePrivateSkuReq 创建私域SKU请求 +type CreatePrivateSkuReq struct { + g.Meta `path:"/createPrivateSku" method:"post" tags:"私域SKU管理" summary:"创建私域SKU" dc:"创建新的私域SKU"` + + SkuName string `json:"skuName" v:"required" dc:"SKU名称"` + ImageURL string `json:"imageUrl" dc:"SKU主图"` + Price int `json:"price" v:"required|min:0" dc:"价格(分为单位)"` + Stock int `json:"stock" v:"min:0" dc:"库存数量"` + Sort int `json:"sort" dc:"排序"` + CapacityUnitType stock.CapacityUnitType `json:"capacityUnitType" v:"required" dc:"容量单位类型"` + Capacity config.Capacity `json:"capacity" v:"required" dc:"容量信息"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` +} + +// CreatePrivateSkuRes 创建私域SKU响应 +type CreatePrivateSkuRes struct { + ID *bson.ObjectID `json:"id"` // SKU ID +} + +// BatchCreatePrivateSkuReq 批量创建私域SKU请求 +type BatchCreatePrivateSkuReq struct { + g.Meta `path:"/batchCreatePrivateSku" method:"post" tags:"私域SKU管理" summary:"批量创建私域SKU" dc:"批量创建私域SKU"` + + Skus []CreatePrivateSkuReq `json:"skus" v:"required" dc:"SKU列表"` +} + +// BatchCreatePrivateSkuRes 批量创建私域SKU响应 +type BatchCreatePrivateSkuRes struct { + IDs []*bson.ObjectID `json:"ids"` // 创建的ID列表 +} + +// UpdatePrivateSkuReq 更新私域SKU请求 +type UpdatePrivateSkuReq struct { + g.Meta `path:"/updatePrivateSku" method:"put" tags:"私域SKU管理" summary:"更新私域SKU" dc:"更新私域SKU信息"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` + SkuName string `json:"skuName" dc:"SKU名称"` + ImageURL string `json:"imageUrl" dc:"SKU主图"` + Price int `json:"price" v:"min:0" dc:"价格(分为单位)"` + Stock int `json:"stock" v:"min:0" dc:"库存数量"` + Sort int `json:"sort" dc:"排序"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` +} + +// DeletePrivateSkuReq 删除私域SKU请求 +type DeletePrivateSkuReq struct { + g.Meta `path:"/deletePrivateSku" method:"delete" tags:"私域SKU管理" summary:"删除私域SKU" dc:"删除私域SKU"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` +} + +// GetPrivateSkuReq 获取私域SKU详情请求 +type GetPrivateSkuReq struct { + g.Meta `path:"/getPrivateSku" method:"get" tags:"私域SKU管理" summary:"获取私域SKU详情" dc:"获取私域SKU详情"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` +} + +// GetPrivateSkuRes 获取私域SKU详情响应 +type GetPrivateSkuRes struct { + ID *bson.ObjectID `json:"id"` + SkuName string `json:"skuName"` + ImageURL string `json:"imageUrl"` + Price int `json:"price"` + Stock int `json:"stock"` + Sort int `json:"sort"` + PrivateCategoryPath string `json:"privateCategoryPath"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListPrivateSkuReq 获取私域SKU列表请求 +type ListPrivateSkuReq struct { + g.Meta `path:"/listPrivateSku" method:"get" tags:"私域SKU管理" summary:"获取私域SKU列表" dc:"分页查询私域SKU列表"` + + SkuName string `json:"skuName" dc:"SKU名称(模糊查询)"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"分类路径"` + MinPrice int `json:"minPrice" dc:"最低价格"` + MaxPrice int `json:"maxPrice" dc:"最高价格"` + PageNum int `json:"pageNum" dc:"页码"` + PageSize int `json:"pageSize" dc:"每页大小"` +} + +// ListPrivateSkuRes 获取私域SKU列表响应 +type ListPrivateSkuRes struct { + List []*PrivateSkuListItem `json:"list" dc:"SKU列表"` + Total int64 `json:"total" dc:"总数"` +} + +// PrivateSkuListItem 私域SKU列表项 +type PrivateSkuListItem struct { + ID *bson.ObjectID `json:"id"` + SkuName string `json:"skuName"` + ImageURL string `json:"imageUrl"` + Price int `json:"price"` + Stock int `json:"stock"` + Sort int `json:"sort"` + PrivateCategoryPath string `json:"privateCategoryPath"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// UpdatePrivateSkuStockReq 更新私域SKU库存请求 +type UpdatePrivateSkuStockReq struct { + g.Meta `path:"/updatePrivateSkuStock" method:"put" tags:"私域SKU管理" summary:"更新私域SKU库存" dc:"更新私域SKU库存"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"SKU ID"` + StockChange int `json:"stockChange" v:"required" dc:"库存变化量(正数增加,负数减少)"` +} diff --git a/model/dto/enum/enum_dto.go b/model/dto/enum/enum_dto.go new file mode 100644 index 0000000..77f8101 --- /dev/null +++ b/model/dto/enum/enum_dto.go @@ -0,0 +1,73 @@ +package dto + +import ( + "assets/consts/asset" + "github.com/gogf/gf/v2/frame/g" +) + +// GetAssetTypeReq 获取资产类型请求 +type GetAssetTypeReq struct { + g.Meta `path:"/getAssetType" method:"get" tags:"资产管理" summary:"获取资产类型选项" dc:"获取所有资产类型的选项列表"` +} + +// GetAssetTypeRes 获取资产类型响应 +type GetAssetTypeRes struct { + Options []KeyValue `json:"options" dc:"资产类型选项列表"` +} + +// GetCategoryAttrTypeReq 获取分类属性类型请求 +type GetCategoryAttrTypeReq struct { + g.Meta `path:"/getCategoryAttrType" method:"get" tags:"枚举管理" summary:"获取分类属性类型" dc:"获取分类属性类型"` +} + +// GetCategoryAttrTypeRes 获取分类属性类型响应 +type GetCategoryAttrTypeRes struct { + Options []KeyValue `json:"options" dc:"分类属性类型选项列表"` +} + +// GetSpecsUnitReq 获取规格单位 +type GetSpecsUnitReq struct { + g.Meta `path:"/getSpecsUnit" method:"get" tags:"枚举管理" summary:"获取规格单位" dc:"获取规格单位"` + AssetType *consts.AssetType `json:"assetType" v:"required|in:physical,virtual,service" dc:"资产类型"` +} + +// GetSpecsUnitRes 获取规格单位响应 +type GetSpecsUnitRes struct { + Options []KeyValue `json:"options" dc:"规格单位选项列表"` +} + +// GetTenantModuleTypeReq 获取租户模块类型请求 +type GetTenantModuleTypeReq struct { + g.Meta `path:"/getTenantModuleType" method:"get" tags:"枚举管理" summary:"获取租户模块类型" dc:"获取租户模块类型"` + AssetId string `json:"assetId" v:"required|in:physical,virtual,service" dc:"资产id"` +} + +// GetTenantModuleTypeRes 获取租户模块类型响应 +type GetTenantModuleTypeRes struct { + Options []KeyValue `json:"options" dc:"租户模块类型列表"` +} + +type KeyValue struct { + Key interface{} `json:"key"` // 对应原有常量值 + Value interface{} `json:"value"` // 对应描述信息 +} + +type GetDictRes struct { + g.Meta `mime:"application/json"` + Info *DictTypeRes `json:"info"` + Values []*DictDataRes `json:"values"` +} + +type DictTypeRes struct { + DictName string `json:"name"` + DictType string `json:"type"` + Remark string `json:"remark"` +} + +type DictDataRes struct { + DictValue string `json:"key"` + DictLabel string `json:"value"` + DictType string `json:"type"` + IsDefault int `json:"isDefault"` + Remark string `json:"remark"` +} diff --git a/model/dto/procurement/purchase_inbound_dto.go b/model/dto/procurement/purchase_inbound_dto.go new file mode 100644 index 0000000..9245747 --- /dev/null +++ b/model/dto/procurement/purchase_inbound_dto.go @@ -0,0 +1,96 @@ +package dto + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreatePurchaseInboundReq 创建采购入库请求 +type CreatePurchaseInboundReq struct { + g.Meta `path:"/createPurchaseInbound" method:"post" tags:"采购入库管理" summary:"创建采购入库" dc:"将采购订单明细入库到私域库存"` + + OrderItemId *bson.ObjectID `json:"orderItemId" v:"required" dc:"采购订单明细ID"` + InboundQty int `json:"inboundQty" v:"required|min:1" dc:"入库数量"` + + // 仓储信息(非必填) + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + LocationId *bson.ObjectID `json:"locationId" dc:"库位ID"` + + // 私域SKU和分类(必填) + PrivateSkuId *bson.ObjectID `json:"privateSkuId" v:"required" dc:"私域SKU ID"` + PrivateCategoryId *bson.ObjectID `json:"privateCategoryId" v:"required" dc:"私域分类ID"` + + Remark string `json:"remark" dc:"入库备注"` +} + +// CreatePurchaseInboundRes 创建采购入库响应 +type CreatePurchaseInboundRes struct { + Id *bson.ObjectID `json:"id" dc:"入库记录ID"` + InboundNo string `json:"inboundNo" dc:"入库单号"` + BatchNo string `json:"batchNo" dc:"批次号"` +} + +// GetPurchaseInboundReq 获取入库详情请求 +type GetPurchaseInboundReq struct { + g.Meta `path:"/getPurchaseInbound" method:"get" tags:"采购入库管理" summary:"获取入库详情" dc:"根据ID获取入库记录详情"` + + Id *bson.ObjectID `json:"id" v:"required" dc:"入库记录ID"` +} + +// GetPurchaseInboundRes 获取入库详情响应 +type GetPurchaseInboundRes struct { + Id *bson.ObjectID `json:"id" dc:"入库记录ID"` + InboundNo string `json:"inboundNo" dc:"入库单号"` + BatchNo string `json:"batchNo" dc:"批次号"` + + // 关联信息 + OrderId *bson.ObjectID `json:"orderId" dc:"采购订单ID"` + OrderItemId *bson.ObjectID `json:"orderItemId" dc:"采购订单明细ID"` + + // 入库数量和时间 + InboundQty int `json:"inboundQty" dc:"本次入库数量"` + InboundDate string `json:"inboundDate" dc:"入库日期"` + + // 仓储信息 + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationId *bson.ObjectID `json:"locationId" dc:"库位ID"` + LocationName string `json:"locationName" dc:"库位名称"` + + // 私域SKU和分类 + PrivateSkuId *bson.ObjectID `json:"privateSkuId" dc:"私域SKU ID"` + PrivateSkuName string `json:"privateSkuName" dc:"私域SKU名称"` + PrivateCategoryId *bson.ObjectID `json:"privateCategoryId" dc:"私域分类ID"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` + + // 生成的库存信息 + PrivateStockId *bson.ObjectID `json:"privateStockId" dc:"关联的私域库存ID"` + + Remark string `json:"remark" dc:"入库备注"` + CreatedAt string `json:"createdAt" dc:"创建时间"` + UpdatedAt string `json:"updatedAt" dc:"更新时间"` +} + +// ListPurchaseInboundReq 获取入库列表请求 +type ListPurchaseInboundReq struct { + g.Meta `path:"/listPurchaseInbounds" method:"get" tags:"采购入库管理" summary:"获取入库列表" dc:"分页查询入库记录列表"` + + OrderId *bson.ObjectID `json:"orderId" dc:"采购订单ID"` + OrderItemId *bson.ObjectID `json:"orderItemId" dc:"采购订单明细ID"` + InboundNo string `json:"inboundNo" dc:"入库单号"` + StartDate string `json:"startDate" dc:"开始日期(YYYY-MM-DD)"` + EndDate string `json:"endDate" dc:"结束日期(YYYY-MM-DD)"` + + beans.Page `json:",inline"` + beans.OrderBy `json:",inline"` +} + +// ListPurchaseInboundRes 获取入库列表响应 +type ListPurchaseInboundRes struct { + Total int64 `json:"total" dc:"总数"` + List []*GetPurchaseInboundRes `json:"list" dc:"列表"` +} diff --git a/model/dto/procurement/purchase_order_dto.go b/model/dto/procurement/purchase_order_dto.go new file mode 100644 index 0000000..fd7d8b2 --- /dev/null +++ b/model/dto/procurement/purchase_order_dto.go @@ -0,0 +1,232 @@ +package dto + +import ( + "assets/consts/procurement" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GeneratePurchaseOrderTestDataReq 生成采购订单测试数据请求 +type GeneratePurchaseOrderTestDataReq struct { + g.Meta `path:"/generateTestData" method:"post" tags:"采购订单管理" summary:"生成测试数据" dc:"生成采购订单测试数据"` +} + +// CreatePurchaseOrderReq 创建采购订单请求 +type CreatePurchaseOrderReq struct { + g.Meta `path:"/createPurchaseOrder" method:"post" tags:"采购订单管理" summary:"创建采购订单" dc:"创建新的采购订单"` + + // 基础订单信息 + OrderNo string `json:"orderNo" v:"required" dc:"订单编号"` + Title string `json:"title" v:"required" dc:"订单标题"` + Description string `json:"description" dc:"订单描述"` + OrderType consts.PurchaseOrderType `json:"orderType" v:"required" dc:"订单类型:direct/assignment/bidding"` + + // 需求方信息 + BuyerId *bson.ObjectID `json:"buyerId" v:"required" dc:"采购方ID(经销商/门店)"` + BuyerName string `json:"buyerName" v:"required" dc:"采购方名称"` + BuyerType string `json:"buyerType" v:"required" dc:"采购方类型"` + + // 通用状态信息 + Priority int `json:"priority" dc:"优先级"` + + // 通用字段 + ExpectedDelivery *gtime.Time `json:"expectedDelivery" dc:"期望交付时间"` + ExpiryTime *gtime.Time `json:"expiryTime" dc:"订单有效期/竞价结束时间"` + + // 模式特定信息 + DirectPurchase *CreateDirectPurchaseReq `json:"directPurchase" dc:"指定供应商模式信息"` + BiddingInfo *CreateBiddingReq `json:"biddingInfo" dc:"竞价模式信息"` +} + +// CreateDirectPurchaseReq 指定供应商模式信息 +type CreateDirectPurchaseReq struct { + SupplierId *bson.ObjectID `json:"supplierId" v:"required" dc:"指定供应商ID"` + SupplierName string `json:"supplierName" dc:"指定供应商名称"` + SupplierCode string `json:"supplierCode" dc:"供应商编码"` + AssignReason string `json:"assignReason" dc:"指派原因"` + DeliveryAddress string `json:"deliveryAddress" dc:"交付地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` +} + +// CreateBiddingReq 竞价模式信息 +type CreateBiddingReq struct { + BidMode consts.BidMode `json:"bidMode" v:"required" dc:"竞价模式:price/quality/time/mixed"` + MinSuppliers int `json:"minSuppliers" v:"min:1" dc:"最少参与供应商数"` + MaxSuppliers int `json:"maxSuppliers" v:"min:1" dc:"最多参与供应商数"` + BidDuration int `json:"bidDuration" v:"min:1" dc:"竞价持续时长(分钟)"` + BidStartAt *gtime.Time `json:"bidStartAt" dc:"竞价开始时间"` + BidEndAt *gtime.Time `json:"bidEndAt" dc:"竞价结束时间"` +} + +// CreatePurchaseOrderRes 创建采购订单响应 +type CreatePurchaseOrderRes struct { + ID *bson.ObjectID `json:"id"` // 采购订单ID +} + +// UpdatePurchaseOrderReq 更新采购订单请求 +type UpdatePurchaseOrderReq struct { + g.Meta `path:"/updatePurchaseOrder" method:"put" tags:"采购订单管理" summary:"更新采购订单" dc:"更新采购订单信息"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单ID"` + + // 基础订单信息 + Title string `json:"title" dc:"订单标题"` + Description string `json:"description" dc:"订单描述"` + OrderType consts.PurchaseOrderType `json:"orderType" dc:"订单类型"` + + // 需求方信息 + BuyerId *bson.ObjectID `json:"buyerId" dc:"采购方ID"` + BuyerName string `json:"buyerName" dc:"采购方名称"` + BuyerType string `json:"buyerType" dc:"采购方类型"` + + // 通用状态信息 + Status consts.PurchaseOrderStatus `json:"status" dc:"订单状态"` + Priority int `json:"priority" dc:"优先级"` + + // 通用字段 + ExpectedDelivery *gtime.Time `json:"expectedDelivery" dc:"期望交付时间"` + ExpiryTime *gtime.Time `json:"expiryTime" dc:"订单有效期/竞价结束时间"` + + // 模式特定信息 + DirectPurchase *UpdateDirectPurchaseReq `json:"directPurchase" dc:"指定供应商模式信息"` + BiddingInfo *UpdateBiddingReq `json:"biddingInfo" dc:"竞价模式信息"` +} + +// UpdateDirectPurchaseReq 更新指定供应商模式信息 +type UpdateDirectPurchaseReq struct { + SupplierId *bson.ObjectID `json:"supplierId" dc:"指定供应商ID"` + SupplierName string `json:"supplierName" dc:"指定供应商名称"` + SupplierCode string `json:"supplierCode" dc:"供应商编码"` + AssignReason string `json:"assignReason" dc:"指派原因"` + DeliveryAddress string `json:"deliveryAddress" dc:"交付地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` + ResponseStatus string `json:"responseStatus" dc:"供应商响应状态"` +} + +// UpdateBiddingReq 更新竞价模式信息 +type UpdateBiddingReq struct { + BidMode consts.BidMode `json:"bidMode" dc:"竞价模式"` + MinSuppliers int `json:"minSuppliers" dc:"最少参与供应商数"` + MaxSuppliers int `json:"maxSuppliers" dc:"最多参与供应商数"` + BidDuration int `json:"bidDuration" dc:"竞价持续时长(分钟)"` + BidSupplierCount int `json:"bidSupplierCount" dc:"参与竞价的供应商数量"` + BidStartAt *gtime.Time `json:"bidStartAt" dc:"竞价开始时间"` + BidEndAt *gtime.Time `json:"bidEndAt" dc:"竞价结束时间"` +} + +// DeletePurchaseOrderReq 删除采购订单请求 +type DeletePurchaseOrderReq struct { + g.Meta `path:"/deletePurchaseOrder" method:"delete" tags:"采购订单管理" summary:"删除采购订单" dc:"删除采购订单"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单ID"` +} + +// GetPurchaseOrderReq 获取采购订单详情请求 +type GetPurchaseOrderReq struct { + g.Meta `path:"/getPurchaseOrder" method:"get" tags:"采购订单管理" summary:"获取采购订单详情" dc:"获取采购订单详情"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单ID"` +} + +// GetPurchaseOrderRes 获取采购订单详情响应 +type GetPurchaseOrderRes struct { + ID *bson.ObjectID `json:"id"` + OrderNo string `json:"orderNo"` + Title string `json:"title"` + Description string `json:"description"` + OrderType consts.PurchaseOrderType `json:"orderType"` + BuyerId *bson.ObjectID `json:"buyerId"` + BuyerName string `json:"buyerName"` + BuyerType string `json:"buyerType"` + Status consts.PurchaseOrderStatus `json:"status"` + StatusText string `json:"statusText"` + Priority int `json:"priority"` + DirectPurchase *DirectPurchaseInfoRes `json:"directPurchase"` + BiddingInfo *BiddingInfoRes `json:"biddingInfo"` + ExpectedDelivery string `json:"expectedDelivery"` + ExpiryTime string `json:"expiryTime"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// DirectPurchaseInfoRes 指定供应商模式信息响应 +type DirectPurchaseInfoRes struct { + SupplierId *bson.ObjectID `json:"supplierId"` + SupplierName string `json:"supplierName"` + SupplierCode string `json:"supplierCode"` + AssignReason string `json:"assignReason"` + DeliveryAddress string `json:"deliveryAddress"` + ContactPerson string `json:"contactPerson"` + ContactPhone string `json:"contactPhone"` + ResponseStatus string `json:"responseStatus"` + AssignedAt string `json:"assignedAt"` + AcceptedAt string `json:"acceptedAt"` + RejectedAt string `json:"rejectedAt"` + DeliveredAt string `json:"deliveredAt"` +} + +// BiddingInfoRes 竞价模式信息响应 +type BiddingInfoRes struct { + BidMode consts.BidMode `json:"bidMode"` + BidModeText string `json:"bidModeText"` + MinSuppliers int `json:"minSuppliers"` + MaxSuppliers int `json:"maxSuppliers"` + BidDuration int `json:"bidDuration"` + BidSupplierCount int `json:"bidSupplierCount"` + BidStartAt string `json:"bidStartAt"` + BidEndAt string `json:"bidEndAt"` + ResultPublishedAt string `json:"resultPublishedAt"` +} + +// ListPurchaseOrdersReq 获取采购订单列表请求 +type ListPurchaseOrdersReq struct { + g.Meta `path:"/listPurchaseOrders" method:"get" tags:"采购订单管理" summary:"获取采购订单列表" dc:"分页查询采购订单列表"` + + OrderNo string `json:"orderNo" dc:"订单编号(精确查询)"` + Title string `json:"title" dc:"订单标题(模糊查询)"` + BuyerId *bson.ObjectID `json:"buyerId" dc:"采购方ID(精确查询)"` + OrderType consts.PurchaseOrderType `json:"orderType" dc:"订单类型"` + Status *consts.PurchaseOrderStatus `json:"status" dc:"订单状态"` + PageNum int `json:"pageNum" dc:"页码"` + PageSize int `json:"pageSize" dc:"每页大小"` +} + +// ListPurchaseOrdersRes 获取采购订单列表响应 +type ListPurchaseOrdersRes struct { + List []*PurchaseOrderListItem `json:"list" dc:"采购订单列表"` + Total int64 `json:"total" dc:"总数"` +} + +// PurchaseOrderListItem 采购订单列表项 +type PurchaseOrderListItem struct { + ID *bson.ObjectID `json:"id"` + OrderNo string `json:"orderNo"` + Title string `json:"title"` + OrderType consts.PurchaseOrderType `json:"orderType"` + OrderTypeText string `json:"orderTypeText"` + BuyerName string `json:"buyerName"` + BuyerType string `json:"buyerType"` + Status consts.PurchaseOrderStatus `json:"status"` + StatusText string `json:"statusText"` + Priority int `json:"priority"` + ExpectedDelivery string `json:"expectedDelivery"` + ExpiryTime string `json:"expiryTime"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// BatchCreatePurchaseOrdersReq 批量创建采购订单请求 +type BatchCreatePurchaseOrdersReq struct { + g.Meta `path:"/batchCreatePurchaseOrders" method:"post" tags:"采购订单管理" summary:"批量创建采购订单" dc:"批量创建采购订单"` + + Orders []CreatePurchaseOrderReq `json:"orders" v:"required" dc:"采购订单列表"` +} + +// BatchCreatePurchaseOrdersRes 批量创建采购订单响应 +type BatchCreatePurchaseOrdersRes struct { + IDs []*bson.ObjectID `json:"ids"` // 创建的ID列表 +} diff --git a/model/dto/procurement/purchase_order_item_dto.go b/model/dto/procurement/purchase_order_item_dto.go new file mode 100644 index 0000000..04eaad7 --- /dev/null +++ b/model/dto/procurement/purchase_order_item_dto.go @@ -0,0 +1,168 @@ +package dto + +import ( + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GeneratePurchaseOrderItemTestDataReq 生成采购订单明细测试数据请求 +type GeneratePurchaseOrderItemTestDataReq struct { + g.Meta `path:"/generateTestData" method:"post" tags:"采购订单明细管理" summary:"生成测试数据" dc:"生成采购订单明细测试数据"` +} + +// CreatePurchaseOrderItemReq 创建采购订单明细请求 +type CreatePurchaseOrderItemReq struct { + g.Meta `path:"/createPurchaseOrderItem" method:"post" tags:"采购订单明细管理" summary:"创建采购订单明细" dc:"创建新的采购订单明细"` + + // 关联信息 + OrderId *bson.ObjectID `json:"orderId" v:"required" dc:"订单ID"` + AssetId *bson.ObjectID `json:"assetId" v:"required" dc:"资产ID"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + + // 商品信息 + ProductName string `json:"productName" v:"required" dc:"商品名称"` + Specification string `json:"specification" dc:"规格描述"` + Brand string `json:"brand" dc:"品牌"` + + // 数量和价格 + Quantity int `json:"quantity" v:"required|min:1" dc:"订购数量"` + Unit string `json:"unit" v:"required" dc:"单位"` + UnitPrice int `json:"unitPrice" v:"required|min:0" dc:"单价(分)"` + TotalPrice int `json:"totalPrice" v:"required|min:0" dc:"总价(分)"` + DiscountPrice int `json:"discountPrice" v:"min:0" dc:"折扣价(分)"` + + // 要求信息 + RequirementDesc string `json:"requirementDesc" dc:"特殊要求描述"` + DeliveryAddress string `json:"deliveryAddress" dc:"交付地址"` +} + +// CreatePurchaseOrderItemRes 创建采购订单明细响应 +type CreatePurchaseOrderItemRes struct { + ID *bson.ObjectID `json:"id"` // 采购订单明细ID +} + +// UpdatePurchaseOrderItemReq 更新采购订单明细请求 +type UpdatePurchaseOrderItemReq struct { + g.Meta `path:"/updatePurchaseOrderItem" method:"put" tags:"采购订单明细管理" summary:"更新采购订单明细" dc:"更新采购订单明细信息"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单明细ID"` + + // 关联信息 + AssetId *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + + // 商品信息 + ProductName string `json:"productName" dc:"商品名称"` + Specification string `json:"specification" dc:"规格描述"` + Brand string `json:"brand" dc:"品牌"` + + // 数量和价格 + Quantity int `json:"quantity" v:"min:1" dc:"订购数量"` + Unit string `json:"unit" dc:"单位"` + UnitPrice int `json:"unitPrice" v:"min:0" dc:"单价(分)"` + TotalPrice int `json:"totalPrice" v:"min:0" dc:"总价(分)"` + DiscountPrice int `json:"discountPrice" v:"min:0" dc:"折扣价(分)"` + + // 签收和入库 + PassQuantity int `json:"passQuantity" dc:"签收数量"` + InboundQty int `json:"inboundQty" dc:"已入库数量"` + + // 要求信息 + RequirementDesc string `json:"requirementDesc" dc:"特殊要求描述"` + DeliveryAddress string `json:"deliveryAddress" dc:"交付地址"` +} + +// UpdatePurchaseOrderItemRes 更新采购订单明细响应 +type UpdatePurchaseOrderItemRes struct { + ID *bson.ObjectID `json:"id"` // 采购订单明细ID +} + +// DeletePurchaseOrderItemReq 删除采购订单明细请求 +type DeletePurchaseOrderItemReq struct { + g.Meta `path:"/deletePurchaseOrderItem" method:"delete" tags:"采购订单明细管理" summary:"删除采购订单明细" dc:"删除采购订单明细"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单明细ID"` +} + +// DeletePurchaseOrderItemRes 删除采购订单明细响应 +type DeletePurchaseOrderItemRes struct { + ID *bson.ObjectID `json:"id"` // 采购订单明细ID +} + +// GetPurchaseOrderItemReq 获取采购订单明细详情请求 +type GetPurchaseOrderItemReq struct { + g.Meta `path:"/getPurchaseOrderItem" method:"get" tags:"采购订单明细管理" summary:"获取采购订单明细详情" dc:"获取采购订单明细详情"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"采购订单明细ID"` +} + +// GetPurchaseOrderItemRes 获取采购订单明细详情响应 +type GetPurchaseOrderItemRes struct { + ID *bson.ObjectID `json:"id"` + OrderId *bson.ObjectID `json:"orderId"` + AssetId *bson.ObjectID `json:"assetId"` + AssetSkuId *bson.ObjectID `json:"assetSkuId"` + ProductName string `json:"productName"` + Specification string `json:"specification"` + Brand string `json:"brand"` + Quantity int `json:"quantity"` + Unit string `json:"unit"` + UnitPrice int `json:"unitPrice"` + TotalPrice int `json:"totalPrice"` + DiscountPrice int `json:"discountPrice"` + RequirementDesc string `json:"requirementDesc"` + DeliveryAddress string `json:"deliveryAddress"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListPurchaseOrderItemsReq 获取采购订单明细列表请求 +type ListPurchaseOrderItemsReq struct { + g.Meta `path:"/listPurchaseOrderItems" method:"get" tags:"采购订单明细管理" summary:"获取采购订单明细列表" dc:"分页查询采购订单明细列表"` + + OrderId *bson.ObjectID `json:"orderId" dc:"订单ID(精确查询)"` + AssetId *bson.ObjectID `json:"assetId" dc:"资产ID(精确查询)"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID(精确查询)"` + ProductName string `json:"productName" dc:"商品名称(模糊查询)"` + Brand string `json:"brand" dc:"品牌(模糊查询)"` + PageNum int `json:"pageNum" dc:"页码"` + PageSize int `json:"pageSize" dc:"每页大小"` +} + +// ListPurchaseOrderItemsRes 获取采购订单明细列表响应 +type ListPurchaseOrderItemsRes struct { + List []*PurchaseOrderItemListItem `json:"list" dc:"采购订单明细列表"` + Total int64 `json:"total" dc:"总数"` +} + +// PurchaseOrderItemListItem 采购订单明细列表项 +type PurchaseOrderItemListItem struct { + ID *bson.ObjectID `json:"id"` + OrderId *bson.ObjectID `json:"orderId"` + AssetId *bson.ObjectID `json:"assetId"` + AssetSkuId *bson.ObjectID `json:"assetSkuId"` + ProductName string `json:"productName"` + Specification string `json:"specification"` + Brand string `json:"brand"` + Quantity int `json:"quantity"` + Unit string `json:"unit"` + UnitPrice int `json:"unitPrice"` + TotalPrice int `json:"totalPrice"` + DiscountPrice int `json:"discountPrice"` + RequirementDesc string `json:"requirementDesc"` + DeliveryAddress string `json:"deliveryAddress"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// BatchCreatePurchaseOrderItemsReq 批量创建采购订单明细请求 +type BatchCreatePurchaseOrderItemsReq struct { + g.Meta `path:"/batchCreatePurchaseOrderItems" method:"post" tags:"采购订单明细管理" summary:"批量创建采购订单明细" dc:"批量创建采购订单明细"` + + Items []CreatePurchaseOrderItemReq `json:"items" v:"required" dc:"采购订单明细列表"` +} + +// BatchCreatePurchaseOrderItemsRes 批量创建采购订单明细响应 +type BatchCreatePurchaseOrderItemsRes struct { + IDs []*bson.ObjectID `json:"ids"` // 创建的ID列表 +} diff --git a/model/dto/procurement/supplier_dto.go b/model/dto/procurement/supplier_dto.go new file mode 100644 index 0000000..8bb9c70 --- /dev/null +++ b/model/dto/procurement/supplier_dto.go @@ -0,0 +1,253 @@ +package dto + +import ( + "assets/consts/procurement" + + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GenerateSupplierTestDataReq 生成供应商测试数据请求 +type GenerateSupplierTestDataReq struct { + g.Meta `path:"/generateTestData" method:"post" tags:"供应商管理" summary:"生成测试数据" dc:"生成供应商测试数据"` +} + +// CreateSupplierReq 创建供应商请求 +type CreateSupplierReq struct { + g.Meta `path:"/createSupplier" method:"post" tags:"供应商管理" summary:"创建供应商" dc:"创建新的供应商(仅基础信息)"` + + // 基础信息(最小必要字段) + Name string `json:"name" v:"required|max-length:100#供应商名称不能为空|供应商名称不能超过100个字符" dc:"供应商名称(必填)"` + Code string `json:"code" v:"required|max-length:50#供应商编码不能为空|供应商编码不能超过50个字符" dc:"供应商编码(必填)"` + ShortName string `json:"shortName" dc:"供应商简称"` + Alias []string `json:"alias" dc:"别名列表"` + Logo string `json:"logo" dc:"供应商LOGO URL"` + + // 联系信息 + Phone string `json:"phone" dc:"供应商电话"` + Mobile string `json:"mobile" dc:"手机号码"` + Email string `json:"email" dc:"邮箱地址"` + Website string `json:"website" dc:"官网地址"` + ContactPerson string `json:"contactPerson" dc:"联系人姓名"` + ContactPhone string `json:"contactPhone" dc:"联系人电话"` + ContactEmail string `json:"contactEmail" dc:"联系人邮箱"` + ContactPosition string `json:"contactPosition" dc:"联系人职位"` + + // 地址信息 + Country string `json:"country" dc:"国家"` + Province string `json:"province" dc:"省份"` + City string `json:"city" dc:"城市"` + District string `json:"district" dc:"区县"` + Address string `json:"address" dc:"详细地址"` + PostalCode string `json:"postalCode" dc:"邮政编码"` + + // 企业资质信息 + BusinessLicense string `json:"businessLicense" dc:"营业执照号"` + LegalPerson string `json:"legalPerson" dc:"法定代表人"` + TaxNumber string `json:"taxNumber" dc:"税务登记号"` + BankName string `json:"bankName" dc:"开户银行"` + BankAccount string `json:"bankAccount" dc:"银行账号"` + BankAccountName string `json:"bankAccountName" dc:"账户名称"` + + // 业务合作信息 + SupplierLevel string `json:"supplierLevel" dc:"供应商等级"` + PaymentMethod string `json:"paymentMethod" dc:"结算方式"` + PaymentPeriod int `json:"paymentPeriod" dc:"付款周期(天)"` + TaxRate float64 `json:"taxRate" dc:"税率(如0.13表示13%)"` + MinOrderAmount int `json:"minOrderAmount" dc:"最小订货金额(分)"` + + // 经营品类信息 + MainCategories []string `json:"mainCategories" dc:"主营品类ID列表"` + BusinessScope string `json:"businessScope" dc:"经营范围"` + + // 状态信息 + Status consts.SupplierStatus `json:"status" dc:"供应商状态"` + + // 备注和标签 + Remark string `json:"remark" dc:"备注信息"` + Tags []string `json:"tags" dc:"标签列表"` +} + +// CreateSupplierRes 创建供应商响应 +type CreateSupplierRes struct { + ID *bson.ObjectID `json:"id"` // 供应商ID +} + +// BatchCreateSuppliersReq 批量创建供应商请求 +type BatchCreateSuppliersReq struct { + g.Meta `path:"/batchCreateSuppliers" method:"post" tags:"供应商管理" summary:"批量创建供应商" dc:"批量创建供应商"` + + Suppliers []CreateSupplierReq `json:"suppliers" v:"required" dc:"供应商列表"` +} + +// BatchCreateSuppliersRes 批量创建供应商响应 +type BatchCreateSuppliersRes struct { + IDs []*bson.ObjectID `json:"ids"` // 创建的ID列表 +} + +// UpdateSupplierReq 更新供应商请求 +type UpdateSupplierReq struct { + g.Meta `path:"/updateSupplier" method:"put" tags:"供应商管理" summary:"更新供应商" dc:"更新供应商信息"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"供应商ID"` + + // 基础信息 + Name string `json:"name" dc:"供应商名称"` + Code string `json:"code" dc:"供应商编码"` + ShortName string `json:"shortName" dc:"供应商简称"` + Alias []string `json:"alias" dc:"别名列表"` + Logo string `json:"logo" dc:"供应商LOGO URL"` + + // 联系信息 + Phone string `json:"phone" dc:"供应商电话"` + Mobile string `json:"mobile" dc:"手机号码"` + Email string `json:"email" dc:"邮箱地址"` + Website string `json:"website" dc:"官网地址"` + ContactPerson string `json:"contactPerson" dc:"联系人姓名"` + ContactPhone string `json:"contactPhone" dc:"联系人电话"` + ContactEmail string `json:"contactEmail" dc:"联系人邮箱"` + ContactPosition string `json:"contactPosition" dc:"联系人职位"` + + // 地址信息 + Country string `json:"country" dc:"国家"` + Province string `json:"province" dc:"省份"` + City string `json:"city" dc:"城市"` + District string `json:"district" dc:"区县"` + Address string `json:"address" dc:"详细地址"` + PostalCode string `json:"postalCode" dc:"邮政编码"` + + // 企业资质信息 + BusinessLicense string `json:"businessLicense" dc:"营业执照号"` + LegalPerson string `json:"legalPerson" dc:"法定代表人"` + TaxNumber string `json:"taxNumber" dc:"税务登记号"` + BankName string `json:"bankName" dc:"开户银行"` + BankAccount string `json:"bankAccount" dc:"银行账号"` + BankAccountName string `json:"bankAccountName" dc:"账户名称"` + + // 业务合作信息 + SupplierLevel string `json:"supplierLevel" dc:"供应商等级"` + PaymentMethod string `json:"paymentMethod" dc:"结算方式"` + PaymentPeriod int `json:"paymentPeriod" dc:"付款周期(天)"` + TaxRate float64 `json:"taxRate" dc:"税率(如0.13表示13%)"` + MinOrderAmount int `json:"minOrderAmount" dc:"最小订货金额(分)"` + + // 经营品类信息 + MainCategories []string `json:"mainCategories" dc:"主营品类ID列表"` + BusinessScope string `json:"businessScope" dc:"经营范围"` + + // 状态信息 + Status consts.SupplierStatus `json:"status" dc:"供应商状态"` + + // 备注和标签 + Remark string `json:"remark" dc:"备注信息"` + Tags []string `json:"tags" dc:"标签列表"` +} + +// DeleteSupplierReq 删除供应商请求 +type DeleteSupplierReq struct { + g.Meta `path:"/deleteSupplier" method:"delete" tags:"供应商管理" summary:"删除供应商" dc:"删除供应商"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"供应商ID"` +} + +// GetSupplierReq 获取供应商详情请求 +type GetSupplierReq struct { + g.Meta `path:"/getSupplier" method:"get" tags:"供应商管理" summary:"获取供应商详情" dc:"获取供应商详情"` + + ID *bson.ObjectID `json:"id" v:"required" dc:"供应商ID"` +} + +// GetSupplierRes 获取供应商详情响应(简化版) +type GetSupplierRes struct { + ID *bson.ObjectID `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + ShortName string `json:"shortName"` + Alias []string `json:"alias"` + Logo string `json:"logo"` + Phone string `json:"phone"` + Mobile string `json:"mobile"` + Email string `json:"email"` + Website string `json:"website"` + ContactPerson string `json:"contactPerson"` + ContactPhone string `json:"contactPhone"` + ContactEmail string `json:"contactEmail"` + ContactPosition string `json:"contactPosition"` + Country string `json:"country"` + Province string `json:"province"` + City string `json:"city"` + District string `json:"district"` + Address string `json:"address"` + PostalCode string `json:"postalCode"` + BusinessLicense string `json:"businessLicense"` + LegalPerson string `json:"legalPerson"` + TaxNumber string `json:"taxNumber"` + BankName string `json:"bankName"` + BankAccount string `json:"bankAccount"` + BankAccountName string `json:"bankAccountName"` + SupplierLevel string `json:"supplierLevel"` + PaymentMethod string `json:"paymentMethod"` + PaymentPeriod int `json:"paymentPeriod"` + TaxRate float64 `json:"taxRate"` + MinOrderAmount int `json:"minOrderAmount"` + MainCategories []string `json:"mainCategories"` + BusinessScope string `json:"businessScope"` + Status consts.SupplierStatus `json:"status"` + StatusText string `json:"statusText"` + Rating float64 `json:"rating"` + DeliveryRating float64 `json:"deliveryRating"` + QualityRating float64 `json:"qualityRating"` + ServiceRating float64 `json:"serviceRating"` + TotalOrders int64 `json:"totalOrders"` + TotalAmount int64 `json:"totalAmount"` + Remark string `json:"remark"` + Tags []string `json:"tags"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListSuppliersReq 获取供应商列表请求 +type ListSuppliersReq struct { + g.Meta `path:"/listSuppliers" method:"get" tags:"供应商管理" summary:"获取供应商列表" dc:"分页查询供应商列表"` + + Name string `json:"name" dc:"供应商名称(模糊查询)"` + Code string `json:"code" dc:"供应商编码(精确查询)"` + Status *consts.SupplierStatus `json:"status" dc:"供应商状态"` + PageNum int `json:"pageNum" dc:"页码"` + PageSize int `json:"pageSize" dc:"每页大小"` +} + +// ListSuppliersRes 获取供应商列表响应 +type ListSuppliersRes struct { + List []*SupplierListItem `json:"list" dc:"供应商列表"` + Total int64 `json:"total" dc:"总数"` +} + +// GetSupplierOptionsReq 获取供应商选项请求 +type GetSupplierOptionsReq struct { + g.Meta `path:"/getSupplierOptions" method:"get" tags:"供应商管理" summary:"获取供应商选项" dc:"获取供应商选项(用于下拉选择)"` +} + +// GetSupplierOptionsRes 获取供应商选项响应 +type GetSupplierOptionsRes struct { + List []*SupplierListItem `json:"list" dc:"供应商选项列表"` +} + +// SupplierListItem 供应商列表项(简化版) +type SupplierListItem struct { + ID *bson.ObjectID `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + ShortName string `json:"shortName"` + Logo string `json:"logo"` + Phone string `json:"phone"` + Mobile string `json:"mobile"` + Email string `json:"email"` + Website string `json:"website"` + Address string `json:"address"` + Status consts.SupplierStatus `json:"status"` + StatusText string `json:"statusText"` + Rating float64 `json:"rating"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/model/dto/stock/inventory_count_adjust_history_dto.go b/model/dto/stock/inventory_count_adjust_history_dto.go new file mode 100644 index 0000000..4434294 --- /dev/null +++ b/model/dto/stock/inventory_count_adjust_history_dto.go @@ -0,0 +1,110 @@ +package dto + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateInventoryCountAdjustHistoryReq 创建盘点调整历史请求 +type CreateInventoryCountAdjustHistoryReq struct { + g.Meta `path:"/createInventoryCountAdjustHistory" method:"post" tags:"盘点调整历史管理" summary:"创建盘点调整历史" dc:"创建新的盘点调整历史记录"` + CountID *bson.ObjectID `json:"countId" v:"required" dc:"盘点任务ID"` + DetailID *bson.ObjectID `json:"detailId" v:"required" dc:"盘点明细ID"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" v:"required" dc:"商品SKU ID"` + WarehouseID *bson.ObjectID `json:"warehouseId" v:"required" dc:"仓库ID"` + ZoneID *bson.ObjectID `json:"zoneId" dc:"库区ID"` + LocationID *bson.ObjectID `json:"locationId" dc:"库位ID"` + BeforeQuantity int `json:"beforeQuantity" v:"required" dc:"调整前库存"` + AfterQuantity int `json:"afterQuantity" v:"required" dc:"调整后库存"` + Difference int `json:"difference" v:"required" dc:"差值"` + Reason string `json:"reason" dc:"调整原因"` + AdjustedBy string `json:"adjustedBy" dc:"调整人ID"` + AdjustedByName string `json:"adjustedByName" dc:"调整人姓名"` + BatchNo string `json:"batchNo" dc:"批次号"` +} + +// CreateInventoryCountAdjustHistoryRes 创建盘点调整历史响应 +type CreateInventoryCountAdjustHistoryRes struct { + Id *bson.ObjectID `json:"id" dc:"历史记录ID"` +} + +// GetInventoryCountAdjustHistoryReq 获取盘点调整历史详情请求 +type GetInventoryCountAdjustHistoryReq struct { + g.Meta `path:"/getInventoryCountAdjustHistory" method:"get" tags:"盘点调整历史管理" summary:"获取盘点调整历史详情" dc:"获取盘点调整历史详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"历史记录ID"` +} + +// GetInventoryCountAdjustHistoryRes 获取盘点调整历史详情响应 +type GetInventoryCountAdjustHistoryRes struct { + Id *bson.ObjectID `json:"id" dc:"历史记录ID"` + CountID *bson.ObjectID `json:"countId" dc:"盘点任务ID"` + DetailID *bson.ObjectID `json:"detailId" dc:"盘点明细ID"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"商品SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"商品SKU名称"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneID *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationID *bson.ObjectID `json:"locationId" dc:"库位ID"` + LocationName string `json:"locationName" dc:"库位名称"` + BeforeQuantity int `json:"beforeQuantity" dc:"调整前库存"` + AfterQuantity int `json:"afterQuantity" dc:"调整后库存"` + Difference int `json:"difference" dc:"差值"` + Reason string `json:"reason" dc:"调整原因"` + AdjustedBy string `json:"adjustedBy" dc:"调整人ID"` + AdjustedByName string `json:"adjustedByName" dc:"调整人姓名"` + AdjustedAt *gtime.Time `json:"adjustedAt" dc:"调整时间"` + BatchNo string `json:"batchNo" dc:"批次号"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +// DeleteInventoryCountAdjustHistoryReq 删除盘点调整历史请求 +type DeleteInventoryCountAdjustHistoryReq struct { + g.Meta `path:"/deleteInventoryCountAdjustHistory" method:"delete" tags:"盘点调整历史管理" summary:"删除盘点调整历史" dc:"删除盘点调整历史记录"` + Id *bson.ObjectID `json:"id" v:"required" dc:"历史记录ID"` +} + +// DeleteInventoryCountAdjustHistoryRes 删除盘点调整历史响应 +type DeleteInventoryCountAdjustHistoryRes struct { + Id *bson.ObjectID `json:"id" dc:"历史记录ID"` +} + +// ListInventoryCountAdjustHistoryReq 获取盘点调整历史列表请求 +type ListInventoryCountAdjustHistoryReq struct { + g.Meta `path:"/listInventoryCountAdjustHistories" method:"get" tags:"盘点调整历史管理" summary:"获取盘点调整历史列表" dc:"分页查询盘点调整历史列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + CountID string `json:"countId" dc:"盘点任务ID"` + DetailID string `json:"detailId" dc:"盘点明细ID"` + AssetSkuID string `json:"assetSkuId" dc:"商品SKU ID"` + WarehouseID string `json:"warehouseId" dc:"仓库ID"` + BatchNo string `json:"batchNo" dc:"批次号"` +} + +// ListInventoryCountAdjustHistoryRes 获取盘点调整历史列表响应 +type ListInventoryCountAdjustHistoryRes struct { + List []InventoryCountAdjustHistoryListItem `json:"list" dc:"调整历史列表"` + Total int64 `json:"total" dc:"总数"` +} + +// InventoryCountAdjustHistoryListItem 盘点调整历史列表项 +type InventoryCountAdjustHistoryListItem struct { + Id *bson.ObjectID `json:"id" dc:"历史记录ID"` + CountID *bson.ObjectID `json:"countId" dc:"盘点任务ID"` + DetailID *bson.ObjectID `json:"detailId" dc:"盘点明细ID"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"商品SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"商品SKU名称"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + BeforeQuantity int `json:"beforeQuantity" dc:"调整前库存"` + AfterQuantity int `json:"afterQuantity" dc:"调整后库存"` + Difference int `json:"difference" dc:"差值"` + Reason string `json:"reason" dc:"调整原因"` + AdjustedBy string `json:"adjustedBy" dc:"调整人ID"` + AdjustedByName string `json:"adjustedByName" dc:"调整人姓名"` + AdjustedAt *gtime.Time `json:"adjustedAt" dc:"调整时间"` + BatchNo string `json:"batchNo" dc:"批次号"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} diff --git a/model/dto/stock/inventory_count_detail_dto.go b/model/dto/stock/inventory_count_detail_dto.go new file mode 100644 index 0000000..e5b3977 --- /dev/null +++ b/model/dto/stock/inventory_count_detail_dto.go @@ -0,0 +1,179 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateInventoryCountDetailReq 创建盘点明细请求 +type CreateInventoryCountDetailReq struct { + g.Meta `path:"/createInventoryCountDetail" method:"post" tags:"盘点明细管理" summary:"创建盘点明细" dc:"创建新的盘点明细"` + CountID string `json:"countId" v:"required" dc:"盘点单ID"` + AssetID string `json:"assetId" v:"required" dc:"资产ID"` + AssetSkuID string `json:"assetSkuId" v:"required" dc:"资产SKU ID"` + WarehouseID string `json:"warehouseId" v:"required" dc:"仓库ID"` + ZoneID string `json:"zoneId" dc:"库区ID"` + LocationID string `json:"locationId" dc:"库位ID"` + BookQuantity int `json:"bookQuantity" v:"required" dc:"账面数量"` + BookBatchInfo map[string]int `json:"bookBatchInfo" dc:"账面批次信息"` + ActualQuantity int `json:"actualQuantity" dc:"实盘数量"` + ActualBatchInfo map[string]int `json:"actualBatchInfo" dc:"实盘批次信息"` + Remark string `json:"remark" dc:"备注"` +} + +// CreateInventoryCountDetailRes 创建盘点明细响应 +type CreateInventoryCountDetailRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点明细ID"` +} + +// UpdateInventoryCountDetailReq 更新盘点明细请求 +type UpdateInventoryCountDetailReq struct { + g.Meta `path:"/updateInventoryCountDetail" method:"put" tags:"盘点明细管理" summary:"更新盘点明细" dc:"更新盘点明细信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点明细ID"` + ActualQuantity *int `json:"actualQuantity" dc:"实盘数量"` + ActualBatchInfo map[string]int `json:"actualBatchInfo" dc:"实盘批次信息"` + DiscrepancyReason string `json:"discrepancyReason" dc:"差异原因"` + Status *stock.InventoryDetailStatus `json:"status" dc:"明细状态"` + Remark string `json:"remark" dc:"备注"` +} + +// UpdateInventoryCountDetailRes 更新盘点明细响应 +type UpdateInventoryCountDetailRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点明细ID"` +} + +// DeleteInventoryCountDetailReq 删除盘点明细请求 +type DeleteInventoryCountDetailReq struct { + g.Meta `path:"/deleteInventoryCountDetail" method:"delete" tags:"盘点明细管理" summary:"删除盘点明细" dc:"删除盘点明细"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点明细ID"` +} + +// DeleteInventoryCountDetailRes 删除盘点明细响应 +type DeleteInventoryCountDetailRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点明细ID"` +} + +// GetInventoryCountDetailReq 获取盘点明细详情请求 +type GetInventoryCountDetailReq struct { + g.Meta `path:"/getInventoryCountDetail" method:"get" tags:"盘点明细管理" summary:"获取盘点明细详情" dc:"获取盘点明细详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点明细ID"` +} + +// GetInventoryCountDetailRes 获取盘点明细详情响应 +type GetInventoryCountDetailRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点明细ID"` + CountID *bson.ObjectID `json:"countId" dc:"盘点单ID"` + CountNo string `json:"countNo" dc:"盘点单号"` + AssetID *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneID *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationID *bson.ObjectID `json:"locationId" dc:"库位ID"` + LocationName string `json:"locationName" dc:"库位名称"` + BookQuantity int `json:"bookQuantity" dc:"账面数量"` + BookBatchInfo map[string]int `json:"bookBatchInfo" dc:"账面批次信息"` + ActualQuantity int `json:"actualQuantity" dc:"实盘数量"` + ActualBatchInfo map[string]int `json:"actualBatchInfo" dc:"实盘批次信息"` + CountBy string `json:"countBy" dc:"盘点人ID"` + CountByName string `json:"countByName" dc:"盘点人名称"` + CountAt *gtime.Time `json:"countAt" dc:"盘点时间"` + Difference int `json:"difference" dc:"差异数量"` + DifferenceRate float64 `json:"differenceRate" dc:"差异率"` + DiscrepancyType stock.DiscrepancyType `json:"discrepancyType" dc:"差异类型"` + DiscrepancyTypeText string `json:"discrepancyTypeText" dc:"差异类型文本"` + DiscrepancyReason string `json:"discrepancyReason" dc:"差异原因"` + Status stock.InventoryDetailStatus `json:"status" dc:"明细状态"` + StatusText string `json:"statusText" dc:"状态文本"` + IsAdjusted bool `json:"isAdjusted" dc:"是否已调整"` + AdjustedAt *gtime.Time `json:"adjustedAt" dc:"调整时间"` + AdjustedBy string `json:"adjustedBy" dc:"调整人ID"` + AdjustedByName string `json:"adjustedByName" dc:"调整人名称"` + Remark string `json:"remark" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListInventoryCountDetailReq 获取盘点明细列表请求 +type ListInventoryCountDetailReq struct { + g.Meta `path:"/listInventoryCountDetails" method:"get" tags:"盘点明细管理" summary:"获取盘点明细列表" dc:"分页查询盘点明细列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + CountID string `json:"countId" v:"required" dc:"盘点单ID"` + AssetID string `json:"assetId" dc:"资产ID"` + AssetSkuID string `json:"assetSkuId" dc:"资产SKU ID"` + WarehouseID string `json:"warehouseId" dc:"仓库ID"` + ZoneID string `json:"zoneId" dc:"库区ID"` + LocationID string `json:"locationId" dc:"库位ID"` + DiscrepancyType *stock.DiscrepancyType `json:"discrepancyType" dc:"差异类型"` + Status *stock.InventoryDetailStatus `json:"status" dc:"明细状态"` + IsAdjusted *bool `json:"isAdjusted" dc:"是否已调整"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListInventoryCountDetailRes 获取盘点明细列表响应 +type ListInventoryCountDetailRes struct { + List []InventoryCountDetailListItem `json:"list" dc:"盘点明细列表"` + Total int64 `json:"total" dc:"总数"` +} + +// InventoryCountDetailListItem 盘点明细列表项 +type InventoryCountDetailListItem struct { + Id *bson.ObjectID `json:"id" dc:"盘点明细ID"` + CountID *bson.ObjectID `json:"countId" dc:"盘点单ID"` + AssetID *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneID *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationID *bson.ObjectID `json:"locationId" dc:"库位ID"` + LocationName string `json:"locationName" dc:"库位名称"` + BookQuantity int `json:"bookQuantity" dc:"账面数量"` + ActualQuantity int `json:"actualQuantity" dc:"实盘数量"` + Difference int `json:"difference" dc:"差异数量"` + DifferenceRate float64 `json:"differenceRate" dc:"差异率"` + DiscrepancyType stock.DiscrepancyType `json:"discrepancyType" dc:"差异类型"` + DiscrepancyTypeText string `json:"discrepancyTypeText" dc:"差异类型文本"` + Status stock.InventoryDetailStatus `json:"status" dc:"明细状态"` + StatusText string `json:"statusText" dc:"状态文本"` + IsAdjusted bool `json:"isAdjusted" dc:"是否已调整"` + CountBy string `json:"countBy" dc:"盘点人ID"` + CountByName string `json:"countByName" dc:"盘点人名称"` + CountAt *gtime.Time `json:"countAt" dc:"盘点时间"` + Remark string `json:"remark" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// SearchSimilarAssetsReq 查询相似商品请求 +type SearchSimilarAssetsReq struct { + g.Meta `path:"/searchSimilarAssets" method:"get" tags:"盘点明细管理" summary:"查询相似商品" dc:"单字模糊查询相似商品"` + Keyword string `json:"keyword" v:"required" dc:"关键词"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` +} + +// SearchSimilarAssetsRes 查询相似商品响应 +type SearchSimilarAssetsRes struct { + List []SimilarAssetItem `json:"list" dc:"相似商品列表"` +} + +// SimilarAssetItem 相似商品项 +type SimilarAssetItem struct { + AssetID *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + AvailableQty int `json:"availableQty" dc:"可用库存"` + WarehouseID *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` +} diff --git a/model/dto/stock/inventory_count_dto.go b/model/dto/stock/inventory_count_dto.go new file mode 100644 index 0000000..268aa4f --- /dev/null +++ b/model/dto/stock/inventory_count_dto.go @@ -0,0 +1,202 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateInventoryCountReq 创建盘点任务请求 +type CreateInventoryCountReq struct { + g.Meta `path:"/createInventoryCount" method:"post" tags:"盘点管理" summary:"创建盘点任务" dc:"创建新的盘点任务"` + Title string `json:"title" v:"required|max-length:200#盘点标题不能为空|盘点标题不能超过200个字符" dc:"盘点标题"` + Description string `json:"description" dc:"盘点描述"` + WarehouseIDs []string `json:"warehouseId" dc:"仓库ID列表(按仓库/库区/库位盘点时必填)"` + ZoneIDs []string `json:"zoneId" dc:"库区ID列表(可选)"` + LocationIDs []string `json:"locationId" dc:"库位ID列表(可选)"` + AssetSkuIDs []string `json:"assetSkuId" dc:"资产SKU ID列表(可选)"` + CountType stock.InventoryCountType `json:"countType" v:"required" dc:"盘点类型"` + Scope stock.InventoryCountScope `json:"scope" v:"required" dc:"盘点范围"` + AssigneeID string `json:"assigneeId" v:"required" dc:"负责人ID"` + AssigneeName string `json:"assigneeName" dc:"负责人名称"` + Participants []string `json:"participants" dc:"参与人员ID列表"` + Remark string `json:"remark" dc:"备注"` +} + +// CreateInventoryCountRes 创建盘点任务响应 +type CreateInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` + CountNo string `json:"countNo" dc:"盘点单号"` +} + +// UpdateInventoryCountReq 更新盘点任务请求 +type UpdateInventoryCountReq struct { + g.Meta `path:"/updateInventoryCount" method:"put" tags:"盘点管理" summary:"更新盘点任务" dc:"更新盘点任务信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` + Title string `json:"title" dc:"盘点标题"` + Description string `json:"description" dc:"盘点描述"` + AssigneeID string `json:"assigneeId" dc:"负责人ID"` + AssigneeName string `json:"assigneeName" dc:"负责人名称"` + Participants []string `json:"participants" dc:"参与人员ID列表"` + Status *stock.InventoryCountStatus `json:"status" dc:"盘点状态"` + Remark string `json:"remark" dc:"备注"` +} + +// UpdateInventoryCountRes 更新盘点任务响应 +type UpdateInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` +} + +// DeleteInventoryCountReq 删除盘点任务请求 +type DeleteInventoryCountReq struct { + g.Meta `path:"/deleteInventoryCount" method:"delete" tags:"盘点管理" summary:"删除盘点任务" dc:"删除盘点任务"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` +} + +// DeleteInventoryCountRes 删除盘点任务响应 +type DeleteInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` +} + +// GetInventoryCountReq 获取盘点任务详情请求 +type GetInventoryCountReq struct { + g.Meta `path:"/getInventoryCount" method:"get" tags:"盘点管理" summary:"获取盘点任务详情" dc:"获取盘点任务详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` +} + +// GetInventoryCountRes 获取盘点任务详情响应 +type GetInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` + CountNo string `json:"countNo" dc:"盘点单号"` + Title string `json:"title" dc:"盘点标题"` + Description string `json:"description" dc:"盘点描述"` + WarehouseIDs []*bson.ObjectID `json:"warehouseIds" dc:"仓库ID列表"` + WarehouseNames []string `json:"warehouseNames" dc:"仓库名称列表"` + ZoneIDs []*bson.ObjectID `json:"zoneIds" dc:"库区ID列表"` + ZoneNames []string `json:"zoneNames" dc:"库区名称列表"` + LocationIDs []*bson.ObjectID `json:"locationIds" dc:"库位ID列表"` + LocationNames []string `json:"locationNames" dc:"库位名称列表"` + AssetSkuIDs []*bson.ObjectID `json:"assetSkuIds" dc:"资产SKU ID列表"` + AssetSkuNames []string `json:"assetSkuNames" dc:"资产SKU名称列表"` + CountType stock.InventoryCountType `json:"countType" dc:"盘点类型"` + CountTypeText string `json:"countTypeText" dc:"盘点类型文本"` + Scope stock.InventoryCountScope `json:"scope" dc:"盘点范围"` + ScopeText string `json:"scopeText" dc:"盘点范围文本"` + ActualStartTime *gtime.Time `json:"actualStartTime" dc:"实际开始时间"` + ActualEndTime *gtime.Time `json:"actualEndTime" dc:"实际结束时间"` + Status stock.InventoryCountStatus `json:"status" dc:"盘点状态"` + StatusText string `json:"statusText" dc:"状态文本"` + Progress float64 `json:"progress" dc:"进度百分比"` + CreatorID string `json:"creatorId" dc:"创建人ID"` + CreatorName string `json:"creatorName" dc:"创建人名称"` + AssigneeID string `json:"assigneeId" dc:"负责人ID"` + AssigneeName string `json:"assigneeName" dc:"负责人名称"` + Participants []string `json:"participants" dc:"参与人员ID列表"` + ParticipantNames []string `json:"participantNames" dc:"参与人员名称列表"` + TotalItems int `json:"totalItems" dc:"盘点条目总数"` + CompletedItems int `json:"completedItems" dc:"已完成条目数"` + DiscrepancyItems int `json:"discrepancyItems" dc:"有差异条目数"` + Remark string `json:"remark" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListInventoryCountReq 获取盘点任务列表请求 +type ListInventoryCountReq struct { + g.Meta `path:"/listInventoryCounts" method:"get" tags:"盘点管理" summary:"获取盘点任务列表" dc:"分页查询盘点任务列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + WarehouseID string `json:"warehouseId" dc:"仓库ID(单个,兼容旧版)"` + WarehouseIDs []string `json:"warehouseIds" dc:"仓库ID列表(批量查询)"` + ZoneID string `json:"zoneId" dc:"库区ID(单个,兼容旧版)"` + ZoneIDs []string `json:"zoneIds" dc:"库区ID列表(批量查询)"` + CountType *stock.InventoryCountType `json:"countType" dc:"盘点类型"` + Status *stock.InventoryCountStatus `json:"status" dc:"盘点状态"` + AssigneeID string `json:"assigneeId" dc:"负责人ID"` + StartDate string `json:"startDate" dc:"开始日期"` + EndDate string `json:"endDate" dc:"结束日期"` + Keyword string `json:"keyword" dc:"关键词搜索(单号/标题)"` +} + +// ListInventoryCountRes 获取盘点任务列表响应 +type ListInventoryCountRes struct { + List []InventoryCountListItem `json:"list" dc:"盘点任务列表"` + Total int64 `json:"total" dc:"总数"` +} + +// InventoryCountListItem 盘点任务列表项 +type InventoryCountListItem struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` + CountNo string `json:"countNo" dc:"盘点单号"` + Title string `json:"title" dc:"盘点标题"` + WarehouseIDs []*bson.ObjectID `json:"warehouseIds" dc:"仓库ID列表"` + WarehouseNames []string `json:"warehouseNames" dc:"仓库名称列表"` + ZoneIDs []*bson.ObjectID `json:"zoneIds" dc:"库区ID列表"` + ZoneNames []string `json:"zoneNames" dc:"库区名称列表"` + CountType stock.InventoryCountType `json:"countType" dc:"盘点类型"` + CountTypeText string `json:"countTypeText" dc:"盘点类型文本"` + Scope stock.InventoryCountScope `json:"scope" dc:"盘点范围"` + ScopeText string `json:"scopeText" dc:"盘点范围文本"` + Status stock.InventoryCountStatus `json:"status" dc:"盘点状态"` + StatusText string `json:"statusText" dc:"状态文本"` + Progress float64 `json:"progress" dc:"进度百分比"` + AssigneeID string `json:"assigneeId" dc:"负责人ID"` + AssigneeName string `json:"assigneeName" dc:"负责人名称"` + ActualStartTime *gtime.Time `json:"actualStartTime" dc:"实际开始时间"` + ActualEndTime *gtime.Time `json:"actualEndTime" dc:"实际结束时间"` + TotalItems int `json:"totalItems" dc:"盘点条目总数"` + CompletedItems int `json:"completedItems" dc:"已完成条目数"` + DiscrepancyItems int `json:"discrepancyItems" dc:"有差异条目数"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// CompleteInventoryCountReq 完成盘点请求 +type CompleteInventoryCountReq struct { + g.Meta `path:"/completeInventoryCount" method:"post" tags:"盘点管理" summary:"完成盘点" dc:"完成盘点任务"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` +} + +// CompleteInventoryCountRes 完成盘点响应 +type CompleteInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` +} + +// ExportInventoryCountTemplateReq 导出盘点模板请求 +type ExportInventoryCountTemplateReq struct { + g.Meta `path:"/exportInventoryCountTemplate" method:"get" tags:"盘点管理" summary:"导出盘点模板" dc:"导出Excel盘点模板(明盘/盲盘)"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` +} + +// CancelInventoryCountReq 取消盘点请求 +type CancelInventoryCountReq struct { + g.Meta `path:"/cancelInventoryCount" method:"post" tags:"盘点管理" summary:"取消盘点" dc:"取消盘点任务"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` + Reason string `json:"reason" dc:"取消原因"` +} + +// CancelInventoryCountRes 取消盘点响应 +type CancelInventoryCountRes struct { + Id *bson.ObjectID `json:"id" dc:"盘点任务ID"` +} + +// ExportInventoryCountTemplateRes 导出盘点模板响应 +type ExportInventoryCountTemplateRes struct { + FileName string `json:"fileName" dc:"文件名"` + FileData []byte `json:"fileData" dc:"文件数据(Base64编码)"` +} + +// ImportInventoryCountReq 上传盘点Excel请求 +type ImportInventoryCountReq struct { + g.Meta `path:"/importInventoryCount" method:"post" tags:"盘点管理" summary:"上传盘点Excel" dc:"上传盘点结果Excel"` + Id *bson.ObjectID `json:"id" v:"required" dc:"盘点任务ID"` +} + +// ImportInventoryCountRes 上传盘点Excel响应 +type ImportInventoryCountRes struct { + SuccessCount int `json:"successCount" dc:"成功导入数量"` + FailCount int `json:"failCount" dc:"失败数量"` +} diff --git a/model/dto/stock/inventory_traceability_dto.go b/model/dto/stock/inventory_traceability_dto.go new file mode 100644 index 0000000..76d3a17 --- /dev/null +++ b/model/dto/stock/inventory_traceability_dto.go @@ -0,0 +1 @@ +package dto diff --git a/model/dto/stock/inventory_transaction_dto.go b/model/dto/stock/inventory_transaction_dto.go new file mode 100644 index 0000000..76d3a17 --- /dev/null +++ b/model/dto/stock/inventory_transaction_dto.go @@ -0,0 +1 @@ +package dto diff --git a/model/dto/stock/inventory_warning_dto.go b/model/dto/stock/inventory_warning_dto.go new file mode 100644 index 0000000..0bc00ae --- /dev/null +++ b/model/dto/stock/inventory_warning_dto.go @@ -0,0 +1,214 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateInventoryWarningReq 创建库存预警请求 +type CreateInventoryWarningReq struct { + g.Meta `path:"/createInventoryWarning" method:"post" tags:"库存预警管理" summary:"创建库存预警" dc:"创建新的库存预警"` + + WarningType stock.WarningType `json:"warningType" v:"required" dc:"预警类型"` + BatchID *bson.ObjectID `json:"batchId" v:"required" dc:"关联批次ID"` + AssetID *bson.ObjectID `json:"assetId" dc:"关联资产ID"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"关联资产SKU ID"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"关联供应商ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessNote string `json:"processNote" dc:"处理备注"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + PromotionPlanID string `json:"promotionPlanId" dc:"促销方案ID"` + + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + Notes string `json:"notes" dc:"备注"` +} + +// CreateInventoryWarningRes 创建库存预警响应 +type CreateInventoryWarningRes struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` +} + +// UpdateInventoryWarningReq 更新库存预警请求 +type UpdateInventoryWarningReq struct { + g.Meta `path:"/updateInventoryWarning" method:"put" tags:"库存预警管理" summary:"更新库存预警" dc:"更新库存预警信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"预警ID"` + + WarningType stock.WarningType `json:"warningType" v:"required" dc:"预警类型"` + BatchID *bson.ObjectID `json:"batchId" v:"required" dc:"关联批次ID"` + AssetID *bson.ObjectID `json:"assetId" dc:"关联资产ID"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"关联资产SKU ID"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"关联供应商ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessNote string `json:"processNote" dc:"处理备注"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + PromotionPlanID string `json:"promotionPlanId" dc:"促销方案ID"` + + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + Notes string `json:"notes" dc:"备注"` +} + +// UpdateInventoryWarningRes 更新库存预警响应 +type UpdateInventoryWarningRes struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` +} + +// DeleteInventoryWarningReq 删除库存预警请求 +type DeleteInventoryWarningReq struct { + g.Meta `path:"/deleteInventoryWarning" method:"delete" tags:"库存预警管理" summary:"删除库存预警" dc:"删除库存预警"` + Id *bson.ObjectID `json:"id" v:"required" dc:"预警ID"` +} + +// DeleteInventoryWarningRes 删除库存预警响应 +type DeleteInventoryWarningRes struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` +} + +// GetInventoryWarningReq 获取库存预警详情请求 +type GetInventoryWarningReq struct { + g.Meta `path:"/getInventoryWarning" method:"get" tags:"库存预警管理" summary:"获取库存预警详情" dc:"获取库存预警详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"预警ID"` +} + +// GetInventoryWarningRes 获取库存预警详情响应 +type GetInventoryWarningRes struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` + WarningType stock.WarningType `json:"warningType" dc:"预警类型"` + WarningTypeText string `json:"warningTypeText" dc:"预警类型文本"` + BatchID *bson.ObjectID `json:"batchId" dc:"关联批次ID"` + AssetID *bson.ObjectID `json:"assetId" dc:"关联资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"关联资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"关联供应商ID"` + SupplierName string `json:"supplierName" dc:"供应商名称"` + BatchNo string `json:"batchNo" dc:"批次号"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + StatusText string `json:"statusText" dc:"状态文本"` + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessorName string `json:"processorName" dc:"处理人名称"` + ProcessNote string `json:"processNote" dc:"处理备注"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + ProcessMethodText string `json:"processMethodText" dc:"处理方式文本"` + PromotionPlanID string `json:"promotionPlanId" dc:"促销方案ID"` + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + Notes string `json:"notes" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListInventoryWarningReq 获取库存预警列表请求 +type ListInventoryWarningReq struct { + g.Meta `path:"/listInventoryWarnings" method:"get" tags:"库存预警管理" summary:"获取库存预警列表" dc:"分页查询库存预警列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + + WarningType *stock.WarningType `json:"warningType" dc:"预警类型"` + + BatchID string `json:"batchId" dc:"关联批次ID(单个,兼容旧版)"` + BatchIDs []string `json:"batchIds" dc:"批次ID列表(批量查询)"` + AssetID string `json:"assetId" dc:"关联资产ID(单个,兼容旧版)"` + AssetIDs []string `json:"assetIds" dc:"资产ID列表(批量查询)"` + AssetSkuID string `json:"assetSkuId" dc:"关联资产SKU ID(单个,兼容旧版)"` + AssetSkuIDs []string `json:"assetSkuIds" dc:"资产SKU ID列表(批量查询)"` + SupplierID string `json:"supplierId" dc:"关联供应商ID(单个,兼容旧版)"` + SupplierIDs []string `json:"supplierIds" dc:"供应商ID列表(批量查询)"` + + Status *stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + + SupportsRecycle *bool `json:"supportsRecycle" dc:"是否支持回收"` + StartDate string `json:"startDate" dc:"开始日期"` + EndDate string `json:"endDate" dc:"结束日期"` + Keyword string `json:"keyword" dc:"关键词搜索(批次号/备注)"` +} + +// ListInventoryWarningRes 获取库存预警列表响应 +type ListInventoryWarningRes struct { + List []InventoryWarningListItem `json:"list" dc:"库存预警列表"` + Total int64 `json:"total" dc:"总数"` +} + +// InventoryWarningListItem 库存预警列表项 +type InventoryWarningListItem struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` + WarningType stock.WarningType `json:"warningType" dc:"预警类型"` + WarningTypeText string `json:"warningTypeText" dc:"预警类型文本"` + BatchID *bson.ObjectID `json:"batchId" dc:"关联批次ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + AssetID *bson.ObjectID `json:"assetId" dc:"关联资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"关联资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"关联供应商ID"` + SupplierName string `json:"supplierName" dc:"供应商名称"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + StatusText string `json:"statusText" dc:"状态文本"` + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessorName string `json:"processorName" dc:"处理人名称"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + ProcessMethodText string `json:"processMethodText" dc:"处理方式文本"` + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +// ProcessInventoryWarningReq 处理库存预警请求 +type ProcessInventoryWarningReq struct { + g.Meta `path:"/processInventoryWarning" method:"post" tags:"库存预警管理" summary:"处理库存预警" dc:"处理库存预警"` + + Id *bson.ObjectID `json:"id" v:"required" dc:"预警ID"` + + Status stock.ExpiryMessageStatus `json:"status" v:"required" dc:"处理状态"` + Processor string `json:"processor" v:"required" dc:"处理人"` + ProcessNote string `json:"processNote" dc:"处理备注"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + PromotionPlanID string `json:"promotionPlanId" dc:"促销方案ID"` +} + +// ProcessInventoryWarningRes 处理库存预警响应 +type ProcessInventoryWarningRes struct { + Id *bson.ObjectID `json:"id" dc:"预警ID"` +} diff --git a/model/dto/stock/inventory_warning_history_dto.go b/model/dto/stock/inventory_warning_history_dto.go new file mode 100644 index 0000000..ab8d9aa --- /dev/null +++ b/model/dto/stock/inventory_warning_history_dto.go @@ -0,0 +1,117 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GetInventoryWarningHistoryReq 获取预警历史详情请求 +type GetInventoryWarningHistoryReq struct { + g.Meta `path:"/getInventoryWarningHistory" method:"get" tags:"预警历史管理" summary:"获取预警历史详情" dc:"获取预警历史详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"预警历史ID"` +} + +// GetInventoryWarningHistoryRes 获取预警历史详情响应 +type GetInventoryWarningHistoryRes struct { + Id *bson.ObjectID `json:"id" dc:"预警历史ID"` + WarningType stock.WarningType `json:"warningType" dc:"预警类型"` + WarningTypeText string `json:"warningTypeText" dc:"预警类型文本"` + BatchID *bson.ObjectID `json:"batchId" dc:"批次ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + AssetID *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"供应商ID"` + SupplierName string `json:"supplierName" dc:"供应商名称"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + StatusText string `json:"statusText" dc:"状态文本"` + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessorName string `json:"processorName" dc:"处理人名称"` + ProcessNote string `json:"processNote" dc:"处理备注"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + ProcessMethodText string `json:"processMethodText" dc:"处理方式文本"` + PromotionPlanID *bson.ObjectID `json:"promotionPlanId" dc:"促销方案ID"` + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + Notes string `json:"notes" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListInventoryWarningHistoryReq 获取预警历史列表请求 +type ListInventoryWarningHistoryReq struct { + g.Meta `path:"/listInventoryWarningHistories" method:"get" tags:"预警历史管理" summary:"获取预警历史列表" dc:"分页查询预警历史列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + WarningType *stock.WarningType `json:"warningType" dc:"预警类型"` + BatchID string `json:"batchId" dc:"批次ID(单个,兼容旧版)"` + BatchIDs []string `json:"batchIds" dc:"批次ID列表(批量查询)"` + AssetID string `json:"assetId" dc:"资产ID(单个,兼容旧版)"` + AssetIDs []string `json:"assetIds" dc:"资产ID列表(批量查询)"` + AssetSkuID string `json:"assetSkuId" dc:"资产SKU ID(单个,兼容旧版)"` + AssetSkuIDs []string `json:"assetSkuIds" dc:"资产SKU ID列表(批量查询)"` + SupplierID string `json:"supplierId" dc:"供应商ID(单个,兼容旧版)"` + SupplierIDs []string `json:"supplierIds" dc:"供应商ID列表(批量查询)"` + Status *stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + StartDate string `json:"startDate" dc:"开始日期"` + EndDate string `json:"endDate" dc:"结束日期"` + Keyword string `json:"keyword" dc:"关键词搜索(批次号)"` +} + +// ListInventoryWarningHistoryRes 获取预警历史列表响应 +type ListInventoryWarningHistoryRes struct { + List []InventoryWarningHistoryListItem `json:"list" dc:"预警历史列表"` + Total int64 `json:"total" dc:"总数"` +} + +// InventoryWarningHistoryListItem 预警历史列表项 +type InventoryWarningHistoryListItem struct { + Id *bson.ObjectID `json:"id" dc:"预警历史ID"` + WarningType stock.WarningType `json:"warningType" dc:"预警类型"` + WarningTypeText string `json:"warningTypeText" dc:"预警类型文本"` + BatchID *bson.ObjectID `json:"batchId" dc:"批次ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + AssetID *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetName string `json:"assetName" dc:"资产名称"` + AssetSkuID *bson.ObjectID `json:"assetSkuId" dc:"资产SKU ID"` + AssetSkuName string `json:"assetSkuName" dc:"资产SKU名称"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"供应商ID"` + SupplierName string `json:"supplierName" dc:"供应商名称"` + BatchQty int `json:"batchQty" dc:"批次数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + MinStockThreshold int `json:"minStockThreshold" dc:"最低库存阈值"` + Status stock.ExpiryMessageStatus `json:"status" dc:"消息状态"` + StatusText string `json:"statusText" dc:"状态文本"` + ProcessedAt *gtime.Time `json:"processedAt" dc:"处理时间"` + Processor string `json:"processor" dc:"处理人"` + ProcessorName string `json:"processorName" dc:"处理人名称"` + ProcessMethod *stock.ExpiryProcessMethod `json:"processMethod" dc:"处理方式"` + ProcessMethodText string `json:"processMethodText" dc:"处理方式文本"` + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +} + +// DeleteInventoryWarningHistoryReq 删除预警历史请求 +type DeleteInventoryWarningHistoryReq struct { + g.Meta `path:"/deleteInventoryWarningHistory" method:"delete" tags:"预警历史管理" summary:"删除预警历史" dc:"删除预警历史记录"` + Id *bson.ObjectID `json:"id" v:"required" dc:"预警历史ID"` +} + +// DeleteInventoryWarningHistoryRes 删除预警历史响应 +type DeleteInventoryWarningHistoryRes struct { + Id *bson.ObjectID `json:"id" dc:"预警历史ID"` +} diff --git a/model/dto/stock/location_dto.go b/model/dto/stock/location_dto.go new file mode 100644 index 0000000..601e2af --- /dev/null +++ b/model/dto/stock/location_dto.go @@ -0,0 +1,136 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateLocationReq 创建库位请求 +type CreateLocationReq struct { + g.Meta `path:"/createLocation" method:"post" tags:"库位管理" summary:"创建库位" dc:"创建新的库位"` + WarehouseId string `json:"warehouseId" v:"required" dc:"仓库ID"` + ZoneId string `json:"zoneId" v:"required" dc:"库区ID"` + LocationCode string `json:"locationCode" v:"required|max-length:50#库位编码不能为空|库位编码不能超过50个字符" dc:"库位编码"` + LocationName string `json:"locationName" v:"required|max-length:100#库位名称不能为空|库位名称不能超过100个字符" dc:"库位名称"` + LocationType stock.LocationType `json:"locationType" v:"required" dc:"库位类型"` + CapacityUnitType stock.CapacityUnitType `json:"capacityUnitType" v:"required" dc:"容量单位类型"` + CapacityUnit string `json:"capacityUnit" v:"required" dc:"容量单位(如箱、托盘)"` + MaxCapacity int `json:"maxCapacity" v:"required|min:1#最大容量不能为空|最大容量必须大于0" dc:"最大容量"` + Status *stock.LocationStatus `json:"status" dc:"库位状态(可选,默认空闲)"` + Remark string `json:"remark" dc:"备注"` +} + +// CreateLocationRes 创建库位响应 +type CreateLocationRes struct { + Id *bson.ObjectID `json:"id" dc:"库位ID"` +} + +// UpdateLocationReq 更新库位请求 +type UpdateLocationReq struct { + g.Meta `path:"/updateLocation" method:"put" tags:"库位管理" summary:"更新库位" dc:"更新库位信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库位ID"` + WarehouseId string `json:"warehouseId" dc:"仓库ID"` + ZoneId string `json:"zoneId" dc:"库区ID"` + LocationCode string `json:"locationCode" dc:"库位编码"` + LocationName string `json:"locationName" dc:"库位名称"` + LocationType *stock.LocationType `json:"locationType" dc:"库位类型"` + CapacityUnitType *stock.CapacityUnitType `json:"capacityUnitType" dc:"容量单位类型"` + CapacityUnit string `json:"capacityUnit" dc:"容量单位"` + MaxCapacity *int `json:"maxCapacity" dc:"最大容量"` + Status *stock.LocationStatus `json:"status" dc:"库位状态"` + Remark string `json:"remark" dc:"备注"` +} + +// UpdateLocationRes 更新库位响应 +type UpdateLocationRes struct { + Id *bson.ObjectID `json:"id" dc:"库位ID"` +} + +// DeleteLocationReq 删除库位请求 +type DeleteLocationReq struct { + g.Meta `path:"/deleteLocation" method:"delete" tags:"库位管理" summary:"删除库位" dc:"删除库位"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库位ID"` +} + +// UpdateLocationStatusReq 更新库位状态请求 +type UpdateLocationStatusReq struct { + g.Meta `path:"/updateLocationStatus" method:"put" tags:"库位管理" summary:"更新库位状态" dc:"单独更新库位状态(空闲/占用/锁定/维护)"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库位ID"` + Status stock.LocationStatus `json:"status" v:"required|in:idle,occupied,locked,maintenance#状态不能为空|状态值无效" dc:"库位状态"` +} + +// DeleteLocationRes 删除库位响应 +type DeleteLocationRes struct { + Id *bson.ObjectID `json:"id" dc:"库位ID"` +} + +// GetLocationReq 获取库位详情请求 +type GetLocationReq struct { + g.Meta `path:"/getLocation" method:"get" tags:"库位管理" summary:"获取库位详情" dc:"获取库位详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库位ID"` +} + +// GetLocationRes 获取库位详情响应 +type GetLocationRes struct { + Id *bson.ObjectID `json:"id" dc:"库位ID"` + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationCode string `json:"locationCode" dc:"库位编码"` + LocationName string `json:"locationName" dc:"库位名称"` + LocationType stock.LocationType `json:"locationType" dc:"库位类型"` + LocationTypeText string `json:"locationTypeText" dc:"库位类型文本"` + MaxCapacity int `json:"maxCapacity" dc:"最大容量"` + CurrentCapacity int `json:"currentCapacity" dc:"当前容量"` + UsageRate float64 `json:"usageRate" dc:"使用率"` + Status stock.LocationStatus `json:"status" dc:"库位状态"` + StatusText string `json:"statusText" dc:"状态文本"` + Remark string `json:"remark" dc:"备注"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListLocationReq 获取库位列表请求 +type ListLocationReq struct { + g.Meta `path:"/listLocations" method:"get" tags:"库位管理" summary:"获取库位列表" dc:"分页查询库位列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + WarehouseId string `json:"warehouseId" dc:"仓库ID(单个,兼容旧版)"` + WarehouseIds []string `json:"warehouseIds" dc:"仓库ID列表(批量查询)"` + ZoneId string `json:"zoneId" dc:"库区ID(单个,兼容旧版)"` + ZoneIds []string `json:"zoneIds" dc:"库区ID列表(批量查询)"` + LocationType *stock.LocationType `json:"locationType" dc:"库位类型"` + Status *stock.LocationStatus `json:"status" dc:"库位状态"` + Keyword string `json:"keyword" dc:"关键词搜索(编码/名称)"` +} + +// ListLocationRes 获取库位列表响应 +type ListLocationRes struct { + List []LocationListItem `json:"list" dc:"库位列表"` + Total int64 `json:"total" dc:"总数"` +} + +// LocationListItem 库位列表项 +type LocationListItem struct { + Id *bson.ObjectID `json:"id" dc:"库位ID"` + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + ZoneName string `json:"zoneName" dc:"库区名称"` + LocationCode string `json:"locationCode" dc:"库位编码"` + LocationName string `json:"locationName" dc:"库位名称"` + LocationType stock.LocationType `json:"locationType" dc:"库位类型"` + LocationTypeText string `json:"locationTypeText" dc:"库位类型文本"` + MaxCapacity int `json:"maxCapacity" dc:"最大容量"` + CurrentCapacity int `json:"currentCapacity" dc:"当前容量"` + UsageRate float64 `json:"usageRate" dc:"使用率"` + Status stock.LocationStatus `json:"status" dc:"库位状态"` + StatusText string `json:"statusText" dc:"状态文本"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/model/dto/stock/private_stock_dto.go b/model/dto/stock/private_stock_dto.go new file mode 100644 index 0000000..049bf9c --- /dev/null +++ b/model/dto/stock/private_stock_dto.go @@ -0,0 +1,137 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreatePrivateStockReq 创建实物库存批次请求 +type CreatePrivateStockReq struct { + g.Meta `path:"/createPrivateStock" method:"post" tags:"实物库存批次管理" summary:"创建实物库存批次" dc:"创建新的实物库存批次"` + + WarehouseId *bson.ObjectID `json:"warehouseId" v:"required" dc:"仓库ID"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + LocationId *bson.ObjectID `json:"locationId" dc:"库位ID"` + PrivateSkuID *bson.ObjectID `json:"privateSkuId" v:"required" dc:"私域SKU ID"` + BatchNo string `json:"batchNo" v:"required" dc:"批次号"` + BatchQty int `json:"batchQty" v:"required|min:1" dc:"批次总数量"` + AvailableQty int `json:"availableQty" v:"required|min:0" dc:"可用数量"` + BatchStatus *stock.BatchStatus `json:"batchStatus" dc:"批次状态"` + StockStatus *stock.StockStatus `json:"stockStatus" dc:"库存状态"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"供应商ID"` + SupportsRecycle bool `json:"supportsRecycle" dc:"是否支持回收"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` + StockType stock.StockLocationType `json:"stockType" dc:"库存类型"` +} + +// CreatePrivateStockRes 创建实物库存批次响应 +type CreatePrivateStockRes struct { + Id *bson.ObjectID `json:"id"` +} + +// UpdatePrivateStockReq 更新实物库存批次请求 +type UpdatePrivateStockReq struct { + g.Meta `path:"/updatePrivateStock" method:"put" tags:"实物库存批次管理" summary:"更新实物库存批次" dc:"更新实物库存批次信息"` + + Id *bson.ObjectID `json:"id" v:"required" dc:"实物库存批次ID"` + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + LocationId *bson.ObjectID `json:"locationId" dc:"库位ID"` + PrivateSkuID *bson.ObjectID `json:"privateSkuId" dc:"私域SKU ID"` + BatchNo string `json:"batchNo" dc:"批次号"` + BatchQty int `json:"batchQty" dc:"批次总数量"` + AvailableQty int `json:"availableQty" dc:"可用数量"` + BatchStatus *stock.BatchStatus `json:"batchStatus" dc:"批次状态"` + StockStatus *stock.StockStatus `json:"stockStatus" dc:"库存状态"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"供应商ID"` + SupportsRecycle *bool `json:"supportsRecycle" dc:"是否支持回收"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` + StockType stock.StockLocationType `json:"stockType" dc:"库存类型"` +} + +// DeletePrivateStockReq 删除私域库存请求 +type DeletePrivateStockReq struct { + g.Meta `path:"/deletePrivateStock" method:"delete" tags:"私域库存管理" summary:"删除私域库存" dc:"删除私域库存"` + Id *bson.ObjectID `json:"id" v:"required" dc:"私域库存ID"` +} + +// GetPrivateStockReq 获取私域库存详情请求 +type GetPrivateStockReq struct { + g.Meta `path:"/getPrivateStock" method:"get" tags:"私域库存管理" summary:"获取私域库存详情" dc:"获取私域库存详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"私域库存ID"` +} + +// GetPrivateStockRes 获取私域库存详情响应 +type GetPrivateStockRes struct { + Id *bson.ObjectID `json:"id"` + StockType stock.StockLocationType `json:"stockType"` + WarehouseId *bson.ObjectID `json:"warehouseId"` + WarehouseCode string `json:"warehouseCode"` + WarehouseName string `json:"warehouseName"` + ZoneId *bson.ObjectID `json:"zoneId"` + ZoneCode string `json:"zoneCode"` + ZoneName string `json:"zoneName"` + ZoneType stock.ZoneType `json:"zoneType"` + LocationId *bson.ObjectID `json:"locationId"` + LocationCode string `json:"locationCode"` + LocationName string `json:"locationName"` + LocationType stock.LocationType `json:"locationType"` + PrivateSkuID *bson.ObjectID `json:"privateSkuId"` + BatchNo string `json:"batchNo"` + BatchQty int `json:"batchQty"` + AvailableQty int `json:"availableQty"` + BatchStatus stock.BatchStatus `json:"batchStatus"` + OrderID *bson.ObjectID `json:"orderId"` + SupplierID *bson.ObjectID `json:"supplierId"` + SupportsRecycle bool `json:"supportsRecycle"` + ProductionDate *gtime.Time `json:"productionDate"` + ExpiryDate *gtime.Time `json:"expiryDate"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate"` + PrivateCategoryPath string `json:"privateCategoryPath"` + StockStatus stock.StockStatus `json:"stockStatus"` + LastMovedAt *gtime.Time `json:"lastMovedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListPrivateStockReq 获取私域库存列表请求 +type ListPrivateStockReq struct { + g.Meta `path:"/listPrivateStocks" method:"get" tags:"私域库存管理" summary:"获取私域库存列表" dc:"分页查询私域库存列表"` + + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + WarehouseId *bson.ObjectID `json:"warehouseId" dc:"仓库ID"` + ZoneId *bson.ObjectID `json:"zoneId" dc:"库区ID"` + LocationId *bson.ObjectID `json:"locationId" dc:"库位ID"` + PrivateSkuID *bson.ObjectID `json:"privateSkuId" dc:"私域SKU ID"` + BatchStatus *stock.BatchStatus `json:"batchStatus" dc:"批次状态"` + StockStatus *stock.StockStatus `json:"stockStatus" dc:"库存状态"` + SupplierID *bson.ObjectID `json:"supplierId" dc:"供应商ID"` + PrivateCategoryPath string `json:"privateCategoryPath" dc:"私域分类路径"` + StockType stock.StockLocationType `json:"stockType" dc:"库存类型"` +} + +// ListPrivateStockRes 获取私域库存列表响应 +type ListPrivateStockRes struct { + List []GetPrivateStockRes `json:"list"` + Total int64 `json:"total"` +} + +// OutboundPrivateStockReq 实物库存批次出库请求 +type OutboundPrivateStockReq struct { + g.Meta `path:"/outboundPrivateStock" method:"post" tags:"实物库存批次管理" summary:"实物库存批次出库" dc:"实物库存批次出库操作"` + + StockId *bson.ObjectID `json:"stockId" v:"required" dc:"实物库存批次ID"` + OutboundQty int `json:"outboundQty" v:"required|min:1" dc:"出库数量"` + StockType stock.StockLocationType `json:"stockType" v:"required" dc:"库存类型(必须为PrivateStock)"` +} diff --git a/model/dto/stock/stock_batch_dto.go b/model/dto/stock/stock_batch_dto.go new file mode 100644 index 0000000..223bc5a --- /dev/null +++ b/model/dto/stock/stock_batch_dto.go @@ -0,0 +1,90 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CommonResp 通用响应 +type CommonResp struct { + Success bool `json:"success"` // 是否成功 + Message string `json:"message"` // 消息 +} + +// --- 批次管理API相关结构 --- + +// CreateBatchReq 创建批次请求 +type CreateBatchReq struct { + g.Meta `path:"/createBatch" method:"post" tags:"库存批次管理" summary:"创建批次" dc:"创建新的库存批次"` + + AssetId *bson.ObjectID `json:"assetId" v:"required" dc:"资产ID"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" v:"required" dc:"SKU ID"` + BatchNo string `json:"batchNo" v:"required" dc:"批次号"` + BatchQty int `json:"batchQty" v:"required|min:1" dc:"批次数量"` + AvailableQty int `json:"availableQty" v:"required|min:1" dc:"可用数量"` + Metadata []map[string]interface{} `json:"metadata" dc:"元数据"` + Status stock.BatchStatus `json:"status" dc:"状态"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期(格式:2006-01-02)"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期(格式:2006-01-02)"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间(格式:2006-01-02)"` +} + +// CreateBatchRes 创建批次响应 +type CreateBatchRes struct { + Id *bson.ObjectID `json:"id"` // 批次ID +} + +// UpdateBatchReq 更新批次请求 +type UpdateBatchReq struct { + g.Meta `path:"/updateBatch" method:"put" tags:"库存批次管理" summary:"更新批次" dc:"更新批次信息"` + + Id *bson.ObjectID `json:"id" v:"required" dc:"批次ID"` + BatchQty int `json:"batchQty" v:"required|min:1" dc:"批次数量"` + AvailableQty int `json:"availableQty" v:"required|min:1" dc:"可用数量"` +} + +// DeleteBatchReq 删除批次请求 +type DeleteBatchReq struct { + g.Meta `path:"/deleteBatch" method:"delete" tags:"库存批次管理" summary:"删除批次" dc:"删除批次"` + Id *bson.ObjectID `json:"id" v:"required" dc:"批次ID"` +} + +// GetBatchReq 获取批次详情请求 +type GetBatchReq struct { + g.Meta `path:"/getBatch" method:"get" tags:"库存批次管理" summary:"获取批次详情" dc:"获取批次详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"批次ID"` +} + +// GetBatchRes 获取批次详情响应 +type GetBatchRes struct { + Id *bson.ObjectID `json:"id"` + AssetId *bson.ObjectID `json:"assetId"` + AssetSkuId *bson.ObjectID `json:"assetSkuId"` + BatchNo string `json:"batchNo"` + BatchQty int `json:"batchQty"` + AvailableQty int `json:"availableQty"` + Metadata []map[string]interface{} `json:"metadata"` + Status stock.BatchStatus `json:"status"` + ProductionDate *gtime.Time `json:"productionDate"` + ExpiryDate *gtime.Time `json:"expiryDate"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate"` +} + +// ListBatchReq 获取批次列表请求 +type ListBatchReq struct { + g.Meta `path:"/listBatches" method:"get" tags:"库存批次管理" summary:"获取批次列表" dc:"分页查询批次列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + AssetId *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" dc:"SKU ID"` +} + +// ListBatchRes 获取批次列表响应 +type ListBatchRes struct { + List []GetBatchRes `json:"list"` + Total int64 `json:"total"` +} diff --git a/model/dto/stock/stock_details_dto.go b/model/dto/stock/stock_details_dto.go new file mode 100644 index 0000000..a3d23cd --- /dev/null +++ b/model/dto/stock/stock_details_dto.go @@ -0,0 +1,67 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// GetStockDetailsReq 获取库存明细详情请求 +type GetStockDetailsReq struct { + g.Meta `path:"/getStockDetails" method:"get" tags:"库存明细管理" summary:"获取库存明细详情" dc:"获取库存明细详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库存明细ID"` +} + +// GetStockDetailsRes 获取库存明细详情响应 +type GetStockDetailsRes struct { + Id *bson.ObjectID `json:"id"` + AssetId *bson.ObjectID `json:"assetId"` + AssetSkuId *bson.ObjectID `json:"assetSkuId"` + Status stock.StockStatus `json:"status"` + OrderId *bson.ObjectID `json:"orderId"` + LockExpire string `json:"lockExpire"` + Metadata map[string]interface{} `json:"metadata"` + TokenId string `json:"tokenId"` + AssignedChannel string `json:"assignedChannel"` + ChannelSKU string `json:"channelSku"` + AllocatedAt string `json:"allocatedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ListStockDetailsReq 获取库存明细列表请求 +type ListStockDetailsReq struct { + g.Meta `path:"/listStockDetails" method:"get" tags:"库存明细管理" summary:"获取库存明细列表" dc:"分页查询库存明细列表,支持多条件筛选"` + + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + AssetId *bson.ObjectID `json:"assetId" dc:"资产ID"` + AssetSkuId *bson.ObjectID `json:"assetSkuId" dc:"SKU ID"` + CategoryPath string `json:"categoryPath" dc:"分类路径"` + Status stock.StockStatus `json:"status" dc:"状态"` +} + +// ListStockDetailsRes 获取库存明细列表响应 +type ListStockDetailsRes struct { + List []*StockDetailsListItem `json:"list" dc:"库存明细列表"` + Total int64 `json:"total" dc:"总数"` +} + +// StockDetailsListItem 库存明细列表项 +type StockDetailsListItem struct { + Id *bson.ObjectID `json:"id"` + AssetId *bson.ObjectID `json:"assetId"` + AssetSkuId *bson.ObjectID `json:"assetSkuId"` + Status stock.StockStatus `json:"status"` + OrderId *bson.ObjectID `json:"orderId"` + LockExpire string `json:"lockExpire"` + Metadata map[string]interface{} `json:"metadata"` + TokenId *bson.ObjectID `json:"tokenId"` + AssignedChannel string `json:"assignedChannel"` + ChannelSKU string `json:"channelSku"` + AllocatedAt string `json:"allocatedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/model/dto/stock/stock_manage_dto.go b/model/dto/stock/stock_manage_dto.go new file mode 100644 index 0000000..fb70548 --- /dev/null +++ b/model/dto/stock/stock_manage_dto.go @@ -0,0 +1,77 @@ +package dto + +import ( + "assets/consts/stock" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type StockOperationReq struct { + g.Meta `path:"/stockOperation" method:"post" tags:"库存管理" summary:"库存操作(创建/修改)" dc:"库存操作(创建/修改)"` + + AssetSkuId *bson.ObjectID `json:"assetSkuId" v:"required" dc:"关联资产SKU ID"` + Stock int `json:"stock" v:"required|min:1" dc:"库存数量"` + // 批次模式专用字段 + BatchNo string `json:"batchNo" dc:"批次号(批次模式必填)"` + ProductionDate *gtime.Time `json:"productionDate" dc:"生产日期(批次模式,格式:2006-01-02)"` + ExpiryDate *gtime.Time `json:"expiryDate" dc:"过期日期(批次模式,格式:2006-01-02)"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate" dc:"临期预警时间(格式:2006-01-02)"` +} + +// StockPublishMessage 库存发布消息 +type StockPublishMessage struct { + AssetId string `json:"assetId"` + AssetSkuId string `json:"assetSkuId"` + TenantId interface{} `json:"tenantId"` + UserName interface{} `json:"userName"` + StockCount int `json:"stockCount"` + OperationType string `json:"operationType"` + Metadata []map[string]interface{} `json:"metadata"` + StockId string `json:"stockId"` + StockMode int `json:"stockMode"` + BatchNo string `json:"batchNo"` + ProductionDate *gtime.Time `json:"productionDate"` + ExpiryDate *gtime.Time `json:"expiryDate"` + ExpiryWarningDate *gtime.Time `json:"expiryWarningDate"` +} + +// GetStockFormFieldsReq 获取库存表单字段请求 +type GetStockFormFieldsReq struct { + g.Meta `path:"/getStockFormFields" method:"get" tags:"库存管理" summary:"获取库存操作表单字段" dc:"根据资产SKU的库存管理模式动态返回表单字段"` + + AssetSkuId *bson.ObjectID `json:"assetSkuId" v:"required" dc:"关联资产ID"` +} + +// GetStockFormFieldsRes 获取库存表单字段响应 +type GetStockFormFieldsRes struct { + StockMode stock.StockMode `json:"stockMode" dc:"库存管理模式:1-明细模式 2-批次模式"` + Fields []map[string]interface{} `json:"fields" dc:"表单字段列表"` +} + +// MoveStockReq 移库请求(库位间移动) +type MoveStockReq struct { + g.Meta `path:"/moveStock" method:"post" tags:"库存管理" summary:"移库" dc:"将库存从一个库位移动到另一个库位(仅支持私域库存)"` + + StockType stock.StockLocationType `json:"stockType" v:"required|in:2" dc:"库存类型(2-PrivateStock,仅支持私域库存)"` + StockId *bson.ObjectID `json:"stockId" v:"required" dc:"私域库存ID"` + FromLocationId *bson.ObjectID `json:"fromLocationId" v:"required" dc:"源库位ID"` + ToLocationId *bson.ObjectID `json:"toLocationId" v:"required" dc:"目标库位ID"` + Quantity int `json:"quantity" dc:"移动数量(保留字段,暂未使用)"` + Remark string `json:"remark" dc:"备注"` +} + +// TransferStockReq 调拨请求(仓库间调拨) +type TransferStockReq struct { + g.Meta `path:"/transferStock" method:"post" tags:"库存管理" summary:"调拨" dc:"将库存从一个仓库调拨到另一个仓库(仅支持私域库存)"` + + StockType stock.StockLocationType `json:"stockType" v:"required|in:2" dc:"库存类型(2-PrivateStock,仅支持私域库存)"` + StockId *bson.ObjectID `json:"stockId" v:"required" dc:"私域库存ID"` + FromWarehouseId *bson.ObjectID `json:"fromWarehouseId" v:"required" dc:"源仓库ID"` + ToWarehouseId *bson.ObjectID `json:"toWarehouseId" v:"required" dc:"目标仓库ID"` + ToZoneId *bson.ObjectID `json:"toZoneId" dc:"目标库区ID(可选)"` + ToLocationId *bson.ObjectID `json:"toLocationId" dc:"目标库位ID(可选)"` + Quantity int `json:"quantity" dc:"调拨数量(保留字段,暂未使用)"` + Remark string `json:"remark" dc:"备注"` +} diff --git a/model/dto/stock/stock_traceability_dto.go b/model/dto/stock/stock_traceability_dto.go new file mode 100644 index 0000000..76d3a17 --- /dev/null +++ b/model/dto/stock/stock_traceability_dto.go @@ -0,0 +1 @@ +package dto diff --git a/model/dto/stock/stock_transaction_dto.go b/model/dto/stock/stock_transaction_dto.go new file mode 100644 index 0000000..76d3a17 --- /dev/null +++ b/model/dto/stock/stock_transaction_dto.go @@ -0,0 +1 @@ +package dto diff --git a/model/dto/stock/unit_conversion_dto.go b/model/dto/stock/unit_conversion_dto.go new file mode 100644 index 0000000..0f87dcb --- /dev/null +++ b/model/dto/stock/unit_conversion_dto.go @@ -0,0 +1,56 @@ +package dto + +import ( + "assets/consts/stock" + entity "assets/model/entity/stock" + + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateUnitConversionReq 创建单位换算请求 +type CreateUnitConversionReq struct { + g.Meta `path:"/createUnitConversion" method:"post" tags:"单位换算" summary:"创建单位换算" dc:"创建新的单位换算规则"` + ConversionCode string `json:"conversionCode" v:"required" dc:"换算编码"` + ConversionName string `json:"conversionName" v:"required" dc:"换算名称"` + UnitType stock.CapacityUnitType `json:"unitType" v:"required" dc:"单位类型"` + FromUnit string `json:"fromUnit" v:"required" dc:"源单位"` + ToUnit string `json:"toUnit" v:"required" dc:"目标单位"` + ConversionFactor float64 `json:"conversionFactor" v:"required|min:0" dc:"换算系数"` + Remark string `json:"remark" dc:"备注"` +} + +type CreateUnitConversionRes struct { + Id *bson.ObjectID `json:"id" dc:"换算规则ID"` +} + +// UpdateUnitConversionReq 更新单位换算请求 +type UpdateUnitConversionReq struct { + g.Meta `path:"/updateUnitConversion" method:"put" tags:"单位换算" summary:"更新单位换算" dc:"更新单位换算规则"` + Id *bson.ObjectID `json:"id" v:"required" dc:"换算规则ID"` + ConversionCode string `json:"conversionCode" dc:"换算编码"` + ConversionName string `json:"conversionName" dc:"换算名称"` + UnitType stock.CapacityUnitType `json:"unitType" dc:"单位类型"` + FromUnit string `json:"fromUnit" dc:"源单位"` + ToUnit string `json:"toUnit" dc:"目标单位"` + ConversionFactor float64 `json:"conversionFactor" v:"min:0" dc:"换算系数"` + Remark string `json:"remark" dc:"备注"` +} + +// DeleteUnitConversionReq 删除单位换算请求 +type DeleteUnitConversionReq struct { + g.Meta `path:"/deleteUnitConversion" method:"delete" tags:"单位换算" summary:"删除单位换算" dc:"删除单位换算规则"` + Id *bson.ObjectID `json:"id" v:"required" dc:"换算规则ID"` +} + +// ListUnitConversionReq 查询单位换算列表请求 +type ListUnitConversionReq struct { + g.Meta `path:"/listUnitConversion" method:"get" tags:"单位换算" summary:"查询单位换算列表" dc:"查询单位换算规则列表"` + UnitType *stock.CapacityUnitType `json:"unitType" dc:"过滤单位类型"` + FromUnit string `json:"fromUnit" dc:"过滤源单位"` + ToUnit string `json:"toUnit" dc:"过滤目标单位"` +} + +type ListUnitConversionRes struct { + List []entity.UnitConversion `json:"list" dc:"换算规则列表"` +} diff --git a/model/dto/stock/warehouse_dto.go b/model/dto/stock/warehouse_dto.go new file mode 100644 index 0000000..2047842 --- /dev/null +++ b/model/dto/stock/warehouse_dto.go @@ -0,0 +1,115 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateWarehouseReq 创建仓库请求 +type CreateWarehouseReq struct { + g.Meta `path:"/createWarehouse" method:"post" tags:"仓库管理" summary:"创建仓库" dc:"创建新的仓库"` + WarehouseCode string `json:"warehouseCode" v:"required|max-length:50#仓库编码不能为空|仓库编码不能超过50个字符" dc:"仓库编码"` + WarehouseName string `json:"warehouseName" v:"required|max-length:100#仓库名称不能为空|仓库名称不能超过100个字符" dc:"仓库名称"` + Address string `json:"address" dc:"仓库地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` + Status *stock.WarehouseStatus `json:"status" dc:"仓库状态(可选,默认启用)"` + Remark string `json:"remark" dc:"备注"` +} + +// CreateWarehouseRes 创建仓库响应 +type CreateWarehouseRes struct { + Id *bson.ObjectID `json:"id" dc:"仓库ID"` +} + +// UpdateWarehouseReq 更新仓库请求 +type UpdateWarehouseReq struct { + g.Meta `path:"/updateWarehouse" method:"put" tags:"仓库管理" summary:"更新仓库" dc:"更新仓库信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"仓库ID"` + WarehouseCode string `json:"warehouseCode" dc:"仓库编码"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + Address string `json:"address" dc:"仓库地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` + Status *stock.WarehouseStatus `json:"status" dc:"仓库状态"` + Remark string `json:"remark" dc:"备注"` +} + +// UpdateWarehouseRes 更新仓库响应 +type UpdateWarehouseRes struct { + Id *bson.ObjectID `json:"id" dc:"仓库ID"` +} + +// DeleteWarehouseReq 删除仓库请求 +type DeleteWarehouseReq struct { + g.Meta `path:"/deleteWarehouse" method:"delete" tags:"仓库管理" summary:"删除仓库" dc:"删除仓库"` + Id *bson.ObjectID `json:"id" v:"required" dc:"仓库ID"` +} + +// UpdateWarehouseStatusReq 更新仓库状态请求 +type UpdateWarehouseStatusReq struct { + g.Meta `path:"/updateWarehouseStatus" method:"put" tags:"仓库管理" summary:"更新仓库状态" dc:"单独更新仓库状态(启用/停用)"` + Id *bson.ObjectID `json:"id" v:"required" dc:"仓库ID"` + Status stock.WarehouseStatus `json:"status" v:"required|in:enable,disable#状态不能为空|状态值无效" dc:"仓库状态"` +} + +// DeleteWarehouseRes 删除仓库响应 +type DeleteWarehouseRes struct { + Id *bson.ObjectID `json:"id" dc:"仓库ID"` +} + +// GetWarehouseReq 获取仓库详情请求 +type GetWarehouseReq struct { + g.Meta `path:"/getWarehouse" method:"get" tags:"仓库管理" summary:"获取仓库详情" dc:"获取仓库详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"仓库ID"` +} + +// GetWarehouseRes 获取仓库详情响应 +type GetWarehouseRes struct { + Id *bson.ObjectID `json:"id" dc:"仓库ID"` + WarehouseCode string `json:"warehouseCode" dc:"仓库编码"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + Address string `json:"address" dc:"仓库地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` + Status stock.WarehouseStatus `json:"status" dc:"仓库状态"` + StatusText string `json:"statusText" dc:"状态文本"` + Remark string `json:"remark" dc:"备注"` + ZoneCount int `json:"zoneCount" dc:"库区数量"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListWarehouseReq 获取仓库列表请求 +type ListWarehouseReq struct { + g.Meta `path:"/listWarehouses" method:"get" tags:"仓库管理" summary:"获取仓库列表" dc:"分页查询仓库列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + Status *stock.WarehouseStatus `json:"status" dc:"仓库状态"` + Keyword string `json:"keyword" dc:"关键词搜索(编码/名称)"` +} + +// ListWarehouseRes 获取仓库列表响应 +type ListWarehouseRes struct { + List []WarehouseListItem `json:"list" dc:"仓库列表"` + Total int64 `json:"total" dc:"总数"` +} + +// WarehouseListItem 仓库列表项 +type WarehouseListItem struct { + Id *bson.ObjectID `json:"id" dc:"仓库ID"` + WarehouseCode string `json:"warehouseCode" dc:"仓库编码"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + Address string `json:"address" dc:"仓库地址"` + ContactPerson string `json:"contactPerson" dc:"联系人"` + ContactPhone string `json:"contactPhone" dc:"联系电话"` + Status stock.WarehouseStatus `json:"status" dc:"仓库状态"` + StatusText string `json:"statusText" dc:"状态文本"` + ZoneCount int `json:"zoneCount" dc:"库区数量"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/model/dto/stock/zone_dto.go b/model/dto/stock/zone_dto.go new file mode 100644 index 0000000..7f478ed --- /dev/null +++ b/model/dto/stock/zone_dto.go @@ -0,0 +1,122 @@ +package dto + +import ( + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateZoneReq 创建库区请求 +type CreateZoneReq struct { + g.Meta `path:"/createZone" method:"post" tags:"库区管理" summary:"创建库区" dc:"创建新的库区"` + WarehouseId string `json:"warehouseId" v:"required" dc:"仓库ID"` + ZoneCode string `json:"zoneCode" v:"required|max-length:50#库区编码不能为空|库区编码不能超过50个字符" dc:"库区编码"` + ZoneName string `json:"zoneName" v:"required|max-length:100#库区名称不能为空|库区名称不能超过100个字符" dc:"库区名称"` + ZoneType stock.ZoneType `json:"zoneType" v:"required" dc:"库区类型"` + Capacity int `json:"capacity" dc:"容量"` + Status *stock.ZoneStatus `json:"status" dc:"库区状态(可选,默认启用)"` + Remark string `json:"remark" dc:"备注"` +} + +// CreateZoneRes 创建库区响应 +type CreateZoneRes struct { + Id *bson.ObjectID `json:"id" dc:"库区ID"` +} + +// UpdateZoneReq 更新库区请求 +type UpdateZoneReq struct { + g.Meta `path:"/updateZone" method:"put" tags:"库区管理" summary:"更新库区" dc:"更新库区信息"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库区ID"` + WarehouseId string `json:"warehouseId" dc:"仓库ID"` + ZoneCode string `json:"zoneCode" dc:"库区编码"` + ZoneName string `json:"zoneName" dc:"库区名称"` + ZoneType *stock.ZoneType `json:"zoneType" dc:"库区类型"` + Capacity *int `json:"capacity" dc:"容量"` + Status *stock.ZoneStatus `json:"status" dc:"库区状态"` + Remark string `json:"remark" dc:"备注"` +} + +// UpdateZoneRes 更新库区响应 +type UpdateZoneRes struct { + Id *bson.ObjectID `json:"id" dc:"库区ID"` +} + +// DeleteZoneReq 删除库区请求 +type DeleteZoneReq struct { + g.Meta `path:"/deleteZone" method:"delete" tags:"库区管理" summary:"删除库区" dc:"删除库区"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库区ID"` +} + +// UpdateZoneStatusReq 更新库区状态请求 +type UpdateZoneStatusReq struct { + g.Meta `path:"/updateZoneStatus" method:"put" tags:"库区管理" summary:"更新库区状态" dc:"单独更新库区状态(启用/停用)"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库区ID"` + Status stock.ZoneStatus `json:"status" v:"required|in:enable,disable#状态不能为空|状态值无效" dc:"库区状态"` +} + +// DeleteZoneRes 删除库区响应 +type DeleteZoneRes struct { + Id *bson.ObjectID `json:"id" dc:"库区ID"` +} + +// GetZoneReq 获取库区详情请求 +type GetZoneReq struct { + g.Meta `path:"/getZone" method:"get" tags:"库区管理" summary:"获取库区详情" dc:"获取库区详情"` + Id *bson.ObjectID `json:"id" v:"required" dc:"库区ID"` +} + +// GetZoneRes 获取库区详情响应 +type GetZoneRes struct { + Id *bson.ObjectID `json:"id" dc:"库区ID"` + WarehouseId string `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneCode string `json:"zoneCode" dc:"库区编码"` + ZoneName string `json:"zoneName" dc:"库区名称"` + ZoneType stock.ZoneType `json:"zoneType" dc:"库区类型"` + ZoneTypeText string `json:"zoneTypeText" dc:"库区类型文本"` + Capacity int `json:"capacity" dc:"容量"` + Status stock.ZoneStatus `json:"status" dc:"库区状态"` + StatusText string `json:"statusText" dc:"状态文本"` + Remark string `json:"remark" dc:"备注"` + LocationCount int `json:"locationCount" dc:"库位数量"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ListZoneReq 获取库区列表请求 +type ListZoneReq struct { + g.Meta `path:"/listZones" method:"get" tags:"库区管理" summary:"获取库区列表" dc:"分页查询库区列表"` + *beans.Page + OrderBy []beans.OrderBy `json:"orderBy" dc:"排序规则"` + WarehouseId string `json:"warehouseId" dc:"仓库ID(单个,兼容旧版)"` + WarehouseIds []string `json:"warehouseIds" dc:"仓库ID列表(批量查询)"` + ZoneType *stock.ZoneType `json:"zoneType" dc:"库区类型"` + Status *stock.ZoneStatus `json:"status" dc:"库区状态"` + Keyword string `json:"keyword" dc:"关键词搜索(编码/名称)"` +} + +// ListZoneRes 获取库区列表响应 +type ListZoneRes struct { + List []ZoneListItem `json:"list" dc:"库区列表"` + Total int64 `json:"total" dc:"总数"` +} + +// ZoneListItem 库区列表项 +type ZoneListItem struct { + Id *bson.ObjectID `json:"id" dc:"库区ID"` + WarehouseId string `json:"warehouseId" dc:"仓库ID"` + WarehouseName string `json:"warehouseName" dc:"仓库名称"` + ZoneCode string `json:"zoneCode" dc:"库区编码"` + ZoneName string `json:"zoneName" dc:"库区名称"` + ZoneType stock.ZoneType `json:"zoneType" dc:"库区类型"` + ZoneTypeText string `json:"zoneTypeText" dc:"库区类型文本"` + Capacity int `json:"capacity" dc:"容量"` + Status stock.ZoneStatus `json:"status" dc:"库区状态"` + StatusText string `json:"statusText" dc:"状态文本"` + LocationCount int `json:"locationCount" dc:"库位数量"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/model/dto/sync/platform_dto.go b/model/dto/sync/platform_dto.go new file mode 100644 index 0000000..13265e0 --- /dev/null +++ b/model/dto/sync/platform_dto.go @@ -0,0 +1,248 @@ +package dto + +import ( + consts "assets/consts/public" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// AssetRequest 通用资产请求结构体 +type AssetRequest struct { + ID *bson.ObjectID `json:"id" dc:"资产ID"` + Name string `json:"name" dc:"资产名称"` + CategoryID *bson.ObjectID `json:"categoryId" dc:"分类ID"` + ImageURL string `json:"imageUrl" dc:"主图URL"` + Images interface{} `json:"images" dc:"图片列表"` + Description string `json:"description" dc:"描述"` + BasePrice float64 `json:"basePrice" dc:"基础价格"` + SalePrice float64 `json:"salePrice" dc:"销售价格"` + BrandID string `json:"brandId" dc:"品牌ID"` + Status string `json:"status" dc:"状态"` +} + +// AssetResponse 通用资产响应结构体 +type AssetResponse struct { + ID *bson.ObjectID `json:"id" dc:"资产ID"` + Name string `json:"name" dc:"资产名称"` + CategoryID *bson.ObjectID `json:"categoryId" dc:"分类ID"` + ImageURL string `json:"imageUrl" dc:"主图URL"` + Images interface{} `json:"images" dc:"图片列表"` + Description string `json:"description" dc:"描述"` + BasePrice float64 `json:"basePrice" dc:"基础价格"` + SalePrice float64 `json:"salePrice" dc:"销售价格"` + Platform string `json:"platform" dc:"平台"` +} + +// APIResponse 通用API响应结构体 +type APIResponse struct { + Success bool `json:"success" dc:"是否成功"` + ErrorCode int64 `json:"error_code,omitempty" dc:"错误码"` + ErrorMsg string `json:"error_msg,omitempty" dc:"错误信息"` + Data interface{} `json:"data,omitempty" dc:"响应数据"` +} + +// PlatformMapping 平台映射结构体 +type PlatformMapping struct { + AssetID string `json:"asset_id" dc:"资产ID"` + PlatformID string `json:"platform_id" dc:"平台ID"` + ExternalID string `json:"external_id" dc:"外部ID"` + PlatformType string `json:"platform_type" dc:"平台类型"` +} + +// KuaishouAssetRequest 快手资产请求结构体 +type KuaishouAssetRequest struct { + Title string `json:"title" dc:"商品标题"` + CategoryID int64 `json:"category_id" dc:"分类ID"` + MainImage string `json:"main_image" dc:"主图"` + Images interface{} `json:"images" dc:"图片列表"` + Description string `json:"description" dc:"描述"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + Price float64 `json:"price" dc:"价格"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` + ExternalID string `json:"external_id" dc:"外部ID"` +} + +// KuaishouAssetResponse 快手资产响应结构体 +type KuaishouAssetResponse struct { + ItemID string `json:"item_id" dc:"商品ID"` + Title string `json:"title" dc:"商品标题"` + CategoryID int64 `json:"category_id" dc:"分类ID"` + MainImage string `json:"main_image" dc:"主图"` + Price float64 `json:"price" dc:"价格"` + ExternalID string `json:"external_id" dc:"外部ID"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// KuaishouSkuRequest 快手SKU请求结构体 +type KuaishouSkuRequest struct { + SkuName string `json:"sku_name" dc:"SKU名称"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + Price float64 `json:"price" dc:"价格"` + StockNum int64 `json:"stock_num" dc:"库存数量"` + Specs interface{} `json:"specs" dc:"规格参数"` + Status int `json:"status" dc:"状态"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// KuaishouStockRequest 快手库存请求结构体 +type KuaishouStockRequest struct { + SkuID string `json:"sku_id" dc:"SKU ID"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + StockNum int64 `json:"stock_num" dc:"库存数量"` + LockStockNum int64 `json:"lock_stock_num" dc:"锁定库存"` + InTransitNum int64 `json:"in_transit_num" dc:"在途数量"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// DouyinAssetRequest 抖音资产请求结构体 +type DouyinAssetRequest struct { + Goods struct { + ProductName string `json:"product_name" dc:"商品名称"` + SecCategoryID int64 `json:"sec_category_id" dc:"二级分类ID"` + MainImg string `json:"main_img" dc:"主图"` + Imgs interface{} `json:"imgs" dc:"图片列表"` + DetailHTML string `json:"detail_html" dc:"详情HTML"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + DiscountPrice float64 `json:"discount_price" dc:"折扣价"` + MobileDetail string `json:"mobile_detail" dc:"移动端详情"` + PayType int `json:"pay_type" dc:"支付类型"` + DeliveryInfo interface{} `json:"delivery_info" dc:"配送信息"` + } `json:"goods" dc:"商品信息"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` + ExternalID string `json:"external_id" dc:"外部ID"` +} + +// DouyinAssetResponse 抖音资产响应结构体 +type DouyinAssetResponse struct { + GoodsID string `json:"goods_id" dc:"商品ID"` + ProductName string `json:"product_name" dc:"商品名称"` + SecCategoryID int64 `json:"sec_category_id" dc:"二级分类ID"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + DiscountPrice float64 `json:"discount_price" dc:"折扣价"` + ExternalID string `json:"external_id" dc:"外部ID"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// DouyinSkuRequest 抖音SKU请求结构体 +type DouyinSkuRequest struct { + SkuName string `json:"sku_name" dc:"SKU名称"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + Price float64 `json:"price" dc:"价格"` + StockNum int64 `json:"stock_num" dc:"库存数量"` + Specs interface{} `json:"specs" dc:"规格参数"` + Status int `json:"status" dc:"状态"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// DouyinStockRequest 抖音库存请求结构体 +type DouyinStockRequest struct { + SkuID string `json:"sku_id" dc:"SKU ID"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + StockNum int64 `json:"stock_num" dc:"库存数量"` + LockStockNum int64 `json:"lock_stock_num" dc:"锁定库存"` + InTransitNum int64 `json:"in_transit_num" dc:"在途数量"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// JDAssetRequest 京东资产请求结构体 +type JDAssetRequest struct { + Product struct { + Name string `json:"name" dc:"商品名称"` + CategoryID string `json:"category_id" dc:"分类ID"` + BrandID string `json:"brand_id" dc:"品牌ID"` + MainImage string `json:"main_image" dc:"主图"` + Images interface{} `json:"images" dc:"图片列表"` + Description string `json:"description" dc:"描述"` + Specifications interface{} `json:"specifications" dc:"规格参数"` + Logistics interface{} `json:"logistics" dc:"物流信息"` + AfterSales interface{} `json:"after_sales" dc:"售后信息"` + SupplyPrice float64 `json:"supply_price" dc:"供货价"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + Status string `json:"status" dc:"状态"` + } `json:"product" dc:"商品信息"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` + ExternalID string `json:"external_id" dc:"外部ID"` +} + +// JDAssetResponse 京东资产响应结构体 +type JDAssetResponse struct { + SkuID string `json:"sku_id" dc:"SKU ID"` + WareID string `json:"ware_id" dc:"商品ID"` + ProductName string `json:"product_name" dc:"商品名称"` + CategoryID string `json:"category_id" dc:"分类ID"` + SupplyPrice float64 `json:"supply_price" dc:"供货价"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + ExternalID string `json:"external_id" dc:"外部ID"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// JDSkuRequest 京东SKU请求结构体 +type JDSkuRequest struct { + SkuName string `json:"sku_name" dc:"SKU名称"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + Price float64 `json:"price" dc:"价格"` + StockNum int64 `json:"stock_num" dc:"库存数量"` + Specification string `json:"specification" dc:"规格"` + Status string `json:"status" dc:"状态"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// JDStockRequest 京东库存请求结构体 +type JDStockRequest struct { + SkuID string `json:"sku_id" dc:"SKU ID"` + OuterSkuID string `json:"outer_sku_id" dc:"外部SKU ID"` + Quantity int64 `json:"quantity" dc:"库存数量"` + LockQuantity int64 `json:"lock_quantity" dc:"锁定库存"` + InTransit int64 `json:"in_transit" dc:"在途数量"` + Location string `json:"location" dc:"仓库位置"` + SyncPlatform string `json:"sync_platform" dc:"同步平台"` +} + +// PinduoduoAssetRequest 拼多多资产请求结构体 +type PinduoduoAssetRequest struct { + GoodsID string `json:"goods_id" dc:"商品ID"` + GoodsName string `json:"goods_name" dc:"商品名称"` + GoodsDesc string `json:"goods_desc" dc:"商品描述"` + CatID string `json:"cat_id" dc:"分类ID"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + GoodsImageURL string `json:"goods_image_url" dc:"商品主图"` + GoodsGalleryURLs interface{} `json:"goods_gallery_urls" dc:"商品轮播图"` +} + +// PinduoduoAssetResponse 拼多多资产响应结构体 +type PinduoduoAssetResponse struct { + GoodsID string `json:"goods_id" dc:"商品ID"` + GoodsName string `json:"goods_name" dc:"商品名称"` + GoodsDesc string `json:"goods_desc" dc:"商品描述"` + CatID string `json:"cat_id" dc:"分类ID"` + MarketPrice float64 `json:"market_price" dc:"市场价"` + GoodsImageURL string `json:"goods_image_url" dc:"商品主图"` +} + +// SkuRequest 通用SKU请求结构体 +type SkuRequest struct { + ID string `json:"id" dc:"SKU ID"` + SkuCode string `json:"skuCode" dc:"SKU编码"` + Price float64 `json:"price" dc:"价格"` + Stock int64 `json:"stock" dc:"库存"` + Specification interface{} `json:"specification" dc:"规格"` +} + +// StockRequest 通用库存请求结构体 +type StockRequest struct { + ID string `json:"id" dc:"库存ID"` + AssetSkuID string `json:"assetSkuId" dc:"资产SKU ID"` + AvailableQty int64 `json:"availableQty" dc:"可用数量"` + LockedQty int64 `json:"lockedQty" dc:"锁定数量"` + InTransitQty int64 `json:"inTransitQty" dc:"在途数量"` + Location string `json:"location" dc:"位置"` +} + +// SyncResult 同步结果结构体 +type SyncResult struct { + Success bool `json:"success" dc:"是否成功"` + Message string `json:"message" dc:"消息"` + Data interface{} `json:"data" dc:"数据"` + Platform consts.SyncPlatform `json:"platform" dc:"平台"` + ExternalID string `json:"external_id,omitempty" dc:"外部ID"` +} diff --git a/model/dto/sync/sync_dto.go b/model/dto/sync/sync_dto.go new file mode 100644 index 0000000..8e78eea --- /dev/null +++ b/model/dto/sync/sync_dto.go @@ -0,0 +1,142 @@ +package dto + +import ( + consts "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateSyncTaskReq 创建同步任务请求 +type CreateSyncTaskReq struct { + g.Meta `path:"/createSyncTask" method:"post" tags:"同步任务" summary:"创建同步任务" dc:"创建新的同步任务"` + + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` + SyncType consts.SyncType `json:"syncType" v:"required" dc:"同步类型:full全量/incremental增量"` + AssetID *bson.ObjectID `json:"assetId,omitempty" dc:"资产ID"` + AssetSKUID *bson.ObjectID `json:"assetSkuId,omitempty" dc:"资产SKU ID"` + StockID *bson.ObjectID `json:"stockId,omitempty" dc:"库存ID"` +} + +// CreateSyncTaskRes 创建同步任务响应 +type CreateSyncTaskRes struct { + TaskID *bson.ObjectID `json:"taskId" dc:"任务ID"` +} + +// ListSyncTaskReq 同步任务列表请求 +type ListSyncTaskReq struct { + g.Meta `path:"/listSyncTasks" method:"get" tags:"同步任务" summary:"获取同步任务列表" dc:"分页查询同步任务列表,支持多条件筛选"` + beans.Page + Platform consts.SyncPlatform `json:"platform,omitempty" dc:"同步平台"` + Status consts.SyncStatus `json:"status,omitempty" dc:"同步状态"` + StartTime *gtime.Time `json:"startTime,omitempty" dc:"开始时间"` + EndTime *gtime.Time `json:"endTime,omitempty" dc:"结束时间"` +} + +// ListSyncTaskRes 同步任务列表响应 +type ListSyncTaskRes struct { + List []*SyncTaskItem `json:"list" dc:"同步任务列表"` + Total int64 `json:"total" dc:"总数"` +} + +// SyncTaskItem 同步任务项 +type SyncTaskItem struct { + ID *bson.ObjectID `json:"id" dc:"任务ID"` + Platform consts.SyncPlatform `json:"platform" dc:"同步平台"` + SyncType consts.SyncType `json:"syncType" dc:"同步类型"` + Status consts.SyncStatus `json:"status" dc:"同步状态"` + AssetID *bson.ObjectID `json:"assetId,omitempty" dc:"资产ID"` + AssetSKUID *bson.ObjectID `json:"assetSkuId,omitempty" dc:"资产SKU ID"` + StockID *bson.ObjectID `json:"stockId,omitempty" dc:"库存ID"` + ErrorMessage string `json:"errorMessage,omitempty" dc:"错误信息"` + ErrorCount int `json:"errorCount" dc:"错误次数"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + StartedAt *gtime.Time `json:"startedAt,omitempty" dc:"开始时间"` + FinishedAt *gtime.Time `json:"finishedAt,omitempty" dc:"完成时间"` +} + +// GetSyncTaskReq 获取同步任务详情请求 +type GetSyncTaskReq struct { + g.Meta `path:"/getSyncTask" method:"get" tags:"同步任务" summary:"获取同步任务详情" dc:"获取同步任务详情"` + ID *bson.ObjectID `json:"id" v:"required" dc:"任务ID"` +} + +// GetSyncTaskRes 获取同步任务详情响应 +type GetSyncTaskRes struct { + SyncTaskItem +} + +// UpdateSyncTaskStatusReq 更新同步任务状态请求 +type UpdateSyncTaskStatusReq struct { + g.Meta `path:"/updateSyncTaskStatus" method:"put" tags:"同步任务" summary:"更新同步任务状态" dc:"更新同步任务状态"` + ID *bson.ObjectID `json:"id" v:"required" dc:"任务ID"` + Status consts.SyncStatus `json:"status" v:"required" dc:"同步状态"` + ErrorMessage string `json:"errorMessage,omitempty" dc:"错误信息"` +} + +// SyncAssetReq 同步资产请求 +type SyncAssetReq struct { + g.Meta `path:"/syncAsset" method:"post" tags:"资产同步" summary:"同步资产" dc:"同步资产到指定平台"` + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` + AssetID *bson.ObjectID `json:"assetId" v:"required" dc:"资产ID"` +} + +// SyncAssetRes 同步资产响应 +type SyncAssetRes struct { + TaskID *bson.ObjectID `json:"taskId" dc:"任务ID"` +} + +// SyncAssetSkuReq 同步资产SKU请求 +type SyncAssetSkuReq struct { + g.Meta `path:"/syncAssetSku" method:"post" tags:"SKU同步" summary:"同步资产SKU" dc:"同步资产SKU到指定平台"` + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` + AssetSKUID *bson.ObjectID `json:"assetSkuId" v:"required" dc:"资产SKU ID"` +} + +// SyncAssetSkuRes 同步资产SKU响应 +type SyncAssetSkuRes struct { + TaskID *bson.ObjectID `json:"taskId" dc:"任务ID"` +} + +// SyncStockReq 同步库存请求 +type SyncStockReq struct { + g.Meta `path:"/syncStock" method:"post" tags:"库存同步" summary:"同步库存" dc:"同步库存到指定平台"` + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` + StockID *bson.ObjectID `json:"stockId" v:"required" dc:"库存ID"` +} + +// SyncStockRes 同步库存响应 +type SyncStockRes struct { + TaskID *bson.ObjectID `json:"taskId" dc:"任务ID"` +} + +// BatchSyncAssetsReq 批量同步资产请求 +type BatchSyncAssetsReq struct { + g.Meta `path:"/batchSyncAssets" method:"post" tags:"批量同步" summary:"批量同步资产" dc:"批量同步资产到指定平台"` + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` + AssetIDs []*bson.ObjectID `json:"assetIds" v:"required" dc:"资产ID列表"` +} + +// BatchSyncAssetsRes 批量同步资产响应 +type BatchSyncAssetsRes struct { + TaskIDs []*bson.ObjectID `json:"taskIds" dc:"任务ID列表"` +} + +// GetPlatformSyncStatusReq 获取平台同步状态请求 +type GetPlatformSyncStatusReq struct { + g.Meta `path:"/getPlatformSyncStatus" method:"get" tags:"平台状态" summary:"获取平台同步状态" dc:"获取指定平台的同步状态"` + Platform consts.SyncPlatform `json:"platform" v:"required" dc:"同步平台"` +} + +// GetPlatformSyncStatusRes 获取平台同步状态响应 +type GetPlatformSyncStatusRes struct { + Platform consts.SyncPlatform `json:"platform" dc:"同步平台"` + IsEnabled bool `json:"isEnabled" dc:"是否启用"` + LastSyncTime *gtime.Time `json:"lastSyncTime,omitempty" dc:"最后同步时间"` + NextSyncTime *gtime.Time `json:"nextSyncTime,omitempty" dc:"下次同步时间"` + SyncCount int64 `json:"syncCount" dc:"同步次数"` + SuccessCount int64 `json:"successCount" dc:"成功次数"` + FailedCount int64 `json:"failedCount" dc:"失败次数"` +} diff --git a/model/entity/asset/asset.go b/model/entity/asset/asset.go new file mode 100644 index 0000000..7009863 --- /dev/null +++ b/model/entity/asset/asset.go @@ -0,0 +1,43 @@ +package entity + +import ( + consts "assets/consts/asset" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gtime" +) + +// Asset 资产实体 +type Asset struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, IsDeleted + // 基础信息 + Name string `orm:"name" json:"name" description:"资产名称"` + Description string `orm:"description" json:"description" description:"资产描述"` + Type consts.AssetType `orm:"type" json:"type" description:"资产类型:physical实物/virtual虚拟/service服务"` + CategoryId uint64 `orm:"category_id" json:"categoryId" description:"分类ID"` + CategoryPath string `orm:"category_path" json:"categoryPath" description:"分类路径"` + ImageURL string `orm:"image_url" json:"imageUrl" description:"主图URL"` + Images []string `orm:"images" json:"images" description:"图片列表(JSONB)"` + Status *consts.AssetStatus `orm:"status" json:"status" description:"资产状态:1启用/0停用"` + BasePrice int `orm:"base_price" json:"basePrice" description:"基础价格(分为单位)"` + Currency string `orm:"currency" json:"currency" description:"货币单位,默认CNY"` + UnlimitedStock bool `orm:"unlimited_stock" json:"unlimitedStock" description:"是否无库存限制"` + StockMode stock.StockMode `orm:"stock_mode" json:"stockMode" description:"库存管理模式:1-明细模式 2-批次模式"` + // 上线和下线时间配置(由定时任务处理资产状态) + OnlineTime *gtime.Time `orm:"online_time" json:"onlineTime,omitempty" description:"上线时间"` + OfflineTime *gtime.Time `orm:"offline_time" json:"offlineTime,omitempty" description:"下线时间"` + + // 类型专用配置 - 实物资产配置(JSONB) + PhysicalAssetConfig *gjson.Json `orm:"physical_asset_config" json:"physicalAssetConfig" description:"实物资产配置(JSONB)"` + // 类型专用配置 - 服务资产配置(JSONB) + ServiceAssetConfig *gjson.Json `orm:"service_asset_config" json:"serviceAssetConfig" description:"服务资产配置(JSONB)"` + // 类型专用配置 - 虚拟资产配置(JSONB) + VirtualAssetConfig *gjson.Json `orm:"virtual_asset_config" json:"virtualAssetConfig" description:"虚拟资产配置(JSONB)"` + // 扩展字段(JSONB) + Metadata *gjson.Json `orm:"metadata" json:"metadata" description:"动态元数据(JSONB)"` + + TenantModuleType string `orm:"tenant_module_type" json:"tenantModuleType" description:"租户模块类型"` +} diff --git a/model/entity/asset/asset_sku.go b/model/entity/asset/asset_sku.go new file mode 100644 index 0000000..4987075 --- /dev/null +++ b/model/entity/asset/asset_sku.go @@ -0,0 +1,44 @@ +package entity + +import ( + consts "assets/consts/asset" + "assets/consts/public" + "assets/consts/stock" + "assets/model/config" + + "gitea.com/red-future/common/beans" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// AssetSku 资产SKU实体 +type AssetSku struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + AssetId *bson.ObjectID `bson:"assetId" json:"assetId"` // 关联资产ID + AssetName string `bson:"assetName" json:"assetName"` // 资产名称 + SkuName string `bson:"skuName" json:"skuName"` // SKU名称 + ImageURL string `bson:"imageUrl,omitempty" json:"imageUrl"` // SKU主图 + SpecValues []map[string]interface{} `bson:"specValues" json:"specValues"` // 规格值:{"颜色":"红色","尺寸":"L","时长":"1个月","平台":"抖音"} + Price int `bson:"price" json:"price"` // 价格(分为单位) + UnlimitedStock bool `bson:"unlimitedStock" json:"unlimitedStock"` // 是否无库存限制 + Stock int `bson:"stock" json:"stock"` // 库存数量 + SpecsCount int `bson:"specsCount" json:"specsCount"` // 规格数量 + SpecsUnit *SpecsUnitKeyValue `bson:"specsUnit" json:"specsUnit"` // 规格单位 + Sort int `bson:"sort" json:"sort"` // 排序 + Status *consts.AssetSkuStatus `bson:"status" json:"status"` // 状态:active/inactive/disabled + StockMode stock.StockMode `bson:"stockMode" json:"stockMode"` // 库存管理模式:1-明细模式 2-批次模式 + CategoryId *bson.ObjectID `bson:"categoryId" json:"categoryId"` // 分类ID + CategoryPath string `bson:"categoryPath" json:"categoryPath"` // 分类路径 + CapacityUnitType stock.CapacityUnitType `bson:"capacityUnitType" json:"capacityUnitType"` // 容量单位类型 + Capacity config.Capacity `bson:"capacity" json:"capacity"` //容量 + TenantModuleType beans.TenantModuleType `bson:"tenantModuleType" json:"tenantModuleType"` +} + +type SpecsUnitKeyValue struct { + Key string `bson:"key" json:"key"` // 对应原有常量值 + Value string `bson:"value" json:"value"` // 对应描述信息 +} + +// CollectionName 获取集合名称 +func (AssetSku) CollectionName() string { + return public.AssetSkuCollection +} diff --git a/model/entity/asset/category.go b/model/entity/asset/category.go new file mode 100644 index 0000000..8817b24 --- /dev/null +++ b/model/entity/asset/category.go @@ -0,0 +1,74 @@ +package entity + +import ( + consts "assets/consts/category" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/encoding/gjson" +) + +type categoryCol struct { + beans.SQLBaseCol + Name string + ParentId string + Path string + Level string + IsLeafNode string + Sort string + Image string + Attrs string + Status string +} + +var CategoryCol = categoryCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + ParentId: "parent_id", + Path: "path", + Level: "level", + IsLeafNode: "isLeaf_node", + Sort: "sort", + Image: "image", + Attrs: "attrs", + Status: "status", +} + +// Category 分类实体 +type Category struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, IsDeleted + Name string `orm:"name" json:"name"` // 分类名称 + ParentId string `orm:"parent_id" json:"parentId"` // 父分类ID,为空表示根分类 + Path string `orm:"path" json:"path"` // 分类路径,如:/root/parent/child + Level int `orm:"level" json:"level"` // 分类层级 + IsLeafNode bool `orm:"isLeaf_node" json:"isLeafNode"` // 是叶子节点 + Sort int `orm:"sort" json:"sort"` // 排序 + Image string `orm:"image" json:"image"` // 分类图片 + Attrs []gjson.Json `orm:"attrs" json:"attrs,omitempty"` // 分类属性 + // 使用场景说明: + // 1. 商品分类属性:为该分类下的商品定义标准化的属性模板,如服装分类可定义尺寸、颜色、材质等属性 + // 2. 服务分类属性:为服务类目定义特性参数,如咨询服务可定义服务时长、服务方式、专业领域等 + // 3. 商品发布约束:当商品归属于某个分类时,必须填写该分类定义的必填属性 + // 4. 搜索筛选:基于分类属性进行商品筛选和搜索,提升用户体验 + // 5. 数据标准化:确保同一分类下的商品具有统一的属性结构,便于数据管理 + // 支持的属性类型:文本(text)、数字(number)、日期(date)、单选(select)、多选(multi_select)、布尔(boolean)、图片(image) + Status consts.CategoryStatusType `orm:"status" json:"status"` // 状态:1启用/0禁用 +} + +// CategoryAttr 分类属性 +// 用于定义分类下商品或服务的标准化属性模板,确保同类商品属性统一 +type CategoryAttr struct { + Name string `json:"name"` // 属性名称,如:尺寸、颜色、品牌等 + Type string `json:"type"` // 属性类型:text文本/number数字/date日期/select选择/multi_select多选/boolean布尔/image图片 + DictType string `json:"dictType"` // 字典类型,如果是select/multi_select类型时有效 + Required bool `json:"required"` // 是否必填,true表示商品发布时必须填写此属性 + Options []FieldOption `json:"options"` // 选项配置,JSON字符串格式,用于select/multi_select类型的可选值列表 + // 示例:'{"options":[{"label":"红色","value":"red"},{"label":"蓝色","value":"blue"}]}' + Description string `json:"description"` // 属性描述,向用户说明此属性的具体含义和填写要求 + Sort int `json:"sort"` // 排序权重,数值越小排序越靠前,用于属性在界面的显示顺序 +} + +// FieldOption 字段选项 +type FieldOption struct { + Label string `json:"label" dc:"选项标签"` + Value interface{} `json:"value" dc:"选项值"` +} diff --git a/model/entity/asset/private_category.go b/model/entity/asset/private_category.go new file mode 100644 index 0000000..79b9fbd --- /dev/null +++ b/model/entity/asset/private_category.go @@ -0,0 +1,25 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" +) + +// PrivateCategory 私域分类实体 +type PrivateCategory struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + Name string `bson:"name" json:"name"` // 分类名称 + ParentID string `bson:"parentId" json:"parentId"` // 父分类ID,为空表示根分类 + Path string `bson:"path" json:"path"` // 分类路径,如:/root/parent + Level int `bson:"level" json:"level"` // 分类层级 + IsLeafNode bool `bson:"isLeafNode" json:"isLeafNode"` // 是叶子节点 + Sort int `bson:"sort" json:"sort"` // 排序 + Image string `bson:"image" json:"image"` // 分类图片 +} + +// CollectionName 分类集合名称 +func (PrivateCategory) CollectionName() string { + return public.PrivateCategoryCollection +} diff --git a/model/entity/asset/private_sku.go b/model/entity/asset/private_sku.go new file mode 100644 index 0000000..6aff327 --- /dev/null +++ b/model/entity/asset/private_sku.go @@ -0,0 +1,27 @@ +package entity + +import ( + "assets/consts/public" + consts "assets/consts/stock" + "assets/model/config" + + "gitea.com/red-future/common/beans" +) + +// PrivateSku 私域资产SKU实体 +type PrivateSku struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + SkuName string `bson:"skuName" json:"skuName"` // SKU名称 + ImageURL string `bson:"imageUrl,omitempty" json:"imageUrl"` // SKU主图 + Price int `bson:"price" json:"price"` // 价格(分为单位) + Stock int `bson:"stock" json:"stock"` // 库存数量 + Sort int `bson:"sort" json:"sort"` // 排序 + CapacityUnitType consts.CapacityUnitType `bson:"capacityUnitType" json:"capacityUnitType"` // 容量单位类型 + Capacity config.Capacity `bson:"capacity" json:"capacity"` //容量 + PrivateCategoryPath string `bson:"privateCategoryPath" json:"privateCategoryPath"` // 私域分类路径 +} + +// CollectionName 获取集合名称 +func (PrivateSku) CollectionName() string { + return public.PrivateSkuCollection +} diff --git a/model/entity/procurement/purchase_bid.go b/model/entity/procurement/purchase_bid.go new file mode 100644 index 0000000..754660b --- /dev/null +++ b/model/entity/procurement/purchase_bid.go @@ -0,0 +1,46 @@ +package entity + +import ( + consts "assets/consts/procurement" + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PurchaseBid 采购投标记录(供应商参与竞价订单的记录) +type PurchaseBid struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息(核心关联:直接关联到采购订单) + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 采购订单ID(必填,直接关联) + OrderNo string `bson:"orderNo" json:"orderNo"` // 订单编号(冗余存储,便于查询) + + // 投标供应商信息 + SupplierId *bson.ObjectID `bson:"supplierId" json:"supplierId"` // 供应商ID + SupplierName string `bson:"supplierName" json:"supplierName"` // 供应商名称 + SupplierCode string `bson:"supplierCode" json:"supplierCode"` // 供应商编码 + + // 投标报价信息(核心业务数据) + TotalBidPrice int `bson:"totalBidPrice" json:"totalBidPrice"` // 总投标价格(分) + UnitPrices map[string]int `bson:"unitPrices" json:"unitPrices"` // 各商品单价 {itemId: price} + DeliveryDays int `bson:"deliveryDays" json:"deliveryDays"` // 承诺交付天数 + QualityScore float64 `bson:"qualityScore" json:"qualityScore"` // 质量评分(0-100) + ServiceScore float64 `bson:"serviceScore" json:"serviceScore"` // 服务评分(0-100) + + // 投标文件 + BidRemark string `bson:"bidRemark" json:"bidRemark"` // 投标说明 + AttachmentUrls []string `bson:"attachmentUrls" json:"attachmentUrls"` // 附件URL列表 + + // 状态信息(单一状态源,IsWinner和BidRank通过Status推断) + Status consts.BidStatus `bson:"status" json:"status"` // 投标状态:draft/submitted/viewed/winning/lost/withdrawn/expired + + // 时间戳(仅保留有意义的业务时间) + BidAt *gtime.Time `bson:"bidAt" json:"bidAt"` // 投标时间(核心业务时间) +} + +// CollectionName 获取集合名称 +func (PurchaseBid) CollectionName() string { + return public.PurchaseBidCollection +} diff --git a/model/entity/procurement/purchase_inbound.go b/model/entity/procurement/purchase_inbound.go new file mode 100644 index 0000000..a7dff9f --- /dev/null +++ b/model/entity/procurement/purchase_inbound.go @@ -0,0 +1,47 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PurchaseInbound 采购入库记录实体 +type PurchaseInbound struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息 + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 采购订单ID + OrderItemId *bson.ObjectID `bson:"orderItemId" json:"orderItemId"` // 采购订单明细ID + + // 入库数量和时间 + InboundQty int `bson:"inboundQty" json:"inboundQty"` // 本次入库数量 + InboundDate *gtime.Time `bson:"inboundDate" json:"inboundDate"` // 入库日期 + + // 仓储信息(非必填) + WarehouseId *bson.ObjectID `bson:"warehouseId,omitempty" json:"warehouseId,omitempty"` // 仓库ID + WarehouseName string `bson:"warehouseName" json:"warehouseName"` // 仓库名称 + ZoneId *bson.ObjectID `bson:"zoneId,omitempty" json:"zoneId,omitempty"` // 库区ID + ZoneName string `bson:"zoneName" json:"zoneName"` // 库区名称 + LocationId *bson.ObjectID `bson:"locationId,omitempty" json:"locationId,omitempty"` // 库位ID + LocationName string `bson:"locationName" json:"locationName"` // 库位名称 + + // 私域SKU和分类 + PrivateSkuId *bson.ObjectID `bson:"privateSkuId" json:"privateSkuId"` // 私域SKU ID + PrivateSkuName string `bson:"privateSkuName" json:"privateSkuName"` // 私域SKU名称 + PrivateCategoryId *bson.ObjectID `bson:"privateCategoryId" json:"privateCategoryId"` // 私域分类ID + PrivateCategoryPath string `bson:"privateCategoryPath" json:"privateCategoryPath"` // 私域分类路径 + + // 生成的批次信息 + BatchNo string `bson:"batchNo" json:"batchNo"` // 生成的批次号 + PrivateStockId *bson.ObjectID `bson:"privateStockId" json:"privateStockId"` // 关联的私域库存ID + InboundNo string `bson:"inboundNo" json:"inboundNo"` // 入库单号(自动生成) + Remark string `bson:"remark" json:"remark"` // 入库备注 +} + +// CollectionName 获取集合名称 +func (PurchaseInbound) CollectionName() string { + return public.PurchaseInboundCollection +} diff --git a/model/entity/procurement/purchase_order.go b/model/entity/procurement/purchase_order.go new file mode 100644 index 0000000..c5acf82 --- /dev/null +++ b/model/entity/procurement/purchase_order.go @@ -0,0 +1,91 @@ +package entity + +import ( + consts "assets/consts/procurement" + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// ============================================ +// 内嵌结构体定义 +// ============================================ + +// DirectPurchaseInfo 指定供应商模式信息 +type DirectPurchaseInfo struct { + // 指定供应商信息 + SupplierId *bson.ObjectID `bson:"supplierId" json:"supplierId"` // 指定供应商ID + SupplierName string `bson:"supplierName" json:"supplierName"` // 指定供应商名称 + SupplierCode string `bson:"supplierCode" json:"supplierCode"` // 供应商编码 + AssignReason string `bson:"assignReason" json:"assignReason"` // 指派原因 + DeliveryAddress string `bson:"deliveryAddress" json:"deliveryAddress"` // 交付地址 + ContactPerson string `bson:"contactPerson" json:"contactPerson"` // 联系人 + ContactPhone string `bson:"contactPhone" json:"contactPhone"` // 联系电话 + ResponseStatus string `bson:"responseStatus" json:"responseStatus"` // 供应商响应状态 + + // 时间戳 + AssignedAt *gtime.Time `bson:"assignedAt" json:"assignedAt"` // 指派时间 + AcceptedAt *gtime.Time `bson:"acceptedAt" json:"acceptedAt"` // 接受时间 + RejectedAt *gtime.Time `bson:"rejectedAt" json:"rejectedAt"` // 拒绝时间 + DeliveredAt *gtime.Time `bson:"deliveredAt" json:"deliveredAt"` // 交付时间 +} + +// BiddingInfo 竞价模式信息 +type BiddingInfo struct { + // 竞价设置 + BidMode consts.BidMode `bson:"bidMode" json:"bidMode"` // 竞价模式:price/quality/time/mixed + MinSuppliers int `bson:"minSuppliers" json:"minSuppliers"` // 最少参与供应商数 + MaxSuppliers int `bson:"maxSuppliers" json:"maxSuppliers"` // 最多参与供应商数 + BidDuration int `bson:"bidDuration" json:"bidDuration"` // 竞价持续时长(分钟) + BidSupplierCount int `bson:"bidSupplierCount" json:"bidSupplierCount"` // 参与竞价的供应商数量 + + // 时间戳 + BidStartAt *gtime.Time `bson:"bidStartAt" json:"bidStartAt"` // 竞价开始时间 + BidEndAt *gtime.Time `bson:"bidEndAt" json:"bidEndAt"` // 竞价结束时间 + ResultPublishedAt *gtime.Time `bson:"resultPublishedAt" json:"resultPublishedAt"` // 结果发布时间 +} + +// ============================================ +// 主实体定义 +// ============================================ + +// PurchaseOrder 采购订单实体(统一模式,结构化设计) +type PurchaseOrder struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // ============================================ + // 基础订单信息(所有模式共用) + // ============================================ + OrderNo string `bson:"orderNo" json:"orderNo"` // 订单编号 + Title string `bson:"title" json:"title"` // 订单标题 + Description string `bson:"description" json:"description"` // 订单描述 + OrderType consts.PurchaseOrderType `bson:"orderType" json:"orderType"` // 订单类型:direct/assignment/bidding + + // 需求方信息 + BuyerId *bson.ObjectID `bson:"buyerId" json:"buyerId"` // 采购方ID(经销商/门店) + BuyerName string `bson:"buyerName" json:"buyerName"` // 采购方名称 + BuyerType string `bson:"buyerType" json:"buyerType"` // 采购方类型 + + // 通用状态信息 + Status consts.PurchaseOrderStatus `bson:"status" json:"status"` // 订单状态 + Priority int `bson:"priority" json:"priority"` // 优先级 + + // ============================================ + // 模式特定信息(内嵌结构体) + // ============================================ + DirectPurchase *DirectPurchaseInfo `bson:"directPurchase,omitempty" json:"directPurchase,omitempty"` // 指定供应商模式信息 + BiddingInfo *BiddingInfo `bson:"biddingInfo,omitempty" json:"biddingInfo,omitempty"` // 竞价模式信息 + + // ============================================ + // 通用字段 + // ============================================ + ExpectedDelivery *gtime.Time `bson:"expectedDelivery" json:"expectedDelivery"` // 期望交付时间 + ExpiryTime *gtime.Time `bson:"expiryTime" json:"expiryTime"` // 订单有效期/竞价结束时间 +} + +// CollectionName 获取集合名称 +func (PurchaseOrder) CollectionName() string { + return public.PurchaseOrderCollection +} diff --git a/model/entity/procurement/purchase_order_item.go b/model/entity/procurement/purchase_order_item.go new file mode 100644 index 0000000..980fe89 --- /dev/null +++ b/model/entity/procurement/purchase_order_item.go @@ -0,0 +1,42 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PurchaseOrderItem 采购订单明细实体 +type PurchaseOrderItem struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息 + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 订单ID + AssetId *bson.ObjectID `bson:"assetId" json:"assetId"` // 资产ID + AssetSkuId *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 资产SKU ID + + // 商品信息 + ProductName string `bson:"productName" json:"productName"` // 商品名称 + Specification string `bson:"specification" json:"specification"` // 规格描述 + Brand string `bson:"brand" json:"brand"` // 品牌 + + // 数量和价格 + Quantity int `bson:"quantity" json:"quantity"` // 订购数量 + Unit string `bson:"unit" json:"unit"` // 单位 + UnitPrice int `bson:"unitPrice" json:"unitPrice"` // 单价(分) + TotalPrice int `bson:"totalPrice" json:"totalPrice"` // 总价(分) + DiscountPrice int `bson:"discountPrice" json:"discountPrice"` // 折扣价(分) + // 签收和入库 + PassQuantity int `bson:"passQuantity" json:"passQuantity"` // 签收数量(与订购数量有差异时应生成退换单) + InboundQty int `bson:"inboundQty" json:"inboundQty"` // 已入库累计数量 + + // 要求信息 + RequirementDesc string `bson:"requirementDesc" json:"requirementDesc"` // 特殊要求描述 + DeliveryAddress string `bson:"deliveryAddress" json:"deliveryAddress"` // 交付地址 +} + +// CollectionName 获取集合名称 +func (PurchaseOrderItem) CollectionName() string { + return public.PurchaseOrderItemCollection +} diff --git a/model/entity/procurement/purchase_return.go b/model/entity/procurement/purchase_return.go new file mode 100644 index 0000000..874d3ed --- /dev/null +++ b/model/entity/procurement/purchase_return.go @@ -0,0 +1,74 @@ +package entity + +import ( + consts "assets/consts/procurement" + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PurchaseReturn 采购退换单主表实体 +type PurchaseReturn struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 退换单基本信息 + ReturnNo string `bson:"returnNo" json:"returnNo"` // 退换单编号 + Title string `bson:"title" json:"title"` // 退换单标题 + Description string `bson:"description" json:"description"` // 退换说明 + ReturnType consts.ReturnType `bson:"returnType" json:"returnType"` // 退换类型:return退货/refund退款/exchange换货 + + // 关联订单信息 + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 关联采购订单ID + OrderNo string `bson:"orderNo" json:"orderNo"` // 关联采购订单编号 + AssignmentId *bson.ObjectID `bson:"assignmentId" json:"assignmentId"` // 关联指派单ID(如果有) + BiddingId *bson.ObjectID `bson:"biddingId" json:"biddingId"` // 关联竞价单ID(如果有) + + // 供应商信息 + SupplierId *bson.ObjectID `bson:"supplierId" json:"supplierId"` // 供应商ID + SupplierName string `bson:"supplierName" json:"supplierName"` // 供应商名称 + SupplierCode string `bson:"supplierCode" json:"supplierCode"` // 供应商编码 + + // 退换货物信息 + TotalItems int `bson:"totalItems" json:"totalItems"` // 退换商品种类数 + TotalQuantity int `bson:"totalQuantity" json:"totalQuantity"` // 退换商品总数量 + TotalAmount int `bson:"totalAmount" json:"totalAmount"` // 退换总金额(分) + RefundAmount int `bson:"refundAmount" json:"refundAmount"` // 实际退款金额(分) + + // 状态和流程信息 + Status consts.ReturnStatus `bson:"status" json:"status"` // 退换状态 + Priority int `bson:"priority" json:"priority"` // 优先级 + + // 地址信息 + ReturnAddress string `bson:"returnAddress" json:"returnAddress"` // 退货地址 + ReturnContact string `bson:"returnContact" json:"returnContact"` // 退货联系人 + ReturnPhone string `bson:"returnPhone" json:"returnPhone"` // 退货联系电话 + ReceiveAddress string `bson:"receiveAddress" json:"receiveAddress"` // 收货地址(换货时使用) + + // 物流信息 + LogisticsCompany string `bson:"logisticsCompany" json:"logisticsCompany"` // 物流公司 + TrackingNo string `bson:"trackingNo" json:"trackingNo"` // 物流单号 + ShipAt *gtime.Time `bson:"shipAt" json:"shipAt"` // 发货时间 + ReceiveAt *gtime.Time `bson:"receiveAt" json:"receiveAt"` // 收货时间 + + // 审批信息 + ApprovalStatus consts.ApprovalStatus `bson:"approvalStatus" json:"approvalStatus"` // 审批状态 + ApprovedBy string `bson:"approvedBy" json:"approvedBy"` // 审批人ID + ApprovedAt *gtime.Time `bson:"approvedAt" json:"approvedAt"` // 审批时间 + ApprovalRemark string `bson:"approvalRemark" json:"approvalRemark"` // 审批备注 + + // 时间戳 + RequestedAt *gtime.Time `bson:"requestedAt" json:"requestedAt"` // 申请时间 + ExpectedProcessAt *gtime.Time `bson:"expectedProcessAt" json:"expectedProcessAt"` // 期望处理时间 + CompletedAt *gtime.Time `bson:"completedAt" json:"completedAt"` // 完成时间 + + // 备注信息 + Remark string `bson:"remark" json:"remark"` // 备注 + InternalRemark string `bson:"internalRemark" json:"internalRemark"` // 内部备注 +} + +// CollectionName 获取集合名称 +func (PurchaseReturn) CollectionName() string { + return public.PurchaseReturnCollection +} diff --git a/model/entity/procurement/purchase_return_item.go b/model/entity/procurement/purchase_return_item.go new file mode 100644 index 0000000..2210e66 --- /dev/null +++ b/model/entity/procurement/purchase_return_item.go @@ -0,0 +1,57 @@ +package entity + +import ( + consts "assets/consts/procurement" + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PurchaseReturnItem 采购退换单明细实体 +type PurchaseReturnItem struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息 + ReturnId *bson.ObjectID `bson:"returnId" json:"returnId"` // 退换单ID + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 采购订单ID + OrderItemId *bson.ObjectID `bson:"orderItemId" json:"orderItemId"` // 订单明细ID + + // 商品信息 + AssetId *bson.ObjectID `bson:"assetId" json:"assetId"` // 资产ID + AssetSkuId *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 资产SKU ID + ProductName string `bson:"productName" json:"productName"` // 商品名称 + Specification string `bson:"specification" json:"specification"` // 规格 + Brand string `bson:"brand" json:"brand"` // 品牌 + Unit string `bson:"unit" json:"unit"` // 单位 + + // 数量和价格信息 + OriginalQuantity int `bson:"originalQuantity" json:"originalQuantity"` // 原始采购数量 + ReturnQuantity int `bson:"returnQuantity" json:"returnQuantity"` // 退换数量 + UnitPrice int `bson:"unitPrice" json:"unitPrice"` // 单价(分) + TotalPrice int `bson:"totalPrice" json:"totalPrice"` // 退换小计(分) + RefundPrice int `bson:"refundPrice" json:"refundPrice"` // 实际退款金额(分) + + // 退换原因和处理信息 + Reason consts.ReturnReason `bson:"reason" json:"reason"` // 退换原因 + ReasonDetail string `bson:"reasonDetail" json:"reasonDetail"` // 详细原因说明 + ProblemDesc string `bson:"problemDesc" json:"problemDesc"` // 问题描述 + ProblemImages []string `bson:"problemImages" json:"problemImages"` // 问题图片URL列表 + + // 状态信息 + Status consts.ReturnItemStatus `bson:"status" json:"status"` // 明细状态 + ProcessMethod consts.ProcessMethod `bson:"processMethod" json:"processMethod"` // 处理方式 + + // 处理结果 + ProcessedQuantity int `bson:"processedQuantity" json:"processedQuantity"` // 已处理数量 + RemainingQuantity int `bson:"remainingQuantity" json:"remainingQuantity"` // 剩余待处理数量 + ProcessResult string `bson:"processResult" json:"processResult"` // 处理结果描述 + + // 备注 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (PurchaseReturnItem) CollectionName() string { + return public.PurchaseReturnItemCollection +} diff --git a/model/entity/procurement/supplier.go b/model/entity/procurement/supplier.go new file mode 100644 index 0000000..dda84bc --- /dev/null +++ b/model/entity/procurement/supplier.go @@ -0,0 +1,87 @@ +package entity + +import ( + consts "assets/consts/procurement" + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" +) + +// Supplier 供应商实体 +type Supplier struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 基础信息 + Name string `bson:"name" json:"name"` // 供应商名称(必填) + Code string `bson:"code" json:"code"` // 供应商编码(必填,唯一) + ShortName string `bson:"shortName" json:"shortName"` // 供应商简称 + Alias []string `bson:"alias" json:"alias"` // 别名列表 + Logo string `bson:"logo" json:"logo"` // 供应商LOGO URL + + // 联系信息 + Phone string `bson:"phone" json:"phone"` // 固定电话 + Mobile string `bson:"mobile" json:"mobile"` // 手机号码 + Email string `bson:"email" json:"email"` // 邮箱地址 + Website string `bson:"website" json:"website"` // 官网地址 + ContactPerson string `bson:"contactPerson" json:"contactPerson"` // 联系人姓名 + ContactPhone string `bson:"contactPhone" json:"contactPhone"` // 联系人电话 + ContactEmail string `bson:"contactEmail" json:"contactEmail"` // 联系人邮箱 + ContactPosition string `bson:"contactPosition" json:"contactPosition"` // 联系人职位 + + // 地址信息 + Country string `bson:"country" json:"country"` // 国家 + Province string `bson:"province" json:"province"` // 省份 + City string `bson:"city" json:"city"` // 城市 + District string `bson:"district" json:"district"` // 区县 + Address string `bson:"address" json:"address"` // 详细地址 + PostalCode string `bson:"postalCode" json:"postalCode"` // 邮政编码 + + // 企业资质信息 + BusinessLicense string `bson:"businessLicense" json:"businessLicense"` // 营业执照号 + LegalPerson string `bson:"legalPerson" json:"legalPerson"` // 法定代表人 + TaxNumber string `bson:"taxNumber" json:"taxNumber"` // 税务登记号 + BankName string `bson:"bankName" json:"bankName"` // 开户银行 + BankAccount string `bson:"bankAccount" json:"bankAccount"` // 银行账号 + BankAccountName string `bson:"bankAccountName" json:"bankAccountName"` // 账户名称 + + // 业务合作信息 + CooperationStart *gtime.Time `bson:"cooperationStart" json:"cooperationStart"` // 合作开始时间 + CooperationEnd *gtime.Time `bson:"cooperationEnd" json:"cooperationEnd"` // 合作结束时间 + SupplierLevel string `bson:"supplierLevel" json:"supplierLevel"` // 供应商等级 + PaymentMethod string `bson:"paymentMethod" json:"paymentMethod"` // 结算方式 + PaymentPeriod int `bson:"paymentPeriod" json:"paymentPeriod"` // 付款周期(天) + TaxRate float64 `bson:"taxRate" json:"taxRate"` // 税率(如0.13表示13%) + MinOrderAmount int `bson:"minOrderAmount" json:"minOrderAmount"` // 最小订货金额(分) + + // 经营品类信息 + MainCategories []string `bson:"mainCategories" json:"mainCategories"` // 主营品类ID列表 + BusinessScope string `bson:"businessScope" json:"businessScope"` // 经营范围 + + // 状态和审核信息 + Status consts.SupplierStatus `bson:"status" json:"status"` // 供应商状态 + ReviewStatus consts.ReviewStatus `bson:"reviewStatus" json:"reviewStatus"` // 审核状态 + ReviewBy string `bson:"reviewBy" json:"reviewBy"` // 审核人ID + ReviewAt *gtime.Time `bson:"reviewAt" json:"reviewAt"` // 审核时间 + ReviewRemark string `bson:"reviewRemark" json:"reviewRemark"` // 审核备注 + + // 评分和评价 + Rating float64 `bson:"rating" json:"rating"` // 综合评分(0-5分) + DeliveryRating float64 `bson:"deliveryRating" json:"deliveryRating"` // 交货及时率评分 + QualityRating float64 `bson:"qualityRating" json:"qualityRating"` // 质量合格率评分 + ServiceRating float64 `bson:"serviceRating" json:"serviceRating"` // 服务满意度评分 + TotalOrders int64 `bson:"totalOrders" json:"totalOrders"` // 历史订单总数 + TotalAmount int64 `bson:"totalAmount" json:"totalAmount"` // 历史交易总额(分) + + // 备注和标签 + Remark string `bson:"remark" json:"remark"` // 备注信息 + Tags []string `bson:"tags" json:"tags"` // 标签列表 + + // 扩展字段 + Extra map[string]interface{} `bson:"extra" json:"extra"` // 扩展字段 +} + +// CollectionName 获取集合名称 +func (Supplier) CollectionName() string { + return public.SupplierCollection +} diff --git a/model/entity/stock/inventory_count.go b/model/entity/stock/inventory_count.go new file mode 100644 index 0000000..a733955 --- /dev/null +++ b/model/entity/stock/inventory_count.go @@ -0,0 +1,56 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// InventoryCount 库存盘点主表实体(盘点任务执行时冻结所选盘点范围的库存) +type InventoryCount struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 基本信息 + CountNo string `bson:"countNo" json:"countNo"` // 盘点单号 + Title string `bson:"title" json:"title"` // 盘点标题 + Description string `bson:"description" json:"description"` // 盘点描述 + WarehouseIDs []*bson.ObjectID `bson:"warehouseIds" json:"warehouseIds"` // 仓库ID列表 + ZoneIDs []*bson.ObjectID `bson:"zoneIds" json:"zoneIds"` // 库区ID列表 + LocationIDs []*bson.ObjectID `bson:"locationIds" json:"locationIds"` // 库位ID列表 + AssetSkuIDs []*bson.ObjectID `bson:"assetSkuIds" json:"assetSkuIds"` // 资产SKU ID列表 + + // 盘点范围 + IsFrozen bool `bson:"isFrozen" json:"isFrozen"` //是否冻结(冻结时不允许修改) + CountType stock.InventoryCountType `bson:"countType" json:"countType"` // 盘点类型 + Scope stock.InventoryCountScope `bson:"scope" json:"scope"` // 盘点范围 + + // 时间信息 + ActualStartTime *gtime.Time `bson:"actualStartTime" json:"actualStartTime"` // 实际开始时间 + ActualEndTime *gtime.Time `bson:"actualEndTime" json:"actualEndTime"` // 实际结束时间 + + // 状态信息 + Status stock.InventoryCountStatus `bson:"status" json:"status"` // 盘点状态 + Progress float64 `bson:"progress" json:"progress"` // 进度百分比 0-100 + + // 人员信息 + CreatorID string `bson:"creatorId" json:"creatorId"` // 创建人ID + AssigneeID string `bson:"assigneeId" json:"assigneeId"` // 负责人ID + AssigneeName string `bson:"assigneeName" json:"assigneeName"` // 负责人名称 + Participants []string `bson:"participants" json:"participants"` // 参与人员ID列表 + + // 统计信息 + TotalItems int `bson:"totalItems" json:"totalItems"` // 盘点条目总数 + CompletedItems int `bson:"completedItems" json:"completedItems"` // 已完成条目数 + DiscrepancyItems int `bson:"discrepancyItems" json:"discrepancyItems"` // 有差异条目数 + + // 备注信息 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (InventoryCount) CollectionName() string { + return public.InventoryCountCollection +} diff --git a/model/entity/stock/inventory_count_adjust_history.go b/model/entity/stock/inventory_count_adjust_history.go new file mode 100644 index 0000000..4668388 --- /dev/null +++ b/model/entity/stock/inventory_count_adjust_history.go @@ -0,0 +1,39 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// InventoryCountAdjustHistory 盘点调整历史表实体 +type InventoryCountAdjustHistory struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息 + CountID *bson.ObjectID `bson:"countId" json:"countId"` // 盘点任务ID + DetailID *bson.ObjectID `bson:"detailId" json:"detailId"` // 盘点明细ID + AssetSkuID *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 商品SKU ID + WarehouseID *bson.ObjectID `bson:"warehouseId" json:"warehouseId"` // 仓库ID + ZoneID *bson.ObjectID `bson:"zoneId" json:"zoneId"` // 库区ID + LocationID *bson.ObjectID `bson:"locationId" json:"locationId"` // 库位ID + + // 调整数据 + BeforeQuantity int `bson:"beforeQuantity" json:"beforeQuantity"` // 调整前库存 + AfterQuantity int `bson:"afterQuantity" json:"afterQuantity"` // 调整后库存 + Difference int `bson:"difference" json:"difference"` // 差值(可正可负) + + // 元数据 + Reason string `bson:"reason" json:"reason"` // 调整原因 + AdjustedBy string `bson:"adjustedBy" json:"adjustedBy"` // 调整人ID + AdjustedByName string `bson:"adjustedByName" json:"adjustedByName"` // 调整人姓名 + AdjustedAt *gtime.Time `bson:"adjustedAt" json:"adjustedAt"` // 调整时间 + BatchNo string `bson:"batchNo" json:"batchNo"` // 批次号(批量调整时) +} + +// CollectionName 获取集合名称 +func (InventoryCountAdjustHistory) CollectionName() string { + return public.InventoryCountAdjustHistoryCollection +} diff --git a/model/entity/stock/inventory_count_detail.go b/model/entity/stock/inventory_count_detail.go new file mode 100644 index 0000000..9570386 --- /dev/null +++ b/model/entity/stock/inventory_count_detail.go @@ -0,0 +1,60 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// InventoryCountDetail 库存盘点明细表实体 +type InventoryCountDetail struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 关联信息 + CountID *bson.ObjectID `bson:"countId" json:"countId"` // 盘点单ID + AssetID *bson.ObjectID `bson:"assetId" json:"assetId"` // 资产ID + AssetSkuID *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 资产SKU ID + WarehouseID *bson.ObjectID `bson:"warehouseId" json:"warehouseId"` // 仓库ID + ZoneID *bson.ObjectID `bson:"zoneId" json:"zoneId"` // 库区ID + LocationID *bson.ObjectID `bson:"locationId" json:"locationId"` // 库位ID + + // 账面数据 + BookQuantity int `bson:"bookQuantity" json:"bookQuantity"` // 账面数量 + BookBatchInfo map[string]int `bson:"bookBatchInfo" json:"bookBatchInfo"` // 账面批次信息 {batchNo: quantity} + + // 实盘数据 + ActualQuantity int `bson:"actualQuantity" json:"actualQuantity"` // 实盘数量 + ActualBatchInfo map[string]int `bson:"actualBatchInfo" json:"actualBatchInfo"` // 实盘批次信息 {batchNo: quantity} + CountBy string `bson:"countBy" json:"countBy"` // 盘点人ID + CountAt *gtime.Time `bson:"countAt" json:"countAt"` // 盘点时间 + + // 差异信息 + Difference int `bson:"difference" json:"difference"` // 差异数量 (实际-账面) + DifferenceRate float64 `bson:"differenceRate" json:"differenceRate"` // 差异率 + DiscrepancyType stock.DiscrepancyType `bson:"discrepancyType" json:"discrepancyType"` // 差异类型 + DiscrepancyReason string `bson:"discrepancyReason" json:"discrepancyReason"` // 差异原因 + + // 状态信息 + Status stock.InventoryDetailStatus `bson:"status" json:"status"` // 明细状态 + IsAdjusted bool `bson:"isAdjusted" json:"isAdjusted"` // 是否已调整 + AdjustedAt *gtime.Time `bson:"adjustedAt" json:"adjustedAt"` // 调整时间 + AdjustedBy string `bson:"adjustedBy" json:"adjustedBy"` // 调整人ID + AdjustedByName string `bson:"adjustedByName" json:"adjustedByName"` // 调整人姓名 + + // 上传信息 + UploadBy string `bson:"uploadBy" json:"uploadBy"` // 上传人ID + UploadByName string `bson:"uploadByName" json:"uploadByName"` // 上传人姓名 + UploadAt *gtime.Time `bson:"uploadAt" json:"uploadAt"` // 上传时间 + UploadFileName string `bson:"uploadFileName" json:"uploadFileName"` // 文件名 + + // 备注信息 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (InventoryCountDetail) CollectionName() string { + return public.InventoryCountDetailCollection +} diff --git a/model/entity/stock/inventory_warning.go b/model/entity/stock/inventory_warning.go new file mode 100644 index 0000000..a0ea6c4 --- /dev/null +++ b/model/entity/stock/inventory_warning.go @@ -0,0 +1,54 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// InventoryWarning 库存预警实体 +type InventoryWarning struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + // 预警类型 + WarningType stock.WarningType `bson:"warningType" json:"warningType"` // 预警类型 + + // 关联信息 + BatchID *bson.ObjectID `bson:"batchId" json:"batchId"` // 关联批次ID + AssetID *bson.ObjectID `bson:"assetId" json:"assetId"` // 关联资产ID + AssetSkuID *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 关联资产SKU ID + SupplierID *bson.ObjectID `bson:"supplierId" json:"supplierId"` // 关联供应商ID + BatchNo string `bson:"batchNo" json:"batchNo"` // 批次号 + BatchQty int `bson:"batchQty" json:"batchQty"` // 批次数量 + AvailableQty int `bson:"availableQty" json:"availableQty"` // 可用数量 + + // 时间信息(临期预警使用) + ProductionDate *gtime.Time `bson:"productionDate" json:"productionDate"` // 生产日期 + ExpiryDate *gtime.Time `bson:"expiryDate" json:"expiryDate"` // 过期日期 + ExpiryWarningDate *gtime.Time `bson:"expiryWarningDate" json:"expiryWarningDate"` // 临期预警时间 + + // 库存信息(库存不足预警使用) + MinStockThreshold int `bson:"minStockThreshold" json:"minStockThreshold"` // 最低库存阈值 + + // 消息状态 + Status stock.ExpiryMessageStatus `bson:"status" json:"status"` // 消息状态 + + // 处理信息 + ProcessedAt *gtime.Time `bson:"processedAt" json:"processedAt"` // 处理时间 + Processor string `bson:"processor" json:"processor"` // 处理人 + ProcessNote string `bson:"processNote" json:"processNote"` // 处理备注 + ProcessMethod *stock.ExpiryProcessMethod `bson:"processMethod" json:"processMethod"` // 处理方式 + PromotionPlanID string `bson:"promotionPlanId" json:"promotionPlanId"` // 促销方案ID + + // 其他信息 + SupportsRecycle bool `bson:"supportsRecycle" json:"supportsRecycle"` // 是否支持回收 + Notes string `bson:"notes" json:"notes"` // 备注 +} + +// CollectionName 获取集合名称 +func (InventoryWarning) CollectionName() string { + return public.InventoryWarningCollection +} diff --git a/model/entity/stock/inventory_warning_history.go b/model/entity/stock/inventory_warning_history.go new file mode 100644 index 0000000..0549d8e --- /dev/null +++ b/model/entity/stock/inventory_warning_history.go @@ -0,0 +1,43 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// InventoryWarningHistory 库存预警历史归档实体 +type InventoryWarningHistory struct { + beans.MongoBaseDO `bson:",inline"` + + // 预警类型 + WarningType stock.WarningType `bson:"warningType" json:"warningType"` // 预警类型 + + BatchID *bson.ObjectID `bson:"batchId" json:"batchId"` + AssetID *bson.ObjectID `bson:"assetId" json:"assetId"` + AssetSkuID *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` + SupplierID *bson.ObjectID `bson:"supplierId" json:"supplierId"` + BatchNo string `bson:"batchNo" json:"batchNo"` + BatchQty int `bson:"batchQty" json:"batchQty"` + AvailableQty int `bson:"availableQty" json:"availableQty"` + ProductionDate *gtime.Time `bson:"productionDate" json:"productionDate"` + ExpiryDate *gtime.Time `bson:"expiryDate" json:"expiryDate"` + ExpiryWarningDate *gtime.Time `bson:"expiryWarningDate" json:"expiryWarningDate"` + MinStockThreshold int `bson:"minStockThreshold" json:"minStockThreshold"` // 最低库存阈值 + Status stock.ExpiryMessageStatus `bson:"status" json:"status"` + ProcessedAt *gtime.Time `bson:"processedAt" json:"processedAt"` + Processor string `bson:"processor" json:"processor"` + ProcessNote string `bson:"processNote" json:"processNote"` + ProcessMethod *stock.ExpiryProcessMethod `bson:"processMethod" json:"processMethod"` + PromotionPlanID string `bson:"promotionPlanId" json:"promotionPlanId"` + SupportsRecycle bool `bson:"supportsRecycle" json:"supportsRecycle"` + Notes string `bson:"notes" json:"notes"` +} + +// CollectionName 获取集合名称 +func (InventoryWarningHistory) CollectionName() string { + return public.InventoryWarningHistoryCollection +} diff --git a/model/entity/stock/location.go b/model/entity/stock/location.go new file mode 100644 index 0000000..afc93ee --- /dev/null +++ b/model/entity/stock/location.go @@ -0,0 +1,29 @@ +package entity + +import ( + "assets/consts/public" + consts "assets/consts/stock" + "assets/model/config" + + "gitea.com/red-future/common/beans" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// Location 库位 +type Location struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + WarehouseId *bson.ObjectID `bson:"warehouseId" json:"warehouseId"` // 仓库ID + ZoneId *bson.ObjectID `bson:"zoneId" json:"zoneId"` // 库区ID + LocationCode string `bson:"locationCode" json:"locationCode"` // 库位编码 + LocationName string `bson:"locationName" json:"locationName"` // 库位名称 + LocationType consts.LocationType `bson:"locationType" json:"locationType"` // 库位类型 + Status consts.LocationStatus `bson:"status" json:"status"` // 库位状态 + CapacityUnitType consts.CapacityUnitType `bson:"capacityUnitType" json:"capacityUnitType"` // 容量单位类型 + Capacity *config.Capacity `bson:"capacity" json:"capacity"` //容量 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (Location) CollectionName() string { + return public.LocationCollection +} diff --git a/model/entity/stock/private_stock.go b/model/entity/stock/private_stock.go new file mode 100644 index 0000000..ff97445 --- /dev/null +++ b/model/entity/stock/private_stock.go @@ -0,0 +1,56 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// PrivateStock 实物库存批次(记录SKU批次在具体仓库/库区/库位的实际存放位置和数量) +type PrivateStock struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + // 关联信息 - 支持多种库存类型 + StockType stock.StockLocationType `bson:"stockType" json:"stockType"` // 库存类型:stock_details/private_stock/stock_batch + + // 位置信息 + WarehouseID *bson.ObjectID `bson:"warehouseId" json:"warehouseId"` // 仓库ID + WarehouseCode string `bson:"warehouseCode" json:"warehouseCode"` // 仓库编码 + WarehouseName string `bson:"warehouseName" json:"warehouseName"` // 仓库名称 + + ZoneID *bson.ObjectID `bson:"zoneId" json:"zoneId"` // 库区ID(可选) + ZoneCode string `bson:"zoneCode" json:"zoneCode"` // 库区编码 + ZoneName string `bson:"zoneName" json:"zoneName"` // 库区名称 + ZoneType stock.ZoneType `bson:"zoneType" json:"zoneType"` // 库区类型 + + LocationID *bson.ObjectID `bson:"locationId" json:"locationId"` // 库位ID(可选) + LocationCode string `bson:"locationCode" json:"locationCode"` // 库位编码 + LocationName string `bson:"locationName" json:"locationName"` // 库位名称 + LocationType stock.LocationType `bson:"locationType" json:"locationType"` // 库位类型 + + PrivateSkuID *bson.ObjectID `bson:"privateSkuId" json:"privateSkuId"` // 私域SKU ID + BatchNo string `bson:"batchNo" json:"batchNo"` // 批次号 + BatchQty int `bson:"batchQty" json:"batchQty"` // 批次总数量(入库后不可变) + AvailableQty int `bson:"availableQty" json:"availableQty"` // 可用数量(实时变化) + // 状态 + BatchStatus stock.BatchStatus `bson:"batchStatus" json:"batchStatus"` // 批次状态 + // 订单关联 + OrderID *bson.ObjectID `bson:"orderId" json:"orderId"` // 关联订单ID(如果有) + // 供应商关联和临期管理 + SupplierID *bson.ObjectID `bson:"supplierId" json:"supplierId"` // 关联供应商ID + SupportsRecycle bool `bson:"supportsRecycle" json:"supportsRecycle"` // 是否支持回收 + ProductionDate *gtime.Time `bson:"productionDate" json:"productionDate"` // 生产日期 + ExpiryDate *gtime.Time `bson:"expiryDate" json:"expiryDate"` // 过期日期 + ExpiryWarningDate *gtime.Time `bson:"expiryWarningDate" json:"expiryWarningDate"` // 临期预警时间(有过期日期时建议填写) + PrivateCategoryPath string `bson:"privateCategoryPath" json:"privateCategoryPath"` // 私域分类路径 + + StockStatus stock.StockStatus `bson:"stockStatus" json:"stockStatus"` // 库存状态 + LastMovedAt *gtime.Time `bson:"lastMovedAt" json:"lastMovedAt"` // 最后移动时间 +} + +// CollectionName 获取集合名称 +func (PrivateStock) CollectionName() string { + return public.PrivateStockCollection +} diff --git a/model/entity/stock/stock_batch.go b/model/entity/stock/stock_batch.go new file mode 100644 index 0000000..7a6f93f --- /dev/null +++ b/model/entity/stock/stock_batch.go @@ -0,0 +1,48 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// StockBatch 库存批次实体(用于批次管理模式) +type StockBatch struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + AssetID *bson.ObjectID `bson:"assetId" json:"assetId"` // 关联资产ID + AssetSkuID *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 关联资产SKU ID + BatchNo string `bson:"batchNo" json:"batchNo"` // 批次号 + BatchQty int `bson:"batchQty" json:"batchQty"` // 批次总数量(入库后不可变) + AvailableQty int `bson:"availableQty" json:"availableQty"` // 可用数量(实时变化) + + // 批次元数据 + Metadata []map[string]interface{} `bson:"metadata" json:"metadata"` // 其他元数据 + + // 状态 + Status *stock.BatchStatus `bson:"status" json:"status"` // 批次状态 + // 锁定数量 = BatchQty - AvailableQty + + // 订单关联 + OrderID *bson.ObjectID `bson:"orderId" json:"orderId"` // 关联订单ID(如果有) + + // 渠道分配信息 + AssignedChannel string `bson:"assignedChannel" json:"assignedChannel"` // 分配的销售渠道 + ChannelSKU string `bson:"channelSku" json:"channelSku"` // 渠道商品SKU + ChannelMetadata map[string]interface{} `bson:"channelMetadata" json:"channelMetadata"` // 渠道专属数据 + AllocatedAt *gtime.Time `bson:"allocatedAt" json:"allocatedAt"` // 分配时间 + + // 临期管理 + ProductionDate *gtime.Time `bson:"productionDate" json:"productionDate"` // 生产日期 + ExpiryDate *gtime.Time `bson:"expiryDate" json:"expiryDate"` // 过期日期 + ExpiryWarningDate *gtime.Time `bson:"expiryWarningDate" json:"expiryWarningDate"` // 临期预警时间(有过期日期时建议填写) + CategoryPath string `bson:"categoryPath" json:"categoryPath"` // 分类路径 +} + +// CollectionName 获取集合名称 +func (StockBatch) CollectionName() string { + return public.StockBatchCollection +} diff --git a/model/entity/stock/stock_details.go b/model/entity/stock/stock_details.go new file mode 100644 index 0000000..a456137 --- /dev/null +++ b/model/entity/stock/stock_details.go @@ -0,0 +1,35 @@ +package entity + +import ( + "assets/consts/public" + "assets/consts/stock" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// StockDetails 库存实体,每一件商品都有独立ID,用于后期做区块链虚拟资产 +type StockDetails struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + AssetId *bson.ObjectID `bson:"assetId" json:"assetId"` // 关联资产ID + AssetSkuId *bson.ObjectID `bson:"assetSkuId" json:"assetSkuId"` // 关联资产SKU ID + Status stock.StockStatus `bson:"status" json:"status"` // 库存状态 + OrderId *bson.ObjectID `bson:"orderId" json:"orderId"` // 关联订单ID(如果有) + LockExpire *gtime.Time `bson:"lockExpire" json:"lockExpire"` // 锁定过期时间 + Metadata []map[string]interface{} `bson:"metadata" json:"metadata"` // 其他元数据 + TokenId string `bson:"tokenId" json:"tokenId"` // 区块链TokenID(如果有) + + // 渠道分配信息 + AssignedChannel string `bson:"assignedChannel" json:"assignedChannel"` // 分配的销售渠道 + ChannelSKU string `bson:"channelSku" json:"channelSku"` // 渠道商品SKU + ChannelMetadata map[string]interface{} `bson:"channelMetadata" json:"channelMetadata"` // 渠道专属数据 + AllocatedAt *gtime.Time `bson:"allocatedAt" json:"allocatedAt"` // 分配时间 + CategoryPath string `bson:"categoryPath" json:"categoryPath"` // 分类路径 +} + +// CollectionName 库存集合名称 +func (StockDetails) CollectionName() string { + return public.StockDetailsCollection +} diff --git a/model/entity/stock/unit_conversion.go b/model/entity/stock/unit_conversion.go new file mode 100644 index 0000000..4f79e30 --- /dev/null +++ b/model/entity/stock/unit_conversion.go @@ -0,0 +1,25 @@ +package entity + +import ( + "assets/consts/public" + consts "assets/consts/stock" + + "gitea.com/red-future/common/beans" +) + +// UnitConversion 单位换算 +type UnitConversion struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + ConversionCode string `bson:"conversionCode" json:"conversionCode"` // 换算编码 + ConversionName string `bson:"conversionName" json:"conversionName"` // 换算名称 + UnitType consts.CapacityUnitType `bson:"unitType" json:"unitType"` // 单位类型 + FromUnit string `bson:"fromUnit" json:"fromUnit"` // 源单位 + ToUnit string `bson:"toUnit" json:"toUnit"` // 目标单位 + ConversionFactor float64 `bson:"conversionFactor" json:"conversionFactor"` // 换算系数(1 toUnit = ConversionFactor × fromUnit,如1箱=20瓶则factor=20) + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (UnitConversion) CollectionName() string { + return public.UnitConversionCollection +} diff --git a/model/entity/stock/warehouse.go b/model/entity/stock/warehouse.go new file mode 100644 index 0000000..f458c43 --- /dev/null +++ b/model/entity/stock/warehouse.go @@ -0,0 +1,27 @@ +package entity + +import ( + public "assets/consts/public" + consts "assets/consts/stock" + "assets/model/config" + + "gitea.com/red-future/common/beans" +) + +// Warehouse 仓库 +type Warehouse struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + WarehouseCode string `bson:"warehouseCode" json:"warehouseCode"` // 仓库编码 + WarehouseName string `bson:"warehouseName" json:"warehouseName"` // 仓库名称 + Address string `bson:"address" json:"address"` // 仓库地址 + ContactPerson string `bson:"contactPerson" json:"contactPerson"` // 联系人 + ContactPhone string `bson:"contactPhone" json:"contactPhone"` // 联系电话 + Status consts.WarehouseStatus `bson:"status" json:"status"` // 仓库状态 + Capacity *map[consts.CapacityUnitType]config.Capacity `bson:"capacity" json:"capacity"` // 容量 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (Warehouse) CollectionName() string { + return public.WarehouseCollection +} diff --git a/model/entity/stock/zone.go b/model/entity/stock/zone.go new file mode 100644 index 0000000..13c6165 --- /dev/null +++ b/model/entity/stock/zone.go @@ -0,0 +1,26 @@ +package entity + +import ( + public "assets/consts/public" + consts "assets/consts/stock" + "assets/model/config" + + "gitea.com/red-future/common/beans" +) + +// Zone 库区 +type Zone struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + WarehouseId string `bson:"warehouseId" json:"warehouseId"` // 仓库ID + ZoneCode string `bson:"zoneCode" json:"zoneCode"` // 库区编码 + ZoneName string `bson:"zoneName" json:"zoneName"` // 库区名称 + ZoneType consts.ZoneType `bson:"zoneType" json:"zoneType"` // 库区类型 + Status consts.ZoneStatus `bson:"status" json:"status"` // 库区状态 + Capacity *map[consts.CapacityUnitType]config.Capacity `bson:"capacity" json:"capacity"` // 容量 + Remark string `bson:"remark" json:"remark"` // 备注 +} + +// CollectionName 获取集合名称 +func (Zone) CollectionName() string { + return public.ZoneCollection +} diff --git a/model/entity/sync/channel_config.go b/model/entity/sync/channel_config.go new file mode 100644 index 0000000..1065f78 --- /dev/null +++ b/model/entity/sync/channel_config.go @@ -0,0 +1,108 @@ +package entity + +import "gitea.com/red-future/common/beans" + +// ChannelConfig 渠道配置 - 租户针对三方渠道的配置信息 +type ChannelConfig struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段 + + // 渠道专用配置 + TaobaoConfig *TaobaoConfig `bson:"taobaoConfig,omitempty" json:"taobaoConfig,omitempty"` + JDConfig *JDConfig `bson:"jdConfig,omitempty" json:"jdConfig,omitempty"` + KuaishouConfig *KuaishouConfig `bson:"kuaishouConfig,omitempty" json:"kuaishouConfig,omitempty"` + DouyinConfig *DouyinConfig `bson:"douyinConfig,omitempty" json:"douyinConfig,omitempty"` + XhsConfig *XhsConfig `bson:"xhsConfig,omitempty" json:"xhsConfig,omitempty"` + PddConfig *PddConfig `bson:"pddConfig,omitempty" json:"pddConfig,omitempty"` + XianyuConfig *XianyuConfig `bson:"xianyuConfig,omitempty" json:"xianyuConfig,omitempty"` + BlockchainConfig *BlockchainConfig `bson:"blockchainConfig,omitempty" json:"blockchainConfig,omitempty"` +} + +// 各渠道专用配置结构 + +// TaobaoConfig 淘宝配置 +type TaobaoConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppKey string `bson:"appKey" json:"appKey"` // 应用Key + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + RefreshToken string `bson:"refreshToken" json:"refreshToken"` // 刷新令牌 + ShopID string `bson:"shopId" json:"shopId"` // 店铺ID + SellerID string `bson:"sellerId" json:"sellerId"` // 卖家ID +} + +// JDConfig 京东配置 +type JDConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppKey string `bson:"appKey" json:"appKey"` // 应用Key + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + VenderID string `bson:"venderId" json:"venderId"` // 商家ID + WarehouseID string `bson:"warehouseId" json:"warehouseId"` // 仓库ID +} + +// KuaishouConfig 快手配置 +type KuaishouConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppID string `bson:"appId" json:"appId"` // 应用ID + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + LiveRoomID string `bson:"liveRoomId" json:"liveRoomId"` // 直播间ID +} + +// DouyinConfig 抖音配置 +type DouyinConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppID string `bson:"appId" json:"appId"` // 应用ID + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + LiveRoomID string `bson:"liveRoomId" json:"liveRoomId"` // 直播间ID + VideoID string `bson:"videoId" json:"videoId"` // 视频ID +} + +// XhsConfig 小红书配置 +type XhsConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppKey string `bson:"appKey" json:"appKey"` // 应用Key + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + NoteID string `bson:"noteId" json:"noteId"` // 笔记ID +} + +// PddConfig 拼多多配置 +type PddConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + ClientID string `bson:"clientId" json:"clientId"` // 客户端ID + ClientSecret string `bson:"clientSecret" json:"clientSecret"` // 客户端密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 + MallID string `bson:"mallId" json:"mallId"` // 店铺ID +} + +// XianyuConfig 闲鱼配置 +type XianyuConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + AppKey string `bson:"appKey" json:"appKey"` // 应用Key + AppSecret string `bson:"appSecret" json:"appSecret"` // 应用密钥 + AccessToken string `bson:"accessToken" json:"accessToken"` // 访问令牌 +} + +// BlockchainConfig 区块链配置 +type BlockchainConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` // 是否启用 + DefaultPriceAdjustment float64 `bson:"defaultPriceAdjustment" json:"defaultPriceAdjustment"` // 默认价格调整系数 + ChainName string `bson:"chainName" json:"chainName"` // 链名称 + ContractAddress string `bson:"contractAddress" json:"contractAddress"` // 合约地址 + PrivateKey string `bson:"privateKey" json:"privateKey"` // 私钥 + RPCURL string `bson:"rpcUrl" json:"rpcUrl"` // RPC地址 +} + +// CollectionName 渠道配置集合名称 +func (ChannelConfig) CollectionName() string { + return "channel_config" +} diff --git a/model/entity/sync/channel_sync_record.go b/model/entity/sync/channel_sync_record.go new file mode 100644 index 0000000..5d064fa --- /dev/null +++ b/model/entity/sync/channel_sync_record.go @@ -0,0 +1,45 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// ChannelSyncRecord 渠道同步记录 +type ChannelSyncRecord struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段 + + // 关联关系 + AssetID *bson.ObjectID `bson:"assetId" json:"assetId"` // 关联资产ID + SkuID *bson.ObjectID `bson:"skuId" json:"skuId"` // 关联SKU ID + Channel public.SyncPlatform `bson:"channel" json:"channel"` // 渠道类型 + + // 同步状态信息 + Status public.SyncStatus `bson:"status" json:"status"` // 同步状态 + + // 渠道商品信息 + ChannelProductID *bson.ObjectID `bson:"channelProductId" json:"channelProductId"` // 渠道商品ID + ChannelSkuID *bson.ObjectID `bson:"channelSkuId" json:"channelSkuId"` // 渠道SKU ID + ChannelURL string `bson:"channelUrl" json:"channelUrl"` // 渠道商品链接 + + // 同步信息 + LastSyncTime *gtime.Time `bson:"lastSyncTime" json:"lastSyncTime"` // 最后同步时间 + LastSyncResult *SyncResult `bson:"lastSyncResult" json:"lastSyncResult"` // 最后同步结果 + ErrorCount int `bson:"errorCount" json:"errorCount"` // 错误次数 +} + +// CollectionName 渠道同步记录集合名称 +func (ChannelSyncRecord) CollectionName() string { + return "channel_sync_record" +} + +// SyncResult 同步结果 +type SyncResult struct { + Success bool `bson:"success" json:"success"` // 是否成功 + ErrorMsg string `bson:"errorMsg" json:"errorMsg"` // 错误信息 + SyncTime *gtime.Time `bson:"syncTime" json:"syncTime"` // 同步时间 + Data map[string]interface{} `bson:"data" json:"data"` // 同步数据 +} diff --git a/model/entity/sync/sync_task.go b/model/entity/sync/sync_task.go new file mode 100644 index 0000000..0375932 --- /dev/null +++ b/model/entity/sync/sync_task.go @@ -0,0 +1,30 @@ +package entity + +import ( + "assets/consts/public" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/os/gtime" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// SyncTask 同步任务实体 +type SyncTask struct { + beans.MongoBaseDO `bson:",inline"` // 嵌入基础字段:Id, Creator, CreatedAt, Updater, UpdatedAt, TenantId, IsDeleted + + Platform public.SyncPlatform `json:"platform" bson:"platform"` + SyncType public.SyncType `json:"syncType" bson:"syncType"` + Status public.SyncStatus `json:"status" bson:"status"` + AssetID *bson.ObjectID `json:"assetId" bson:"assetId"` + AssetSKUID *bson.ObjectID `json:"assetSkuId" bson:"assetSkuId"` + StockID *bson.ObjectID `json:"stockId" bson:"stockId"` + ErrorMessage string `json:"errorMessage" bson:"errorMessage"` + ErrorCount int `json:"errorCount" bson:"errorCount"` + StartedAt *gtime.Time `json:"startedAt" bson:"startedAt"` + FinishedAt *gtime.Time `json:"finishedAt" bson:"finishedAt"` +} + +// CollectionName 获取集合名称 +func (SyncTask) CollectionName() string { + return "sync_task" +} diff --git a/service/asset/asset_service.go b/service/asset/asset_service.go new file mode 100644 index 0000000..b002345 --- /dev/null +++ b/service/asset/asset_service.go @@ -0,0 +1,176 @@ +package service + +import ( + "assets/consts/stock" + dao "assets/dao/asset" + "assets/dao/base" + dto "assets/model/dto/asset" + "context" + "errors" + "fmt" + "gitea.com/red-future/common/http" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/minio" + "gitea.com/red-future/common/utils" +) + +type asset struct{} + +// Asset 资产服务 +var Asset = new(asset) + +// Create 创建资产 +func (s *asset) Create(ctx context.Context, req *dto.CreateAssetReq) (res *dto.CreateAssetRes, err error) { + count, err := dao.Asset.Count(ctx, &dto.ListAssetReq{Name: req.Name}) + if err != nil { + return + } + if count > 0 { + return nil, errors.New("资产名称已存在") + } + // 检查是否是超级管理员 + isSuperAdmin := false + // 获取当前请求的 headers 并传递到下游 + headers := make(map[string]string) + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + } + if err = http.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &isSuperAdmin); err != nil { + return + } + + if !isSuperAdmin { + req.StockMode = stock.StockModeDetail + + //var getUserInfo beans.User + //getUserInfo, err = utils.GetUserInfo(ctx) + //if err != nil { + // return + //} + //var get *gvar.Var + //get, err = message.GetRedisClientTest("test").Get(ctx, fmt.Sprintf("module_tenant:tenantId-%v", getUserInfo.TenantId)) + //if err != nil { + // return + //} + //if !g.IsEmpty(get.String()) { + // list := new(beans.ModuleTenant) + // if err = json.Unmarshal(get.Bytes(), &list); err != nil { + // return + // } + // req.TenantModuleType = list.TenantModuleType + //} else { + // moduleTenantRes := new(beans.ModuleTenant) + // err = message.CallRPC(ctx, "moduleService.AddRedisByTenantId", map[string]interface{}{"tenantId": getUserInfo.TenantId}, moduleTenantRes) + // if err != nil { + // return + // } + // if !g.IsEmpty(moduleTenantRes.TenantModuleType) { + // req.TenantModuleType = moduleTenantRes.TenantModuleType + // } else { + // return nil, errors.New("您未开通此模块,请开通后再使用") + // } + //} + } else { + req.TenantModuleType = beans.TenantModuleTypePlatform + } + + // 插入数据库 + id, err := dao.Asset.Insert(ctx, req) + if err != nil { + return + } + res = &dto.CreateAssetRes{ + Id: gconv.Uint64(id), + } + return +} + +// List 获取资产列表 +func (s *asset) List(ctx context.Context, req *dto.ListAssetReq) (res *dto.ListAssetRes, err error) { + assetList, total, err := dao.Asset.List(ctx, req) + if err != nil { + return + } + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + fmt.Println(user) + res = &dto.ListAssetRes{ + Total: total, + } + err = utils.Struct(assetList, &res.List) + return +} + +// GetOne 获取单个资产 +func (s *asset) GetOne(ctx context.Context, req *dto.GetAssetReq) (res *dto.GetAssetRes, err error) { + assetOne, err := dao.Asset.GetOne(ctx, req) + if err != nil { + return + } + // TODO: CategoryId类型不匹配,需要同步修改category为uint64 + // getCategoryRes, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{ + // Id: assetOne.CategoryId, + // }) + // if err != nil { + // return + // } + return &dto.GetAssetRes{ + Asset: assetOne, + CategoryName: "", // getCategoryRes.Name, + ImgAddressPrefix: minio.GetFileAddressPrefix(ctx), + }, nil +} + +// GetAssetAndSku 获取资产和Sku详情 +func (s *asset) GetAssetAndSku(ctx context.Context, req *dto.GetAssetAndSkuReq) (res *dto.GetAssetAndSkuRes, err error) { + // 跳过租户ID过滤获取资产 + // TODO: AssetId 类型不匹配,bson.ObjectID 需要转换为 uint64 + // 使用 SkipTenantId 跳过租户ID过滤 + assetOne, err := dao.Asset.GetOneById(base.SkipTenantId(ctx), 0) + _ = req.AssetId + if err != nil { + return + } + // TODO: AssetId类型不匹配,需要适配 + // moduleType, err := service.Enum.GetTenantModuleType(ctx, &enumDto.GetTenantModuleTypeReq{AssetId: req.AssetId.Hex()}) + // if err != nil { + // return + // } + // TODO: AssetId类型不匹配,需要同步修改AssetSku为uint64 + // skus, _, err := dao.AssetSku.List(ctx, &dto.ListAssetSkuReq{AssetId: req.AssetId}, true) + // if err != nil { + // return + // } + return &dto.GetAssetAndSkuRes{ + Asset: assetOne, + Skus: nil, // skus, + TenantModuleType: nil, // moduleType.Options, + ImgAddressPrefix: minio.GetFileAddressPrefix(ctx), + }, nil +} + +// Update 更新资产 +func (s *asset) Update(ctx context.Context, req *dto.UpdateAssetReq) error { + return dao.Asset.Update(ctx, req) +} + +// UpdateStatus 更新资产状态 +func (s *asset) UpdateStatus(ctx context.Context, req *dto.UpdateAssetStatusReq) (err error) { + var updateReq *dto.UpdateAssetReq + err = utils.Struct(req, &updateReq) + return dao.Asset.Update(ctx, updateReq) +} + +// Delete 删除资产 +func (s *asset) Delete(ctx context.Context, req *dto.DeleteAssetReq) error { + return dao.Asset.DeleteFake(ctx, req) +} diff --git a/service/asset/asset_sku_service.go b/service/asset/asset_sku_service.go new file mode 100644 index 0000000..7a6fc2d --- /dev/null +++ b/service/asset/asset_sku_service.go @@ -0,0 +1,191 @@ +package service + +import ( + "assets/consts/public" + dao "assets/dao/asset" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + "errors" + "reflect" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type assetSku struct{} + +var AssetSku = new(assetSku) + +// CreateAssetSku 创建SKU +func (s *assetSku) CreateAssetSku(ctx context.Context, req *dto.CreateAssetSkuReq) (res *dto.CreateAssetSkuRes, err error) { + // 验证资产是否存在, 如果存在,则获取资产信息(用于下面验证,自定义属性传过来的全不全) + assetEntity, err := dao.Asset.GetOne(ctx, &dto.GetAssetReq{Id: req.AssetId}) + if err != nil { + return nil, errors.New("关联资产不存在") + } + // 根据资产ID查询SKU列表(用于下面验证,自定义属性和sku名称重不重复) + skusList, _, err := dao.AssetSku.List(ctx, &dto.ListAssetSkuReq{AssetId: req.AssetId}, false) + if err != nil { + return + } + // 验证参数 + err = s.parameterValidation(ctx, assetEntity, skusList, req.SkuName, req.SpecValues) + if err != nil { + return + } + req.UnlimitedStock = assetEntity.UnlimitedStock + req.StockMode = assetEntity.StockMode + // TODO: 类型不匹配 uint64 vs *bson.ObjectID + // req.CategoryId = assetEntity.CategoryId + req.CategoryPath = assetEntity.CategoryPath + // TODO: 类型不匹配 string vs beans.TenantModuleType + // req.TenantModuleType = assetEntity.TenantModuleType + // 插入数据库 + ids, err := dao.AssetSku.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateAssetSkuRes{ + Id: &id, + } + return +} + +// UpdateAssetSku 更新SKU +func (s *assetSku) UpdateAssetSku(ctx context.Context, req *dto.UpdateAssetSkuReq) error { + getOne, err := dao.AssetSku.GetOne(ctx, &dto.GetAssetSkuReq{Id: req.Id}, false) + if err != nil { + return errors.New("SUK不存在") + } + // 根据资产ID查询SKU列表(用于下面验证,自定义属性和sku名称重不重复) + skusList, _, err := dao.AssetSku.GetListByAssetIdExcludeCurrentSku(ctx, getOne.AssetId, &dto.ListAssetSkuReq{Id: req.Id, Page: &beans.Page{PageSize: -1}}) + if err != nil { + return err + } + // 验证资产是否存在, 如果存在,则获取资产信息(用于下面验证,自定义属性传过来的全不全) + assetEntity, err := dao.Asset.GetOne(ctx, &dto.GetAssetReq{Id: getOne.AssetId}) + if err != nil { + return errors.New("关联资产不存在") + } + // 验证参数 + err = s.parameterValidation(ctx, assetEntity, skusList, req.SkuName, req.SpecValues) + if err != nil { + return err + } + // 更新数据库 + return dao.AssetSku.Update(ctx, req) +} + +func (s *assetSku) parameterValidation(ctx context.Context, assetEntity *entity.Asset, list []entity.AssetSku, skuName string, specValues []map[string]interface{}) (err error) { + _ = ctx + + specNoExist := true + metadataList := make([]string, 0) + // 验证,自定义属性传过来的全不全 + // TODO: Metadata类型从[]map[string]interface{}变为*gjson.Json,需要适配 + // if assetEntity.Metadata != nil { + // for _, metadata := range assetEntity.Metadata { + // attributeType := gconv.String(gconv.Map(metadata)["type"]) + // if attributeType == string(consts.AttributeTypeMultiSelect) { + // metadataList = append(metadataList, attributeType) + // } + // } + // if len(metadataList) != len(specValues) { + // // 如果请求参数中不存在该键,则跳过 + // return errors.New("规格参数填写不完整") + // } + // } + // 验证,自定义属性和sku名称重不重复 + for _, list := range list { + if list.SkuName == skuName { + return errors.New("SKU名称已存在") + } + if !g.IsEmpty(metadataList) && len(metadataList) > 0 { + if !g.IsEmpty(specValues) { + exist := true + // 遍历请求参数 map + for key, subValue := range specValues { + // 1. 检查请求参数 map 中是否存在该键 + for _, subValues := range list.SpecValues { + parentValue, ok := subValues[gconv.String(key)] + if ok { + // 2. 检查对应的值是否相等 + if reflect.DeepEqual(parentValue, subValue) { + exist = false + } else { + exist = true + break + } + } else { + exist = true + break + } + } + + } + if exist { + specNoExist = true + } else { + specNoExist = false + break + } + } else { + return errors.New("规格参数不能为空") + } + } + } + if !specNoExist { + return errors.New("规格参数已存在") + } + return nil +} + +// DeleteAssetSku 删除SKU(软删除) +func (s *assetSku) DeleteAssetSku(ctx context.Context, req *dto.DeleteAssetSkuReq) error { + return dao.AssetSku.DeleteFake(ctx, req) +} + +// GetAssetSku 获取SKU详情 +func (s *assetSku) GetAssetSku(ctx context.Context, req *dto.GetAssetSkuReq) (res *dto.GetAssetSkuRes, err error) { + one, err := dao.AssetSku.GetOne(ctx, req, false) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +// ListAssetSkus 获取SKU列表 +func (s *assetSku) ListAssetSkus(ctx context.Context, req *dto.ListAssetSkuReq) (res *dto.ListAssetSkuRes, err error) { + // 查询数据库 + list, total, err := dao.AssetSku.List(ctx, req, false) + if err != nil { + return + } + res = &dto.ListAssetSkuRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} + +// GetAssetSkuModule 获取SKU详情 +func (s *assetSku) GetAssetSkuModule(ctx context.Context, req *dto.GetAssetSkuModuleReq) (res *dto.GetAssetSkuModuleRes, err error) { + one, err := dao.AssetSku.GetOne(ctx, &dto.GetAssetSkuReq{Id: req.Id}, true) + if err != nil { + return + } + res = &dto.GetAssetSkuModuleRes{} + // 计算到期时间 + if one.SpecsUnit != nil && one.SpecsCount > 0 { + durationType := public.DurationType(one.SpecsUnit.Key) + res.ExpireAt = durationType.AddTime(one.SpecsCount) + res.AssetId = one.AssetId + } + return +} diff --git a/service/asset/category_service.go b/service/asset/category_service.go new file mode 100644 index 0000000..1b76b04 --- /dev/null +++ b/service/asset/category_service.go @@ -0,0 +1,265 @@ +package service + +import ( + dao "assets/dao/asset" + dto "assets/model/dto/asset" + entity "assets/model/entity/asset" + "context" + "errors" + + "gitea.com/red-future/common/db/gfdb" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gconv" + + "github.com/gogf/gf/v2/frame/g" +) + +const ( + // DefaultPathSeparator 路径分隔符 + DefaultPathSeparator = "/" +) + +// CategoryService 分类服务 +type CategoryService struct{} + +var Category = new(CategoryService) + +// Create 创建分类 +func (s *CategoryService) Create(ctx context.Context, req *dto.CreateCategoryReq) (res *dto.CreateCategoryRes, err error) { + + // 构建分类路径和层级 + parent, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Bid: req.ParentId}) + if err != nil { + return nil, err + } + req.Path = parent.Path + DefaultPathSeparator + req.ParentId + req.Level = parent.Level + 1 + + err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + var r *entity.Category + + if r, err = dao.Category.Insert(ctx, req); err != nil { + return err + } + + // 更新新创建分类的 IsLeafNode 为 true + if err = s.updateLeafNode(ctx, r.Bid, true); err != nil { + return err + } + + // 更新父节点的 IsLeafNode 为 false + if err = s.updateLeafNode(ctx, req.ParentId, false); err != nil { + return err + } + + res = &dto.CreateCategoryRes{ + Bid: r.Bid, + } + + return nil + }) + + return +} + +// updateLeafNode 更新分类的 IsLeafNode 字段 +func (s *CategoryService) updateLeafNode(ctx context.Context, categoryId string, isLeaf bool) error { + return dao.Category.Update(ctx, &dto.UpdateCategoryReq{ + Bid: categoryId, + IsLeafNode: isLeaf, + }) +} + +// GetOne 获取单个分类 +func (s *CategoryService) GetOne(ctx context.Context, req *dto.GetCategoryReq) (*dto.GetCategoryRes, error) { + one, err := dao.Category.GetOne(ctx, req) + if err != nil { + return nil, err + } + res := new(dto.GetCategoryRes) + err = utils.Struct(one, &res) + if err != nil { + return nil, err + } + return res, nil +} + +// List 获取分类列表 +func (s *CategoryService) List(ctx context.Context, req *dto.ListCategoryReq) (*dto.ListCategoryRes, error) { + list, total, err := dao.Category.List(ctx, req) + if err != nil { + return nil, err + } + res := &dto.ListCategoryRes{Total: total} + err = gconv.Struct(list, &res.List) + if err != nil { + return nil, err + } + return res, nil +} + +// GetTree 获取分类树 +func (s *CategoryService) GetTree(ctx context.Context, req *dto.GetCategoryTreeReq) (*dto.GetCategoryTreeRes, error) { + list, _, err := dao.Category.List(ctx, &dto.ListCategoryReq{ + Keyword: req.Name, + }) + if err != nil { + return nil, err + } + tree, err := s.buildTree(ctx, list, "") + if err != nil { + return nil, err + } + return &dto.GetCategoryTreeRes{Tree: tree}, nil +} + +// buildTree 构建树形结构 +func (s *CategoryService) buildTree(ctx context.Context, categories []entity.Category, parentId string) ([]*dto.CategoryTreeNode, error) { + tree := make([]*dto.CategoryTreeNode, 0) + for _, cat := range categories { + if !s.isChildOf(cat.ParentId, parentId) { + continue + } + node, err := s.convertToTreeNode(ctx, &cat, categories) + if err != nil { + return nil, err + } + tree = append(tree, node) + } + return tree, nil +} + +// isChildOf 判断分类是否为指定父节点的子节点 +func (s *CategoryService) isChildOf(childParentId, parentId string) bool { + if g.IsEmpty(childParentId) && g.IsEmpty(parentId) { + return true + } + if g.IsEmpty(childParentId) || g.IsEmpty(parentId) { + return false + } + return childParentId == parentId +} + +// convertToTreeNode 将分类实体转换为树节点 +func (s *CategoryService) convertToTreeNode(ctx context.Context, category *entity.Category, allCategories []entity.Category) (*dto.CategoryTreeNode, error) { + res := new(dto.CategoryTreeNode) + if err := gconv.Struct(&category, &res); err != nil { + return res, err + } + children, err := s.buildTree(ctx, allCategories, category.Bid) + if err != nil { + return res, err + } + res.Children = children + res.IsLeafNode = len(children) == 0 + return res, nil +} + +// hasChildren 检查分类是否有子分类 +func (s *CategoryService) hasChildren(ctx context.Context, parentId string) (bool, error) { + count, err := dao.Category.Count(ctx, &dto.ListCategoryReq{ + ParentId: parentId, + }) + return count > 0, err +} + +// updateLeafNodeIfNoChildren 如果父分类没有子分类了,更新为叶子节点 +func (s *CategoryService) updateLeafNodeIfNoChildren(ctx context.Context, parentId string) error { + hasChildren, err := s.hasChildren(ctx, parentId) + if err != nil { + return err + } + return s.updateLeafNode(ctx, parentId, !hasChildren) +} + +// UpdateStatus 更新分类状态 +func (s *CategoryService) UpdateStatus(ctx context.Context, req *dto.UpdateCategoryStatusReq) error { + var updateReq *dto.UpdateCategoryReq + if err := gconv.Struct(req, &updateReq); err != nil { + return err + } + return dao.Category.Update(ctx, updateReq) +} + +// Update 更新分类 +func (s *CategoryService) Update(ctx context.Context, req *dto.UpdateCategoryReq) error { + oldCategory, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Id: req.Id}) + if err != nil { + return err + } + err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + if err = dao.Category.Update(ctx, req); err != nil { + return err + } + + // 如果修改了父分类,需要更新相关节点的 IsLeafNode + if s.parentIdChanged(req.ParentId, oldCategory.ParentId) { + if !g.IsEmpty(req.ParentId) { + if err = s.updateLeafNode(ctx, req.ParentId, false); err != nil { + return err + } + } + if !g.IsEmpty(oldCategory.ParentId) { + if err = s.updateLeafNodeIfNoChildren(ctx, oldCategory.ParentId); err != nil { + return err + } + } + } + return nil + }) + + return err +} + +// parentIdChanged 判断父分类是否发生变化 +func (s *CategoryService) parentIdChanged(newParentId, oldParentId string) bool { + if g.IsEmpty(newParentId) && g.IsEmpty(oldParentId) { + return false + } + if g.IsEmpty(newParentId) || g.IsEmpty(oldParentId) { + return true + } + return newParentId != oldParentId +} + +// Delete 删除分类 +func (s *CategoryService) Delete(ctx context.Context, req *dto.DeleteCategoryReq) error { + // 检查是否有子分类 + hasChildren, err := s.hasChildren(ctx, req.Bid) + if err != nil { + return err + } + if hasChildren { + return errors.New("该分类下有子分类,无法删除") + } + + // 检查是否有资产 + if count, err := dao.Asset.Count(ctx, &dto.ListAssetReq{ + CategoryId: req.Bid, + }); err != nil { + return err + } else if count > 0 { + return errors.New("该分类下有资产信息,无法删除") + } + + // 获取分类信息用于后续操作 + category, err := dao.Category.GetOne(ctx, &dto.GetCategoryReq{Bid: req.Bid}) + if err != nil { + return err + } + err = gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 删除分类 + if err := dao.Category.DeleteFake(ctx, req); err != nil { + return err + } + + // 如果删除的是叶子节点,更新父节点为叶子节点 + if !g.IsEmpty(category.ParentId) { + return s.updateLeafNodeIfNoChildren(ctx, category.ParentId) + } + return nil + }) + + return nil +} diff --git a/service/asset/private_category_service.go b/service/asset/private_category_service.go new file mode 100644 index 0000000..288ab98 --- /dev/null +++ b/service/asset/private_category_service.go @@ -0,0 +1,176 @@ +package service + +import ( + dao "assets/dao/asset" + dto "assets/model/dto/asset" + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type privateCategory struct{} + +// PrivateCategory 私域分类服务 +var PrivateCategory = new(privateCategory) + +// CreatePrivateCategory 创建私域分类 +func (s *privateCategory) CreatePrivateCategory(ctx context.Context, req *dto.CreatePrivateCategoryReq) (*dto.CreatePrivateCategoryRes, error) { + // 自动设置level + if !req.ParentID.IsZero() { + parentCategory, err := dao.PrivateCategory.GetOne(ctx, req.ParentID) + if err == nil && parentCategory != nil { + req.Level = parentCategory.Level + 1 + req.Path = parentCategory.Path + "/" + parentCategory.Id.Hex() + } + } else { + req.Level = 0 + req.Path = "/root" + } + + // 保存到数据库 + ids, err := dao.PrivateCategory.Insert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "创建私域分类失败") + } + + var id *bson.ObjectID + if len(ids) > 0 { + if objectID, ok := ids[0].(bson.ObjectID); ok { + id = &objectID + } + } + + return &dto.CreatePrivateCategoryRes{ID: id}, nil +} + +// BatchCreatePrivateCategory 批量创建私域分类 +func (s *privateCategory) BatchCreatePrivateCategory(ctx context.Context, req *dto.BatchCreatePrivateCategoryReq) (*dto.BatchCreatePrivateCategoryRes, error) { + // 保存到数据库 + ids, err := dao.PrivateCategory.BatchInsert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "批量创建私域分类失败") + } + + // 转换ID列表 + idList := make([]*bson.ObjectID, 0, len(ids)) + for _, id := range ids { + if objectID, ok := id.(bson.ObjectID); ok { + idList = append(idList, &objectID) + } + } + + return &dto.BatchCreatePrivateCategoryRes{IDs: idList}, nil +} + +// UpdatePrivateCategory 更新私域分类 +func (s *privateCategory) UpdatePrivateCategory(ctx context.Context, req *dto.UpdatePrivateCategoryReq) error { + // 更新到数据库 + err := dao.PrivateCategory.Update(ctx, req) + if err != nil { + return gerror.Wrap(err, "更新私域分类失败") + } + + return nil +} + +// DeletePrivateCategory 删除私域分类 +func (s *privateCategory) DeletePrivateCategory(ctx context.Context, id *bson.ObjectID) error { + return dao.PrivateCategory.DeleteFake(ctx, id) +} + +// GetPrivateCategory 获取私域分类详情 +func (s *privateCategory) GetPrivateCategory(ctx context.Context, id *bson.ObjectID) (*dto.GetPrivateCategoryRes, error) { + category, err := dao.PrivateCategory.GetOne(ctx, id) + if err != nil { + return nil, gerror.Wrap(err, "获取私域分类失败") + } + + // 转换为响应 + res := &dto.GetPrivateCategoryRes{ + ID: category.Id, + Name: category.Name, + ParentID: category.ParentID, + Path: category.Path, + Level: category.Level, + IsLeafNode: category.IsLeafNode, + Sort: category.Sort, + Image: category.Image, + CreatedAt: category.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: category.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return res, nil +} + +// ListPrivateCategory 获取私域分类列表 +func (s *privateCategory) ListPrivateCategory(ctx context.Context, req *dto.ListPrivateCategoryReq) (*dto.ListPrivateCategoryRes, error) { + // 获取数据 + categories, total, err := dao.PrivateCategory.List(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "获取私域分类列表失败") + } + + // 转换为响应 + listItems := make([]*dto.PrivateCategoryListItem, 0, len(categories)) + for _, category := range categories { + listItems = append(listItems, &dto.PrivateCategoryListItem{ + ID: category.Id, + Name: category.Name, + ParentID: category.ParentID, + Path: category.Path, + Level: category.Level, + IsLeafNode: category.IsLeafNode, + Sort: category.Sort, + Image: category.Image, + CreatedAt: category.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: category.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &dto.ListPrivateCategoryRes{ + List: listItems, + Total: total, + }, nil +} + +// GetPrivateCategoryTree 获取私域分类树 +func (s *privateCategory) GetPrivateCategoryTree(ctx context.Context) (*dto.GetPrivateCategoryTreeRes, error) { + categories, err := dao.PrivateCategory.GetTree(ctx) + if err != nil { + return nil, gerror.Wrap(err, "获取私域分类树失败") + } + + // 转换为响应 + treeItems := make([]*dto.PrivateCategoryTreeItem, 0, len(categories)) + for _, category := range categories { + treeItems = append(treeItems, &dto.PrivateCategoryTreeItem{ + ID: category.Id, + Name: category.Name, + ParentID: category.ParentID, + Path: category.Path, + Level: category.Level, + IsLeafNode: category.IsLeafNode, + Sort: category.Sort, + Image: category.Image, + }) + } + + return &dto.GetPrivateCategoryTreeRes{ + Tree: treeItems, + }, nil +} + +// GenerateTestData 生成测试数据 +func (s *privateCategory) GenerateTestData(ctx context.Context) error { + testData := &dto.BatchCreatePrivateCategoryReq{ + Categories: []dto.CreatePrivateCategoryReq{}, + } + + _, err := s.BatchCreatePrivateCategory(ctx, testData) + if err != nil { + return gerror.Wrap(err, "生成测试数据失败") + } + + return nil +} diff --git a/service/asset/private_sku_service.go b/service/asset/private_sku_service.go new file mode 100644 index 0000000..c469a5e --- /dev/null +++ b/service/asset/private_sku_service.go @@ -0,0 +1,189 @@ +package service + +import ( + dao "assets/dao/asset" + dto "assets/model/dto/asset" + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type privateSku struct{} + +// PrivateSku 私域SKU服务 +var PrivateSku = new(privateSku) + +// CreatePrivateSku 创建私域SKU +func (s *privateSku) CreatePrivateSku(ctx context.Context, req *dto.CreatePrivateSkuReq) (*dto.CreatePrivateSkuRes, error) { + // 保存到数据库 + ids, err := dao.PrivateSku.Insert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "创建私域SKU失败") + } + + var id *bson.ObjectID + if len(ids) > 0 { + if objectID, ok := ids[0].(bson.ObjectID); ok { + id = &objectID + } + } + + return &dto.CreatePrivateSkuRes{ID: id}, nil +} + +// BatchCreatePrivateSku 批量创建私域SKU +func (s *privateSku) BatchCreatePrivateSku(ctx context.Context, req *dto.BatchCreatePrivateSkuReq) (*dto.BatchCreatePrivateSkuRes, error) { + // 保存到数据库 + ids, err := dao.PrivateSku.BatchInsert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "批量创建私域SKU失败") + } + + // 转换ID列表 + idList := make([]*bson.ObjectID, 0, len(ids)) + for _, id := range ids { + if objectID, ok := id.(bson.ObjectID); ok { + idList = append(idList, &objectID) + } + } + + return &dto.BatchCreatePrivateSkuRes{IDs: idList}, nil +} + +// UpdatePrivateSku 更新私域SKU +func (s *privateSku) UpdatePrivateSku(ctx context.Context, req *dto.UpdatePrivateSkuReq) error { + // 更新到数据库 + err := dao.PrivateSku.Update(ctx, req) + if err != nil { + return gerror.Wrap(err, "更新私域SKU失败") + } + + return nil +} + +// DeletePrivateSku 删除私域SKU +func (s *privateSku) DeletePrivateSku(ctx context.Context, id *bson.ObjectID) error { + return dao.PrivateSku.DeleteFake(ctx, id) +} + +// GetPrivateSku 获取私域SKU详情 +func (s *privateSku) GetPrivateSku(ctx context.Context, id *bson.ObjectID) (*dto.GetPrivateSkuRes, error) { + sku, err := dao.PrivateSku.GetOne(ctx, id) + if err != nil { + return nil, gerror.Wrap(err, "获取私域SKU失败") + } + + // 转换为响应 + res := &dto.GetPrivateSkuRes{ + ID: sku.Id, + SkuName: sku.SkuName, + ImageURL: sku.ImageURL, + Price: sku.Price, + Stock: sku.Stock, + Sort: sku.Sort, + PrivateCategoryPath: sku.PrivateCategoryPath, + CreatedAt: sku.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: sku.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return res, nil +} + +// ListPrivateSku 获取私域SKU列表 +func (s *privateSku) ListPrivateSku(ctx context.Context, req *dto.ListPrivateSkuReq) (*dto.ListPrivateSkuRes, error) { + // 获取数据 + skus, total, err := dao.PrivateSku.List(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "获取私域SKU列表失败") + } + + // 转换为响应 + listItems := make([]*dto.PrivateSkuListItem, 0, len(skus)) + for _, sku := range skus { + listItems = append(listItems, &dto.PrivateSkuListItem{ + ID: sku.Id, + SkuName: sku.SkuName, + ImageURL: sku.ImageURL, + Price: sku.Price, + Stock: sku.Stock, + Sort: sku.Sort, + PrivateCategoryPath: sku.PrivateCategoryPath, + CreatedAt: sku.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: sku.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &dto.ListPrivateSkuRes{ + List: listItems, + Total: total, + }, nil +} + +// UpdatePrivateSkuStock 更新私域SKU库存 +func (s *privateSku) UpdatePrivateSkuStock(ctx context.Context, id *bson.ObjectID, stockChange int) error { + return dao.PrivateSku.UpdateStock(ctx, id, stockChange) +} + +// GenerateTestData 生成测试数据 +func (s *privateSku) GenerateTestData(ctx context.Context) error { + testData := &dto.BatchCreatePrivateSkuReq{ + Skus: []dto.CreatePrivateSkuReq{ + { + SkuName: "联想ThinkPad X1 Carbon 14英寸 i7 16G 512G", + ImageURL: "https://example.com/images/lenovo_x1_carbon.jpg", + Price: 899900, // 8999.00元 + Stock: 100, + Sort: 1, + PrivateCategoryPath: "/笔记本电脑/ThinkPad", + }, + { + SkuName: "联想ThinkPad X1 Carbon 14英寸 i7 32G 1T", + ImageURL: "https://example.com/images/lenovo_x1_carbon_32g.jpg", + Price: 1199900, // 11999.00元 + Stock: 50, + Sort: 2, + PrivateCategoryPath: "/笔记本电脑/ThinkPad", + }, + { + SkuName: "戴尔Latitude 7440 14英寸 i7 16G 512G", + ImageURL: "https://example.com/images/dell_latitude_7440.jpg", + Price: 750000, // 7500.00元 + Stock: 80, + Sort: 1, + PrivateCategoryPath: "/笔记本电脑/Latitude", + }, + { + SkuName: "戴尔Latitude 7440 14英寸 i7 32G 1T", + ImageURL: "https://example.com/images/dell_latitude_7440_32g.jpg", + Price: 999900, // 9999.00元 + Stock: 60, + Sort: 2, + PrivateCategoryPath: "/笔记本电脑/Latitude", + }, + { + SkuName: "惠普暗影精灵9 16.1英寸 i7 32G 1T RTX4060", + ImageURL: "https://example.com/images/hp_omen_9.jpg", + Price: 849900, // 8499.00元 + Stock: 70, + Sort: 1, + PrivateCategoryPath: "/游戏笔记本/暗影精灵", + }, + { + SkuName: "惠普暗影精灵9 16.1英寸 i9 32G 2T RTX4070", + ImageURL: "https://example.com/images/hp_omen_9_i9.jpg", + Price: 1249900, // 12499.00元 + Stock: 40, + Sort: 2, + PrivateCategoryPath: "/游戏笔记本/暗影精灵", + }, + }, + } + + _, err := s.BatchCreatePrivateSku(ctx, testData) + if err != nil { + return gerror.Wrap(err, "生成测试数据失败") + } + + return nil +} diff --git a/service/enum/enum_service.go b/service/enum/enum_service.go new file mode 100644 index 0000000..e34cd5d --- /dev/null +++ b/service/enum/enum_service.go @@ -0,0 +1,62 @@ +package service + +import ( + consts "assets/consts/asset" + "assets/consts/public" + dto "assets/model/dto/enum" + "context" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/util/gconv" +) + +type enum struct{} + +// Enum 枚举服务 +var Enum = new(enum) + +// GetAssetType 获取资产类型 +func (s *enum) GetAssetType(ctx context.Context, req *dto.GetAssetTypeReq) (res *dto.GetAssetTypeRes, err error) { + _, _ = ctx, req + res = new(dto.GetAssetTypeRes) + err = gconv.Structs(consts.GetAllAssetTypeKeyValue(), &res.Options) + return +} + +func (s *enum) GetCategoryAttrType(ctx context.Context, req *dto.GetCategoryAttrTypeReq) (res *dto.GetCategoryAttrTypeRes, err error) { + _, _ = ctx, req + res = new(dto.GetCategoryAttrTypeRes) + err = gconv.Structs(consts.GetAllAttrTypeKeyValue(), &res.Options) + return +} + +func (s *enum) GetSpecsUnit(ctx context.Context, req *dto.GetSpecsUnitReq) (res *dto.GetSpecsUnitRes, err error) { + res = new(dto.GetSpecsUnitRes) + if *req.AssetType == consts.AssetTypeVirtual { + keyValue := public.GetAllDurationTypeKeyValue() + err = gconv.Structs(keyValue, &res.Options) + if err != nil { + return + } + } else { + // 使用简化的 RPC 调用方式 - 直接传 map 参数 + //dictData := &dto.GetDictRes{} + //if err = message.CallRPC(ctx, "dictService.GetDictWithDataByType", map[string]interface{}{"dictType": gconv.String(req.AssetType)}, dictData); err != nil { + // return + //} + //for _, v := range dictData.Values { + // res.Options = append(res.Options, dto.KeyValue{ + // Key: v.DictValue, + // Value: v.DictLabel, + // }) + //} + } + return +} + +func (s *enum) GetTenantModuleType(ctx context.Context, req *dto.GetTenantModuleTypeReq) (res *dto.GetTenantModuleTypeRes, err error) { + _ = ctx + res = new(dto.GetTenantModuleTypeRes) + err = gconv.Structs(beans.GetTenantModuleTypes(req.AssetId), &res.Options) + return +} diff --git a/service/procurement/purchase_inbound_service.go b/service/procurement/purchase_inbound_service.go new file mode 100644 index 0000000..e98b2ff --- /dev/null +++ b/service/procurement/purchase_inbound_service.go @@ -0,0 +1,201 @@ +package service + +import ( + "assets/consts/public" + daoAsset "assets/dao/asset" + daoProcurement "assets/dao/procurement" + daoStock "assets/dao/stock" + dtoProcurement "assets/model/dto/procurement" + dtoStock "assets/model/dto/stock" + serviceStock "assets/service/stock" + "context" + "errors" + "fmt" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type purchaseInbound struct{} + +var PurchaseInbound = new(purchaseInbound) + +// Create 创建采购入库 +func (s *purchaseInbound) Create(ctx context.Context, req *dtoProcurement.CreatePurchaseInboundReq) (res *dtoProcurement.CreatePurchaseInboundRes, err error) { + // 1. 查询采购订单明细 + orderItem, err := daoProcurement.PurchaseOrderItem.GetOne(ctx, req.OrderItemId) + if err != nil { + return nil, errors.New("采购订单明细不存在") + } + if g.IsEmpty(orderItem) { + return nil, errors.New("采购订单明细不存在") + } + + // 2. 校验+更新入库数量(原子操作,防止并发竞态) + // 使用$inc + $expr条件:inboundQty + delta <= passQuantity + if err = daoProcurement.PurchaseOrderItem.IncrementInboundQty(ctx, req.OrderItemId, req.InboundQty); err != nil { + return nil, fmt.Errorf("入库数量校验失败: %v", err) + } + + // 3. 生成入库单号和批次号 + inboundNo, err := s.generateIncrSequence(ctx, public.StockInboundNoKeyPrefix) + if err != nil { + return nil, fmt.Errorf("生成入库单号失败: %v", err) + } + batchNo, err := s.generateIncrSequence(ctx, public.StockBatchNoKeyPrefix) + if err != nil { + return nil, fmt.Errorf("生成批次号失败: %v", err) + } + + // 4. 查询关联信息 + warehouseName, zoneName, locationName := s.getStorageNames(ctx, req.WarehouseId, req.ZoneId, req.LocationId) + privateSkuName, privateCategoryPath := s.getPrivateSkuInfo(ctx, req.PrivateSkuId, req.PrivateCategoryId) + + // 5. 创建入库记录 + inboundReq := &dtoProcurement.CreatePurchaseInboundReq{ + OrderItemId: req.OrderItemId, + InboundQty: req.InboundQty, + WarehouseId: req.WarehouseId, + ZoneId: req.ZoneId, + LocationId: req.LocationId, + PrivateSkuId: req.PrivateSkuId, + PrivateCategoryId: req.PrivateCategoryId, + Remark: req.Remark, + } + + ids, err := daoProcurement.PurchaseInbound.Insert(ctx, inboundReq) + if err != nil { + return nil, fmt.Errorf("创建入库记录失败: %v", err) + } + + inboundId := ids[0].(bson.ObjectID) + + // 6. 更新入库记录的关联信息 + err = s.updateInboundDetails(ctx, &inboundId, orderItem.OrderId, inboundNo, batchNo, + warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath) + if err != nil { + return nil, fmt.Errorf("更新入库记录失败: %v", err) + } + + // 7. 创建PrivateStock批次记录 + privateStockReq := &dtoStock.CreatePrivateStockReq{ + PrivateSkuID: req.PrivateSkuId, + BatchNo: batchNo, + BatchQty: req.InboundQty, + AvailableQty: req.InboundQty, + WarehouseId: req.WarehouseId, + ZoneId: req.ZoneId, + LocationId: req.LocationId, + PrivateCategoryPath: privateCategoryPath, + } + privateStockIds, err := daoStock.PrivateStock.Insert(ctx, privateStockReq) + if err != nil { + return nil, fmt.Errorf("创建库存批次失败: %v", err) + } + + privateStockId := privateStockIds[0].(bson.ObjectID) + + // 8. 触发库位容量更新(异步,失败不影响入库) + if req.LocationId != nil && !req.LocationId.IsZero() { + if capErr := serviceStock.Capacity.UpdateLocationCapacity(ctx, req.LocationId); capErr != nil { + g.Log().Warningf(ctx, "更新库位容量失败(不影响入库): %v", capErr) + } + } + + // 9. 更新入库记录关联的库存ID + err = s.updateInboundPrivateStockId(ctx, &inboundId, &privateStockId) + if err != nil { + return nil, fmt.Errorf("更新入库记录库存ID失败: %v", err) + } + + // 10. 入库数量已在步骤2原子更新,无需重复操作 + + return &dtoProcurement.CreatePurchaseInboundRes{ + Id: &inboundId, + InboundNo: inboundNo, + BatchNo: batchNo, + }, nil +} + +// GetOne 获取入库详情 +func (s *purchaseInbound) GetOne(ctx context.Context, req *dtoProcurement.GetPurchaseInboundReq) (res *dtoProcurement.GetPurchaseInboundRes, err error) { + one, err := daoProcurement.PurchaseInbound.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +// List 获取入库列表 +func (s *purchaseInbound) List(ctx context.Context, req *dtoProcurement.ListPurchaseInboundReq) (res *dtoProcurement.ListPurchaseInboundRes, err error) { + list, total, err := daoProcurement.PurchaseInbound.List(ctx, req) + if err != nil { + return + } + res = &dtoProcurement.ListPurchaseInboundRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} + +// generateInboundNo 生成入库单号(Redis自增,按天重置) +func (s *purchaseInbound) generateIncrSequence(ctx context.Context, keyPrefix string) (string, error) { + seq, err := utils.IncrSequence(ctx, keyPrefix, 6, "-") + if err != nil { + return "", err + } + return seq, nil +} + +// getStorageNames 获取仓储名称 +func (s *purchaseInbound) getStorageNames(ctx context.Context, warehouseId, zoneId, locationId *bson.ObjectID) (warehouseName, zoneName, locationName string) { + if !g.IsEmpty(warehouseId) { + warehouse, _ := daoStock.Warehouse.GetOne(ctx, &dtoStock.GetWarehouseReq{Id: warehouseId}) + if !g.IsEmpty(warehouse) { + warehouseName = warehouse.WarehouseName + } + } + if !g.IsEmpty(zoneId) { + zone, _ := daoStock.Zone.GetOne(ctx, &dtoStock.GetZoneReq{Id: zoneId}) + if !g.IsEmpty(zone) { + zoneName = zone.ZoneName + } + } + if !g.IsEmpty(locationId) { + location, _ := daoStock.Location.GetOne(ctx, &dtoStock.GetLocationReq{Id: locationId}) + if !g.IsEmpty(location) { + locationName = location.LocationName + } + } + return +} + +// getPrivateSkuInfo 获取私域SKU信息 +func (s *purchaseInbound) getPrivateSkuInfo(ctx context.Context, privateSkuId, privateCategoryId *bson.ObjectID) (privateSkuName, privateCategoryPath string) { + if !g.IsEmpty(privateSkuId) { + privateSku, err := daoAsset.PrivateSku.GetOne(ctx, privateSkuId) + if err == nil && !g.IsEmpty(privateSku) { + privateSkuName = privateSku.SkuName + } + } + if !g.IsEmpty(privateCategoryId) { + privateCategory, err := daoAsset.PrivateCategory.GetOne(ctx, privateCategoryId) + if err == nil && !g.IsEmpty(privateCategory) { + privateCategoryPath = privateCategory.Path + } + } + return +} + +// updateInboundDetails 更新入库记录详细信息 +func (s *purchaseInbound) updateInboundDetails(ctx context.Context, inboundId, orderId *bson.ObjectID, inboundNo, batchNo, warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath string) (err error) { + return daoProcurement.PurchaseInbound.UpdateDetails(ctx, inboundId, orderId, inboundNo, batchNo, + warehouseName, zoneName, locationName, privateSkuName, privateCategoryPath) +} + +// updateInboundPrivateStockId 更新入库记录关联的库存ID +func (s *purchaseInbound) updateInboundPrivateStockId(ctx context.Context, inboundId, privateStockId *bson.ObjectID) (err error) { + return daoProcurement.PurchaseInbound.UpdatePrivateStockId(ctx, inboundId, privateStockId) +} diff --git a/service/procurement/purchase_order_item_service.go b/service/procurement/purchase_order_item_service.go new file mode 100644 index 0000000..939ef41 --- /dev/null +++ b/service/procurement/purchase_order_item_service.go @@ -0,0 +1,182 @@ +package service + +import ( + "assets/dao/procurement" + "assets/model/dto/procurement" + "assets/model/entity/procurement" + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type purchaseOrderItem struct{} + +// PurchaseOrderItem 采购订单明细服务 +var PurchaseOrderItem = new(purchaseOrderItem) + +// CreatePurchaseOrderItem 创建采购订单明细 +func (s *purchaseOrderItem) CreatePurchaseOrderItem(ctx context.Context, req *dto.CreatePurchaseOrderItemReq) (*dto.CreatePurchaseOrderItemRes, error) { + // 自动计算总价(如果未提供) + if req.TotalPrice == 0 { + req.TotalPrice = req.UnitPrice * req.Quantity + } + // 默认折扣价为单价(如果未提供) + if req.DiscountPrice == 0 { + req.DiscountPrice = req.UnitPrice + } + + // 保存到数据库 + ids, err := dao.PurchaseOrderItem.Insert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "创建采购订单明细失败") + } + + var id *bson.ObjectID + if len(ids) > 0 { + if objectID, ok := ids[0].(bson.ObjectID); ok { + id = &objectID + } + } + + return &dto.CreatePurchaseOrderItemRes{ID: id}, nil +} + +// BatchCreatePurchaseOrderItems 批量创建采购订单明细 +func (s *purchaseOrderItem) BatchCreatePurchaseOrderItems(ctx context.Context, req *dto.BatchCreatePurchaseOrderItemsReq) (*dto.BatchCreatePurchaseOrderItemsRes, error) { + // 自动计算总价和设置默认折扣价 + for i := range req.Items { + if req.Items[i].TotalPrice == 0 { + req.Items[i].TotalPrice = req.Items[i].UnitPrice * req.Items[i].Quantity + } + if req.Items[i].DiscountPrice == 0 { + req.Items[i].DiscountPrice = req.Items[i].UnitPrice + } + } + + // 保存到数据库 + ids, err := dao.PurchaseOrderItem.BatchInsert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "批量创建采购订单明细失败") + } + + // 转换ID列表 + idList := make([]*bson.ObjectID, 0, len(ids)) + for _, id := range ids { + if objectID, ok := id.(bson.ObjectID); ok { + idList = append(idList, &objectID) + } + } + + return &dto.BatchCreatePurchaseOrderItemsRes{IDs: idList}, nil +} + +// UpdatePurchaseOrderItem 更新采购订单明细 +func (s *purchaseOrderItem) UpdatePurchaseOrderItem(ctx context.Context, req *dto.UpdatePurchaseOrderItemReq) error { + // 自动计算总价(如果提供了数量和单价) + if req.Quantity > 0 && req.UnitPrice > 0 && req.TotalPrice == 0 { + req.TotalPrice = req.UnitPrice * req.Quantity + } + + // 更新到数据库 + err := dao.PurchaseOrderItem.Update(ctx, req) + if err != nil { + return gerror.Wrap(err, "更新采购订单明细失败") + } + + return nil +} + +// DeletePurchaseOrderItem 删除采购订单明细 +func (s *purchaseOrderItem) DeletePurchaseOrderItem(ctx context.Context, id *bson.ObjectID) error { + return dao.PurchaseOrderItem.DeleteFake(ctx, id) +} + +// GetPurchaseOrderItem 获取采购订单明细详情 +func (s *purchaseOrderItem) GetPurchaseOrderItem(ctx context.Context, id *bson.ObjectID) (*dto.GetPurchaseOrderItemRes, error) { + item, err := dao.PurchaseOrderItem.GetOne(ctx, id) + if err != nil { + return nil, gerror.Wrap(err, "获取采购订单明细失败") + } + + // 转换为响应 + res := &dto.GetPurchaseOrderItemRes{ + ID: item.Id, + OrderId: item.OrderId, + AssetId: item.AssetId, + AssetSkuId: item.AssetSkuId, + ProductName: item.ProductName, + Specification: item.Specification, + Brand: item.Brand, + Quantity: item.Quantity, + Unit: item.Unit, + UnitPrice: item.UnitPrice, + TotalPrice: item.TotalPrice, + DiscountPrice: item.DiscountPrice, + RequirementDesc: item.RequirementDesc, + DeliveryAddress: item.DeliveryAddress, + CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return res, nil +} + +// ListPurchaseOrderItems 获取采购订单明细列表 +func (s *purchaseOrderItem) ListPurchaseOrderItems(ctx context.Context, req *dto.ListPurchaseOrderItemsReq) (*dto.ListPurchaseOrderItemsRes, error) { + // 获取数据 + items, total, err := dao.PurchaseOrderItem.List(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "获取采购订单明细列表失败") + } + + // 转换为响应 + listItems := make([]*dto.PurchaseOrderItemListItem, 0, len(items)) + for _, item := range items { + listItems = append(listItems, &dto.PurchaseOrderItemListItem{ + ID: item.Id, + OrderId: item.OrderId, + AssetId: item.AssetId, + AssetSkuId: item.AssetSkuId, + ProductName: item.ProductName, + Specification: item.Specification, + Brand: item.Brand, + Quantity: item.Quantity, + Unit: item.Unit, + UnitPrice: item.UnitPrice, + TotalPrice: item.TotalPrice, + DiscountPrice: item.DiscountPrice, + RequirementDesc: item.RequirementDesc, + DeliveryAddress: item.DeliveryAddress, + CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &dto.ListPurchaseOrderItemsRes{ + List: listItems, + Total: total, + }, nil +} + +// ListByOrderId 根据订单ID获取采购订单明细列表 +func (s *purchaseOrderItem) ListByOrderId(ctx context.Context, orderId *bson.ObjectID) ([]*entity.PurchaseOrderItem, error) { + items, err := dao.PurchaseOrderItem.ListByOrderId(ctx, orderId) + if err != nil { + return nil, gerror.Wrap(err, "获取订单明细列表失败") + } + return items, nil +} + +// GenerateTestData 生成测试数据 +func (s *purchaseOrderItem) GenerateTestData(ctx context.Context) error { + + testData := &dto.BatchCreatePurchaseOrderItemsReq{} + + _, err := s.BatchCreatePurchaseOrderItems(ctx, testData) + if err != nil { + return gerror.Wrap(err, "生成测试数据失败") + } + + return nil +} diff --git a/service/procurement/purchase_order_service.go b/service/procurement/purchase_order_service.go new file mode 100644 index 0000000..e7296c7 --- /dev/null +++ b/service/procurement/purchase_order_service.go @@ -0,0 +1,199 @@ +package service + +import ( + "assets/consts/procurement" + "assets/dao/procurement" + "assets/model/dto/procurement" + "assets/model/entity/procurement" + "context" + "github.com/gogf/gf/v2/os/gtime" + + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type purchaseOrder struct{} + +// PurchaseOrder 采购订单服务 +var PurchaseOrder = new(purchaseOrder) + +// CreatePurchaseOrder 创建采购订单 +func (s *purchaseOrder) CreatePurchaseOrder(ctx context.Context, req *dto.CreatePurchaseOrderReq) (*dto.CreatePurchaseOrderRes, error) { + + // 保存到数据库 + ids, err := dao.PurchaseOrder.Insert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "创建采购订单失败") + } + + var id *bson.ObjectID + if len(ids) > 0 { + if objectID, ok := ids[0].(bson.ObjectID); ok { + id = &objectID + } + } + + return &dto.CreatePurchaseOrderRes{ID: id}, nil +} + +// BatchCreatePurchaseOrders 批量创建采购订单 +func (s *purchaseOrder) BatchCreatePurchaseOrders(ctx context.Context, req *dto.BatchCreatePurchaseOrdersReq) (*dto.BatchCreatePurchaseOrdersRes, error) { + // 保存到数据库 + ids, err := dao.PurchaseOrder.BatchInsert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "批量创建采购订单失败") + } + + // 转换ID列表 + idList := make([]*bson.ObjectID, 0, len(ids)) + for _, id := range ids { + if objectID, ok := id.(bson.ObjectID); ok { + idList = append(idList, &objectID) + } + } + + return &dto.BatchCreatePurchaseOrdersRes{IDs: idList}, nil +} + +// UpdatePurchaseOrder 更新采购订单 +func (s *purchaseOrder) UpdatePurchaseOrder(ctx context.Context, req *dto.UpdatePurchaseOrderReq) error { + // 更新到数据库 + err := dao.PurchaseOrder.Update(ctx, req) + if err != nil { + return gerror.Wrap(err, "更新采购订单失败") + } + + return nil +} + +// DeletePurchaseOrder 删除采购订单 +func (s *purchaseOrder) DeletePurchaseOrder(ctx context.Context, id *bson.ObjectID) error { + return dao.PurchaseOrder.DeleteFake(ctx, id) +} + +// GetPurchaseOrder 获取采购订单详情 +func (s *purchaseOrder) GetPurchaseOrder(ctx context.Context, id *bson.ObjectID) (*dto.GetPurchaseOrderRes, error) { + order, err := dao.PurchaseOrder.GetOne(ctx, id) + if err != nil { + return nil, gerror.Wrap(err, "获取采购订单失败") + } + + // 转换为响应 + res := &dto.GetPurchaseOrderRes{ + ID: order.Id, + OrderNo: order.OrderNo, + Title: order.Title, + Description: order.Description, + OrderType: order.OrderType, + BuyerId: order.BuyerId, + BuyerName: order.BuyerName, + BuyerType: order.BuyerType, + Status: order.Status, + StatusText: consts.GetPurchaseOrderStatusText(order.Status), + Priority: order.Priority, + ExpectedDelivery: formatGTime(order.ExpectedDelivery), + ExpiryTime: formatGTime(order.ExpiryTime), + CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: order.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + // 转换指定供应商模式信息 + if order.DirectPurchase != nil { + res.DirectPurchase = &dto.DirectPurchaseInfoRes{ + SupplierId: order.DirectPurchase.SupplierId, + SupplierName: order.DirectPurchase.SupplierName, + SupplierCode: order.DirectPurchase.SupplierCode, + AssignReason: order.DirectPurchase.AssignReason, + DeliveryAddress: order.DirectPurchase.DeliveryAddress, + ContactPerson: order.DirectPurchase.ContactPerson, + ContactPhone: order.DirectPurchase.ContactPhone, + ResponseStatus: order.DirectPurchase.ResponseStatus, + AssignedAt: formatGTime(order.DirectPurchase.AssignedAt), + AcceptedAt: formatGTime(order.DirectPurchase.AcceptedAt), + RejectedAt: formatGTime(order.DirectPurchase.RejectedAt), + DeliveredAt: formatGTime(order.DirectPurchase.DeliveredAt), + } + } + + // 转换竞价模式信息 + if order.BiddingInfo != nil { + res.BiddingInfo = &dto.BiddingInfoRes{ + BidMode: order.BiddingInfo.BidMode, + BidModeText: consts.GetBidModeText(order.BiddingInfo.BidMode), + MinSuppliers: order.BiddingInfo.MinSuppliers, + MaxSuppliers: order.BiddingInfo.MaxSuppliers, + BidDuration: order.BiddingInfo.BidDuration, + BidSupplierCount: order.BiddingInfo.BidSupplierCount, + BidStartAt: formatGTime(order.BiddingInfo.BidStartAt), + BidEndAt: formatGTime(order.BiddingInfo.BidEndAt), + ResultPublishedAt: formatGTime(order.BiddingInfo.ResultPublishedAt), + } + } + + return res, nil +} + +// ListPurchaseOrders 获取采购订单列表 +func (s *purchaseOrder) ListPurchaseOrders(ctx context.Context, req *dto.ListPurchaseOrdersReq) (*dto.ListPurchaseOrdersRes, error) { + // 获取数据 + orders, total, err := dao.PurchaseOrder.List(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "获取采购订单列表失败") + } + + // 转换为响应 + listItems := make([]*dto.PurchaseOrderListItem, 0, len(orders)) + for _, order := range orders { + listItems = append(listItems, &dto.PurchaseOrderListItem{ + ID: order.Id, + OrderNo: order.OrderNo, + Title: order.Title, + OrderType: order.OrderType, + OrderTypeText: consts.GetPurchaseOrderTypeText(order.OrderType), + BuyerName: order.BuyerName, + BuyerType: order.BuyerType, + Status: order.Status, + StatusText: consts.GetPurchaseOrderStatusText(order.Status), + Priority: order.Priority, + ExpectedDelivery: formatGTime(order.ExpectedDelivery), + ExpiryTime: formatGTime(order.ExpiryTime), + CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: order.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &dto.ListPurchaseOrdersRes{ + List: listItems, + Total: total, + }, nil +} + +// ListByBuyerId 根据采购方ID获取采购订单列表 +func (s *purchaseOrder) ListByBuyerId(ctx context.Context, buyerId *bson.ObjectID) ([]*entity.PurchaseOrder, error) { + orders, err := dao.PurchaseOrder.ListByBuyerId(ctx, buyerId) + if err != nil { + return nil, gerror.Wrap(err, "获取订单列表失败") + } + return orders, nil +} + +// GenerateTestData 生成测试数据 +func (s *purchaseOrder) GenerateTestData(ctx context.Context) error { + + testData := &dto.BatchCreatePurchaseOrdersReq{} + + _, err := s.BatchCreatePurchaseOrders(ctx, testData) + if err != nil { + return gerror.Wrap(err, "生成测试数据失败") + } + + return nil +} + +// formatGTime 格式化时间 +func formatGTime(t *gtime.Time) string { + if t == nil { + return "" + } + return t.Format("2006-01-02 15:04:05") +} diff --git a/service/procurement/supplier_service.go b/service/procurement/supplier_service.go new file mode 100644 index 0000000..4377f10 --- /dev/null +++ b/service/procurement/supplier_service.go @@ -0,0 +1,172 @@ +package service + +import ( + "assets/consts/procurement" + "assets/dao/procurement" + "assets/model/dto/procurement" + "assets/model/entity/procurement" + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type supplier struct{} + +// Supplier 供应商服务 +var Supplier = new(supplier) + +// CreateSupplier 创建供应商 +func (s *supplier) CreateSupplier(ctx context.Context, req *dto.CreateSupplierReq) (*dto.CreateSupplierRes, error) { + // 转换为实体 + _ = &entity.Supplier{ + Name: req.Name, + Code: req.Code, + Phone: req.Phone, + Mobile: req.Mobile, + Email: req.Email, + Address: req.Address, + Status: consts.SupplierStatus(1), // 默认活跃状态 + } + + // 保存到数据库 + ids, err := dao.Supplier.Insert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "创建供应商失败") + } + + var id *bson.ObjectID + if len(ids) > 0 { + if objectID, ok := ids[0].(bson.ObjectID); ok { + id = &objectID + } + } + + return &dto.CreateSupplierRes{ID: id}, nil +} + +// BatchCreateSuppliers 批量创建供应商 +func (s *supplier) BatchCreateSuppliers(ctx context.Context, req *dto.BatchCreateSuppliersReq) (*dto.BatchCreateSuppliersRes, error) { + // 保存到数据库 + ids, err := dao.Supplier.BatchInsert(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "批量创建供应商失败") + } + + // 转换ID列表 + idList := make([]*bson.ObjectID, 0, len(ids)) + for _, id := range ids { + if objectID, ok := id.(bson.ObjectID); ok { + idList = append(idList, &objectID) + } + } + + return &dto.BatchCreateSuppliersRes{IDs: idList}, nil +} + +// UpdateSupplier 更新供应商 +func (s *supplier) UpdateSupplier(ctx context.Context, req *dto.UpdateSupplierReq) error { + // 更新到数据库 + err := dao.Supplier.Update(ctx, req) + if err != nil { + return gerror.Wrap(err, "更新供应商失败") + } + + return nil +} + +// DeleteSupplier 删除供应商 +func (s *supplier) DeleteSupplier(ctx context.Context, id *bson.ObjectID) error { + return dao.Supplier.DeleteFake(ctx, id) +} + +// GetSupplier 获取供应商详情 +func (s *supplier) GetSupplier(ctx context.Context, id *bson.ObjectID) (*dto.GetSupplierRes, error) { + supplier, err := dao.Supplier.GetOne(ctx, id) + if err != nil { + return nil, gerror.Wrap(err, "获取供应商失败") + } + + // 转换为响应 + res := &dto.GetSupplierRes{ + ID: supplier.Id, + Name: supplier.Name, + Code: supplier.Code, + Phone: supplier.Phone, + Mobile: supplier.Mobile, + Email: supplier.Email, + Website: supplier.Website, + Address: supplier.Address, + Status: supplier.Status, + StatusText: consts.GetSupplierStatusText(supplier.Status), + Rating: supplier.Rating, + CreatedAt: supplier.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: supplier.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return res, nil +} + +// ListSuppliers 获取供应商列表 +func (s *supplier) ListSuppliers(ctx context.Context, req *dto.ListSuppliersReq) (*dto.ListSuppliersRes, error) { + // 获取数据 + suppliers, total, err := dao.Supplier.List(ctx, req) + if err != nil { + return nil, gerror.Wrap(err, "获取供应商列表失败") + } + + // 转换为响应 + listItems := make([]*dto.SupplierListItem, 0, len(suppliers)) + for _, supplier := range suppliers { + listItems = append(listItems, &dto.SupplierListItem{ + ID: supplier.Id, + Name: supplier.Name, + Code: supplier.Code, + Phone: supplier.Phone, + Mobile: supplier.Mobile, + Email: supplier.Email, + Address: supplier.Address, + Status: supplier.Status, + StatusText: consts.GetSupplierStatusText(supplier.Status), + Rating: supplier.Rating, + CreatedAt: supplier.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: supplier.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return &dto.ListSuppliersRes{ + List: listItems, + Total: total, + }, nil +} + +// GetSupplierOptions 获取供应商选项(用于下拉选择) +func (s *supplier) GetSupplierOptions(ctx context.Context) ([]*dto.SupplierListItem, error) { + suppliers, err := dao.Supplier.FindActiveSuppliers(ctx) + if err != nil { + return nil, gerror.Wrap(err, "获取供应商选项失败") + } + + options := make([]*dto.SupplierListItem, 0, len(suppliers)) + for _, supplier := range suppliers { + options = append(options, &dto.SupplierListItem{ + ID: supplier.Id, + Name: supplier.Name, + Code: supplier.Code, + }) + } + + return options, nil +} + +// GenerateTestData 生成测试数据 +func (s *supplier) GenerateTestData(ctx context.Context) error { + testData := &dto.BatchCreateSuppliersReq{} + + _, err := s.BatchCreateSuppliers(ctx, testData) + if err != nil { + return gerror.Wrap(err, "生成测试数据失败") + } + + return nil +} diff --git a/service/stock/capacity_service.go b/service/stock/capacity_service.go new file mode 100644 index 0000000..a596435 --- /dev/null +++ b/service/stock/capacity_service.go @@ -0,0 +1,289 @@ +// 库位容量管理服务 +// 职责:库位/库区/仓库三级容量计算与同步,支持整入整出换算 +// 调用链:PrivateStock.Create/Update/Delete → UpdateLocationCapacity → SyncCapacityToZone → SyncCapacityToWarehouse +// 紧密耦合:dao.Location(更新容量)、dao.Zone(汇总)、dao.Warehouse(汇总)、dao.UnitConversion(单位换算) +// 注意:使用Redis分布式锁防止并发重算覆盖,锁key格式 lock:location:{id}:capacity +package service + +import ( + "assets/consts/public" + "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + entityAsset "assets/model/entity/asset" + "context" + "fmt" + "math" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/jaeger" + "gitea.com/red-future/common/redis" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var Capacity = new(capacity) + +type capacity struct{} + +// UpdateLocationCapacity 更新库位容量(入口方法,带Redis分布式锁) +func (s *capacity) UpdateLocationCapacity(ctx context.Context, locationId *bson.ObjectID) (err error) { + // Redis分布式锁(防止并发入库/出库同一库位时重算覆盖) + lockKey := fmt.Sprintf("lock:location:%s:capacity", locationId.Hex()) + expireSeconds := int64(30) + + var zoneId *bson.ObjectID + var capacityUnitType stock.CapacityUnitType + + success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error { + // 1. 查询库位信息 + location, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: locationId}) + if err != nil { + g.Log().Errorf(ctx, "查询库位失败: %v", err) + return err + } + zoneId = location.ZoneId + capacityUnitType = location.CapacityUnitType + + // 2. 查询库位下所有库存记录 + privateStocks, _, err := dao.PrivateStock.List(ctx, &dto.ListPrivateStockReq{ + LocationId: locationId, + }) + if err != nil { + g.Log().Errorf(ctx, "查询库位库存失败: %v", err) + return err + } + + // 3. 批量查询PrivateSku(避免N+1查询问题) + skuIds := make([]*bson.ObjectID, 0, len(privateStocks)) + for _, ps := range privateStocks { + if ps.PrivateSkuID != nil && ps.AvailableQty > 0 { + skuIds = append(skuIds, ps.PrivateSkuID) + } + } + + // 批量查询PrivateSku并构建Map缓存 + skuMap := make(map[string]*entityAsset.PrivateSku) + if len(skuIds) > 0 { + var skuList []*entityAsset.PrivateSku + filter := bson.M{"_id": bson.M{"$in": skuIds}} + _, err = mongo.DB().Find(ctx, filter, &skuList, public.PrivateSkuCollection, nil, nil) + if err != nil { + g.Log().Errorf(ctx, "批量查询PrivateSku失败: %v", err) + return err + } + // 构建Map缓存 + for i := range skuList { + skuMap[skuList[i].Id.Hex()] = skuList[i] + } + } + + // 4. 整入整出计算:先按SKU聚合同库位的总数量(同SKU可合箱),再按库位单位换算 + // 聚合同一SKU的总数量,避免逐条取整导致容量虚高 + // 例:2批次各1瓶,逐条取整=2箱(错误),聚合后取整=ceil(2/20)=1箱(正确) + skuQtyMap := make(map[string]int) // key: privateSkuId.Hex(), value: 总可用数量 + for _, ps := range privateStocks { + if ps.PrivateSkuID == nil || ps.AvailableQty <= 0 { + continue + } + skuQtyMap[ps.PrivateSkuID.Hex()] += ps.AvailableQty + } + + totalCapacity := 0 + for skuIdHex, totalQty := range skuQtyMap { + // 从Map缓存中获取PrivateSku + privateSku, exists := skuMap[skuIdHex] + if !exists || privateSku == nil { + g.Log().Warningf(ctx, "PrivateSku不存在,跳过: %s", skuIdHex) + continue + } + + // 检查location和privateSku的Capacity是否为nil + if location.Capacity == nil || privateSku.Capacity.CapacityUnit == "" { + g.Log().Warningf(ctx, "库位或SKU容量信息不完整,跳过") + continue + } + + // 如果库存单位与库位单位相同,直接累加 + if privateSku.Capacity.CapacityUnit == location.Capacity.CapacityUnit { + totalCapacity += totalQty + continue + } + + // 不同单位需要换算 + conversion, err := dao.UnitConversion.GetByUnits(ctx, + location.CapacityUnitType, + privateSku.Capacity.CapacityUnit, + location.Capacity.CapacityUnit, + ) + if err != nil { + err = gerror.Newf("未找到单位换算规则 %s→%s,请在系统中添加该换算规则", + privateSku.Capacity.CapacityUnit, location.Capacity.CapacityUnit) + jaeger.RecordError(ctx, err) + return err + } + + // 检查换算系数是否为0,防止除零错误 + if conversion.ConversionFactor == 0 { + err = gerror.Newf("换算系数为0:%s→%s,请检查换算规则配置", + privateSku.Capacity.CapacityUnit, location.Capacity.CapacityUnit) + jaeger.RecordError(ctx, err) + return err + } + + // 向上取整计算(整入整出:同SKU合箱后取整,不足一箱按一箱计) + convertedQty := int(math.Ceil(float64(totalQty) / conversion.ConversionFactor)) + totalCapacity += convertedQty + + g.Log().Debugf(ctx, "单位换算: %d%s ÷ %.2f = %d%s", + totalQty, privateSku.Capacity.CapacityUnit, + conversion.ConversionFactor, convertedQty, location.Capacity.CapacityUnit) + } + + currentCapacity := totalCapacity + + // 5. 更新库位容量 + err = dao.Location.UpdateCapacity(ctx, locationId, currentCapacity) + if err != nil { + g.Log().Errorf(ctx, "更新库位容量失败: %v", err) + return err + } + + g.Log().Infof(ctx, "库位容量更新成功: locationId=%s, 当前容量=%d", + locationId.Hex(), currentCapacity) + + return nil + }) + + if !success { + return fmt.Errorf("获取库位容量锁失败: %v", err) + } + if err != nil { + return + } + + // 6. 触发向上汇总到库区(在锁外执行,避免嵌套锁时间过长) + if zoneId != nil && !zoneId.IsZero() { + if syncErr := s.SyncCapacityToZone(ctx, zoneId, capacityUnitType); syncErr != nil { + g.Log().Errorf(ctx, "同步库区容量失败: %v", syncErr) + } + } + + return +} + +// SyncCapacityToZone 同步容量到库区(带Redis分布式锁) +func (s *capacity) SyncCapacityToZone(ctx context.Context, zoneId *bson.ObjectID, unitType stock.CapacityUnitType) (err error) { + // 1. Redis分布式锁 + lockKey := fmt.Sprintf("lock:zone:%s:capacity:%s", zoneId.Hex(), unitType) + expireSeconds := int64(30) // 30秒超时 + + success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error { + // 2. 查询该库区下所有使用该单位类型的库位 + locations, err := dao.Location.ListByZoneAndUnitType(ctx, zoneId, unitType) + if err != nil { + return fmt.Errorf("查询库位列表失败: %v", err) + } + + // 3. 汇总所有库位的当前容量 + totalCapacity := 0 + maxCapacity := 0 + var capacityUnit string + for _, loc := range locations { + if loc.Capacity != nil { + totalCapacity += loc.Capacity.CurrentCapacity + maxCapacity += loc.Capacity.MaxCapacity + if capacityUnit == "" { + capacityUnit = loc.Capacity.CapacityUnit + } + } + } + + // 4. 查询库区信息(获取warehouseId) + zone, err := dao.Zone.GetOne(ctx, &dto.GetZoneReq{Id: zoneId}) + if err != nil { + return fmt.Errorf("查询库区失败: %v", err) + } + + // 5. 更新库区该单位类型的容量 + err = dao.Zone.UpdateCapacityByUnitType(ctx, zoneId, unitType, totalCapacity, maxCapacity, capacityUnit) + if err != nil { + return fmt.Errorf("更新库区容量失败: %v", err) + } + + g.Log().Infof(ctx, "库区容量同步成功: zoneId=%s, unitType=%s, 当前容量=%d", + zoneId.Hex(), unitType, totalCapacity) + + // 6. 触发向上汇总到仓库 + if zone.WarehouseId != "" { + warehouseObjId, hexErr := bson.ObjectIDFromHex(zone.WarehouseId) + if hexErr != nil { + g.Log().Errorf(ctx, "库区WarehouseId格式错误: %s, %v", zone.WarehouseId, hexErr) + } else { + if syncErr := s.SyncCapacityToWarehouse(ctx, &warehouseObjId, unitType); syncErr != nil { + g.Log().Errorf(ctx, "同步仓库容量失败: %v", syncErr) + } + } + } + + return nil + }) + + if !success { + return fmt.Errorf("获取Redis锁失败: %v", err) + } + return +} + +// SyncCapacityToWarehouse 同步容量到仓库(带Redis分布式锁) +func (s *capacity) SyncCapacityToWarehouse(ctx context.Context, warehouseId *bson.ObjectID, unitType stock.CapacityUnitType) (err error) { + // 1. Redis分布式锁 + lockKey := fmt.Sprintf("lock:warehouse:%s:capacity:%s", warehouseId.Hex(), unitType) + expireSeconds := int64(30) // 30秒超时 + + success, err := redis.Lock(ctx, lockKey, expireSeconds, func(ctx context.Context) error { + // 2. 查询该仓库下所有库区 + zones, err := dao.Zone.ListByWarehouseAndUnitType(ctx, warehouseId.Hex()) + if err != nil { + return fmt.Errorf("查询库区列表失败: %v", err) + } + + // 3. 汇总所有库区该单位类型的容量 + totalCapacity := 0 + maxCapacity := 0 + var capacityUnit string + for _, zone := range zones { + if zone.Capacity != nil { + if cap, exists := (*zone.Capacity)[unitType]; exists { + totalCapacity += cap.CurrentCapacity + maxCapacity += cap.MaxCapacity + if capacityUnit == "" { + capacityUnit = cap.CapacityUnit + } + } + } + } + + // 4. 更新仓库该单位类型的容量 + err = dao.Warehouse.UpdateCapacityByUnitType(ctx, warehouseId, unitType, totalCapacity, maxCapacity, capacityUnit) + if err != nil { + return fmt.Errorf("更新仓库容量失败: %v", err) + } + + g.Log().Infof(ctx, "仓库容量同步成功: warehouseId=%s, unitType=%s, 当前容量=%d", + warehouseId.Hex(), unitType, totalCapacity) + + return nil + }) + + if !success { + return fmt.Errorf("获取Redis锁失败: %v", err) + } + return +} + +// ConvertWithCeil 向上取整换算(用于容量计算) +func (s *capacity) ConvertWithCeil(fromQty int, conversionFactor float64) int { + return int(math.Ceil(float64(fromQty) / conversionFactor)) +} diff --git a/service/stock/inventory_count_adjust_history_service.go b/service/stock/inventory_count_adjust_history_service.go new file mode 100644 index 0000000..659b371 --- /dev/null +++ b/service/stock/inventory_count_adjust_history_service.go @@ -0,0 +1,55 @@ +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type inventoryCountAdjustHistory struct{} + +var InventoryCountAdjustHistory = new(inventoryCountAdjustHistory) + +// Create 创建盘点调整历史记录 +func (s *inventoryCountAdjustHistory) Create(ctx context.Context, req *dto.CreateInventoryCountAdjustHistoryReq) (res *dto.CreateInventoryCountAdjustHistoryRes, err error) { + ids, err := dao.InventoryCountAdjustHistory.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateInventoryCountAdjustHistoryRes{ + Id: &id, + } + return +} + +// Delete 删除盘点调整历史记录 +func (s *inventoryCountAdjustHistory) Delete(ctx context.Context, req *dto.DeleteInventoryCountAdjustHistoryReq) error { + return dao.InventoryCountAdjustHistory.DeleteFake(ctx, req) +} + +// GetOne 查询单条盘点调整历史记录详情 +func (s *inventoryCountAdjustHistory) GetOne(ctx context.Context, req *dto.GetInventoryCountAdjustHistoryReq) (res *dto.GetInventoryCountAdjustHistoryRes, err error) { + one, err := dao.InventoryCountAdjustHistory.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +// List 分页查询盘点调整历史列表 +func (s *inventoryCountAdjustHistory) List(ctx context.Context, req *dto.ListInventoryCountAdjustHistoryReq) (res *dto.ListInventoryCountAdjustHistoryRes, err error) { + list, total, err := dao.InventoryCountAdjustHistory.List(ctx, req) + if err != nil { + return + } + res = &dto.ListInventoryCountAdjustHistoryRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/inventory_count_detail_service.go b/service/stock/inventory_count_detail_service.go new file mode 100644 index 0000000..220db1f --- /dev/null +++ b/service/stock/inventory_count_detail_service.go @@ -0,0 +1,399 @@ +// 盘点明细服务 +// 职责:盘点明细CRUD、库存调整(adjustStock使用$inc原子操作)、统计更新、相似商品查询 +// 调用链:InventoryCount.Import → adjustStock → validateStockAfterAdjust → autoCompleteIfNoDifference +// 紧密耦合:dao.InventoryCountDetail、PrivateStock(调整库存)、InventoryCount(更新统计) +// 注意:AssetSkuID字段实际存储的是privateSkuId,列表查询使用批量填充避免N+1 +package service + +import ( + "assets/consts/public" + "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + "errors" + "fmt" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type inventoryCountDetail struct{} + +var InventoryCountDetail = new(inventoryCountDetail) + +func (s *inventoryCountDetail) Create(ctx context.Context, req *dto.CreateInventoryCountDetailReq) (res *dto.CreateInventoryCountDetailRes, err error) { + ids, err := dao.InventoryCountDetail.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateInventoryCountDetailRes{ + Id: &id, + } + return +} + +func (s *inventoryCountDetail) Update(ctx context.Context, req *dto.UpdateInventoryCountDetailReq) error { + return dao.InventoryCountDetail.Update(ctx, req) +} + +func (s *inventoryCountDetail) Delete(ctx context.Context, req *dto.DeleteInventoryCountDetailReq) error { + return dao.InventoryCountDetail.DeleteFake(ctx, req) +} + +func (s *inventoryCountDetail) GetOne(ctx context.Context, req *dto.GetInventoryCountDetailReq) (res *dto.GetInventoryCountDetailRes, err error) { + one, err := dao.InventoryCountDetail.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *inventoryCountDetail) List(ctx context.Context, req *dto.ListInventoryCountDetailReq) (res *dto.ListInventoryCountDetailRes, err error) { + list, total, err := dao.InventoryCountDetail.List(ctx, req) + if err != nil { + return + } + res = &dto.ListInventoryCountDetailRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + if err != nil { + return + } + + // 批量查询关联名称(避免N+1查询) + s.fillListItemNames(ctx, list, res.List) + return +} + +// fillListItemNames 批量填充列表项的关联名称 +func (s *inventoryCountDetail) fillListItemNames(ctx context.Context, details []entity.InventoryCountDetail, items []dto.InventoryCountDetailListItem) { + if len(details) == 0 { + return + } + + // 1. 收集所有需要查询的ID(去重) + assetIdSet := make(map[string]*bson.ObjectID) + skuIdSet := make(map[string]*bson.ObjectID) + warehouseIdSet := make(map[string]*bson.ObjectID) + zoneIdSet := make(map[string]*bson.ObjectID) + locationIdSet := make(map[string]*bson.ObjectID) + + for _, d := range details { + if d.AssetID != nil { + assetIdSet[d.AssetID.Hex()] = d.AssetID + } + if d.AssetSkuID != nil { + skuIdSet[d.AssetSkuID.Hex()] = d.AssetSkuID + } + if d.WarehouseID != nil { + warehouseIdSet[d.WarehouseID.Hex()] = d.WarehouseID + } + if d.ZoneID != nil { + zoneIdSet[d.ZoneID.Hex()] = d.ZoneID + } + if d.LocationID != nil { + locationIdSet[d.LocationID.Hex()] = d.LocationID + } + } + + // 2. 批量查询asset名称 + assetNameMap := make(map[string]string) + if len(assetIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(assetIdSet)) + for _, id := range assetIdSet { + ids = append(ids, id) + } + var assets []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"assetName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &assets, public.AssetCollection, nil, nil); e == nil { + for _, a := range assets { + assetNameMap[a.Id.Hex()] = a.Name + } + } + } + + // 3. 批量查询private_sku名称(AssetSkuID实际存的是privateSkuId) + skuNameMap := make(map[string]string) + if len(skuIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(skuIdSet)) + for _, id := range skuIdSet { + ids = append(ids, id) + } + var skus []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"skuName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &skus, public.PrivateSkuCollection, nil, nil); e == nil { + for _, s := range skus { + skuNameMap[s.Id.Hex()] = s.Name + } + } + } + + // 4. 批量查询warehouse名称 + warehouseNameMap := make(map[string]string) + if len(warehouseIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(warehouseIdSet)) + for _, id := range warehouseIdSet { + ids = append(ids, id) + } + var warehouses []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"warehouseName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &warehouses, public.WarehouseCollection, nil, nil); e == nil { + for _, w := range warehouses { + warehouseNameMap[w.Id.Hex()] = w.Name + } + } + } + + // 5. 批量查询zone名称 + zoneNameMap := make(map[string]string) + if len(zoneIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(zoneIdSet)) + for _, id := range zoneIdSet { + ids = append(ids, id) + } + var zones []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"zoneName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &zones, public.ZoneCollection, nil, nil); e == nil { + for _, z := range zones { + zoneNameMap[z.Id.Hex()] = z.Name + } + } + } + + // 6. 批量查询location名称 + locationNameMap := make(map[string]string) + if len(locationIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(locationIdSet)) + for _, id := range locationIdSet { + ids = append(ids, id) + } + var locations []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"locationName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &locations, public.LocationCollection, nil, nil); e == nil { + for _, l := range locations { + locationNameMap[l.Id.Hex()] = l.Name + } + } + } + + // 7. 填充名称到列表项 + for i := range items { + if details[i].AssetID != nil { + items[i].AssetName = assetNameMap[details[i].AssetID.Hex()] + } + if details[i].AssetSkuID != nil { + items[i].AssetSkuName = skuNameMap[details[i].AssetSkuID.Hex()] + } + if details[i].WarehouseID != nil { + items[i].WarehouseName = warehouseNameMap[details[i].WarehouseID.Hex()] + } + if details[i].ZoneID != nil { + items[i].ZoneName = zoneNameMap[details[i].ZoneID.Hex()] + } + if details[i].LocationID != nil { + items[i].LocationName = locationNameMap[details[i].LocationID.Hex()] + } + // 填充差异类型文本 + if details[i].DiscrepancyType != 0 { + items[i].DiscrepancyTypeText = details[i].DiscrepancyType.String() + } + } +} + +// adjustStock 原子操作调整库存(更新private_stock表,使用$inc原子加减) +func (s *inventoryCountDetail) adjustStock(ctx context.Context, detail *entity.InventoryCountDetail) (err error) { + // 使用MongoDB的$inc原子操作(无锁并发安全) + // 注意:盘点明细的AssetSkuID字段实际存储的是privateSkuId + filter := bson.M{ + "privateSkuId": detail.AssetSkuID, + "warehouseId": detail.WarehouseID, + } + if !g.IsEmpty(detail.ZoneID) { + filter["zoneId"] = detail.ZoneID + } + if !g.IsEmpty(detail.LocationID) { + filter["locationId"] = detail.LocationID + } + + update := bson.M{ + "$inc": bson.M{ + "availableQty": detail.Difference, + }, + } + + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) + return +} + +// updateCountStats 更新盘点任务统计信息 +func (s *inventoryCountDetail) updateCountStats(ctx context.Context, countId *bson.ObjectID) (err error) { + details, err := dao.InventoryCountDetail.ListByCountId(ctx, countId) + if err != nil { + return + } + + totalItems := len(details) + completedItems := 0 + discrepancyItems := 0 + + for _, detail := range details { + if detail.IsAdjusted { + completedItems++ + } + if detail.Difference != 0 { + discrepancyItems++ + } + } + + err = dao.InventoryCount.UpdateStats(ctx, countId, totalItems, completedItems, discrepancyItems) + return +} + +// validateStockAfterAdjust 验证调整后库存不能为负数(查询private_stock表) +func (s *inventoryCountDetail) validateStockAfterAdjust(ctx context.Context, detail *entity.InventoryCountDetail) (err error) { + // 查询当前库存(注意:盘点明细的AssetSkuID字段实际存储的是privateSkuId) + filter := bson.M{ + "privateSkuId": detail.AssetSkuID, + "warehouseId": detail.WarehouseID, + } + if !g.IsEmpty(detail.ZoneID) { + filter["zoneId"] = detail.ZoneID + } + if !g.IsEmpty(detail.LocationID) { + filter["locationId"] = detail.LocationID + } + + // 聚合同SKU同位置的所有批次总可用数量 + var stocks []struct { + AvailableQty int `bson:"availableQty" json:"availableQty"` + } + _, err = mongo.DB().Find(ctx, filter, &stocks, public.PrivateStockCollection, nil, nil) + if err != nil { + if detail.Difference < 0 { + err = errors.New("当前库存不存在,无法执行减少操作") + return + } + return + } + + totalQty := 0 + for _, s := range stocks { + totalQty += s.AvailableQty + } + + afterQty := totalQty + detail.Difference + if afterQty < 0 { + err = fmt.Errorf("调整后库存为负数(当前库存%d,差异%d,结果%d),不允许调整", totalQty, detail.Difference, afterQty) + } + return +} + +// autoCompleteIfNoDifference 所有明细都已调整(含无差异自动调整)时,自动完成盘点 +func (s *inventoryCountDetail) autoCompleteIfNoDifference(ctx context.Context, countId *bson.ObjectID) (err error) { + details, err := dao.InventoryCountDetail.ListByCountId(ctx, countId) + if err != nil { + return + } + + for _, detail := range details { + // 只要有一条未调整(含未盘点),就不自动完成 + if !detail.IsAdjusted { + return + } + } + + err = dao.InventoryCount.UpdateStatus(ctx, countId, stock.InventoryCountStatusCompleted) + return +} + +// SearchSimilarAssets 查询相似商品(单字模糊匹配) +// 用于库存不存在时提示用户可能的相似商品 +// 流程:先查private_sku模糊匹配skuName → $in查private_stock获取库存 → 关联填充名称 +func (s *inventoryCountDetail) SearchSimilarAssets(ctx context.Context, req *dto.SearchSimilarAssetsReq) (res *dto.SearchSimilarAssetsRes, err error) { + // 1. 单字分词:将关键词拆分为单个字符 + keywords := []string{} + for _, char := range req.Keyword { + keywords = append(keywords, string(char)) + } + + // 2. 查private_sku表模糊匹配skuName + orConditions := []bson.M{} + for _, keyword := range keywords { + orConditions = append(orConditions, bson.M{"skuName": bson.M{"$regex": keyword, "$options": "i"}}) + } + var matchedSkus []struct { + ID *bson.ObjectID `bson:"_id"` + SkuName string `bson:"skuName"` + } + _, err = mongo.DB().Find(ctx, bson.M{"$or": orConditions}, &matchedSkus, public.PrivateSkuCollection, nil, nil) + if err != nil { + return + } + if len(matchedSkus) == 0 { + res = &dto.SearchSimilarAssetsRes{List: []dto.SimilarAssetItem{}} + return + } + + // 3. 构建skuId列表和名称Map + skuIds := make([]*bson.ObjectID, 0, len(matchedSkus)) + skuNameMap := make(map[string]string, len(matchedSkus)) + for _, sku := range matchedSkus { + skuIds = append(skuIds, sku.ID) + skuNameMap[sku.ID.Hex()] = sku.SkuName + } + + // 4. $in查private_stock表获取库存 + stockFilter := bson.M{ + "privateSkuId": bson.M{"$in": skuIds}, + "availableQty": bson.M{"$gt": 0}, + } + if !g.IsEmpty(req.WarehouseID) { + stockFilter["warehouseId"] = req.WarehouseID + } + var stocks []struct { + PrivateSkuID *bson.ObjectID `bson:"privateSkuId"` + AvailableQty int `bson:"availableQty"` + WarehouseID *bson.ObjectID `bson:"warehouseId"` + WarehouseName string `bson:"warehouseName"` + } + _, err = mongo.DB().Find(ctx, stockFilter, &stocks, public.PrivateStockCollection, nil, nil) + if err != nil { + return + } + + // 5. 转换为响应结构 + var list []dto.SimilarAssetItem + for _, s := range stocks { + skuName := "" + if s.PrivateSkuID != nil { + skuName = skuNameMap[s.PrivateSkuID.Hex()] + } + list = append(list, dto.SimilarAssetItem{ + AssetSkuID: s.PrivateSkuID, + AssetSkuName: skuName, + AvailableQty: s.AvailableQty, + WarehouseID: s.WarehouseID, + WarehouseName: s.WarehouseName, + }) + } + + res = &dto.SearchSimilarAssetsRes{List: list} + return +} diff --git a/service/stock/inventory_count_service.go b/service/stock/inventory_count_service.go new file mode 100644 index 0000000..10824fb --- /dev/null +++ b/service/stock/inventory_count_service.go @@ -0,0 +1,1025 @@ +// 盘点任务服务 +// 职责:盘点任务CRUD、完成/取消、导出模板、导入Excel、创建盘点明细 +// 调用链:Create → createInventoryDetails(快照) → ExportTemplate → ImportInventoryCount → adjustStock +// 紧密耦合:dao.InventoryCount、InventoryCountDetail(明细)、PrivateStock(库存快照)、Capacity(容量更新) +// 注意:同一时间只允许一个盘点任务,Excel导入使用缓存避免N+1查询 +package service + +import ( + "assets/consts/public" + "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "bytes" + "context" + "errors" + "fmt" + "time" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "github.com/xuri/excelize/v2" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type inventoryCount struct{} + +var InventoryCount = new(inventoryCount) + +func (s *inventoryCount) Create(ctx context.Context, req *dto.CreateInventoryCountReq) (res *dto.CreateInventoryCountRes, err error) { + // 检查是否存在未完成的盘点任务(同一时间只允许一个盘点任务) + has, err := dao.InventoryCount.HasUncompletedTask(ctx) + if err != nil { + return nil, err + } + if has { + return nil, errors.New("存在未完成的盘点任务,请先完成或取消后再创建新任务") + } + + // 生成盘点单号 + countNo, err := utils.IncrSequence(ctx, public.StockInventoryNoKeyPrefix, 6, "-") + if err != nil { + return nil, fmt.Errorf("生成盘点单号失败: %v", err) + } + + // 创建盘点任务主表 + ids, err := dao.InventoryCount.Insert(ctx, req, countNo) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + + // 根据盘点范围查询库存并创建明细记录(记录账面数量快照) + err = s.createInventoryDetails(ctx, &id, req) + if err != nil { + // 回滚:物理删除已插入的主记录,避免残留数据阻塞后续创建 + _, _ = mongo.DB().Delete(ctx, bson.M{"_id": id}, public.InventoryCountCollection) + return nil, fmt.Errorf("创建盘点明细失败: %v", err) + } + + res = &dto.CreateInventoryCountRes{ + Id: &id, + CountNo: countNo, + } + return +} + +func (s *inventoryCount) Update(ctx context.Context, req *dto.UpdateInventoryCountReq) error { + return dao.InventoryCount.Update(ctx, req) +} + +func (s *inventoryCount) Delete(ctx context.Context, req *dto.DeleteInventoryCountReq) error { + // 先删除关联的盘点明细 + if err := dao.InventoryCountDetail.DeleteByCountId(ctx, req.Id); err != nil { + return fmt.Errorf("删除盘点明细失败: %v", err) + } + // 再删除盘点任务本身 + return dao.InventoryCount.Delete(ctx, req) +} + +func (s *inventoryCount) GetOne(ctx context.Context, req *dto.GetInventoryCountReq) (res *dto.GetInventoryCountRes, err error) { + one, err := dao.InventoryCount.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *inventoryCount) List(ctx context.Context, req *dto.ListInventoryCountReq) (res *dto.ListInventoryCountRes, err error) { + list, total, err := dao.InventoryCount.List(ctx, req) + if err != nil { + return + } + res = &dto.ListInventoryCountRes{ + Total: total, + } + // 手动构建DTO列表(Entity和DTO字段不完全匹配) + res.List = make([]dto.InventoryCountListItem, len(list)) + for i, item := range list { + res.List[i] = dto.InventoryCountListItem{ + Id: item.Id, + CountNo: item.CountNo, + Title: item.Title, + WarehouseIDs: item.WarehouseIDs, + ZoneIDs: item.ZoneIDs, + CountType: item.CountType, + CountTypeText: item.CountType.String(), + Scope: item.Scope, + ScopeText: item.Scope.String(), + Status: item.Status, + StatusText: item.Status.String(), + Progress: item.Progress, + AssigneeID: item.AssigneeID, + AssigneeName: item.AssigneeName, + ActualStartTime: item.ActualStartTime, + ActualEndTime: item.ActualEndTime, + TotalItems: item.TotalItems, + CompletedItems: item.CompletedItems, + DiscrepancyItems: item.DiscrepancyItems, + CreatedAt: s.timeToGtime(item.CreatedAt), + UpdatedAt: s.timeToGtime(item.UpdatedAt), + } + } + // 批量填充关联名称 + s.fillListItemNames(ctx, list, res.List) + return +} + +// timeToGtime 将time.Time转换为gtime.Time +func (s *inventoryCount) timeToGtime(t *time.Time) *gtime.Time { + if t == nil { + return nil + } + return gtime.New(t) +} + +// Complete 完成盘点 +func (s *inventoryCount) Complete(ctx context.Context, req *dto.CompleteInventoryCountReq) (res *dto.CompleteInventoryCountRes, err error) { + // 查询盘点任务详情,验证任务是否存在 + count, err := dao.InventoryCount.GetOne(ctx, &dto.GetInventoryCountReq{Id: req.Id}) + if err != nil { + return nil, err + } + if count.Status != stock.InventoryCountStatusInProgress { + return nil, fmt.Errorf("盘点任务当前状态为[%s],只有进行中的任务才能完成", count.Status.String()) + } + + // 查询所有盘点明细记录 + details, err := dao.InventoryCountDetail.ListByCountId(ctx, req.Id) + if err != nil { + return nil, err + } + + // 统计存在差异且未调整的明细数量 + discrepancyCount := 0 + for _, detail := range details { + if detail.Difference != 0 && !detail.IsAdjusted { + discrepancyCount++ + } + } + + // 有未调整的差异项,不允许完成 + if discrepancyCount > 0 { + return nil, fmt.Errorf("存在%d个未调整的盘点差异,请先调整后再完成", discrepancyCount) + } + + // 更新状态为已完成 + err = dao.InventoryCount.UpdateStatus(ctx, req.Id, stock.InventoryCountStatusCompleted) + if err != nil { + return nil, err + } + + return &dto.CompleteInventoryCountRes{Id: req.Id}, nil +} + +// Cancel 取消盘点 +func (s *inventoryCount) Cancel(ctx context.Context, req *dto.CancelInventoryCountReq) (res *dto.CancelInventoryCountRes, err error) { + // 查询盘点任务,校验状态 + count, err := dao.InventoryCount.GetOne(ctx, &dto.GetInventoryCountReq{Id: req.Id}) + if err != nil { + return nil, err + } + if count.Status != stock.InventoryCountStatusInProgress { + return nil, fmt.Errorf("盘点任务当前状态为[%s],只有进行中的任务才能取消", count.Status.String()) + } + + // 回滚已调整的库存(反向$inc) + details, err := dao.InventoryCountDetail.ListByCountId(ctx, req.Id) + if err != nil { + return nil, fmt.Errorf("查询盘点明细失败: %v", err) + } + rollbackCount := 0 + for _, detail := range details { + if detail.IsAdjusted && detail.Difference != 0 { + // 反向调整:差异取反 + rollbackDetail := detail + rollbackDetail.Difference = -detail.Difference + if e := InventoryCountDetail.adjustStock(ctx, &rollbackDetail); e != nil { + g.Log().Errorf(ctx, "回滚库存失败[SKU=%s]: %v", detail.AssetSkuID.Hex(), e) + continue + } + rollbackCount++ + // 触发库位容量重算 + if !g.IsEmpty(detail.LocationID) { + if capErr := Capacity.UpdateLocationCapacity(ctx, detail.LocationID); capErr != nil { + g.Log().Warningf(ctx, "回滚后更新库位容量失败: %v", capErr) + } + } + } + } + if rollbackCount > 0 { + g.Log().Infof(ctx, "取消盘点[%s]已回滚%d条库存调整", req.Id.Hex(), rollbackCount) + } + + // 更新状态为已取消 + err = dao.InventoryCount.UpdateStatus(ctx, req.Id, stock.InventoryCountStatusCancelled) + if err != nil { + return nil, err + } + + return &dto.CancelInventoryCountRes{Id: req.Id}, nil +} + +// ExportTemplate 导出盘点模板 +func (s *inventoryCount) ExportTemplate(ctx context.Context, req *dto.ExportInventoryCountTemplateReq) (res *dto.ExportInventoryCountTemplateRes, err error) { + // 1. 查询盘点任务 + count, err := dao.InventoryCount.GetOne(ctx, &dto.GetInventoryCountReq{Id: req.Id}) + if err != nil { + return nil, err + } + + // 2. 根据盘点类型生成Excel模板 + fileData, fileName, err := s.generateExcelTemplate(ctx, count) + if err != nil { + return nil, fmt.Errorf("生成Excel模板失败: %v", err) + } + + return &dto.ExportInventoryCountTemplateRes{ + FileName: fileName, + FileData: fileData, + }, nil +} + +// generateExcelTemplate 生成Excel模板 +func (s *inventoryCount) generateExcelTemplate(ctx context.Context, count *entity.InventoryCount) (fileData []byte, fileName string, err error) { + // 创建Excel文件 + f := excelize.NewFile() + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = closeErr + } + }() + + // 设置工作表名称 + sheetName := "盘点模板" + f.SetSheetName("Sheet1", sheetName) + + // 根据盘点类型设置不同表头 + // 明盘:有账面数量列,可以看到历史库存 + // 盲盘:没有账面数量列,只填实盘数量 + var headers []string + if count.CountType == stock.OpenPrice { + headers = []string{"资产名称", "SKU名称", "仓库", "库区", "库位", "账面数量", "实盘数量", "备注"} + } else { + headers = []string{"资产名称", "SKU名称", "仓库", "库区", "库位", "实盘数量", "备注"} + } + for i, header := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheetName, cell, header) + } + + // 设置样式 + lastCol, _ := excelize.CoordinatesToCellName(len(headers), 1) + headerStyle, err := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#E6E6FA"}, + Pattern: 1, + }, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + }) + if err != nil { + return nil, "", err + } + + f.SetCellStyle(sheetName, "A1", lastCol, headerStyle) + + // 明盘:填充数据行(含账面数量列) + // 盲盘:填充数据行(无账面数量列,工人看不到系统库存) + err = s.fillTemplateData(ctx, f, sheetName, count) + if err != nil { + return nil, "", err + } + + // 生成文件名 + fileName = fmt.Sprintf("盘点模板_%s_%s.xlsx", count.CountNo, count.CountType.String()) + + // 保存到缓冲区 + var buf bytes.Buffer + err = f.Write(&buf) + if err != nil { + return nil, "", err + } + + return buf.Bytes(), fileName, nil +} + +// fillTemplateData 填充模板数据(明盘包含账面数量,盲盘不包含) +func (s *inventoryCount) fillTemplateData(ctx context.Context, f *excelize.File, sheetName string, count *entity.InventoryCount) error { + // 查询盘点明细(包含关联名称信息) + req := &dto.ListInventoryCountDetailReq{ + CountID: count.Id.Hex(), + } + listRes, err := InventoryCountDetail.List(ctx, req) + if err != nil { + return err + } + + // 填充数据 + for i, detail := range listRes.List { + row := i + 2 // 从第2行开始 + f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), detail.AssetName) + f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), detail.AssetSkuName) + f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), detail.WarehouseName) + f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), detail.ZoneName) + f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), detail.LocationName) + if count.CountType == stock.OpenPrice { + // 明盘:F=账面数量, G=实盘数量(空), H=备注 + f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), detail.BookQuantity) + f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), "") + f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), "") + } else { + // 盲盘:F=实盘数量(空), G=备注 + f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "") + f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), "") + } + } + + return nil +} + +// ImportInventoryCount 上传盘点Excel(一步完成:解析→更新明细→$inc调整库存→自动完成) +func (s *inventoryCount) ImportInventoryCount(ctx context.Context, req *dto.ImportInventoryCountReq, fileData []byte) (res *dto.ImportInventoryCountRes, err error) { + // 1. 查询盘点任务详情并校验状态 + count, err := dao.InventoryCount.GetOne(ctx, &dto.GetInventoryCountReq{Id: req.Id}) + if err != nil { + err = fmt.Errorf("查询盘点任务失败: %v", err) + return + } + if count.Status != stock.InventoryCountStatusInProgress { + err = fmt.Errorf("盘点任务当前状态为[%s],只有进行中的任务才能导入Excel", count.Status.String()) + return + } + + // 2. 解析Excel文件,匹配已创建的detail记录并计算差异 + details, err := s.parseExcelFile(ctx, fileData, count) + if err != nil { + err = fmt.Errorf("解析Excel失败: %v", err) + return + } + + if ctx.Err() != nil { + err = fmt.Errorf("客户端已断开连接: %v", ctx.Err()) + return + } + + // 3. 验证所有负差异项调整后库存不为负 + for _, detail := range details { + if detail.Difference < 0 { + if e := InventoryCountDetail.validateStockAfterAdjust(ctx, detail); e != nil { + err = fmt.Errorf("SKU库存校验失败: %v", e) + return + } + } + } + + // 4. 批量更新明细的实盘数据(不标记完成,先记录实盘数量和差异) + now := gtime.Now() + countBy := "" + if user, e := utils.GetUserInfo(ctx); e == nil { + countBy = fmt.Sprintf("%v", user.UserName) + } + + dataFilters := make([]bson.M, 0, len(details)) + dataUpdates := make([]bson.M, 0, len(details)) + for _, detail := range details { + var discrepancyType stock.DiscrepancyType + if detail.Difference > 0 { + discrepancyType = stock.DiscrepancyTypeOverflow + } else if detail.Difference < 0 { + discrepancyType = stock.DiscrepancyTypeShortage + } else { + discrepancyType = stock.DiscrepancyTypeNone + } + var differenceRate float64 + if detail.BookQuantity > 0 { + differenceRate = float64(detail.Difference) / float64(detail.BookQuantity) * 100 + } + + dataFilters = append(dataFilters, bson.M{"_id": detail.Id}) + dataUpdates = append(dataUpdates, bson.M{ + "actualQuantity": detail.ActualQuantity, + "difference": detail.Difference, + "differenceRate": differenceRate, + "discrepancyType": discrepancyType, + "discrepancyReason": detail.DiscrepancyReason, + "countAt": now, + "countBy": countBy, + }) + } + + if _, err = mongo.DB().SaveOrUpdate(ctx, dataFilters, dataUpdates, public.InventoryCountDetailCollection); err != nil { + return nil, fmt.Errorf("批量更新盘点明细失败: %v", err) + } + + // 5. 逐条处理:调整库存 + 成功后标记完成 + successCount := 0 + failCount := 0 + completedFilters := make([]bson.M, 0, len(details)) + completedUpdates := make([]bson.M, 0, len(details)) + + for _, detail := range details { + // 无差异项直接标记完成(无需调整库存) + if detail.Difference == 0 { + completedFilters = append(completedFilters, bson.M{"_id": detail.Id}) + completedUpdates = append(completedUpdates, bson.M{ + "status": stock.InventoryDetailStatusCompleted, "isAdjusted": true, "adjustedAt": now, "adjustedBy": countBy, + }) + successCount++ + continue + } + + // 有差异项:先调整库存,成功后才标记完成 + if e := InventoryCountDetail.adjustStock(ctx, detail); e != nil { + g.Log().Warningf(ctx, "调整库存失败[SKU=%s]: %v", detail.AssetSkuID.Hex(), e) + failCount++ + continue + } + completedFilters = append(completedFilters, bson.M{"_id": detail.Id}) + completedUpdates = append(completedUpdates, bson.M{ + "status": stock.InventoryDetailStatusCompleted, "isAdjusted": true, "adjustedAt": now, "adjustedBy": countBy, + }) + successCount++ + + // 库存变动后触发库位容量重算 + if !g.IsEmpty(detail.LocationID) { + if capErr := Capacity.UpdateLocationCapacity(ctx, detail.LocationID); capErr != nil { + g.Log().Warningf(ctx, "更新库位容量失败: %v", capErr) + } + } + } + + // 批量标记成功项为已完成 + if len(completedFilters) > 0 { + if _, e := mongo.DB().SaveOrUpdate(ctx, completedFilters, completedUpdates, public.InventoryCountDetailCollection); e != nil { + g.Log().Warningf(ctx, "批量标记完成状态失败: %v", e) + } + } + + g.Log().Infof(ctx, "盘点Excel导入完成: 成功=%d, 失败=%d", successCount, failCount) + + // 6. 更新盘点任务统计 + if e := InventoryCountDetail.updateCountStats(ctx, req.Id); e != nil { + g.Log().Warningf(ctx, "更新统计失败: %v", e) + } + + // 7. 所有明细已调整,自动完成盘点 + if e := InventoryCountDetail.autoCompleteIfNoDifference(ctx, req.Id); e != nil { + g.Log().Warningf(ctx, "自动完成检查失败: %v", e) + } + + // 8. 返回结果 + res = &dto.ImportInventoryCountRes{ + SuccessCount: successCount, + FailCount: failCount, + } + return +} + +// excelImportCache Excel导入缓存(预加载基础数据,避免逐行查询数据库) +// 性能优化:1000行Excel从4000次查询降至4次批量查询 +type excelImportCache struct { + SkuMap map[string]*skuCacheItem // skuName -> {privateSkuId} + WarehouseMap map[string]*bson.ObjectID // warehouseName -> id + ZoneMap map[string]*bson.ObjectID // warehouseId_zoneName -> id + LocationMap map[string]*bson.ObjectID // zoneId_locationName -> id +} + +type skuCacheItem struct { + ID *bson.ObjectID +} + +// buildImportCache 从已有明细收集ID,按需加载基础数据到内存缓存(避免全量加载OOM) +func (s *inventoryCount) buildImportCache(ctx context.Context, existingDetails []entity.InventoryCountDetail) (cache *excelImportCache, err error) { + cache = &excelImportCache{ + SkuMap: make(map[string]*skuCacheItem), + WarehouseMap: make(map[string]*bson.ObjectID), + ZoneMap: make(map[string]*bson.ObjectID), + LocationMap: make(map[string]*bson.ObjectID), + } + + // 从已有明细中收集去重ID + skuIdSet := make(map[string]*bson.ObjectID) + warehouseIdSet := make(map[string]*bson.ObjectID) + zoneIdSet := make(map[string]*bson.ObjectID) + locationIdSet := make(map[string]*bson.ObjectID) + for _, d := range existingDetails { + if d.AssetSkuID != nil { + skuIdSet[d.AssetSkuID.Hex()] = d.AssetSkuID + } + if d.WarehouseID != nil { + warehouseIdSet[d.WarehouseID.Hex()] = d.WarehouseID + } + if d.ZoneID != nil { + zoneIdSet[d.ZoneID.Hex()] = d.ZoneID + } + if d.LocationID != nil { + locationIdSet[d.LocationID.Hex()] = d.LocationID + } + } + + // 1. $in查询盘点范围内的PrivateSku + if len(skuIdSet) > 0 { + skuIds := make([]*bson.ObjectID, 0, len(skuIdSet)) + for _, id := range skuIdSet { + skuIds = append(skuIds, id) + } + var skus []struct { + ID bson.ObjectID `bson:"_id"` + SkuName string `bson:"skuName"` + } + _, err = mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": skuIds}}, &skus, public.PrivateSkuCollection, nil, nil) + if err != nil { + return nil, fmt.Errorf("预加载PrivateSku失败: %v", err) + } + for i := range skus { + cache.SkuMap[skus[i].SkuName] = &skuCacheItem{ID: &skus[i].ID} + } + } + + // 2. $in查询盘点范围内的仓库 + if len(warehouseIdSet) > 0 { + whIds := make([]*bson.ObjectID, 0, len(warehouseIdSet)) + for _, id := range warehouseIdSet { + whIds = append(whIds, id) + } + var warehouses []struct { + ID bson.ObjectID `bson:"_id"` + WarehouseName string `bson:"warehouseName"` + } + _, err = mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": whIds}}, &warehouses, public.WarehouseCollection, nil, nil) + if err != nil { + return nil, fmt.Errorf("预加载仓库失败: %v", err) + } + for i := range warehouses { + cache.WarehouseMap[warehouses[i].WarehouseName] = &warehouses[i].ID + } + } + + // 3. $in查询盘点范围内的库区 + if len(zoneIdSet) > 0 { + zIds := make([]*bson.ObjectID, 0, len(zoneIdSet)) + for _, id := range zoneIdSet { + zIds = append(zIds, id) + } + var zones []struct { + ID bson.ObjectID `bson:"_id"` + ZoneName string `bson:"zoneName"` + WarehouseID bson.ObjectID `bson:"warehouseId"` + } + _, err = mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": zIds}}, &zones, public.ZoneCollection, nil, nil) + if err != nil { + return nil, fmt.Errorf("预加载库区失败: %v", err) + } + for i := range zones { + key := zones[i].WarehouseID.Hex() + "_" + zones[i].ZoneName + cache.ZoneMap[key] = &zones[i].ID + } + } + + // 4. $in查询盘点范围内的库位 + if len(locationIdSet) > 0 { + locIds := make([]*bson.ObjectID, 0, len(locationIdSet)) + for _, id := range locationIdSet { + locIds = append(locIds, id) + } + var locations []struct { + ID bson.ObjectID `bson:"_id"` + LocationName string `bson:"locationName"` + ZoneID bson.ObjectID `bson:"zoneId"` + } + _, err = mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": locIds}}, &locations, public.LocationCollection, nil, nil) + if err != nil { + return nil, fmt.Errorf("预加载库位失败: %v", err) + } + for i := range locations { + key := locations[i].ZoneID.Hex() + "_" + locations[i].LocationName + cache.LocationMap[key] = &locations[i].ID + } + } + + g.Log().Infof(ctx, "Excel导入缓存预加载完成: SKU=%d, 仓库=%d, 库区=%d, 库位=%d", + len(cache.SkuMap), len(cache.WarehouseMap), len(cache.ZoneMap), len(cache.LocationMap)) + + return +} + +// parseExcelFile 解析Excel文件并更新盘点明细(匹配已有detail记录) +// 新逻辑:从已创建的detail记录中匹配,更新实盘数量和差异 +func (s *inventoryCount) parseExcelFile(ctx context.Context, fileData []byte, count *entity.InventoryCount) (details []*entity.InventoryCountDetail, err error) { + // 打开Excel文件 + f, err := excelize.OpenReader(bytes.NewReader(fileData)) + if err != nil { + err = fmt.Errorf("打开Excel文件失败: %v", err) + return + } + defer f.Close() + + // 读取第一个工作表的所有行 + sheetName := f.GetSheetName(0) + rows, err := f.GetRows(sheetName) + if err != nil { + err = fmt.Errorf("读取Excel行失败: %v", err) + return + } + + if len(rows) < 2 { + err = errors.New("Excel文件无数据行") + return + } + + // 预加载已创建的detail记录,用于匹配Excel中的数据 + existingDetails, err := dao.InventoryCountDetail.ListByCountId(ctx, count.Id) + if err != nil { + return nil, fmt.Errorf("查询盘点明细失败: %v", err) + } + if len(existingDetails) == 0 { + return nil, errors.New("盘点任务无明细记录,请检查任务创建流程") + } + + // 预加载SKU/仓库/库区/库位映射缓存(仅加载盘点范围内数据,避免OOM) + cache, err := s.buildImportCache(ctx, existingDetails) + if err != nil { + return + } + + // 构建detail Map缓存,key=skuId_warehouseId_zoneId_locationId,O(1)查找替代O(n²)嵌套循环 + detailMap := make(map[string]*entity.InventoryCountDetail, len(existingDetails)) + for j := range existingDetails { + d := &existingDetails[j] + key := buildDetailKey(d.AssetSkuID, d.WarehouseID, d.ZoneID, d.LocationID) + detailMap[key] = d + } + + // 根据盘点类型确定列索引 + // 明盘:资产名称(0) SKU名称(1) 仓库(2) 库区(3) 库位(4) 账面数量(5) 实盘数量(6) 备注(7) + // 盲盘:资产名称(0) SKU名称(1) 仓库(2) 库区(3) 库位(4) 实盘数量(5) 备注(6) + actualQtyCol := 6 // 明盘:实盘数量在第7列 + remarkCol := 7 // 明盘:备注在第8列 + minCols := 7 // 明盘:至少7列 + if count.CountType == stock.HiddenPrice { + actualQtyCol = 5 // 盲盘:实盘数量在第6列 + remarkCol = 6 // 盲盘:备注在第7列 + minCols = 6 // 盲盘:至少6列 + } + + // 逐行解析Excel数据(从第2行开始,第1行是表头) + for i := 1; i < len(rows); i++ { + row := rows[i] + if len(row) < minCols { + continue + } + + // 读取Excel列:SKU名称、仓库、库区、库位 + assetSkuName := row[1] + warehouseName := row[2] + zoneName := "" + if len(row) > 3 { + zoneName = row[3] + } + locationName := "" + if len(row) > 4 { + locationName = row[4] + } + + // 从预加载缓存中查询SKU ID(O(1)查找,避免数据库查询) + skuItem, ok := cache.SkuMap[assetSkuName] + if !ok { + err = fmt.Errorf("第%d行:未找到SKU[%s]", i+1, assetSkuName) + return + } + assetSkuID := skuItem.ID + + // 从缓存查询仓库ID + warehouseID, ok := cache.WarehouseMap[warehouseName] + if !ok { + err = fmt.Errorf("第%d行:未找到仓库[%s]", i+1, warehouseName) + return + } + + // 从缓存查询库区ID(可选字段) + var zoneID *bson.ObjectID + if zoneName != "" { + zoneKey := warehouseID.Hex() + "_" + zoneName + zoneID, ok = cache.ZoneMap[zoneKey] + if !ok { + err = fmt.Errorf("第%d行:未找到库区[%s]", i+1, zoneName) + return + } + } + + // 从缓存查询库位ID(可选字段) + var locationID *bson.ObjectID + if locationName != "" && zoneID != nil { + locationKey := zoneID.Hex() + "_" + locationName + locationID, ok = cache.LocationMap[locationKey] + if !ok { + err = fmt.Errorf("第%d行:未找到库位[%s]", i+1, locationName) + return + } + } + + // 从detailMap中O(1)查找匹配项(替代O(n²)嵌套循环) + detailKey := buildDetailKey(assetSkuID, warehouseID, zoneID, locationID) + matchedDetail := detailMap[detailKey] + + if matchedDetail == nil { + err = fmt.Errorf("第%d行:未找到对应的盘点明细记录[SKU=%s,仓库=%s]", i+1, assetSkuName, warehouseName) + return + } + + // 读取并验证实盘数量 + actualQty := gconv.Int(row[actualQtyCol]) + if actualQty < 0 { + err = fmt.Errorf("第%d行:实盘数量不能为负数", i+1) + return + } + + // 使用创建任务时记录的账面数量计算差异(而非Excel中的账面数量) + difference := actualQty - matchedDetail.BookQuantity + + // 读取备注 + remark := "" + if len(row) > remarkCol { + remark = row[remarkCol] + } + // 明盘+负差异时必须有原因 + if count.CountType == stock.OpenPrice && difference < 0 && remark == "" { + err = fmt.Errorf("第%d行:明盘且实物少于账面时,必须填写备注", i+1) + return + } + + // 更新匹配的detail记录(实盘数量、差异、差异原因) + matchedDetail.ActualQuantity = actualQty + matchedDetail.Difference = difference + matchedDetail.DiscrepancyReason = remark + details = append(details, matchedDetail) + } + + return +} + +// createInventoryDetails 创建盘点明细,根据盘点范围查询private_stock库存并批量生成detail记录,锁定账面数量快照 +func (s *inventoryCount) createInventoryDetails(ctx context.Context, countId *bson.ObjectID, req *dto.CreateInventoryCountReq) error { + // 根据盘点范围查询private_stock库存 + stocks, err := s.queryStocksByScope(ctx, req) + if err != nil { + return err + } + + if len(stocks) == 0 { + return errors.New("查询范围内无库存数据,请检查盘点范围设置") + } + + // 收集所有privateSkuID,准备批量查询 + skuIDs := make([]*bson.ObjectID, 0, len(stocks)) + for _, stk := range stocks { + skuIDs = append(skuIDs, stk.PrivateSkuID) + } + + // 批量查询private_sku表获取assetId(skuIDs是privateSkuId,查PrivateSkuCollection) + var skuList []struct { + ID *bson.ObjectID `bson:"_id"` + AssetID *bson.ObjectID `bson:"assetId"` + } + filter := bson.M{"_id": bson.M{"$in": skuIDs}} + _, err = mongo.DB().Find(ctx, filter, &skuList, public.PrivateSkuCollection, nil, nil) + if err != nil { + return fmt.Errorf("批量查询PrivateSku失败: %v", err) + } + + // 构建Map缓存,用于O(1)查找privateSkuID对应的assetID + skuMap := make(map[string]*bson.ObjectID, len(skuList)) + for i := range skuList { + skuMap[skuList[i].ID.Hex()] = skuList[i].AssetID + } + + // 遍历库存快照,构建盘点明细记录 + details := make([]*entity.InventoryCountDetail, 0, len(stocks)) + for _, stk := range stocks { + assetID, ok := skuMap[stk.PrivateSkuID.Hex()] + if !ok { + g.Log().Warningf(ctx, "未找到私域SKU[%s]的assetId,跳过", stk.PrivateSkuID.Hex()) + continue + } + + detail := &entity.InventoryCountDetail{ + CountID: countId, + AssetID: assetID, + AssetSkuID: stk.PrivateSkuID, + WarehouseID: stk.WarehouseID, + ZoneID: stk.ZoneID, + LocationID: stk.LocationID, + BookQuantity: stk.AvailableQty, + ActualQuantity: 0, + Difference: 0, + Status: stock.InventoryDetailStatusPending, + } + details = append(details, detail) + } + + // 批量插入盘点明细记录 + _, err = dao.InventoryCountDetail.InsertBatch(ctx, details) + if err != nil { + return fmt.Errorf("批量插入盘点明细失败: %v", err) + } + + g.Log().Infof(ctx, "盘点任务[%s]创建明细成功,共%d条", countId.Hex(), len(details)) + return nil +} + +// queryStocksByScope 根据仓库/库区/库位/SKU范围过滤条件查询private_stock表,返回库存快照列表 +func (s *inventoryCount) queryStocksByScope(ctx context.Context, req *dto.CreateInventoryCountReq) (stocks []*stockSnapshot, err error) { + filter := bson.M{} + + // 必填:仓库ID列表($in批量查询) + warehouseIDs, err := hexSliceToObjectIDs(req.WarehouseIDs) + if err != nil { + return nil, fmt.Errorf("仓库ID格式错误: %v", err) + } + if len(warehouseIDs) > 0 { + filter["warehouseId"] = bson.M{"$in": warehouseIDs} + } + + // 可选:库区ID列表 + if len(req.ZoneIDs) > 0 { + zoneIDs, e := hexSliceToObjectIDs(req.ZoneIDs) + if e != nil { + return nil, fmt.Errorf("库区ID格式错误: %v", e) + } + if len(zoneIDs) > 0 { + filter["zoneId"] = bson.M{"$in": zoneIDs} + } + } + + // 可选:库位ID列表 + if len(req.LocationIDs) > 0 { + locationIDs, e := hexSliceToObjectIDs(req.LocationIDs) + if e != nil { + return nil, fmt.Errorf("库位ID格式错误: %v", e) + } + if len(locationIDs) > 0 { + filter["locationId"] = bson.M{"$in": locationIDs} + } + } + + // 可选:资产SKU ID列表 + if len(req.AssetSkuIDs) > 0 { + assetSkuIDs, e := hexSliceToObjectIDs(req.AssetSkuIDs) + if e != nil { + return nil, fmt.Errorf("资产SKU ID格式错误: %v", e) + } + if len(assetSkuIDs) > 0 { + filter["privateSkuId"] = bson.M{"$in": assetSkuIDs} + } + } + + // 查询private_stock表,获取库存快照 + _, err = mongo.DB().Find(ctx, filter, &stocks, public.PrivateStockCollection, nil, nil) + if err != nil { + return nil, fmt.Errorf("查询库存失败: %v", err) + } + + return stocks, nil +} + +// hexSliceToObjectIDs 将hex字符串切片转换为ObjectID切片(跳过空值) +func hexSliceToObjectIDs(hexIDs []string) ([]bson.ObjectID, error) { + result := make([]bson.ObjectID, 0, len(hexIDs)) + for _, hex := range hexIDs { + if g.IsEmpty(hex) { + continue + } + id, err := bson.ObjectIDFromHex(hex) + if err != nil { + return nil, fmt.Errorf("值:%s, %v", hex, err) + } + result = append(result, id) + } + return result, nil +} + +// buildDetailKey 构建盘点明细复合key(skuId_warehouseId_zoneId_locationId),用于O(1) Map查找 +func buildDetailKey(skuID, warehouseID, zoneID, locationID *bson.ObjectID) string { + sk, wk, zk, lk := "", "", "", "" + if skuID != nil { + sk = skuID.Hex() + } + if warehouseID != nil { + wk = warehouseID.Hex() + } + if zoneID != nil { + zk = zoneID.Hex() + } + if locationID != nil { + lk = locationID.Hex() + } + return sk + "_" + wk + "_" + zk + "_" + lk +} + +// stockSnapshot 库存快照结构体,映射private_stock表字段,用于盘点明细的账面数量记录 +type stockSnapshot struct { + PrivateSkuID *bson.ObjectID `bson:"privateSkuId"` + WarehouseID *bson.ObjectID `bson:"warehouseId"` + ZoneID *bson.ObjectID `bson:"zoneId"` + LocationID *bson.ObjectID `bson:"locationId"` + AvailableQty int `bson:"availableQty"` +} + +// fillListItemNames 批量填充列表项的关联名称(避免N+1查询) +func (s *inventoryCount) fillListItemNames(ctx context.Context, entities []entity.InventoryCount, items []dto.InventoryCountListItem) { + if len(entities) == 0 { + return + } + + // 1. 收集所有需要查询的ID(去重) + warehouseIdSet := make(map[string]*bson.ObjectID) + zoneIdSet := make(map[string]*bson.ObjectID) + for _, e := range entities { + for _, id := range e.WarehouseIDs { + if id != nil { + warehouseIdSet[id.Hex()] = id + } + } + for _, id := range e.ZoneIDs { + if id != nil { + zoneIdSet[id.Hex()] = id + } + } + } + + // 2. 批量查询仓库名称 + warehouseNameMap := make(map[string]string) + if len(warehouseIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(warehouseIdSet)) + for _, id := range warehouseIdSet { + ids = append(ids, id) + } + var warehouses []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"warehouseName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &warehouses, public.WarehouseCollection, nil, nil); e == nil { + for _, w := range warehouses { + warehouseNameMap[w.Id.Hex()] = w.Name + } + } + } + + // 3. 批量查询库区名称 + zoneNameMap := make(map[string]string) + if len(zoneIdSet) > 0 { + ids := make([]*bson.ObjectID, 0, len(zoneIdSet)) + for _, id := range zoneIdSet { + ids = append(ids, id) + } + var zones []struct { + Id *bson.ObjectID `bson:"_id"` + Name string `bson:"zoneName"` + } + if _, e := mongo.DB().Find(ctx, bson.M{"_id": bson.M{"$in": ids}}, &zones, public.ZoneCollection, nil, nil); e == nil { + for _, z := range zones { + zoneNameMap[z.Id.Hex()] = z.Name + } + } + } + + // 4. 填充名称到列表项 + for i := range items { + // 填充仓库名称列表 + if len(entities[i].WarehouseIDs) > 0 { + names := make([]string, 0, len(entities[i].WarehouseIDs)) + for _, id := range entities[i].WarehouseIDs { + if id != nil { + if name, ok := warehouseNameMap[id.Hex()]; ok { + names = append(names, name) + } + } + } + items[i].WarehouseNames = names + } + // 填充库区名称列表 + if len(entities[i].ZoneIDs) > 0 { + names := make([]string, 0, len(entities[i].ZoneIDs)) + for _, id := range entities[i].ZoneIDs { + if id != nil { + if name, ok := zoneNameMap[id.Hex()]; ok { + names = append(names, name) + } + } + } + items[i].ZoneNames = names + } + } +} diff --git a/service/stock/inventory_warning_history_service.go b/service/stock/inventory_warning_history_service.go new file mode 100644 index 0000000..0b7f7b2 --- /dev/null +++ b/service/stock/inventory_warning_history_service.go @@ -0,0 +1,42 @@ +// 库存预警历史服务 +// 职责:预警历史查询、删除(无Create/Update,由预警状态变更时自动归档) +// 紧密耦合:dao.InventoryWarningHistory +// 注意:历史记录由系统自动归档,非用户手动创建 +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" +) + +type inventoryWarningHistory struct{} + +var InventoryWarningHistory = new(inventoryWarningHistory) + +func (s *inventoryWarningHistory) GetOne(ctx context.Context, req *dto.GetInventoryWarningHistoryReq) (res *dto.GetInventoryWarningHistoryRes, err error) { + one, err := dao.InventoryWarningHistory.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *inventoryWarningHistory) List(ctx context.Context, req *dto.ListInventoryWarningHistoryReq) (res *dto.ListInventoryWarningHistoryRes, err error) { + list, total, err := dao.InventoryWarningHistory.List(ctx, req) + if err != nil { + return + } + res = &dto.ListInventoryWarningHistoryRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} + +func (s *inventoryWarningHistory) Delete(ctx context.Context, req *dto.DeleteInventoryWarningHistoryReq) error { + return dao.InventoryWarningHistory.DeleteFake(ctx, req) +} diff --git a/service/stock/inventory_warning_service.go b/service/stock/inventory_warning_service.go new file mode 100644 index 0000000..26f0854 --- /dev/null +++ b/service/stock/inventory_warning_service.go @@ -0,0 +1,38 @@ +// 库存预警服务 +// 职责:预警查询(无Create/Update/Delete,由定时任务或库存变动触发) +// 紧密耦合:dao.InventoryWarning +// 注意:预警记录由系统自动生成,非用户手动创建 +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" +) + +type inventoryWarning struct{} + +var InventoryWarning = new(inventoryWarning) + +func (s *inventoryWarning) GetOne(ctx context.Context, req *dto.GetInventoryWarningReq) (res *dto.GetInventoryWarningRes, err error) { + one, err := dao.InventoryWarning.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *inventoryWarning) List(ctx context.Context, req *dto.ListInventoryWarningReq) (res *dto.ListInventoryWarningRes, err error) { + list, total, err := dao.InventoryWarning.List(ctx, req) + if err != nil { + return + } + res = &dto.ListInventoryWarningRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/location_service.go b/service/stock/location_service.go new file mode 100644 index 0000000..3a16aeb --- /dev/null +++ b/service/stock/location_service.go @@ -0,0 +1,92 @@ +// 库位服务 +// 职责:库位CRUD、状态更新、删除前检查(3个库存集合) +// 调用链:Delete → CountStockDetailsByLocationId/CountStockBatchByLocationId/CountPrivateStockByLocationId +// 紧密耦合:dao.Location、Capacity(容量更新入口) +// 注意:删除前检查StockDetails/StockBatch/PrivateStock三个集合是否有库存 +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type location struct{} + +var Location = new(location) + +func (s *location) Create(ctx context.Context, req *dto.CreateLocationReq) (res *dto.CreateLocationRes, err error) { + ids, err := dao.Location.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateLocationRes{ + Id: &id, + } + return +} + +func (s *location) Update(ctx context.Context, req *dto.UpdateLocationReq) error { + return dao.Location.Update(ctx, req) +} + +func (s *location) Delete(ctx context.Context, req *dto.DeleteLocationReq) error { + locationId := req.Id.Hex() + // 检查库位上是否有库存(3个集合) + stockDetailsCount, err := dao.Location.CountStockDetailsByLocationId(ctx, locationId) + if err != nil { + return err + } + if stockDetailsCount > 0 { + return gerror.Newf("库位上存在%d件库存明细,无法删除", stockDetailsCount) + } + + stockBatchCount, err := dao.Location.CountStockBatchByLocationId(ctx, locationId) + if err != nil { + return err + } + if stockBatchCount > 0 { + return gerror.Newf("库位上存在%d个批次库存,无法删除", stockBatchCount) + } + + privateStockCount, err := dao.Location.CountPrivateStockByLocationId(ctx, locationId) + if err != nil { + return err + } + if privateStockCount > 0 { + return gerror.Newf("库位上存在%d件实物库存批次,无法删除", privateStockCount) + } + + return dao.Location.DeleteFake(ctx, req) +} + +// UpdateStatus 更新库位状态(单独的状态修改接口) +func (s *location) UpdateStatus(ctx context.Context, req *dto.UpdateLocationStatusReq) error { + return dao.Location.UpdateStatus(ctx, req) +} + +func (s *location) GetOne(ctx context.Context, req *dto.GetLocationReq) (res *dto.GetLocationRes, err error) { + one, err := dao.Location.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *location) List(ctx context.Context, req *dto.ListLocationReq) (res *dto.ListLocationRes, err error) { + list, total, err := dao.Location.List(ctx, req) + if err != nil { + return + } + res = &dto.ListLocationRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/private_stock_service.go b/service/stock/private_stock_service.go new file mode 100644 index 0000000..f232dc1 --- /dev/null +++ b/service/stock/private_stock_service.go @@ -0,0 +1,305 @@ +// 实物库存批次服务 +// 职责:CRUD、移库(MoveStock库位间)、调拨(TransferStock仓库间)、出库(Outbound) +// 调用链:Controller → Create/Update/Delete → Capacity.UpdateLocationCapacity(容量重算) +// 紧密耦合:dao.PrivateStock、dao.Warehouse/Zone/Location(获取名称)、Capacity(容量更新) +// 注意:区别于StockDetails/StockBatch的逻辑库存,PrivateStock记录实际存放位置 +package service + +import ( + "assets/consts/public" + "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "github.com/gogf/gf/v2/os/gtime" + + "gitea.com/red-future/common/db/mongo" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type privateStock struct{} + +// PrivateStock 实物库存批次服务 +// 职责: +// 1. CRUD:创建、更新、删除、查询实物库存批次 +// 2. 移库(MoveStock):库位间移动 +// 3. 调拨(TransferStock):仓库间调拨 +// 4. 出库(Outbound):减少库存数量 +// 特点:记录SKU批次的实际存放位置(仓库/库区/库位)和数量,区别于StockDetails/StockBatch的逻辑库存 +var PrivateStock = new(privateStock) + +func (s *privateStock) Create(ctx context.Context, req *dto.CreatePrivateStockReq) (res *dto.CreatePrivateStockRes, err error) { + ids, err := dao.PrivateStock.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreatePrivateStockRes{ + Id: &id, + } + + // 触发库位容量更新 + if req.LocationId != nil && !req.LocationId.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, req.LocationId); err != nil { + // 容量更新失败不影响创建 + g.Log().Warningf(ctx, "更新库位容量失败: %v", err) + } + } + return +} + +func (s *privateStock) Update(ctx context.Context, req *dto.UpdatePrivateStockReq) error { + // 查询库存信息获取LocationId + stock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id}) + if err != nil { + return err + } + + err = dao.PrivateStock.Update(ctx, req) + if err != nil { + return err + } + + // 触发库位容量更新 + if stock.LocationID != nil && !stock.LocationID.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, stock.LocationID); err != nil { + g.Log().Warningf(ctx, "更新库位容量失败: %v", err) + } + } + return nil +} + +func (s *privateStock) Delete(ctx context.Context, req *dto.DeletePrivateStockReq) error { + // 查询库存信息获取LocationId(用于删除后更新容量) + stockInfo, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.Id}) + if err != nil { + return err + } + + if err := dao.PrivateStock.DeleteFake(ctx, req); err != nil { + return err + } + + // 触发库位容量更新 + if stockInfo.LocationID != nil && !stockInfo.LocationID.IsZero() { + if capErr := Capacity.UpdateLocationCapacity(ctx, stockInfo.LocationID); capErr != nil { + g.Log().Warningf(ctx, "删除库存后更新库位容量失败: %v", capErr) + } + } + return nil +} + +func (s *privateStock) GetOne(ctx context.Context, req *dto.GetPrivateStockReq) (res *dto.GetPrivateStockRes, err error) { + one, err := dao.PrivateStock.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *privateStock) List(ctx context.Context, req *dto.ListPrivateStockReq) (res *dto.ListPrivateStockRes, err error) { + list, total, err := dao.PrivateStock.List(ctx, req) + if err != nil { + return + } + res = &dto.ListPrivateStockRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} + +// MoveStock 移库(库位间移动) +func (s *privateStock) MoveStock(ctx context.Context, req *dto.MoveStockReq) error { + // 只支持PrivateStock移库,StockDetails/StockBatch是逻辑库存无位置信息 + if req.StockType != stock.StockLocationTypePrivateStock { + return gerror.New("移库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") + } + + // 验证源库位和目标库位不能相同 + if req.FromLocationId.Hex() == req.ToLocationId.Hex() { + return gerror.New("源库位和目标库位不能相同") + } + + // 验证库存是否存在且位置匹配 + privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) + if err != nil { + return err + } + if privateStock.LocationID == nil || privateStock.LocationID.Hex() != req.FromLocationId.Hex() { + return gerror.New("库存当前位置与源库位不匹配") + } + + // 获取目标库位信息 + toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId}) + if err != nil { + return err + } + + // 更新库存的库位信息 + filter := bson.M{"_id": req.StockId} + update := bson.M{ + "$set": bson.M{ + "locationId": req.ToLocationId, + "locationCode": toLocation.LocationCode, + "locationName": toLocation.LocationName, + "locationType": toLocation.LocationType, + "lastMovedAt": gtime.Now(), + }, + } + + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) + if err != nil { + return err + } + + // 触发源库位和目标库位容量更新 + if req.FromLocationId != nil && !req.FromLocationId.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, req.FromLocationId); err != nil { + g.Log().Warningf(ctx, "更新源库位容量失败: %v", err) + } + } + if req.ToLocationId != nil && !req.ToLocationId.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil { + g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err) + } + } + return nil +} + +// TransferStock 调拨(仓库间调拨) +func (s *privateStock) TransferStock(ctx context.Context, req *dto.TransferStockReq) error { + // 只支持PrivateStock调拨,StockDetails/StockBatch是逻辑库存无位置信息 + if req.StockType != stock.StockLocationTypePrivateStock { + return gerror.New("调拨操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") + } + + // 验证源仓库和目标仓库不能相同 + if req.FromWarehouseId.Hex() == req.ToWarehouseId.Hex() { + return gerror.New("源仓库和目标仓库不能相同") + } + + // 验证库存是否存在且仓库匹配 + privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) + if err != nil { + return err + } + if privateStock.WarehouseID == nil || privateStock.WarehouseID.Hex() != req.FromWarehouseId.Hex() { + return gerror.New("库存当前仓库与源仓库不匹配") + } + + // 获取目标仓库信息 + toWarehouse, err := dao.Warehouse.GetOne(ctx, &dto.GetWarehouseReq{Id: req.ToWarehouseId}) + if err != nil { + return err + } + + // 构建更新字段 + updateFields := bson.M{ + "warehouseId": req.ToWarehouseId, + "warehouseCode": toWarehouse.WarehouseCode, + "warehouseName": toWarehouse.WarehouseName, + "lastMovedAt": gtime.Now(), + } + + // 未指定目标库区/库位时清空旧值,避免残留指向源仓库 + unsetFields := bson.M{} + + // 如果指定了目标库区 + if req.ToZoneId != nil && !req.ToZoneId.IsZero() { + toZone, err := dao.Zone.GetOne(ctx, &dto.GetZoneReq{Id: req.ToZoneId}) + if err != nil { + return err + } + updateFields["zoneId"] = req.ToZoneId + updateFields["zoneCode"] = toZone.ZoneCode + updateFields["zoneName"] = toZone.ZoneName + updateFields["zoneType"] = toZone.ZoneType + } else { + unsetFields["zoneId"] = "" + unsetFields["zoneCode"] = "" + unsetFields["zoneName"] = "" + unsetFields["zoneType"] = "" + } + + // 如果指定了目标库位 + if req.ToLocationId != nil && !req.ToLocationId.IsZero() { + toLocation, err := dao.Location.GetOne(ctx, &dto.GetLocationReq{Id: req.ToLocationId}) + if err != nil { + return err + } + updateFields["locationId"] = req.ToLocationId + updateFields["locationCode"] = toLocation.LocationCode + updateFields["locationName"] = toLocation.LocationName + updateFields["locationType"] = toLocation.LocationType + } else { + unsetFields["locationId"] = "" + unsetFields["locationCode"] = "" + unsetFields["locationName"] = "" + unsetFields["locationType"] = "" + } + + filter := bson.M{"_id": req.StockId} + update := bson.M{"$set": updateFields} + if len(unsetFields) > 0 { + update["$unset"] = unsetFields + } + + _, err = mongo.DB().Update(ctx, filter, update, public.PrivateStockCollection) + if err != nil { + return err + } + + // 触发库位容量更新(调拨后涉及库位变化时) + // 更新源库位 + if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil { + g.Log().Warningf(ctx, "更新源库位容量失败: %v", err) + } + } + // 更新目标库位 + if req.ToLocationId != nil && !req.ToLocationId.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, req.ToLocationId); err != nil { + g.Log().Warningf(ctx, "更新目标库位容量失败: %v", err) + } + } + return nil +} + +// Outbound 实物库存批次出库 +func (s *privateStock) Outbound(ctx context.Context, req *dto.OutboundPrivateStockReq) error { + // 只支持PrivateStock出库,StockDetails/StockBatch是逻辑库存无位置信息 + if req.StockType != stock.StockLocationTypePrivateStock { + return gerror.New("出库操作仅支持实物库存批次(PrivateStock),明细和批次库存为逻辑库存无位置信息") + } + + // 验证库存是否存在 + privateStock, err := dao.PrivateStock.GetOne(ctx, &dto.GetPrivateStockReq{Id: req.StockId}) + if err != nil { + return err + } + + // 验证可用数量是否足够 + if privateStock.AvailableQty < req.OutboundQty { + return gerror.Newf("可用库存不足:当前可用%d,需要出库%d", privateStock.AvailableQty, req.OutboundQty) + } + + // 使用IncrementAvailableQty原子更新(传负数表示减少) + err = dao.PrivateStock.IncrementAvailableQty(ctx, req.StockId, -req.OutboundQty) + if err != nil { + return err + } + + // 触发库位容量更新 + if privateStock.LocationID != nil && !privateStock.LocationID.IsZero() { + if err := Capacity.UpdateLocationCapacity(ctx, privateStock.LocationID); err != nil { + g.Log().Warningf(ctx, "更新库位容量失败: %v", err) + } + } + return nil +} diff --git a/service/stock/stock_batch_service.go b/service/stock/stock_batch_service.go new file mode 100644 index 0000000..56f8a9a --- /dev/null +++ b/service/stock/stock_batch_service.go @@ -0,0 +1,60 @@ +// 批次库存服务(逻辑库存) +// 职责:批次CRUD、列表查询 +// 紧密耦合:dao.StockBatch +// 注意:区别于PrivateStock的实物库存,批次库存是逻辑概念,不记录物理位置 +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type stockBatch struct{} + +// StockBatch 批次服务 +var StockBatch = new(stockBatch) + +func (s *stockBatch) Create(ctx context.Context, req *dto.CreateBatchReq) (res *dto.CreateBatchRes, err error) { + ids, err := dao.StockBatch.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateBatchRes{ + Id: &id, + } + return +} + +func (s *stockBatch) Update(ctx context.Context, req *dto.UpdateBatchReq) error { + return dao.StockBatch.Update(ctx, req) +} + +func (s *stockBatch) Delete(ctx context.Context, req *dto.DeleteBatchReq) error { + return dao.StockBatch.DeleteFake(ctx, req) +} + +func (s *stockBatch) GetOne(ctx context.Context, req *dto.GetBatchReq) (res *dto.GetBatchRes, err error) { + one, err := dao.StockBatch.GetOneById(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *stockBatch) List(ctx context.Context, req *dto.ListBatchReq) (res *dto.ListBatchRes, err error) { + list, total, err := dao.StockBatch.List(ctx, req) + if err != nil { + return + } + res = &dto.ListBatchRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/stock_details_service.go b/service/stock/stock_details_service.go new file mode 100644 index 0000000..6aae458 --- /dev/null +++ b/service/stock/stock_details_service.go @@ -0,0 +1,39 @@ +// 库存明细服务(逻辑库存) +// 职责:库存明细查询(无Create/Update/Delete,由StockManage.StockOperation管理) +// 紧密耦合:dao.StockDetails +// 注意:区别于PrivateStock的实物库存,明细库存是逻辑概念,不记录物理位置 +package service + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" +) + +type stockDetails struct{} + +// StockDetails 库存服务 +var StockDetails = new(stockDetails) + +func (s *stockDetails) GetOne(ctx context.Context, req *dto.GetStockDetailsReq) (res *dto.GetStockDetailsRes, err error) { + one, err := dao.StockDetails.GetOneById(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *stockDetails) List(ctx context.Context, req *dto.ListStockDetailsReq) (res *dto.ListStockDetailsRes, err error) { + list, total, err := dao.StockDetails.List(ctx, req) + if err != nil { + return + } + res = &dto.ListStockDetailsRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/stock_manage_service.go b/service/stock/stock_manage_service.go new file mode 100644 index 0000000..c6ccf26 --- /dev/null +++ b/service/stock/stock_manage_service.go @@ -0,0 +1,329 @@ +// 库存管理服务(Stock公共库存) +// 职责:入库/出库操作,支持明细模式(StockDetails)和批次模式(StockBatch) +// 调用链:Controller → StockOperation → stockPublishMessage → NATS → AddStock(消费者) +// 紧密耦合:dao.StockDetails、dao.StockBatch、dao.AssetSku(更新库存数)、common/message(NATS发布) +// 注意:移库/调拨是PrivateStock专属操作,不在此实现 +package service + +import ( + "assets/consts/public" + "assets/consts/stock" + assetDao "assets/dao/asset" + dao "assets/dao/stock" + assetDto "assets/model/dto/asset" + stockDto "assets/model/dto/stock" + assetEntity "assets/model/entity/asset" + entity "assets/model/entity/stock" + "context" + "fmt" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/redis" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type stockManage struct{} + +// StockManage 库存管理服务(Stock公共库存) +// 职责: +// 1. 入库/出库:StockDetails(明细模式)和 StockBatch(批次模式)的库存操作 +// 2. 库存查询表单字段生成 +// 注意:移库(MoveStock)和调拨(TransferStock)是PrivateStock专属操作,不要在这里实现 +var StockManage = new(stockManage) + +// GetStockFormFields 获取库存操作表单字段 +func (s *stockManage) GetStockFormFields(ctx context.Context, req *stockDto.GetStockFormFieldsReq) (*stockDto.GetStockFormFieldsRes, error) { + // 获取资产SKU信息 + assetSku, err := assetDao.AssetSku.GetOne(ctx, &assetDto.GetAssetSkuReq{Id: req.AssetSkuId}, false) + if err != nil { + return nil, err + } + fields := make([]map[string]interface{}, 0) + // Stock 字段在两种模式下都显示 + fields = append(fields, map[string]interface{}{ + "name": "stock", + "label": "库存数量", + "type": "number", + "required": true, + "min": 1, + }) + // 如果是批次模式(2),添加批次相关字段 + if !g.IsEmpty(assetSku.StockMode) && assetSku.StockMode == stock.StockModeBatch { + fields = append(fields, []map[string]interface{}{ + { + "name": "batchNo", + "label": "批次号", + "type": "number", + "required": true, + "default": gconv.Int(gtime.Now().Format("20060102") + "0001"), + "maxLength": 12, + }, + { + "name": "productionDate", + "label": "生产日期", + "type": "date", + }, + { + "name": "expiryDate", + "label": "过期日期", + "type": "date", + }, + { + "name": "expiryWarningDate", + "label": "临期预警时间", + "type": "date", + }, + }...) + } + return &stockDto.GetStockFormFieldsRes{ + StockMode: assetSku.StockMode, + Fields: fields, + }, nil +} + +// StockOperation 库存操作入口(入库/出库) +// 根据SKU的StockMode区分明细模式和批次模式,计算差值后发布消息到NATS +func (s *stockManage) StockOperation(ctx context.Context, req *stockDto.StockOperationReq) (err error) { + assetSku, err := assetDao.AssetSku.GetOne(ctx, &assetDto.GetAssetSkuReq{Id: req.AssetSkuId}, false) + if err != nil { + return + } + if !assetSku.UnlimitedStock && req.Stock >= 0 { + var stockId *bson.ObjectID + count := 0 + if assetSku.StockMode == stock.StockModeDetail { + _count, err := dao.StockDetails.GetStockCountBySkuId(ctx, assetSku.Id) + if err != nil { + return err + } + count = gconv.Int(_count) + } + if assetSku.StockMode == stock.StockModeBatch { + if g.IsEmpty(req.BatchNo) { + return gerror.New("批次号不能为空") + } + getOne, err := dao.StockBatch.GetOne(ctx, req.BatchNo) + if err != nil { + return err + } + if !g.IsEmpty(getOne) { + stockId = getOne.Id + count = getOne.BatchQty + } + } + stockCount := 0 + operationType := "" + if count != req.Stock { + if count > req.Stock { + stockCount = count - req.Stock + operationType = "del" + } else { + stockCount = req.Stock - count + operationType = "add" + } + } + if !g.IsEmpty(operationType) && stockCount > 0 { + if err = s.stockPublishMessage(ctx, assetSku, stockId, stockCount, operationType, req); err != nil { + return err + } + } + } + return +} + +// stockPublishMessage 发布库存变更消息到NATS +// 消费者接收后执行实际的入库/出库操作(异步解耦) +func (s *stockManage) stockPublishMessage(ctx context.Context, assetSku *assetEntity.AssetSku, stockId *bson.ObjectID, stockCount int, operationType string, req *stockDto.StockOperationReq) (err error) { + // 用户信息 + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + publishMessage := stockDto.StockPublishMessage{ + AssetId: assetSku.AssetId.Hex(), + AssetSkuId: assetSku.Id.Hex(), + TenantId: user.TenantId, + UserName: user.UserName, + StockCount: stockCount, + OperationType: operationType, + Metadata: assetSku.SpecValues, + StockMode: int(assetSku.StockMode), + BatchNo: req.BatchNo, + ProductionDate: req.ProductionDate, + ExpiryDate: req.ExpiryDate, + ExpiryWarningDate: req.ExpiryWarningDate, + } + if !g.IsEmpty(stockId) && !stockId.IsZero() { + publishMessage.StockId = stockId.Hex() + } + // 发布到 NATS + //plugin, err := message.GetMsgPlugin(ctx, message.MessageNATS) + //if err != nil { + // return gerror.Newf("NATS插件未就绪: %v", err) + //} + //err = plugin.Publish(ctx, &message.NatsPublishMsgConfig{ + // QueueName: public.StockDetailGroupName, + // Durable: true, + // Data: publishMessage, + //}) + //_, err = message.PublishMessage(ctx, &message.RedisMessageConfig{StreamKey: public.StockDetailStreamKey}, publishMessage) + //plugin, err := message.GetMsgPlugin(message.MessageRedis) + //if err != nil { + // return err + //} + //err = plugin.Publish(ctx, &message.RedisPublishMsgConfig{ + // QueueName: public.StockDetailQueueName, + // Data: publishMessage, + //}) + return +} + +// AddStock NATS消费者调用,执行实际的入库/出库操作 +// 使用Redis分布式锁防止并发冲突,支持明细模式和批次模式 +func (s *stockManage) AddStock(ctx context.Context, msg map[string]interface{}) error { + assetId, err := bson.ObjectIDFromHex(gconv.String(msg["assetId"])) + if err != nil { + return err + } + assetSkuId, err := bson.ObjectIDFromHex(gconv.String(msg["assetSkuId"])) + if err != nil { + return err + } + stockId := bson.ObjectID{} + if !g.IsEmpty(msg["stockId"]) { + stockId, err = bson.ObjectIDFromHex(gconv.String(msg["stockId"])) + if err != nil { + return err + } + } + userName := gconv.String(msg["userName"]) + tenantId := gconv.Float64(msg["tenantId"]) + stockCount := gconv.Int(msg["stockCount"]) + operationType := gconv.String(msg["operationType"]) + metadata := gconv.Maps(msg["metadata"]) + stockMode := stock.StockMode(gconv.Int(msg["stockMode"])) + batchNo := gconv.String(msg["batchNo"]) + productionDate := gtime.New(msg["productionDate"]) + expiryDate := gtime.New(msg["expiryDate"]) + expiryWarningDate := gtime.New(msg["expiryWarningDate"]) + // 设置 userId 和 tenantId 到 ctx + ctx = context.WithValue(ctx, "userName", userName) + ctx = context.WithValue(ctx, "tenantId", tenantId) + // 获取redis-租户存储-锁key + fileLockKey := fmt.Sprintf(public.StockDetailLockKey, assetSkuId) + success, err := redis.Lock(ctx, fileLockKey, int64(60), func(ctx context.Context) error { + if operationType == "add" { + if stockMode == stock.StockModeBatch { + if !stockId.IsZero() { + batch := stockDto.UpdateBatchReq{ + Id: &stockId, + BatchQty: stockCount, + AvailableQty: stockCount, + } + if err := dao.StockBatch.Update(ctx, &batch); err != nil { + return err + } + } else { + batch := stockDto.CreateBatchReq{ + AssetId: &assetId, + AssetSkuId: &assetSkuId, + Status: stock.BatchStatusActive, + Metadata: metadata, + BatchNo: batchNo, + BatchQty: stockCount, + AvailableQty: stockCount, + ProductionDate: productionDate, + ExpiryDate: expiryDate, + ExpiryWarningDate: expiryWarningDate, + } + if _, err := dao.StockBatch.Insert(ctx, &batch); err != nil { + return err + } + } + } + if stockMode == stock.StockModeDetail { + // 创建指定数量的库存 + var stockInterfaces []interface{} + for i := 0; i < stockCount; i++ { + stockInterfaces = append(stockInterfaces, entity.StockDetails{ + AssetId: &assetId, + AssetSkuId: &assetSkuId, + Status: stock.StockStatusAvailable, + Metadata: metadata, + }) + } + // 批量插入数据库 + if _, err = dao.StockDetails.BatchInsert(ctx, stockInterfaces); err != nil { + return err + } + } + } + if operationType == "del" { + if stockMode == stock.StockModeBatch { + stockCount = 0 - stockCount + // 更新批次 + batch := stockDto.UpdateBatchReq{ + Id: &stockId, + BatchQty: stockCount, + AvailableQty: stockCount, + } + if err := dao.StockBatch.Update(ctx, &batch); err != nil { + return err + } + } + if stockMode == stock.StockModeDetail { + // 分页查询所有库存明细,收集所有ID + var allStockIds []*bson.ObjectID + pageSize := int64(50) + for pageNum := int64(1); ; pageNum++ { + details, total, err := dao.StockDetails.List(ctx, + &stockDto.ListStockDetailsReq{ + AssetSkuId: &assetSkuId, + Status: stock.StockStatusAvailable, + Page: &beans.Page{PageNum: pageNum, PageSize: pageSize}, + }) + if err != nil { + return err + } + if pageNum == 1 && int(total) < stockCount { + return gerror.New("可操作库存数量不足") + } + // 收集当前页的ID + for _, detail := range details { + if detail.Id != nil && !detail.Id.IsZero() { + allStockIds = append(allStockIds, detail.Id) + if len(allStockIds) >= stockCount { + break + } + } + } + if len(allStockIds) >= stockCount { + break + } + } + // 根据ID批量删除库存 + delCount, err := dao.StockDetails.DeleteManyByIds(ctx, allStockIds) + if err != nil { + return err + } + if delCount != int64(stockCount) { + return gerror.New("删除库存数量不匹配") + } + stockCount = 0 - stockCount + } + } + return assetDao.AssetSku.Update(ctx, &assetDto.UpdateAssetSkuReq{Id: &assetSkuId, Stock: stockCount}) + }) + if err != nil { + return err + } + if !success { + return fmt.Errorf("获取库存操作锁失败: %v", err) + } + return nil +} diff --git a/service/stock/unit_conversion_service.go b/service/stock/unit_conversion_service.go new file mode 100644 index 0000000..844bd2c --- /dev/null +++ b/service/stock/unit_conversion_service.go @@ -0,0 +1,103 @@ +package service + +// unit_conversion_service.go +// 单位换算服务层 +// +// 职责: +// 1. GetConversionFactor - 获取单位换算系数 +// 查询UnitConversion表,返回fromUnit→toUnit的换算系数 +// +// 2. ConvertWithCeil - 向上取整换算(用于容量计算) +// 公式:ceil(fromQty / factor) +// 示例:50瓶 ÷ 20(瓶/箱)= 2.5 → ceil = 3箱 +// +// 3. ConvertExact - 精确换算(用于出入库数量计算) +// 公式:fromQty / factor +// 示例:50瓶 ÷ 20(瓶/箱)= 2.5箱 +// +// 核心逻辑: +// - 从UnitConversion表查询换算系数 +// - 向上取整确保不满一个单位也占用空间 +// - 只能在同一CapacityUnitType(枚举类型)内换算 + +import ( + dao "assets/dao/stock" + dto "assets/model/dto/stock" + entity "assets/model/entity/stock" + "context" + + "gitea.com/red-future/common/utils" + "go.mongodb.org/mongo-driver/v2/bson" +) + +var UnitConversion = new(unitConversion) + +type unitConversion struct{} + +// Create 创建单位换算规则 +func (s *unitConversion) Create(ctx context.Context, req *dto.CreateUnitConversionReq) (res *dto.CreateUnitConversionRes, err error) { + var conversion *entity.UnitConversion + if err = utils.Struct(req, &conversion); err != nil { + return + } + + ids, err := dao.UnitConversion.Insert(ctx, conversion) + if err != nil { + return + } + + id := ids[0].(bson.ObjectID) + res = &dto.CreateUnitConversionRes{ + Id: &id, + } + return +} + +// Update 更新单位换算规则 +func (s *unitConversion) Update(ctx context.Context, req *dto.UpdateUnitConversionReq) (err error) { + update := bson.M{} + + if req.ConversionCode != "" { + update["conversionCode"] = req.ConversionCode + } + if req.ConversionName != "" { + update["conversionName"] = req.ConversionName + } + if req.UnitType != "" { + update["unitType"] = req.UnitType + } + if req.FromUnit != "" { + update["fromUnit"] = req.FromUnit + } + if req.ToUnit != "" { + update["toUnit"] = req.ToUnit + } + if req.ConversionFactor > 0 { + update["conversionFactor"] = req.ConversionFactor + } + if req.Remark != "" { + update["remark"] = req.Remark + } + + err = dao.UnitConversion.Update(ctx, req.Id, update) + return +} + +// Delete 删除单位换算规则 +func (s *unitConversion) Delete(ctx context.Context, req *dto.DeleteUnitConversionReq) (err error) { + err = dao.UnitConversion.DeleteFake(ctx, req.Id) + return +} + +// List 查询单位换算列表 +func (s *unitConversion) List(ctx context.Context, req *dto.ListUnitConversionReq) (res *dto.ListUnitConversionRes, err error) { + list, err := dao.UnitConversion.List(ctx, req.UnitType, req.FromUnit, req.ToUnit) + if err != nil { + return + } + + res = &dto.ListUnitConversionRes{ + List: list, + } + return +} diff --git a/service/stock/warehouse_service.go b/service/stock/warehouse_service.go new file mode 100644 index 0000000..50a265b --- /dev/null +++ b/service/stock/warehouse_service.go @@ -0,0 +1,96 @@ +// 仓库服务 +// 职责:仓库CRUD、状态更新(联动库区/库位状态) +// 调用链:UpdateStatus → BatchUpdateZoneStatus/BatchUpdateLocationStatus(状态联动) +// 紧密耦合:dao.Warehouse、dao.Zone/dao.Location(级联状态更新、删除前检查) +// 注意:删除前检查是否存在库区,启用/禁用联动子表状态 +package service + +import ( + stockConst "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type warehouse struct{} + +var Warehouse = new(warehouse) + +func (s *warehouse) Create(ctx context.Context, req *dto.CreateWarehouseReq) (res *dto.CreateWarehouseRes, err error) { + ids, err := dao.Warehouse.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateWarehouseRes{ + Id: &id, + } + return +} + +func (s *warehouse) Update(ctx context.Context, req *dto.UpdateWarehouseReq) error { + return dao.Warehouse.Update(ctx, req) +} + +func (s *warehouse) Delete(ctx context.Context, req *dto.DeleteWarehouseReq) error { + warehouseId := req.Id.Hex() + count, err := dao.Warehouse.CountZonesByWarehouseId(ctx, warehouseId) + if err != nil { + return err + } + if count > 0 { + return gerror.Newf("仓库下存在%d个库区,无法删除", count) + } + return dao.Warehouse.DeleteFake(ctx, req) +} + +// UpdateStatus 更新仓库状态(单独的状态修改接口,联动子表) +func (s *warehouse) UpdateStatus(ctx context.Context, req *dto.UpdateWarehouseStatusReq) (err error) { + warehouseId := req.Id.Hex() + if err = dao.Warehouse.UpdateStatus(ctx, req); err != nil { + return + } + if req.Status == stockConst.WarehouseStatusEnabled { + // 启用:只恢复Disabled状态的库区/库位,保留InUse等其他状态 + if _, err = dao.Warehouse.BatchUpdateZoneStatus(ctx, warehouseId, stockConst.ZoneStatusEnabled, stockConst.ZoneStatusDisabled); err != nil { + return + } + if _, err = dao.Warehouse.BatchUpdateLocationStatus(ctx, warehouseId, stockConst.LocationStatusIdle, stockConst.LocationStatusDisabled); err != nil { + return + } + } else { + // 禁用:所有库区/库位统一设为Disabled + if _, err = dao.Warehouse.BatchUpdateZoneStatus(ctx, warehouseId, stockConst.ZoneStatusDisabled); err != nil { + return + } + if _, err = dao.Warehouse.BatchUpdateLocationStatus(ctx, warehouseId, stockConst.LocationStatusDisabled); err != nil { + return + } + } + return +} + +func (s *warehouse) GetOne(ctx context.Context, req *dto.GetWarehouseReq) (res *dto.GetWarehouseRes, err error) { + one, err := dao.Warehouse.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *warehouse) List(ctx context.Context, req *dto.ListWarehouseReq) (res *dto.ListWarehouseRes, err error) { + list, total, err := dao.Warehouse.List(ctx, req) + if err != nil { + return + } + res = &dto.ListWarehouseRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/stock/zone_service.go b/service/stock/zone_service.go new file mode 100644 index 0000000..34721cb --- /dev/null +++ b/service/stock/zone_service.go @@ -0,0 +1,93 @@ +// 库区服务 +// 职责:库区CRUD、状态更新(联动库位状态) +// 调用链:UpdateStatus → BatchUpdateLocationStatus(状态联动) +// 紧密耦合:dao.Zone、dao.Location(级联状态更新、删除前检查) +// 注意:删除前检查是否存在库位,启用/禁用联动库位状态 +package service + +import ( + "assets/consts/stock" + dao "assets/dao/stock" + dto "assets/model/dto/stock" + "context" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type zone struct{} + +var Zone = new(zone) + +func (s *zone) Create(ctx context.Context, req *dto.CreateZoneReq) (res *dto.CreateZoneRes, err error) { + ids, err := dao.Zone.Insert(ctx, req) + if err != nil { + return + } + id := ids[0].(bson.ObjectID) + res = &dto.CreateZoneRes{ + Id: &id, + } + return +} + +func (s *zone) Update(ctx context.Context, req *dto.UpdateZoneReq) error { + return dao.Zone.Update(ctx, req) +} + +func (s *zone) Delete(ctx context.Context, req *dto.DeleteZoneReq) error { + zoneId := req.Id.Hex() + // 删除前检查:是否存在关联的库位 + count, err := dao.Zone.CountLocationsByZoneId(ctx, zoneId) + if err != nil { + return err + } + if count > 0 { + return gerror.Newf("库区下存在%d个库位,无法删除", count) + } + return dao.Zone.DeleteFake(ctx, req) +} + +// UpdateStatus 更新库区状态(单独的状态修改接口,联动子表) +func (s *zone) UpdateStatus(ctx context.Context, req *dto.UpdateZoneStatusReq) (err error) { + zoneId := req.Id.Hex() + // 1. 更新库区状态 + if err = dao.Zone.UpdateStatus(ctx, req); err != nil { + return + } + // 2. 联动更新库位状态 + if req.Status == stock.ZoneStatusEnabled { + // 启用:只恢复Disabled状态的库位,保留InUse等其他状态 + if _, err = dao.Zone.BatchUpdateLocationStatus(ctx, zoneId, stock.LocationStatusIdle, stock.LocationStatusDisabled); err != nil { + return + } + } else { + // 禁用:所有库位统一设为Disabled + if _, err = dao.Zone.BatchUpdateLocationStatus(ctx, zoneId, stock.LocationStatusDisabled); err != nil { + return + } + } + return +} + +func (s *zone) GetOne(ctx context.Context, req *dto.GetZoneReq) (res *dto.GetZoneRes, err error) { + one, err := dao.Zone.GetOne(ctx, req) + if err != nil { + return + } + err = utils.Struct(one, &res) + return +} + +func (s *zone) List(ctx context.Context, req *dto.ListZoneReq) (res *dto.ListZoneRes, err error) { + list, total, err := dao.Zone.List(ctx, req) + if err != nil { + return + } + res = &dto.ListZoneRes{ + Total: total, + } + err = utils.Struct(list, &res.List) + return +} diff --git a/service/sync/default.go b/service/sync/default.go new file mode 100644 index 0000000..2816ec6 --- /dev/null +++ b/service/sync/default.go @@ -0,0 +1,108 @@ +package service + +import ( + consts "assets/consts/public" + "assets/model/entity/sync" + "context" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// BasePlatformService 平台服务基类 +type BasePlatformService struct { + Platform consts.SyncPlatform + Config *entity.ChannelConfig +} + +// NewBasePlatformService 创建基础平台服务 +func NewBasePlatformService(platform consts.SyncPlatform, config *entity.ChannelConfig) *BasePlatformService { + return &BasePlatformService{ + Platform: platform, + Config: config, + } +} + +// DefaultAssetService 默认资产平台服务(用于未明确定义的平台) +type DefaultAssetService struct { + *BasePlatformService +} + +// NewDefaultAssetService 创建默认资产服务 +func NewDefaultAssetService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultAssetService { + return &DefaultAssetService{ + BasePlatformService: NewBasePlatformService(platform, config), + } +} + +func (s *DefaultAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error { + // 默认实现,不执行实际同步 + return nil +} + +func (s *DefaultAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DefaultAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DefaultAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +// DefaultAssetSkuService 默认资产SKU平台服务 +type DefaultAssetSkuService struct { + *BasePlatformService +} + +// NewDefaultAssetSkuService 创建默认资产SKU服务 +func NewDefaultAssetSkuService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultAssetSkuService { + return &DefaultAssetSkuService{ + BasePlatformService: NewBasePlatformService(platform, config), + } +} + +func (s *DefaultAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +func (s *DefaultAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DefaultAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DefaultAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +// DefaultStockService 默认库存平台服务 +type DefaultStockService struct { + *BasePlatformService +} + +// NewDefaultStockService 创建默认库存服务 +func NewDefaultStockService(platform consts.SyncPlatform, config *entity.ChannelConfig) *DefaultStockService { + return &DefaultStockService{ + BasePlatformService: NewBasePlatformService(platform, config), + } +} + +func (s *DefaultStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *DefaultStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DefaultStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DefaultStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} diff --git a/service/sync/platform_factory.go b/service/sync/platform_factory.go new file mode 100644 index 0000000..2a583fe --- /dev/null +++ b/service/sync/platform_factory.go @@ -0,0 +1,156 @@ +package service + +import ( + consts "assets/consts/public" + "assets/dao/sync" + "context" +) + +// DefaultPlatformFactory 默认平台服务工厂实现 +type DefaultPlatformFactory struct{} + +// NewPlatformServiceFactory 创建平台服务工厂 +func NewPlatformServiceFactory() PlatformFactory { + return &DefaultPlatformFactory{} +} + +// CreateAssetService 创建资产平台服务 +func (f *DefaultPlatformFactory) CreateAssetService(platform consts.SyncPlatform) AssetPlatformService { + config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform) + if err != nil || config == nil { + // 如果没有配置,创建默认配置 + //config = &entity.SyncConfig{ + // Platform: platform, + // IsEnabled: true, + // APIEndpoint: f.getAPIEndpoint(platform), + // SyncInterval: 300, + // BatchSize: 100, + // MaxRetries: 3, + //} + } + + switch platform { + case consts.SyncPlatformTaobao: + return NewTaobaoAssetService(config) + case consts.SyncPlatformJD: + return NewJDAssetService(config) + case consts.SyncPlatformDouyin: + return NewDouyinAssetService(config) + case consts.SyncPlatformKuaishou: + return NewKuaishouAssetService(config) + case consts.SyncPlatformXiaohongshu: + return NewXiaohongshuAssetService(config) + case consts.SyncPlatformPinduoduo: + return NewPinduoduoAssetService(config) + case consts.SyncPlatformXianyu: + return NewXianyuAssetService(config) + case consts.SyncPlatformBlockchain: + return NewBlockchainAssetService(config) + case consts.SyncPlatformInternal: + return NewInternalAssetService(config) + default: + return NewDefaultAssetService(platform, config) + } +} + +// CreateAssetSkuService 创建资产SKU平台服务 +func (f *DefaultPlatformFactory) CreateAssetSkuService(platform consts.SyncPlatform) AssetSkuPlatformService { + config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform) + if err != nil || config == nil { + //config = &entity.ChannelConfig{ + // Platform: platform, + // IsEnabled: true, + // APIEndpoint: f.getAPIEndpoint(platform), + // SyncInterval: 300, + // BatchSize: 100, + // MaxRetries: 3, + //} + } + + switch platform { + case consts.SyncPlatformTaobao: + return NewTaobaoAssetSkuService(config) + case consts.SyncPlatformJD: + return NewJDAssetSkuService(config) + case consts.SyncPlatformDouyin: + return NewDouyinAssetSkuService(config) + case consts.SyncPlatformKuaishou: + return NewKuaishouAssetSkuService(config) + case consts.SyncPlatformXiaohongshu: + return NewXiaohongshuAssetSkuService(config) + case consts.SyncPlatformPinduoduo: + return NewPinduoduoAssetSkuService(config) + case consts.SyncPlatformXianyu: + return NewXianyuAssetSkuService(config) + case consts.SyncPlatformBlockchain: + return NewBlockchainAssetSkuService(config) + case consts.SyncPlatformInternal: + return NewInternalAssetSkuService(config) + default: + return NewDefaultAssetSkuService(platform, config) + } +} + +// CreateStockService 创建库存平台服务 +func (f *DefaultPlatformFactory) CreateStockService(platform consts.SyncPlatform) StockPlatformService { + config, err := dao.SyncConfig.GetByPlatform(context.Background(), platform) + if err != nil || config == nil { + //config = &entity.SyncConfig{ + // Platform: platform, + // IsEnabled: true, + // APIEndpoint: f.getAPIEndpoint(platform), + // SyncInterval: 300, + // BatchSize: 100, + // MaxRetries: 3, + //} + } + + switch platform { + case consts.SyncPlatformTaobao: + return NewTaobaoStockService(config) + case consts.SyncPlatformJD: + return NewJDStockService(config) + case consts.SyncPlatformDouyin: + return NewDouyinStockService(config) + case consts.SyncPlatformKuaishou: + return NewKuaishouStockService(config) + case consts.SyncPlatformXiaohongshu: + return NewXiaohongshuStockService(config) + case consts.SyncPlatformPinduoduo: + return NewPinduoduoStockService(config) + case consts.SyncPlatformXianyu: + return NewXianyuStockService(config) + case consts.SyncPlatformBlockchain: + return NewBlockchainStockService(config) + case consts.SyncPlatformInternal: + return NewInternalStockService(config) + default: + return NewDefaultStockService(platform, config) + } +} + +// getAPIEndpoint 根据平台获取API端点 +func (f *DefaultPlatformFactory) getAPIEndpoint(platform consts.SyncPlatform) string { + switch platform { + case consts.SyncPlatformTaobao: + return "https://eco.taobao.com/router/rest" + case consts.SyncPlatformJD: + return "https://api.jd.com/routerjson" + case consts.SyncPlatformDouyin: + return "https://open.douyin.com/api" + case consts.SyncPlatformKuaishou: + return "https://open.kuaishou.com/api" + case consts.SyncPlatformXiaohongshu: + return "https://open.xiaohongshu.com/api" + case consts.SyncPlatformPinduoduo: + return "https://open.pinduoduo.com/api" + case consts.SyncPlatformXianyu: + return "https://api.xianyu.com/api" + case consts.SyncPlatformBlockchain: + return "https://api.blockchain.com/api" + case consts.SyncPlatformInternal: + return "http://localhost:3004/api" + default: + return "" + } +} diff --git a/service/sync/platform_interface.go b/service/sync/platform_interface.go new file mode 100644 index 0000000..045ed23 --- /dev/null +++ b/service/sync/platform_interface.go @@ -0,0 +1,54 @@ +package service + +import ( + consts "assets/consts/public" + "context" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// AssetPlatformService 资产平台服务接口 +type AssetPlatformService interface { + // SyncAsset 同步资产到平台 + SyncAsset(ctx context.Context, assetID *bson.ObjectID) error + // GetAsset 从平台获取资产 + GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) + // UpdateAsset 更新平台资产 + UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error + // DeleteAsset 删除平台资产 + DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error +} + +// AssetSkuPlatformService 资产SKU平台服务接口 +type AssetSkuPlatformService interface { + // SyncAssetSku 同步资产SKU到平台 + SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error + // GetAssetSku 从平台获取资产SKU + GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) + // UpdateAssetSku 更新平台资产SKU + UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error + // DeleteAssetSku 删除平台资产SKU + DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error +} + +// StockPlatformService 库存平台服务接口 +type StockPlatformService interface { + // SyncStock 同步库存到平台 + SyncStock(ctx context.Context, stockID *bson.ObjectID) error + // GetStock 从平台获取库存 + GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) + // UpdateStock 更新平台库存 + UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error + // DeleteStock 删除平台库存 + DeleteStock(ctx context.Context, stockID *bson.ObjectID) error +} + +// PlatformFactory 平台服务工厂 +type PlatformFactory interface { + // CreateAssetService 创建资产平台服务 + CreateAssetService(platform consts.SyncPlatform) AssetPlatformService + // CreateAssetSkuService 创建资产SKU平台服务 + CreateAssetSkuService(platform consts.SyncPlatform) AssetSkuPlatformService + // CreateStockService 创建库存平台服务 + CreateStockService(platform consts.SyncPlatform) StockPlatformService +} diff --git a/service/sync/platforms.go b/service/sync/platforms.go new file mode 100644 index 0000000..6f265a9 --- /dev/null +++ b/service/sync/platforms.go @@ -0,0 +1,755 @@ +package service + +import ( + consts "assets/consts/public" + "assets/model/entity/sync" + "context" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// 为各平台创建占位服务 +// 实际的API调用逻辑将在后续实现 + +// TaobaoAssetService 淘宝资产服务 +type TaobaoAssetService struct { + *BasePlatformService +} + +func NewTaobaoAssetService(config *entity.ChannelConfig) *TaobaoAssetService { + return &TaobaoAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config), + } +} + +func (s *TaobaoAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error { + // TODO: 实现淘宝资产同步逻辑 + return nil +} + +func (s *TaobaoAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *TaobaoAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *TaobaoAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +// TaobaoAssetSkuService 淘宝资产SKU服务 +type TaobaoAssetSkuService struct { + *BasePlatformService +} + +func NewTaobaoAssetSkuService(config *entity.ChannelConfig) *TaobaoAssetSkuService { + return &TaobaoAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config), + } +} + +func (s *TaobaoAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +func (s *TaobaoAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *TaobaoAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *TaobaoAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +// TaobaoStockService 淘宝库存服务 +type TaobaoStockService struct { + *BasePlatformService +} + +func NewTaobaoStockService(config *entity.ChannelConfig) *TaobaoStockService { + return &TaobaoStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformTaobao, config), + } +} + +func (s *TaobaoStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *TaobaoStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *TaobaoStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *TaobaoStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// 其他平台的占位服务结构体... +// 京东、抖音、快手、小红书、拼多多、闲鱼、区块链等 +// 由于代码结构相似,这里只展示模板 + +// JDAssetService 京东资产服务 +type JDAssetService struct { + *BasePlatformService +} + +func NewJDAssetService(config *entity.ChannelConfig) *JDAssetService { + return &JDAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config), + } +} + +func (s *JDAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +func (s *JDAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *JDAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *JDAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +// JDAssetSkuService 京东资产SKU服务 +type JDAssetSkuService struct { + *BasePlatformService +} + +func NewJDAssetSkuService(config *entity.ChannelConfig) *JDAssetSkuService { + return &JDAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config), + } +} + +func (s *JDAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +func (s *JDAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *JDAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *JDAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +// JDStockService 京东库存服务 +type JDStockService struct { + *BasePlatformService +} + +func NewJDStockService(config *entity.ChannelConfig) *JDStockService { + return &JDStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformJD, config), + } +} + +func (s *JDStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *JDStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *JDStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *JDStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// DouyinAssetService 抖音资产服务 +type DouyinAssetService struct { + *BasePlatformService +} + +func NewDouyinAssetService(config *entity.ChannelConfig) *DouyinAssetService { + return &DouyinAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config), + } +} + +func (s *DouyinAssetService) SyncAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +func (s *DouyinAssetService) GetAsset(ctx context.Context, assetID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DouyinAssetService) UpdateAsset(ctx context.Context, assetID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DouyinAssetService) DeleteAsset(ctx context.Context, assetID *bson.ObjectID) error { + return nil +} + +// DouyinAssetSkuService 抖音资产SKU服务 +type DouyinAssetSkuService struct { + *BasePlatformService +} + +func NewDouyinAssetSkuService(config *entity.ChannelConfig) *DouyinAssetSkuService { + return &DouyinAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config), + } +} + +func (s *DouyinAssetSkuService) SyncAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +func (s *DouyinAssetSkuService) GetAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DouyinAssetSkuService) UpdateAssetSku(ctx context.Context, assetSkuID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DouyinAssetSkuService) DeleteAssetSku(ctx context.Context, assetSkuID *bson.ObjectID) error { + return nil +} + +// DouyinStockService 抖音库存服务 +type DouyinStockService struct { + *BasePlatformService +} + +func NewDouyinStockService(config *entity.ChannelConfig) *DouyinStockService { + return &DouyinStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformDouyin, config), + } +} + +func (s *DouyinStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *DouyinStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *DouyinStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *DouyinStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// 其他平台服务的占位函数声明 +// KuaishouAssetService, KuaishouAssetSkuService, KuaishouStockService +// XiaohongshuAssetService, XiaohongshuAssetSkuService, XiaohongshuStockService +// PinduoduoAssetService, PinduoduoAssetSkuService, PinduoduoStockService +// XianyuAssetService, XianyuAssetSkuService, XianyuStockService +// BlockchainAssetService, BlockchainAssetSkuService, BlockchainStockService +// InternalAssetService, InternalAssetSkuService, InternalStockService + +// KuaishouAssetService 快手资产服务 +type KuaishouAssetService struct { + *BasePlatformService +} + +func NewKuaishouAssetService(config *entity.ChannelConfig) *KuaishouAssetService { + return &KuaishouAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config), + } +} + +func (s *KuaishouAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *KuaishouAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *KuaishouAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *KuaishouAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// KuaishouAssetSkuService 快手资产SKU服务 +type KuaishouAssetSkuService struct { + *BasePlatformService +} + +func NewKuaishouAssetSkuService(config *entity.ChannelConfig) *KuaishouAssetSkuService { + return &KuaishouAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config), + } +} + +func (s *KuaishouAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *KuaishouAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *KuaishouAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *KuaishouAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// KuaishouStockService 快手库存服务 +type KuaishouStockService struct { + *BasePlatformService +} + +func NewKuaishouStockService(config *entity.ChannelConfig) *KuaishouStockService { + return &KuaishouStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformKuaishou, config), + } +} + +func (s *KuaishouStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *KuaishouStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *KuaishouStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *KuaishouStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XiaohongshuAssetService 小红书资产服务 +type XiaohongshuAssetService struct { + *BasePlatformService +} + +func NewXiaohongshuAssetService(config *entity.ChannelConfig) *XiaohongshuAssetService { + return &XiaohongshuAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config), + } +} + +func (s *XiaohongshuAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XiaohongshuAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XiaohongshuAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XiaohongshuAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XiaohongshuAssetSkuService 小红书资产SKU服务 +type XiaohongshuAssetSkuService struct { + *BasePlatformService +} + +func NewXiaohongshuAssetSkuService(config *entity.ChannelConfig) *XiaohongshuAssetSkuService { + return &XiaohongshuAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config), + } +} + +func (s *XiaohongshuAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XiaohongshuAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XiaohongshuAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XiaohongshuAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XiaohongshuStockService 小红书库存服务 +type XiaohongshuStockService struct { + *BasePlatformService +} + +func NewXiaohongshuStockService(config *entity.ChannelConfig) *XiaohongshuStockService { + return &XiaohongshuStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXiaohongshu, config), + } +} + +func (s *XiaohongshuStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XiaohongshuStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XiaohongshuStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XiaohongshuStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// PinduoduoAssetService 拼多多资产服务 +type PinduoduoAssetService struct { + *BasePlatformService +} + +func NewPinduoduoAssetService(config *entity.ChannelConfig) *PinduoduoAssetService { + return &PinduoduoAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config), + } +} + +func (s *PinduoduoAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *PinduoduoAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *PinduoduoAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *PinduoduoAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// PinduoduoAssetSkuService 拼多多资产SKU服务 +type PinduoduoAssetSkuService struct { + *BasePlatformService +} + +func NewPinduoduoAssetSkuService(config *entity.ChannelConfig) *PinduoduoAssetSkuService { + return &PinduoduoAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config), + } +} + +func (s *PinduoduoAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *PinduoduoAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *PinduoduoAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *PinduoduoAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// PinduoduoStockService 拼多多库存服务 +type PinduoduoStockService struct { + *BasePlatformService +} + +func NewPinduoduoStockService(config *entity.ChannelConfig) *PinduoduoStockService { + return &PinduoduoStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformPinduoduo, config), + } +} + +func (s *PinduoduoStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *PinduoduoStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *PinduoduoStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *PinduoduoStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XianyuAssetService 闲鱼资产服务 +type XianyuAssetService struct { + *BasePlatformService +} + +func NewXianyuAssetService(config *entity.ChannelConfig) *XianyuAssetService { + return &XianyuAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config), + } +} + +func (s *XianyuAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XianyuAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XianyuAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XianyuAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XianyuAssetSkuService 闲鱼资产SKU服务 +type XianyuAssetSkuService struct { + *BasePlatformService +} + +func NewXianyuAssetSkuService(config *entity.ChannelConfig) *XianyuAssetSkuService { + return &XianyuAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config), + } +} + +func (s *XianyuAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XianyuAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XianyuAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XianyuAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// XianyuStockService 闲鱼库存服务 +type XianyuStockService struct { + *BasePlatformService +} + +func NewXianyuStockService(config *entity.ChannelConfig) *XianyuStockService { + return &XianyuStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformXianyu, config), + } +} + +func (s *XianyuStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *XianyuStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *XianyuStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *XianyuStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// BlockchainAssetService 区块链资产服务 +type BlockchainAssetService struct { + *BasePlatformService +} + +func NewBlockchainAssetService(config *entity.ChannelConfig) *BlockchainAssetService { + return &BlockchainAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config), + } +} + +func (s *BlockchainAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *BlockchainAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *BlockchainAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *BlockchainAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// BlockchainAssetSkuService 区块链资产SKU服务 +type BlockchainAssetSkuService struct { + *BasePlatformService +} + +func NewBlockchainAssetSkuService(config *entity.ChannelConfig) *BlockchainAssetSkuService { + return &BlockchainAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config), + } +} + +func (s *BlockchainAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *BlockchainAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *BlockchainAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *BlockchainAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// BlockchainStockService 区块链库存服务 +type BlockchainStockService struct { + *BasePlatformService +} + +func NewBlockchainStockService(config *entity.ChannelConfig) *BlockchainStockService { + return &BlockchainStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformBlockchain, config), + } +} + +func (s *BlockchainStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *BlockchainStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *BlockchainStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *BlockchainStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// InternalAssetService 内部资产服务(使用 assets-sync 中的完整实现) +type InternalAssetService struct { + *BasePlatformService +} + +func NewInternalAssetService(config *entity.ChannelConfig) *InternalAssetService { + return &InternalAssetService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config), + } +} + +func (s *InternalAssetService) SyncAsset(ctx context.Context, stockID *bson.ObjectID) error { + // TODO: 实现内部资产同步逻辑 + return nil +} + +func (s *InternalAssetService) GetAsset(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *InternalAssetService) UpdateAsset(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *InternalAssetService) DeleteAsset(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// InternalAssetSkuService 内部资产SKU服务 +type InternalAssetSkuService struct { + *BasePlatformService +} + +func NewInternalAssetSkuService(config *entity.ChannelConfig) *InternalAssetSkuService { + return &InternalAssetSkuService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config), + } +} + +func (s *InternalAssetSkuService) SyncAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *InternalAssetSkuService) GetAssetSku(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *InternalAssetSkuService) UpdateAssetSku(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *InternalAssetSkuService) DeleteAssetSku(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +// InternalStockService 内部库存服务 +type InternalStockService struct { + *BasePlatformService +} + +func NewInternalStockService(config *entity.ChannelConfig) *InternalStockService { + return &InternalStockService{ + BasePlatformService: NewBasePlatformService(consts.SyncPlatformInternal, config), + } +} + +func (s *InternalStockService) SyncStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} + +func (s *InternalStockService) GetStock(ctx context.Context, stockID *bson.ObjectID) (interface{}, error) { + return nil, nil +} + +func (s *InternalStockService) UpdateStock(ctx context.Context, stockID *bson.ObjectID, data interface{}) error { + return nil +} + +func (s *InternalStockService) DeleteStock(ctx context.Context, stockID *bson.ObjectID) error { + return nil +} diff --git a/service/sync/sync_service.go b/service/sync/sync_service.go new file mode 100644 index 0000000..5392a3a --- /dev/null +++ b/service/sync/sync_service.go @@ -0,0 +1,292 @@ +package service + +import ( + consts "assets/consts/public" + dao "assets/dao/sync" + dto "assets/model/dto/sync" + entity "assets/model/entity/sync" + "context" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/grpool" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type syncService struct{} + +// Sync 同步服务 +var Sync = new(syncService) + +// PlatformFactory 平台服务工厂实例 +var platformFactory = NewPlatformServiceFactory() + +// SyncPool 同步任务协程池,限制并发数避免goroutine爆炸 +var SyncPool = grpool.New(20) + +// CreateSyncTask 创建同步任务 +func (s *syncService) CreateSyncTask(ctx context.Context, req *dto.CreateSyncTaskReq) (*bson.ObjectID, error) { + task := &entity.SyncTask{ + Platform: req.Platform, + SyncType: req.SyncType, + Status: consts.SyncStatusPending, + AssetID: req.AssetID, + AssetSKUID: req.AssetSKUID, + StockID: req.StockID, + ErrorMessage: "", + ErrorCount: 0, + } + + err := dao.SyncTask.Insert(ctx, task) + if err != nil { + return nil, err + } + + return task.Id, nil +} + +// GetSyncTask 获取同步任务详情 +func (s *syncService) GetSyncTask(ctx context.Context, id *bson.ObjectID) (res *dto.GetSyncTaskRes, err error) { + task, err := dao.SyncTask.GetOne(ctx, id) + if err != nil { + return + } + res = &dto.GetSyncTaskRes{} + err = utils.Struct(task, &res.SyncTaskItem) + return +} + +// ListSyncTasks 获取同步任务列表 +func (s *syncService) ListSyncTasks(ctx context.Context, req *dto.ListSyncTaskReq) (list []*dto.SyncTaskItem, total int64, err error) { + tasks, total, err := dao.SyncTask.List(ctx, req) + if err != nil { + return + } + err = utils.Struct(tasks, &list) + return +} + +// UpdateSyncTaskStatus 更新同步任务状态 +func (s *syncService) UpdateSyncTaskStatus(ctx context.Context, req *dto.UpdateSyncTaskStatusReq) error { + return dao.SyncTask.UpdateStatus(ctx, req.ID, req.Status, req.ErrorMessage) +} + +// SyncAsset 同步资产 +func (s *syncService) SyncAsset(ctx context.Context, req *dto.SyncAssetReq) (*bson.ObjectID, error) { + // 创建同步任务 + taskReq := &dto.CreateSyncTaskReq{ + Platform: req.Platform, + SyncType: consts.SyncTypeIncremental, + AssetID: req.AssetID, + } + + taskID, err := s.CreateSyncTask(ctx, taskReq) + if err != nil { + return nil, err + } + + // 异步执行同步任务(使用协程池限制并发) + asyncCtx := context.WithoutCancel(ctx) + SyncPool.Add(asyncCtx, func(ctx context.Context) { + s.executeAssetSync(ctx, taskID, req.AssetID, req.Platform) + }) + + return taskID, nil +} + +// SyncAssetSku 同步资产SKU +func (s *syncService) SyncAssetSku(ctx context.Context, req *dto.SyncAssetSkuReq) (*bson.ObjectID, error) { + taskReq := &dto.CreateSyncTaskReq{ + Platform: req.Platform, + SyncType: consts.SyncTypeIncremental, + AssetSKUID: req.AssetSKUID, + } + + taskID, err := s.CreateSyncTask(ctx, taskReq) + if err != nil { + return nil, err + } + + // 异步执行同步任务(使用协程池限制并发) + asyncCtx := context.WithoutCancel(ctx) + SyncPool.Add(asyncCtx, func(ctx context.Context) { + s.executeAssetSkuSync(ctx, taskID, req.AssetSKUID, req.Platform) + }) + + return taskID, nil +} + +// SyncStock 同步库存 +func (s *syncService) SyncStock(ctx context.Context, req *dto.SyncStockReq) (*bson.ObjectID, error) { + taskReq := &dto.CreateSyncTaskReq{ + Platform: req.Platform, + SyncType: consts.SyncTypeIncremental, + StockID: req.StockID, + } + + taskID, err := s.CreateSyncTask(ctx, taskReq) + if err != nil { + return nil, err + } + + // 异步执行同步任务(使用协程池限制并发) + asyncCtx := context.WithoutCancel(ctx) + SyncPool.Add(asyncCtx, func(ctx context.Context) { + s.executeStockSync(ctx, taskID, req.StockID, req.Platform) + }) + + return taskID, nil +} + +// BatchSyncAssets 批量同步资产 +func (s *syncService) BatchSyncAssets(ctx context.Context, req *dto.BatchSyncAssetsReq) ([]*bson.ObjectID, error) { + var taskIDs []*bson.ObjectID + for _, assetID := range req.AssetIDs { + taskReq := &dto.CreateSyncTaskReq{ + Platform: req.Platform, + SyncType: consts.SyncTypeIncremental, + AssetID: assetID, + } + + taskID, err := s.CreateSyncTask(ctx, taskReq) + if err != nil { + return taskIDs, err + } + taskIDs = append(taskIDs, taskID) + + // 异步执行同步任务(使用协程池限制并发) + asyncCtx := context.WithoutCancel(ctx) + currentAssetID := assetID + SyncPool.Add(asyncCtx, func(ctx context.Context) { + s.executeAssetSync(ctx, taskID, currentAssetID, req.Platform) + }) + } + + return taskIDs, nil +} + +// GetPlatformSyncStatus 获取平台同步状态 +func (s *syncService) GetPlatformSyncStatus(ctx context.Context, req *dto.GetPlatformSyncStatusReq) (*dto.GetPlatformSyncStatusRes, error) { + // 统计各状态任务数量 + totalReq := &dto.ListSyncTaskReq{Platform: req.Platform} + totalReq.PageNum = 1 + totalReq.PageSize = 1 + _, total, err := dao.SyncTask.List(ctx, totalReq) + if err != nil { + return nil, err + } + + successReq := &dto.ListSyncTaskReq{Platform: req.Platform, Status: consts.SyncStatusSuccess} + successReq.PageNum = 1 + successReq.PageSize = 1 + _, successCount, err := dao.SyncTask.List(ctx, successReq) + if err != nil { + return nil, err + } + + failedReq := &dto.ListSyncTaskReq{Platform: req.Platform, Status: consts.SyncStatusFailed} + failedReq.PageNum = 1 + failedReq.PageSize = 1 + _, failedCount, err := dao.SyncTask.List(ctx, failedReq) + if err != nil { + return nil, err + } + + return &dto.GetPlatformSyncStatusRes{ + Platform: req.Platform, + IsEnabled: true, + SyncCount: total, + SuccessCount: successCount, + FailedCount: failedCount, + }, nil +} + +// executeAssetSync 执行资产同步 +func (s *syncService) executeAssetSync(ctx context.Context, taskID, assetID *bson.ObjectID, platform consts.SyncPlatform) { + // 更新任务状态为同步中 + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "") + + // 获取平台服务 + assetService := platformFactory.CreateAssetService(platform) + + // 执行同步 + err := assetService.SyncAsset(ctx, assetID) + if err != nil { + // 同步失败 + g.Log().Error(ctx, "资产同步失败", g.Map{ + "task_id": taskID, + "asset_id": assetID, + "platform": string(platform), + "error": err.Error(), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error()) + } else { + // 同步成功 + g.Log().Info(ctx, "资产同步成功", g.Map{ + "task_id": taskID, + "asset_id": assetID, + "platform": string(platform), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "") + } +} + +// executeAssetSkuSync 执行资产SKU同步 +func (s *syncService) executeAssetSkuSync(ctx context.Context, taskID, assetSkuID *bson.ObjectID, platform consts.SyncPlatform) { + // 更新任务状态为同步中 + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "") + + // 获取平台服务 + assetSkuService := platformFactory.CreateAssetSkuService(platform) + + // 执行同步 + err := assetSkuService.SyncAssetSku(ctx, assetSkuID) + if err != nil { + // 同步失败 + g.Log().Error(ctx, "资产SKU同步失败", g.Map{ + "task_id": taskID, + "asset_sku_id": assetSkuID, + "platform": string(platform), + "error": err.Error(), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error()) + } else { + // 同步成功 + g.Log().Info(ctx, "资产SKU同步成功", g.Map{ + "task_id": taskID, + "asset_sku_id": assetSkuID, + "platform": string(platform), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "") + } +} + +// executeStockSync 执行库存同步 +func (s *syncService) executeStockSync(ctx context.Context, taskID, stockID *bson.ObjectID, platform consts.SyncPlatform) { + // 更新任务状态为同步中 + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSyncing, "") + + // 获取平台服务 + stockService := platformFactory.CreateStockService(platform) + + // 执行同步 + err := stockService.SyncStock(ctx, stockID) + if err != nil { + // 同步失败 + g.Log().Error(ctx, "库存同步失败", g.Map{ + "task_id": taskID, + "stock_id": stockID, + "platform": string(platform), + "error": err.Error(), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusFailed, err.Error()) + } else { + // 同步成功 + g.Log().Info(ctx, "库存同步成功", g.Map{ + "task_id": taskID, + "stock_id": stockID, + "platform": string(platform), + }) + dao.SyncTask.UpdateStatus(ctx, taskID, consts.SyncStatusSuccess, "") + } +}