Files
zt-closet-backend/.codebuddy/plans/额度体系重构_每日额度_凭证有效期_广告激励_defdda63.md
T
2026-09-11 01:41:52 +08:00

214 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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,幂等键
}
```