Files
zt-closet-backend/.codebuddy/plans/修复_redismodel_redisdao_语义与健壮性问题_P1_P2__832261d9.md
2026-09-11 01:41:52 +08:00

4.9 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
修复 redismodel/redisdao 语义与健壮性问题(P1+P2) 修复 redismodel/redisdao 内部的日期类型不一致、Quota 缓存语义不清,以及扣减/读取的健壮性边界问题,不修改 middleware。
id content status
fix-quota-key-defs 统一日期类型并重命名 redismodel/quota.go 的剩余配额 key completed
id content status dependencies
fix-quota-dao 修改 redisdao/quota.go 函数签名、剩余配额语义与 Lua 扣减逻辑 completed
fix-quota-key-defs
id content status
fix-notification-decr 用 Lua 脚本修复 notification.go 未读数扣减防负 completed
id content status dependencies
verify-build 运行 go build、go vet 与 lint 验证编译及诊断通过 completed
fix-quota-key-defs
fix-quota-dao
fix-notification-decr

产品概述

针对 redismodel 与 redisdao 二次校验后发现的 P1(语义/一致性)与 P2(健壮性边界)问题进行修复,仅限这两层内部改动,不动 middleware。

核心修复内容

  • P1.1 日期类型统一:quota 模块每日计数 key 的日期由 %s(string)统一为 %d(int64 YYYYMMDD),与 horoscope 及 mysqlmodel 的 StatDate int64 对齐。
  • P1.2 Quota 缓存语义澄清:将 KeyQuotaTotal/QuotaTotalSet/Get/Decr 重命名为 KeyQuotaRemain/QuotaRemainSet/Get/Decr,明确 Redis 存的是「剩余可用配额」而非「总配额」,消除与 mysqlmodel.Quota(Total + Used)的语义错位。
  • P2.1 扣减原子化与防负:QuotaRemainDecr 改用 Lua 脚本原子扣减,key 不存在时不产生负数脏 key,剩余不足时不扣减。
  • P2.2 每日计数未命中返回 0:quotaDailyValue 对 redis.Nil 返回 0, nil,与 NotificationUnreadGet 行为一致。
  • P2.3 未读计数扣减防负:NotificationUnreadDecr 改用 Lua 脚本保证结果不低于 0。

技术栈

  • 语言:Go
  • Redis 客户端:github.com/redis/go-redis/v9
  • 仅修改 3 个文件,沿用现有 redismodel(key 常量)+ redisdao(操作函数)分层与 redis.NewScript Lua 脚本模式。

实现方案

P1.1 日期类型统一为 int64

internal/redismodel/quota.go 三个每日 key 的 %s 改为 %d,注释标注 date(YYYYMMDD);internal/redisdao/quota.go 中 6 个 QuotaDaily* 函数签名 date string 改为 date int64。fmt.Sprintf 的 %d 直接接收 int64,无其它改动。

P1.2 配额语义重命名为「剩余配额」

  • redismodel/quota.go:KeyQuotaTotal 重命名为 KeyQuotaRemain,key 由 quota:total:%d:%d 改为 quota:remain:%d:%d,注释改为「剩余可用配额」。
  • redisdao/quota.go:QuotaTotalSet/Get/Decr 重命名为 QuotaRemainSet/Get/Decr,QuotaRemainSet 形参 total 改为 remain,函数注释同步说明存的是「剩余可用配额」。

P2.1 配额扣减原子化(Lua)

新增 quotaRemainDecrScript,QuotaRemainDecr 通过脚本执行,单次 RTT、Redis 端原子,避免并发超扣与脏 key:

local v = redis.call('GET', KEYS[1])
if not v then return -1 end
v = tonumber(v)
local d = tonumber(ARGV[1])
if v < d then return -2 end
return redis.call('DECRBY', KEYS[1], d)

返回值契约:

  • -1:配额未初始化(key 不存在)
  • -2:剩余配额不足
  • >= 0:扣减成功后的剩余值

P2.2 每日计数未命中返回 0

quotaDailyValue 改为:

v, err := global.RedisEngine.Get(ctx, key).Int64()
if err == redis.Nil {
    return 0, nil
}
return v, err

P2.3 未读数扣减防负(Lua)

NotificationUnreadDecr 改用脚本,保证结果不低于 0:

local v = redis.call('GET', KEYS[1])
if v and tonumber(v) > 0 then return redis.call('DECRBY', KEYS[1], 1) end
return 0

目录结构

internal/
├── redismodel/
│   └── quota.go        # [MODIFY] 每日 key %s→%d;KeyQuotaTotal→KeyQuotaRemain(key quota:remain:%d:%d),注释澄清语义
└── redisdao/
    ├── quota.go        # [MODIFY] 6 个 QuotaDaily* date→int64;QuotaTotal*→QuotaRemain* 重命名;QuotaRemainDecr 用 Lua;quotaDailyValue 未命中返回 0
    └── notification.go # [MODIFY] NotificationUnreadDecr 用 Lua 防负

实施要点

  • 影响范围可控:已确认 QuotaDaily*/QuotaTotal* 及 KeyQuotaDaily*/KeyQuotaTotal 无业务层调用方,重命名与签名变更自包含、无连带改动。
  • 兼容性:这些 key 均为新增未上线代码,key 字符串变更(quota:total→quota:remain、日期 %s→%d)不涉及存量数据迁移。
  • 性能:Lua 脚本均为单次往返、Redis 端原子执行,与既有 quotaDailyIncrScript/unlockScript 风格一致,无额外开销。
  • 日志:本次改动不引入日志;错误沿现有 error 返回值上抛,由上层统一处理。