10 KiB
10 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 额度体系改造:每日额度与VIP有效期 | 重构额度体系:AI/Outfit/Closet 三类改为每日重置额度(每日总额度 = 免费配置 + 购买赠送,购买赠送随 VIP 有效期失效);Cos 保持永久累计,但购买赠送的 Cos 额度也随 VIP 过期失效。 |
|
产品概述
将当前「AI 推荐、社区分享(OutfitShare)、衣柜分享(ClosetShare)、上传(Cos)」四类统一永久累计的额度体系,改造为混合额度模型:
- Cos(上传):永久额度,不每日重置;免费部分永久,购买赠送部分随 VIP 有效期生效
- AI / Outfit / Closet:每日额度,每天 0 点重置
核心功能
- Cos 永久额度:总额度 = 免费永久额度(配置 20)+ 购买赠送额度(仅 VIP 有效期内计入);已用量永久累计,删除衣物/处理失败时回退。
- AI / Outfit / Closet 每日额度:每日总额度 = 免费每日额度(配置 AI:3 / OutfitShare:3 / ClosetShare:0)+ 购买赠送额度(仅 VIP 有效期内计入);当日已用每天 0 点自动清零;ClosetShare:0 表示新用户无免费衣柜分享次数,只能靠购买赠送获得。
- 购买赠送额度随 VIP 过期失效:用户 VIP 过期后,购买赠送的 Cos/AI/Outfit/Closet 额度全部失效;重新成为会员后恢复。
- 消耗顺序:免费额度与购买赠送额度合并为单一额度池,无需区分消耗顺序(用户已确认「都可以」)。
- 额度信息接口:
GET /api/v1/quota/info对 Cos 返回永久总额度/累计已用/剩余;对 AI/Outfit/Closet 返回每日总额度/当日已用/当日剩余。
技术栈
- 后端语言/框架:Go + Gin(复用现有)
- 数据层:GORM + MySQL(复用现有)
- 缓存/原子计数:Redis + go-redis(复用现有,新增 Lua 原子限流脚本)
- 迁移:golang-migrate(复用现有迁移机制)
实现方案
核心策略:赠送额度动态计算、不落库
不再通过 grantQuota 把购买赠送额度累加进 user_quotas.total,而是每次查询/消费时动态计算有效总额度:
每日总额度(AI/Outfit/Closet) = 免费配置值 + (VIP有效 ? SUM(有效订单对应赠送额度) : 0)永久总额度(Cos) = 免费配置值 + (VIP有效 ? SUM(有效订单 cos_quota) : 0)
「有效订单」= order_status = paid 且未退款(退款后状态变为 refunded 自动排除);VIP 有效性用 user.vip_expire > now 判断。免费底值直接读配置,不再依赖被历史数据污染过的 user_quotas.total。
分渠道消费机制
- Cos(永久额度):继续走 MySQL 事务条件更新
used,但将total上限从表字段改为动态计算后作为参数传入,避免 total 被历史购买赠送污染。 - AI / Outfit / Closet(每日额度):用 Redis Lua 脚本原子完成「当日已用 + 1 且不超过每日总额度」的校验与计数,key 按
user_id:YYYYMMDD区分并带 48h TTL 跨天自然过期;MySQLuser_daily_usages保留作为统计与 Redis 故障兜底。
为什么这样设计
- 不落库赠送额度:赠送额度与 VIP 有效期强耦合,落库后需要额外的过期回收任务且易产生账实不符;动态计算天然跟随 VIP 状态与订单退款状态,消除过期/退款回收的复杂度和资损窗口。
- Cos 用 MySQL 事务、每日额度用 Redis 原子计数:Cos 的
used需要永久累计并写流水,适合事务;每日额度需要高频原子增减与每日清零,Redis INCR + TTL 天然适配,避免每日重置的定时任务。 - 复用现有半成品:
user_daily_usages表、recordDailyUsage、QuotaDaily*Redis 函数均已存在,只需补上「带限额的原子校验」与业务接入。
关键并发与一致性
- 每日额度消费:Redis Lua 脚本内完成「读当日计数 → 判断 < 每日总额度 → INCR → 首次设置 TTL」,保证不超发、并发安全。
- 业务失败回退:AI 推荐失败、分享创建失败等沿用现有
refundQuota思路,新增refundDailyQuota(Redis DECR 并保证不小于 0)。 - Cos 消费:在事务内先读取动态总额度,再条件更新
used + n <= limit,避免并发超扣;沿用现有「流水与数值同事务」的强一致模式。 - 幂等:购买赠送额度不再创建额度发放流水,
BenefitGrantTask中 Cos/AI/Outfit/Closet 四类改为「记录支付完成但不实际发放」,只保留 VIP 实际发放。
性能考量
- 动态总额度需要
SUM(orders.xxx_quota),已存在索引idx_orders_user_status(user_id, order_status)可支撑;商品 SKU 少、单用户订单量小,直接聚合开销可控。 - 为避免高频消费时反复 SUM + 读用户,可在 Redis 增加「当日总额度缓存」(key 带 YYYYMMDD,TTL 到当天结束),订单支付/退款、VIP 变更时失效;实现时作为可选项,先以直接计算为主、缓存优化为增量。
架构设计
flowchart TD
subgraph 额度查询/消费入口
Q[GET /quota/info] --> E[getEffectiveQuota]
C1[AI/Outfit/Closet 业务] --> D[consumeDailyQuota]
C2[Cos 上传] --> P[consumeCosQuota]
end
E --> V{vip_expire > now?}
V -->|是| S[SUM 有效订单赠送额度]
V -->|否| F[免费配置值]
S --> T[每日/永久总额度]
D --> L[Redis Lua 原子限流<br/>当日计数 +1 且 <= 总额度]
L --> M[user_daily_usages 统计]
P --> TX[MySQL 事务<br/>used + 1 且 <= 动态总额度]
TX --> FLOW[写 quota_flow_logs]
T --> Q
目录结构
internal/
├── service/
│ ├── service.go # [MODIFY] 新增 getEffectiveQuota、consumeDailyQuota、refundDailyQuota;改造 Cos consumeQuota 支持动态总额度
│ ├── constants.go # [MODIFY] 更新额度语义注释,补充每日额度/永久额度常量说明
│ ├── quota.go # [MODIFY] ensureDefaultQuota 仅初始化 Cos 底值;InfoGetBySelf 返回每日额度信息
│ ├── airecommend.go # [MODIFY] AI 消费改 consumeDailyQuota,失败回退 refundDailyQuota
│ ├── outfit.go # [MODIFY] OutfitShare 消费改 consumeDailyQuota
│ ├── closet.go # [MODIFY] ClosetShare 消费改 consumeDailyQuota
│ ├── clothes.go # [MODIFY] Cos 消费适配动态总额度(调用点基本不变,内部语义调整)
│ ├── order.go # [MODIFY] grantOrderBenefits/revokeOrderBenefits 不再发放/回退四类额度,仅保留 VIP;doGrantBenefit 对额度类直接置成功
│ └── benefit_compensation.go # [MODIFY] 额度类补偿任务兼容(不发放直接成功)
├── mysqldao/
│ ├── order.go # [MODIFY] 新增 OrderGrantedQuotaSumByUserID:SUM 有效订单四类赠送额度
│ └── quota.go # [MODIFY] 新增 Cos 动态总额度消费方法(传入 limit 条件更新)
├── mysqlmodel/
│ └── quota.go # [MODIFY] 模型注释更新(total 对 Cos 语义调整,AI/Outfit/Closet 不再使用)
├── redisdao/
│ └── quota.go # [MODIFY] 新增带限额 Lua 脚本及 DailyQuotaConsume/Refund/Value 封装
├── redismodel/
│ └── quota.go # [MODIFY] 复用/补充每日额度 key 定义
└── dto/
└── quota.go # [MODIFY] QuotaItemResponse 字段语义注释(Cos 永久 vs 每日),按 quota_type 区分含义
pkg/
└── setting/
└── section.go # [MODIFY] UserDefaultQuotaSettingS 注释更新(AI/Outfit/Closet 为每日免费额度)
configs/
└── config.example.yaml # [MODIFY] UserDefaultQuota 段注释更新(Cos 永久、AI/Outfit/Closet 每日)
README.md # [MODIFY] 额度语义说明同步
接口文档.md # [MODIFY] /quota/info 返回字段语义同步
关键代码结构
// internal/service/service.go
// getEffectiveQuota 返回某类额度的当日/永久有效总额度:
// Cos = 免费配置 + (VIP有效 ? 订单赠送cos : 0);AI/Outfit/Closet = 免费配置 + (VIP有效 ? 订单赠送对应值 : 0)
func getEffectiveQuota(ctx context.Context, userID int64, quotaType int8) (int64, *errcode.Error)
// consumeDailyQuota 原子扣减 AI/Outfit/Closet 当日额度:Redis Lua 校验限额 + INCR
func consumeDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64, reason string) *errcode.Error
// refundDailyQuota 业务失败时回退当日已用计数(DECR 且不小于 0)
func refundDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64)
-- internal/redisdao/quota.go 中的限流 Lua(伪代码示意)
-- KEYS[1]=daily count key, ARGV[1]=limit, ARGV[2]=ttl
local v = tonumber(redis.call('GET', KEYS[1]) or '0')
if v >= tonumber(ARGV[1]) then return -1 end
local n = redis.call('INCR', KEYS[1])
if n == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end
return n
// internal/mysqldao/order.go
// OrderGrantedQuotaSum 用户有效订单(order_status=paid)四类赠送额度汇总
type OrderGrantedQuotaSum struct {
CosQuota int64
AIRecommendQuota int64
OutfitShareQuota int64
ClosetShareQuota int64
}
func (d *OrderDao) OrderGrantedQuotaSumByUserID(ctx context.Context, userID int64) (*OrderGrantedQuotaSum, error)