diff --git a/openspec/changes/tech-debt-cleanup/.openspec.yaml b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/.openspec.yaml similarity index 100% rename from openspec/changes/tech-debt-cleanup/.openspec.yaml rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/.openspec.yaml diff --git a/openspec/changes/tech-debt-cleanup/design.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/design.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/design.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/design.md diff --git a/openspec/changes/tech-debt-cleanup/proposal.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/proposal.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/proposal.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/proposal.md diff --git a/openspec/changes/tech-debt-cleanup/specs/api-doc-coverage/spec.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/api-doc-coverage/spec.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/specs/api-doc-coverage/spec.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/api-doc-coverage/spec.md diff --git a/openspec/changes/tech-debt-cleanup/specs/dto-name-fields/spec.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/dto-name-fields/spec.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/specs/dto-name-fields/spec.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/dto-name-fields/spec.md diff --git a/openspec/changes/tech-debt-cleanup/specs/payment-dynamic-config/spec.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/payment-dynamic-config/spec.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/specs/payment-dynamic-config/spec.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/payment-dynamic-config/spec.md diff --git a/openspec/changes/tech-debt-cleanup/specs/polling-status-constants/spec.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/polling-status-constants/spec.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/specs/polling-status-constants/spec.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/specs/polling-status-constants/spec.md diff --git a/openspec/changes/tech-debt-cleanup/tasks.md b/openspec/changes/archive/2026-04-14-tech-debt-cleanup/tasks.md similarity index 100% rename from openspec/changes/tech-debt-cleanup/tasks.md rename to openspec/changes/archive/2026-04-14-tech-debt-cleanup/tasks.md diff --git a/openspec/specs/api-doc-coverage/spec.md b/openspec/specs/api-doc-coverage/spec.md new file mode 100644 index 0000000..a731876 --- /dev/null +++ b/openspec/specs/api-doc-coverage/spec.md @@ -0,0 +1,45 @@ +# API 文档完整性规范 + +## MODIFIED Requirements + +### Requirement: OpenAPI 文档 100% 覆盖所有路由接口 + +系统的 OpenAPI 文档应包含所有已注册的 HTTP 路由接口,覆盖率达到 100%。 + +#### Scenario: 文档注册所有 Handler + +- **WHEN** 系统启动或生成 OpenAPI 文档 +- **THEN** `cmd/api/docs.go` 中的 `bootstrap.Handlers` 结构体包含全部 48 个 Handler(包括 Account、AdminOrder、AgentRecharge、Asset、AssetWallet 等) +- **THEN** 每个 Handler 对应一个已注册的路由组(如 `/api/admin/accounts` → `handlers.Account`) +- **THEN** 生成的 OpenAPI 文档包含这 48 个 Handler 对应的全部接口 + +#### Scenario: 新增 Handler 时同步文档生成器 + +- **WHEN** 开发者在 `internal/router/` 中新增一个 Handler 并注册到路由 +- **THEN** 开发者必须同时在 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 中的 `bootstrap.Handlers` 结构体添加该 Handler 字段 +- **THEN** 若遗漏,代码审查应拒绝合并(检查清单项:**新增 Handler 时是否同步更新 docs.go/gendocs/main.go**) + +#### Scenario: OpenAPI 文档校验完整性 + +- **WHEN** 执行 `go run cmd/gendocs/main.go` 生成 OpenAPI 文档 +- **THEN** 文档应包含所有已注册的接口路由 +- **THEN** 无"缺失文档"的警告或错误信息 + +### Requirement: 文档生成器不遗漏 Handler + +文档生成器的 `bootstrap.Handlers` 结构体应显式列出所有 Handler,避免新增后遗漏。 + +#### Scenario: 完整的 Handler 清单 + +- **WHEN** 审阅 `cmd/api/docs.go` 的 `bootstrap.Handlers` 结构体定义 +- **THEN** 该结构体包含以下字段(至少 48 个,按模块分组): + ```go + Account *admin.AccountHandler + AdminOrder *admin.OrderHandler + AgentRecharge *agent.RechargeHandler + Asset *admin.AssetHandler + AssetWallet *admin.AssetWalletHandler + Authorization *admin.AuthorizationHandler + // ... 共 48 个 + ``` +- **THEN** 注释中标注每个 Handler 对应的路由前缀和功能模块 diff --git a/openspec/specs/dto-name-fields/spec.md b/openspec/specs/dto-name-fields/spec.md new file mode 100644 index 0000000..de35552 --- /dev/null +++ b/openspec/specs/dto-name-fields/spec.md @@ -0,0 +1,83 @@ +# Response DTO 规范规范 + +## MODIFIED Requirements + +### Requirement: Response DTO 必须包含状态文字字段 + +项目所有 Response DTO 中,若包含 int 类型状态字段,必须同时包含对应的 `_name` 或 `_text` 文字字段,用于显示该状态的中文描述。 + +#### Scenario: 账号列表 Response DTO + +- **WHEN** API 返回账号列表(`GET /api/admin/accounts`) +- **THEN** 响应中每个账号对象包含 `status` 字段(int,值:0=禁用,1=启用) +- **THEN** 响应中同时包含 `status_name` 字段(string,值:"禁用"或"启用") +- **THEN** 前端可直接使用 `status_name` 显示在 UI 上,无需维护单独的状态枚举映射表 + +#### Scenario: 资产详情 Response DTO + +- **WHEN** API 返回单个资产信息(`GET /api/admin/assets/:id`) +- **THEN** 响应包含 `status`(int)和 `status_name`(string) +- **WHEN** 资产包含多个状态字段(如 `network_status`、`activation_status`、`online_status`) +- **THEN** 每个状态字段对应一个文字字段:`network_status_name`、`activation_status_name`、`online_status_name` + +#### Scenario: 订单详情中的多重状态 + +- **WHEN** API 返回订单详情(`GET /api/admin/orders/:id`) +- **THEN** 订单对象包含 `payment_status`(int)和 `payment_status_name`(string) +- **THEN** 订单对象包含 `commission_status`(int)和 `commission_status_name`(string) +- **THEN** 若订单还有其他 int 类型状态字段,均配有对应的 `_name` 字段 + +### Requirement: DTO 文字字段命名规则 + +状态文字字段的命名应遵循统一规则:`{状态字段名}_name` 或 `{状态字段名}_text`。 + +#### Scenario: 标准命名 + +- **WHEN** 定义 DTO 时,状态字段为 `Status` +- **THEN** 文字字段命名为 `StatusName`(推荐)或 `StatusText` +- **WHEN** 状态字段为 `PaymentStatus` +- **THEN** 文字字段命名为 `PaymentStatusName` 或 `PaymentStatusText` + +#### Scenario: JSON 序列化一致性 + +- **WHEN** DTO 序列化为 JSON 返回给客户端 +- **THEN** 字段名使用 snake_case(符合项目 API 规范) + - Go 字段 `Status` → JSON `status` + - Go 字段 `StatusName` → JSON `status_name` + +### Requirement: Service 层自动赋值 `_name` 字段 + +Service 层在构建 Response DTO 时,应自动赋值 `_name`/`_text` 字段,映射状态常量到中文描述。 + +#### Scenario: 获取账号详情自动赋值 + +- **WHEN** `AccountService.GetAccount(ctx, accountID)` 被调用 +- **THEN** Service 查询数据库获取账号信息 +- **THEN** Service 构建 Response DTO,自动设置 `StatusName = constants.GetAccountStatusName(account.Status)` +- **THEN** 返回完整的 DTO 给 Handler,Handler 直接序列化响应 + +#### Scenario: 列表查询批量赋值 + +- **WHEN** `AccountService.ListAccounts(ctx, query)` 被调用 +- **THEN** Service 查询数据库获取账号列表 +- **THEN** Service 遍历每个账号,批量赋值 `StatusName` 字段 +- **THEN** 返回完整列表 + +### Requirement: 常量映射函数 + +在 `pkg/constants/` 中为每个业务模块定义 `Get{Module}StatusName(status int) string` 函数,用于映射状态值到中文描述。 + +#### Scenario: 账号状态映射函数 + +- **WHEN** Service 层需要获取账号状态的中文描述 +- **THEN** 调用 `constants.GetAccountStatusName(status)` +- **THEN** 函数返回: + - 若 `status == 0`,返回 `"禁用"` + - 若 `status == 1`,返回 `"启用"` + - 若状态值未知,返回 `"未知"`(不返回空字符串) + +#### Scenario: 订单支付状态映射函数 + +- **WHEN** Service 层需要获取订单支付状态描述 +- **THEN** 调用 `constants.GetOrderPaymentStatusName(status)` +- **THEN** 函数返回对应的中文(如 "待支付"、"已支付"、"已完成" 等) diff --git a/openspec/specs/payment-dynamic-config/spec.md b/openspec/specs/payment-dynamic-config/spec.md new file mode 100644 index 0000000..06b9524 --- /dev/null +++ b/openspec/specs/payment-dynamic-config/spec.md @@ -0,0 +1,69 @@ +# 支付配置动态加载能力规范 + +## ADDED Requirements + +### Requirement: 从支付配置 ID 动态加载支付实例 + +系统在处理支付验签(订单支付回调、充值确认等)时,应根据订单/充值记录中的 `payment_config_id` 字段动态加载对应的支付配置,而不是使用全局单例 `s.wechatPayment`。 + +#### Scenario: 订单支付回调验签 + +- **WHEN** 微信支付回调到达 `/api/callback/wechat`,系统解析回调中的商户号和订单数据 +- **THEN** 系统根据订单表中的 `payment_config_id` 从 Redis(TTL 1h)或数据库加载对应的支付配置 +- **THEN** 系统使用该配置的私钥和证书验签回调数据 +- **THEN** 若配置不存在或当前用户无权限访问该配置,返回 `{code: 1103, msg: "支付配置不存在或无权限"}` + +#### Scenario: 充值订单确认支付 + +- **WHEN** 代理用户在充值页面点击"确认支付",提交 `recharge_order_id` 和 `amount` +- **THEN** 系统根据充值订单表中的 `payment_config_id` 动态加载支付配置 +- **THEN** 系统调用该配置对应的支付 SDK 创建预支付单(微信 JSAPI 或 H5) +- **THEN** 若配置无效或超配额,返回 `{code: 1103, msg: "支付配置无效,请联系商户"}` + +### Requirement: 支付配置 Redis 缓存 + +系统应缓存支付配置到 Redis,减少数据库查询。 + +#### Scenario: 首次加载配置 + +- **WHEN** 调用 `PaymentConfigLoader.LoadConfig(ctx, configID)` 且 Redis 中不存在该配置 +- **THEN** 系统从数据库查询配置,存入 Redis(key: `payment:config:{configID}`,TTL: 1 小时) +- **THEN** 返回加载的配置对象 + +#### Scenario: 配置缓存命中 + +- **WHEN** 调用 `PaymentConfigLoader.LoadConfig(ctx, configID)` 且 Redis 中存在该配置 +- **THEN** 系统直接返回 Redis 中缓存的配置 +- **THEN** 不查询数据库 + +#### Scenario: 配置变更后清除缓存 + +- **WHEN** 支付配置被修改(通过管理后台) +- **THEN** 系统主动删除 Redis 中的该配置缓存(`DEL payment:config:{configID}`) +- **THEN** 下次加载时重新从数据库读取最新配置 + +### Requirement: 支付配置访问控制 + +系统在加载支付配置时,根据调用场景采用不同的校验策略: + +> **场景分类说明**: +> - **回调场景**:微信支付回调到达 `/api/callback/wechat`,请求来自微信服务器,无登录态,无用户身份上下文 +> - **用户操作场景**:代理用户在前端主动发起的支付相关操作(如创建充值单、查询支付配置等),有完整的登录态和用户上下文 + +#### Scenario: 回调场景 — 商户号一致性校验 + +- **WHEN** 微信支付回调到达,系统解析回调中的商户号(`mchid`) +- **THEN** 系统根据订单的 `payment_config_id` 加载配置,比较配置中存储的商户号与回调携带的商户号是否一致 +- **THEN** 若不一致,记录告警日志并拒绝处理,返回非 2xx 状态码(微信会重试) +- **NOTE** 此场景**不做用户身份鉴权**,无"代理 A/B"概念,只做商户号匹配验证 + +#### Scenario: 用户操作场景 — 归属权限校验 + +- **WHEN** 代理用户在前端主动操作(如创建充值单)需加载支付配置 +- **THEN** 系统检查该配置的归属(`shop_id` 或 `enterprise_id`)与当前登录用户是否匹配 +- **THEN** 若当前用户无权访问该配置,返回 `{code: 403, msg: "无权限访问该支付配置"}` + +#### Scenario: 平台用户无限制访问 + +- **WHEN** 平台管理员的操作需要加载任意支付配置 +- **THEN** 系统跳过归属权限检查,允许访问所有配置(仅限用户操作场景) diff --git a/openspec/specs/polling-status-constants/spec.md b/openspec/specs/polling-status-constants/spec.md new file mode 100644 index 0000000..936e1ef --- /dev/null +++ b/openspec/specs/polling-status-constants/spec.md @@ -0,0 +1,44 @@ +# 轮询触发日志状态常量规范 + +## ADDED Requirements + +### Requirement: 轮询手动触发日志状态常量 + +系统应在 `pkg/constants/polling.go` 中定义轮询手动触发日志的四种状态常量,避免硬编码字符串。 + +**常量定义**: +- `PollingManualTriggerStatusPending = "pending"` +- `PollingManualTriggerStatusProcessing = "processing"` +- `PollingManualTriggerStatusCompleted = "completed"` +- `PollingManualTriggerStatusCancelled = "cancelled"` + +#### Scenario: 轮询触发日志记录使用常量 + +- **WHEN** 系统记录轮询手动触发日志(插入、更新状态) +- **THEN** 系统使用 `constants.PollingManualTriggerStatusPending` 等常量,而不是直接写字符串 `"pending"` + +#### Scenario: 轮询触发日志查询使用常量 + +- **WHEN** 系统查询轮询日志(按状态筛选、状态转移判断等) +- **THEN** 系统使用常量进行比较,而不是硬编码 `status == "pending"` + +#### Scenario: 常量修改时自动重构 + +- **WHEN** 业务要求修改某个状态值(如 `"pending"` → `"awaiting"`) +- **THEN** 开发者修改 `pkg/constants/polling.go` 中的常量定义 +- **THEN** IDE 自动检测所有使用该常量的代码,支持一键重构替换 +- **THEN** 无需手动搜索和替换散落在各文件中的硬编码字符串 + +### Requirement: 轮询触发日志字段描述 + +轮询手动触发日志模型(`tb_polling_manual_trigger_log`)应包含 `status` 字段,并在 DTO 中加入 `status_name` 文字字段。 + +#### Scenario: 查询轮询触发日志返回状态文本 + +- **WHEN** API 返回轮询手动触发日志列表(`GET /api/admin/polling/manual-triggers`) +- **THEN** 响应中的 `status` 字段(int)对应状态常量值 +- **THEN** 响应中的 `status_name` 字段(string)显示该状态的中文描述 + - `"pending"` → `"待处理"` + - `"processing"` → `"处理中"` + - `"completed"` → `"已完成"` + - `"cancelled"` → `"已取消"`