All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m18s
主要变更: - 重构分配模型:从加价模式(pricing_mode/pricing_value)改为返佣模式(base_commission + tier_commission) - 删除独立的 my_package 接口,统一到 /api/admin/packages(通过数据权限自动过滤) - 新增批量分配和批量调价功能,支持事务和性能优化 - 新增配置版本管理,订单创建时锁定返佣配置 - 新增成本价历史记录,支持审计和纠纷处理 - 新增统计缓存系统(Redis + 异步任务),优化梯度返佣计算性能 - 删除冗余的梯度佣金独立 CRUD 接口(合并到分配配置中) - 归档 3 个已完成的 OpenSpec changes 并同步 8 个新 capabilities 到 main specs 技术细节: - 数据库迁移:000026_refactor_shop_package_allocation - 新增 Store:AllocationConfigStore, PriceHistoryStore, CommissionStatsStore - 新增 Service:BatchAllocationService, BatchPricingService, CommissionStatsService - 新增异步任务:统计更新、定时同步、周期归档 - 测试覆盖:批量操作集成测试、梯度佣金 CRUD 清理验证 影响: - API 变更:删除 4 个梯度 CRUD 接口(POST/GET/PUT/DELETE /:id/tiers) - API 新增:批量分配、批量调价接口 - 数据模型:重构 shop_series_allocation 表结构 - 性能优化:批量操作使用 CreateInBatches,统计使用 Redis 缓存 相关文档: - openspec/changes/archive/2026-01-28-refactor-shop-package-allocation/ - openspec/specs/agent-available-packages/ - openspec/specs/allocation-config-versioning/ - 等 8 个新 capability specs
7.5 KiB
7.5 KiB
Context
Phase 1 完成了套餐系列和套餐的基础管理,但代理商还不能分销套餐。本期实现代理套餐分配机制,使上级代理能够:
- 为下级店铺分配可销售的套餐系列
- 通过加价模式设置下级的成本价
- 配置梯度佣金(基于销量/销售额的阶梯奖励)
当前代理层级结构:
- 店铺通过
Shop.parent_id维护层级关系 - 最多 7 级代理
- 数据权限通过
GetSubordinateShopIDs()递归查询
Goals / Non-Goals
Goals:
- 实现套餐系列级别的分配机制
- 支持固定金额和百分比两种加价模式
- 支持梯度佣金配置(月度/季度/年度/自定义时间范围)
- 代理能查看自己被分配的套餐及成本价
- 可选的单套餐级别成本价覆盖
Non-Goals:
- 不实现卡/设备的套餐系列关联(Phase 3)
- 不实现订单支付流程(Phase 4)
- 不实现佣金计算逻辑(Phase 5)
- 不支持跨级分配(只能分配给直属下级)
Decisions
1. 分配模型设计
决策:三个独立模型
// ShopSeriesAllocation 店铺套餐系列分配
type ShopSeriesAllocation struct {
gorm.Model
BaseModel
ShopID uint // 被分配的店铺 ID
SeriesID uint // 套餐系列 ID
AllocatorShopID uint // 分配者店铺 ID(上级)
PricingMode string // 加价模式: fixed-固定金额 percent-百分比
PricingValue int64 // 加价值(分或千分比)
OneTimeCommissionTrigger string // 一次性佣金触发类型: one_time_recharge-单次充值 accumulated_recharge-累计充值
OneTimeCommissionThreshold int64 // 一次性佣金触发阈值(分)
OneTimeCommissionAmount int64 // 一次性佣金金额(分)
Status int // 状态 1-启用 2-禁用
}
// ShopSeriesCommissionTier 梯度佣金配置
type ShopSeriesCommissionTier struct {
gorm.Model
BaseModel
AllocationID uint // 关联的分配 ID
TierType string // 梯度类型: sales_count-销量 sales_amount-销售额
PeriodType string // 周期类型: monthly-月度 quarterly-季度 yearly-年度 custom-自定义
PeriodStartDate *time.Time // 自定义周期开始日期
PeriodEndDate *time.Time // 自定义周期结束日期
ThresholdValue int64 // 阈值(销量或金额)
CommissionAmount int64 // 佣金金额(分)
}
// ShopPackageAllocation 店铺单套餐分配(可选覆盖)
type ShopPackageAllocation struct {
gorm.Model
BaseModel
ShopID uint // 被分配的店铺 ID
PackageID uint // 套餐 ID
AllocationID uint // 关联的系列分配 ID
CostPrice int64 // 覆盖的成本价(分)
Status int // 状态 1-启用 2-禁用
}
理由:
- 系列级别分配是主要方式,减少配置工作量
- 单套餐分配用于特殊场景(如某个套餐给特定代理优惠价)
- 梯度佣金独立模型,支持多档配置
2. 加价模式与成本价计算
决策:成本价 = 上级成本价 + 加价值
# 固定金额加价
下级成本价 = 上级成本价 + pricing_value
# 百分比加价(pricing_value 为千分比,如 100 = 10%)
下级成本价 = 上级成本价 × (1 + pricing_value / 1000)
理由:
- 基于上级成本价加价,确保每级都有利润空间
- 千分比精度满足业务需求(0.1% 精度)
- 平台作为顶级,其成本价 = Package.suggested_cost_price
约束:
- 下级成本价 ≥ 上级成本价(禁止负加价)
- 验证时需递归获取上级成本价
3. 成本价获取逻辑
决策:递归查询 + 缓存
func GetCostPrice(shopID, packageID uint) int64 {
// 1. 检查是否有单套餐覆盖
if override := GetPackageAllocation(shopID, packageID); override != nil {
return override.CostPrice
}
// 2. 获取系列分配
allocation := GetSeriesAllocation(shopID, package.SeriesID)
if allocation == nil {
return 0 // 未分配,不可购买
}
// 3. 获取上级成本价
parentCostPrice := GetParentCostPrice(allocation.AllocatorShopID, packageID)
// 4. 计算当前成本价
return CalculatePrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
}
理由:
- 单套餐覆盖优先级最高
- 递归到平台级别时,使用 Package.suggested_cost_price
- 可考虑缓存热点套餐的成本价(后续优化)
4. 梯度佣金周期计算
决策:支持固定周期和自定义周期
| PeriodType | 计算方式 |
|---|---|
| monthly | 当月 1 日 00:00 至月末 23:59:59 |
| quarterly | 当季度第一天至最后一天 |
| yearly | 当年 1 月 1 日至 12 月 31 日 |
| custom | PeriodStartDate 至 PeriodEndDate |
理由:
- 固定周期覆盖常见场景
- 自定义周期支持促销活动等特殊需求
5. API 设计
决策:RESTful + 嵌套资源
# 套餐系列分配
POST /api/admin/shop-series-allocations 为下级分配系列
GET /api/admin/shop-series-allocations 查询分配列表
GET /api/admin/shop-series-allocations/:id 分配详情
PUT /api/admin/shop-series-allocations/:id 更新分配
DELETE /api/admin/shop-series-allocations/:id 删除分配
PATCH /api/admin/shop-series-allocations/:id/status 启用/禁用
# 梯度佣金(嵌套在分配下)
POST /api/admin/shop-series-allocations/:id/tiers 添加梯度
GET /api/admin/shop-series-allocations/:id/tiers 梯度列表
PUT /api/admin/shop-series-allocations/:id/tiers/:tierId 更新梯度
DELETE /api/admin/shop-series-allocations/:id/tiers/:tierId 删除梯度
# 单套餐分配
POST /api/admin/shop-package-allocations 分配单套餐
GET /api/admin/shop-package-allocations 查询列表
PUT /api/admin/shop-package-allocations/:id 更新
DELETE /api/admin/shop-package-allocations/:id 删除
# 代理可售套餐
GET /api/admin/my-packages 查询我的可售套餐
GET /api/admin/my-packages/:id 套餐详情(含成本价)
Risks / Trade-offs
风险 1:递归成本价计算性能
风险:多级代理场景下,递归查询成本价可能较慢
缓解:
- 首期不做缓存,观察实际性能
- 如有问题,后续增加 Redis 缓存(按 shop_id + package_id 缓存)
- 缓存失效策略:分配变更时清除相关缓存
风险 2:分配一致性
风险:上级删除分配后,下级的分配关系如何处理
缓解:
- 删除分配时检查是否有下级依赖
- 如有下级依赖,禁止删除或级联禁用
- 本期采用禁止删除策略,要求先清理下级分配
风险 3:梯度佣金统计复杂度
风险:统计周期内的销量/销售额可能涉及大量数据
缓解:
- 佣金计算在 Phase 5 实现
- 可考虑定时任务预计算周期统计数据
- 本期只做配置,不做实际统计
Open Questions
-
是否支持批量分配?
- 当前设计:单个分配
- 待确认:是否需要批量为多个下级分配同一系列?
-
分配删除策略?
- 当前设计:有下级依赖时禁止删除
- 待确认:是否需要级联删除或级联禁用?
-
梯度佣金是否可叠加?
- 当前设计:达到最高档位只拿最高档佣金
- 待确认:是否需要累加所有达标档位的佣金?