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