--- name: 额度体系重构:每日额度+凭证有效期+广告激励 overview: 重构额度体系:引入统一额度凭证表(quota_grants)承载会员包/加量包/广告激励三类有有效期赠送;AI/Outfit/Closet 改为每日重置额度,Cos 保持永久库存;新增广告激励发放接口与加量包有效期配置。 todos: - id: dao-migration content: 新增 quota_grants 凭证表与 goods.valid_days 迁移,扩展模型与 DAO(凭证 CRUD/汇总/扣减) status: completed - id: redis-daily content: 实现 Redis 每日额度原子限流:带限额 Lua 脚本及 consume/refund/value 封装 status: completed - id: service-core content: 实现 Service 层额度核心:getEffectiveQuota、consumeDailyQuota、refundDailyQuota、Cos 凭证消费 status: completed dependencies: - dao-migration - redis-daily - id: biz-switch content: 切换 AI/Outfit/Closet/Cos 业务使用点为每日额度与凭证消费 status: completed dependencies: - service-core - id: order-benefit content: 改造订单权益发放/退款/补偿链路为凭证模型,仅保留 VIP 实际发放 status: completed dependencies: - dao-migration - service-core - id: ad-reward content: 新增广告激励发放接口(服务/路由/DTO/幂等),创建当天有效凭证 status: completed dependencies: - dao-migration - service-core - id: info-config-doc content: 更新额度信息接口响应、配置语义注释与 README/接口文档 status: completed dependencies: - biz-switch - order-benefit - ad-reward --- ## 产品概述 将当前 Cos/AI/Outfit/Closet 四类统一永久累计的额度体系,重构为「永久额度 + 每日额度 + 有有效期赠送凭证」的混合模型,并新增广告激励额度来源。 ## 核心功能 **免费基础额度** - Cos 免费上传额度(配置 20):永久,不每天重置。 - AI/Outfit/Closet 免费额度(配置 3/3/0):每日额度,每天 0 点重置;ClosetShare 为 0 表示新用户无免费衣柜分享次数。 **有有效期赠送额度(统一凭证模型)** - 会员包赠送:随 VIP 有效期,VIP 续期时自动延长(跟随 vip_expire 动态变化)。 - 加量包赠送:独立有效期,每个商品可配置 valid_days 天数,到期失效。 - 广告激励:可配置多类额度(AI/Cos 等),当天有效(当天 23:59:59 到期),后端新增发放接口并幂等防重复领取。 **额度计算与消耗** - Cos:有效总额度 = 免费配置 + 有效凭证剩余(amount-used);used 永久累计,凭证按先到期先用。 - AI/Outfit/Closet:每日总额度 = 免费配置 + 有效凭证总额;当日已用每天 0 点清零。 - 消耗顺序:免费与赠送合并为单一额度池,无需区分顺序。 **额度信息接口** - Cos 返回永久总额度/累计已用/剩余;AI/Outfit/Closet 返回每日总额度/当日已用/当日剩余。 ## 技术栈 - 后端:Go + Gin(复用现有) - 数据层:GORM + MySQL(复用现有) - 缓存与原子计数:Redis + go-redis(复用现有,新增带限额 Lua 脚本) - 迁移:golang-migrate(复用现有 pkg/migration 机制) ## 实现方案 ### 核心策略:统一额度凭证表 quota_grants 新增凭证表承载所有「有有效期赠送」来源(会员包/加量包/广告激励),不再把赠送额度累加进 user_quotas.total,而是每次查询/消费时动态计算有效总额度: - Cos 有效总额 = 免费配置 + SUM(有效凭证 amount - used) - AI/Outfit/Closet 每日总额 = 免费配置 + SUM(有效凭证 amount) 「有效凭证」= expire_time > now,或 expire_time=0 且 user.vip_expire > now(会员包跟随 VIP)。 ### 分渠道消费 - Cos(永久库存):MySQL 事务 + 凭证维度扣减(先到期先用,减少过期浪费),写 quota_flow_logs。 - AI/Outfit/Closet(每日额度):Redis Lua 原子限流(读当日计数 → 判断小于每日总额 → INCR → 首次设置 48h TTL),MySQL user_daily_usages 保留为统计与兜底。 ### 为什么这样设计 - 赠送额度不落库为 total,动态计算天然跟随 VIP 状态、加量包有效期与退款状态,消除过期回收任务与账实不符风险。 - Cos 需永久累计 used 并写流水,走 MySQL 事务;每日额度需高频原子增减与每日清零,走 Redis INCR + TTL。 - 复用现有 user_daily_usages 表与 QuotaDaily* Redis 半成品,只补齐「带限额原子校验」与业务接入。 ### 并发与一致性 - 每日额度:Redis Lua 原子完成校验+计数,保证不超发;业务失败 refundDailyQuota(DECR 且不小于 0)。 - Cos:事务内凭证行锁/条件更新,动态限额作为参数传入,避免并发超扣。 - 广告激励:ref_id 唯一索引防重复领取。 - 退款:将对应订单凭证失效/删除(未用部分回收),VIP 走现有 revokeVIP。 ### 性能 - SUM 有效凭证已有 user_id+quota_type+expire_time 索引可支撑;商品与单用户凭证量小,直接聚合开销可控。 - 每日总额度可加 Redis 当日缓存(key 带 YYYYMMDD,TTL 到当天结束),订单支付/退款、VIP 变更时失效;作为增量优化。 ## 架构设计 ```mermaid flowchart TD subgraph 额度查询/消费 Q[GET /quota/info] --> E[getEffectiveQuota] C1[AI/Outfit/Closet 业务] --> D[consumeDailyQuota] C2[Cos 上传] --> P[consumeCosQuota] A[广告激励接口] --> G[写 quota_grants 当天凭证] end E --> V{vip_expire 或凭证 expire_time} V --> S[SUM 有效 quota_grants] S --> T[每日/永久总额度] D --> L[Redis Lua 原子限流] L --> M[user_daily_usages 统计] P --> TX[MySQL 事务凭证扣减] TX --> FLOW[写 quota_flow_logs] G --> QGRANT[quota_grants] ``` ## 目录结构 ``` internal/ ├── mysqlmodel/ │ ├── quota.go # [MODIFY] 新增 QuotaGrant 凭证模型;Quota 注释语义更新 │ └── goods.go # [MODIFY] 新增 ValidDays 字段 ├── mysqldao/ │ ├── quota.go # [MODIFY] 凭证 CRUD/汇总/扣减;Cos 动态限额消费 │ └── goods.go # [MODIFY] ValidDays 相关查询/更新 ├── service/ │ ├── service.go # [MODIFY] getEffectiveQuota、consumeDailyQuota、refundDailyQuota、Cos 消费改造 │ ├── constants.go # [MODIFY] 额度来源/有效期常量补充 │ ├── quota.go # [MODIFY] ensureDefaultQuota 调整;InfoGetBySelf 返回每日额度 │ ├── airecommend.go # [MODIFY] AI 消费改 consumeDailyQuota │ ├── outfit.go # [MODIFY] OutfitShare 消费改 consumeDailyQuota │ ├── closet.go # [MODIFY] ClosetShare 消费改 consumeDailyQuota │ ├── clothes.go # [MODIFY] Cos 消费适配凭证扣减 │ ├── order.go # [MODIFY] 发放/退款改写凭证,不再 grantQuota 累加 total │ ├── benefit_compensation.go # [MODIFY] 额度类补偿任务兼容 │ └── adreward.go # [NEW] 广告激励发放服务(按类型、当天有效、幂等) ├── routers/ │ ├── api/v1/adreward.go # [NEW] 广告激励发放 handler │ └── router.go # [MODIFY] 注册广告激励路由 ├── redisdao/ │ └── quota.go # [MODIFY] 带限额 Lua 脚本 + DailyQuotaConsume/Refund/Value ├── redismodel/ │ └── quota.go # [MODIFY] 每日额度 key 定义复用/补充 └── dto/ ├── quota.go # [MODIFY] 响应结构区分永久/每日额度语义 ├── goods.go # [MODIFY] 响应增加 valid_days └── adreward.go # [NEW] 广告激励发放请求/响应 pkg/ ├── migration/ # [NEW] quota_grants 表、goods.valid_days、广告激励相关索引 └── setting/section.go # [MODIFY] 配置语义注释/新增广告激励配置 global/setting.go # [MODIFY] 配置解析补充 configs/config.example.yaml # [MODIFY] 配置语义注释 README.md # [MODIFY] 额度语义说明 接口文档.md # [MODIFY] /quota/info 与广告激励接口文档 ``` ## 关键代码结构 ``` // internal/mysqlmodel/quota.go // QuotaGrant 有有效期额度凭证(会员包/加量包/广告激励) type QuotaGrant struct { Model UserID int64 `gorm:"column:user_id;index:idx_grant_user_type_expire,priority:1"` QuotaType int8 `gorm:"column:quota_type;index:idx_grant_user_type_expire,priority:2"` Amount int64 `gorm:"column:amount"` // 赠送总量 Used int64 `gorm:"column:used"` // 已用(Cos 凭证扣减用) Source int8 `gorm:"column:source"` // 1=会员包 2=加量包 3=广告激励 ExpireTime int64 `gorm:"column:expire_time;index:idx_grant_user_type_expire,priority:3"` // 0=跟随VIP RefID string `gorm:"column:ref_id;uniqueIndex:uk_grant_ref"` } ``` ``` // internal/service/service.go // getEffectiveQuota 返回某类额度有效总额度(Cos 永久库存 / 每日额度上限) func getEffectiveQuota(ctx context.Context, userID int64, quotaType int8) (int64, *errcode.Error) // consumeDailyQuota 原子扣减 AI/Outfit/Closet 当日额度 func consumeDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64, reason string) *errcode.Error // refundDailyQuota 业务失败时回退当日计数 func refundDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64) ``` ``` // internal/dto/adreward.go type AdRewardGrantRequest struct { RewardType string `json:"reward_type" binding:"required"` // ai/cos/... 可配置映射到 quota_type Amount int64 `json:"amount" binding:"required,min=1"` RefID string `json:"ref_id" binding:"required"` // 广告记录ID,幂等键 } ```