--- name: 允许免费额度配置为 0 overview: 修复新用户免费额度配置无法设为 0 的问题:配置字段改为 *int64 以区分「未配置」与「显式 0」,未配置回落内置默认,显式 0 生效为 0。 todos: - id: change-setting-fields content: 将 UserDefaultQuotaSettingS 四字段改为 *int64 并更新注释 status: completed - id: fix-default-quota-value content: 改 defaultQuotaValue 为指针判空判据,允许 0 生效 status: completed dependencies: - change-setting-fields - id: sync-constants-comment content: 同步 constants.go 内置默认常量注释语义 status: completed dependencies: - fix-default-quota-value - id: sync-docs content: 同步 README、config.example.yaml 与接口文档说明 status: completed dependencies: - fix-default-quota-value - id: build-verify content: 执行 go build ./... 与 go vet ./... 验证编译 status: completed dependencies: - sync-constants-comment - sync-docs - id: review-config-usage content: 用 [subagent:code-explorer] 复核配置消费点与文档无旧语义残留 status: completed dependencies: - build-verify --- ## 产品概述 修复新用户免费额度初始化的配置语义缺陷:原有实现把「配置为 0」误判为「未配置」而回落内置默认,导致无法将某类免费额度配成 0(例如要求新用户 AI 推荐额度为 0、必须购买后才可用)。 ## 核心功能 - **区分「未配置」与「显式 0」**:`UserDefaultQuota` 四个配置字段改为指针类型,未配置(nil)回落内置默认 20/3/10/5,显式配置 0 则生效为 0 - **保持兜底能力**:整段配置缺失、单个键缺失、配置为负数三种情况仍回落内置默认,不破坏既有部署 - **文档同步**:README 配置项说明、config.example.yaml 注释、接口文档额度说明统一为「允许配 0,未配置回落默认」 - **行为不变**:四类额度的初始化流程、流水记录、唯一索引幂等逻辑完全不动 ## 技术栈 - Go 1.27,模块 `git.lzcwy.cn/zhengchiliu/zt-closet-backend` - 配置:`pkg/setting`(viper 只读 `configs/config.yaml`,`ReadSection` 走 `UnmarshalKey`/mapstructure) - 消费点唯一:`internal/service/quota.go` 的 `defaultQuotaValue`(grep 已确认无其它引用) ## 实现方案 采用**指针字段区分三态**:把 `UserDefaultQuotaSettingS` 的 `Cos/AI/OutfitShare/ClosetShare` 由 `int64` 改为 `*int64`。viper 解码时键存在则分配指针(含 0),键不存在则保持 nil,从而在类型层面区分「未配置」与「配置为 0」。 取值判据(`defaultQuotaValue`): - `cfg == nil`(整段缺失)→ 内置默认 - `cfg.X == nil`(该键缺失)→ 内置默认 - `*cfg.X >= 0` → 用配置值(**0 生效,本次修复目标**) - `*cfg.X < 0` → 内置默认(防御:负数会让 `total` 为负,破坏额度校验) 关键决策与取舍: 1. **用 `*int64` 而非把判据改成 `>= 0`**:后者能让 0 生效,但「键缺失」解码后同样是 0,会静默把兜底默认值变成 0——对现有未显式配置的环境是破坏性变更。指针方案零歧义,代价仅是多一层判空。 2. **用指针而非 `viper.IsSet` 逐键查询**:配置结构仍由结构体承载,`ReadSection` 用法与其它配置段一致,不引入第二套读取方式,符合现有架构。 3. **负数回落而非报错**:保持 best-effort 风格(与额度初始化失败仅告警不阻断一致),不引入启动期配置校验的新失败路径。 4. **保留内置常量值 20/3/10/5 不变**:只改判据与注释,不动 `ensureDefaultQuota` 的创建+流水事务逻辑。 性能与可靠性:该路径仅在首次登录注册时执行 4 次,指针判空为 O(1) 无额外开销;风险点仅为指针解引用,通过判空分支覆盖,并有 `go build`/`go vet` 兜底。 ## 实现要点(防回归) - 改字段类型后必须全量编译,`global.UserDefaultQuotaSetting` 的其它消费点若有解引用会直接编译报错(当前仅 quota.go 一处)。 - 判空顺序必须为 `cfg != nil && cfg.X != nil && *cfg.X >= 0`,避免 nil 解引用 panic。 - 注释需同步:结构体注释「值 <=0 视为未设置」、`constants.go:42`「配置缺省或 <=0 时回落」、`defaultQuotaValue` 函数注释,三处都要改成新语义,否则后续维护者会按旧语义改回去。 - 不要扩大范围:`Upload.MaxSizeMB`、`MattingModel.MaxRetry` 等其它 `<=0` 兜底语义不同(0 无业务意义),本次不动,仅在结论中提示。 - 不改动 `configs/config.yaml`(用户本地文件,已被 .gitignore 忽略)。 ## 目录结构 ``` pkg/setting/section.go # [MODIFY] UserDefaultQuotaSettingS 四字段 int64 → *int64; # 注释改为「nil=未配置回落默认;非 nil 生效,允许 0」 internal/service/quota.go # [MODIFY] defaultQuotaValue 判据改为指针判空 + >=0 生效; # 函数注释同步(未配置/负数回落,0 生效) internal/service/constants.go # [MODIFY] 第 42 行注释「配置缺省或 <=0 时回落」→「未配置(nil)或负数时回落」 README.md # [MODIFY] 第 195 行配置表说明改为「未配置回落内置默认 20/3/10/5;可显式配 0」 configs/config.example.yaml # [MODIFY] UserDefaultQuota 段注释补充「可为 0 表示不赠送;留空则回落内置默认」 接口文档.md # [MODIFY] 第 488/1432/1446 行及 5.2 节额度表格补「可由 UserDefaultQuota 配置,允许为 0」 ```