Files
junhong_cmp_fiber/docs/engineering/工程约束.md
break aab56a6998
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 14m13s
feat(轮询优先队列): AUG26-016 卡轮询优先队列、人工入队与读侧接口,归档并同步主 Spec 与证据矩阵
新增 000228 成对迁移 tb_polling_priority_item:卡、任务类型、状态、触发类型、来源订单/套餐使用记录、
触发次数与来源集合、尝试次数、失败原因、人工原因与操作者、店铺快照与各时间列;以活动项部分唯一索引
uq_polling_priority_item_active(仅 deleted_at IS NULL AND status IN ('pending','processing') 占键位)
表达「同卡同任务类型至多一条活动项」,另有状态/时间索引与全列注释;down 守卫在存在活动项或未终态行时
拒绝回滚并给出中文原因。

新增优先轮询请求可靠事件 polling.priority.requested(载荷版本 v1、事件键前缀 prio:)与消费者:只在原
业务事务内追加、幂等键稳定;消费者按卡 × 纳入任务类型(realname/carddata/card_status/package)逐条
建项并在提交后下发执行提示,重复投递只合并触发次数、来源集合与最近触发时间,不新建行也不重复调用。
触发点为四类自动场景 purchase_activated / renewal_activated(按同载体更早套餐使用记录判定)/
queue_activated / addon_activated 与「无有效套餐」no_valid_package(仅在普通套餐轮询来源且存在待生效
套餐使用记录时追加;事件通道显式拒绝 manual_trigger);入队对象恒为卡,绑定设备资产在触发事务内冻结
在用卡快照逐卡建项,不使用设备当前卡槽口径。

轮询共享基类新增认领接缝:四个 Handler(realname/carddata/card_status/package)在并发信号量之后、调用
上游之前探测活动项——待执行条件认领、执行中且 90 秒租约未到期则跳过并延后、无活动项时行为与既有完全
等价;超租约允许相邻执行接管,尝试次数只在真正发起执行后累加,未达上限(3)回到活动态按既有间隔重排,
达上限或业务校验类失败进入失败终态并保留可安全展示原因;执行前校验卡自身与绑定设备的轮询开关。未引入
通用卡级锁与 Redis 活动标记,分片队列的出队、入队与移除路径未改动。

提示通道按任务类型独立键(polling:priority:{taskType}),与既有手动触发队列分离;调度器在同一周期内先
排空优先提示、再排空手动触发队列,提示排空不受分片背压跳过影响;未新建调度设施或异步任务类型。

新增人工优先入队与只读查询三条路由 POST /api/admin/polling-priority-items、
GET /api/admin/polling-priority-items、GET /api/admin/polling-priority-items/:id:人工入队复用既有轮询
权限判定(抽取为同包共享函数),原因必填,不受每日 500 次上限与 24 小时去重约束,重复抑制由活动项合并
承担;读侧按店铺快照下推数据范围,越权与不存在不可区分,不提供优先级分级、有效期或人工重触发入口。
新增 7 个审计动作(enqueue/claim/fail/retry/complete/dequeue/manual_denied)与资源
polling_priority_item,并按(操作者类型,来源)注册,人工侧与 Worker 侧均通过来源校验。

同步 OpenAPI 文档装配三处与路由注册;归档 Change 至
openspec/changes/archive/2026-09-17-add-priority-polling-queue/ 并同步主 Spec(新增
priority-polling-queue、polling-operations 追加单次执行互斥 Requirement 与三条路由索引)与上下文健康
证据(requirement-evidence 150 行、入口矩阵 http 403 / async 56)。

本机验证:junhong_cmp_test 与隔离 Redis DB 15,未连生产、未启动 Worker/API、未调用运营商上游;迁移
up/down/up 与 down 守卫实测(含 dirty=true 记账口径与 force 恢复),A–F 批 94 PASS、接缝 63 PASS、
提示通道 12 PASS、清理零残留 20 PASS。成功路径 Complete、真并发互斥、尝试上限第 3 次判定、HTTP 层权限
矩阵、通道阈值持锁复机边界与三类生效触发点生产集成留待测试部署验证(见
docs/verification/add-priority-polling-queue-verification.md 第 4 节)。自动化测试按项目决策为 N/A,
未新增 *_test.go。
2026-09-17 14:29:56 +08:00

356 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工程约束
本文件只记录从当前代码、配置、迁移、构建或可复现运行结果证明的长期工程规则。业务行为属于 `openspec/specs/`,第三方协议属于 `docs/integrations/`。自动化测试当前为 N/A用户决策
## ENG-ARCH-001
- **状态**:生效
- **适用范围**`internal/domain/**`
- **规则**Domain MUST NOT import Fiber、GORM、Redis、Asynq 或具体第三方 SDK。
- **理由**:领域不变量必须能脱离传输和基础设施独立演化。
- **最小正例**Domain 只依赖标准库、领域类型和 Port。
- **最小反例**Domain 直接调用 GORM 或 Redis。
- **机械检查/人工原因**`! grep -RIlE 'gofiber|gorm.io|go-redis|hibiken/asynq' internal/domain --include='*.go'`,当前通过。
- **例外条件**:既有兼容行为仍须先通过独立 Change 才能调整边界。
- **Owner**:架构负责人
- **最后验证日期**2026-08-07
- **更新触发条件**Domain 依赖或分层策略变化
## ENG-ARCH-002
- **状态**:生效
- **适用范围**:新增或修改完整用例
- **规则**:复杂写 MUST 使用 Application→Domain→Port简单写 MUST 使用 Application 事务脚本;读取 MUST 使用 Query 或既有只读 Service且不得修改状态。
- **理由**:按用例选择最小边界,避免半迁移和形式化 DDD。
- **最小正例**:订单资金状态机收口 Domain列表查询直接 DTO 投影。
- **最小反例**:只为单表 CRUD 创建聚合,或 Query 内更新状态。
- **机械检查/人工原因**:暂为人工:从 Route 追踪到用例和持久化,确认一个完整用例只有一个规则归属。
- **例外条件**:未触碰的旧 `internal/service` 保持现状。
- **Owner**:架构负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增用例或触碰旧复杂写
## ENG-ERR-001
- **状态**:生效
- **适用范围**:到 HTTP 边界的错误链
- **规则**:到 Handler/HTTP 边界前 MUST 转换为 `pkg/errors` 稳定错误Handler 参数校验 MUST NOT 拼接或返回底层 `err.Error()`
- **理由**:全局 ErrorHandler 只能安全映射稳定错误,底层文本可能泄密。
- **最小正例**`errors.Wrap(code, err)` 后返回给全局 ErrorHandler。
- **最小反例**`return c.JSON(...err.Error())`
- **机械检查/人工原因**:审查变更中的 Handler 返回点;运行 `go build ./cmd/api`。全仓 `fmt.Errorf` 只作候选,不能直接判错。
- **例外条件**:内部不可见、不会越过接口边界的诊断错误允许 `fmt.Errorf`
- **Owner**API 负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:错误系统或 ErrorHandler 变化
## ENG-ERR-002
- **状态**:生效
- **适用范围**:请求 DTO 中来自 URL 路径的字段,以及 Handler 的参数校验失败响应
- **规则**:路径来源字段 MUST 在 Handler 内由 `c.Params` 解析后回填,再执行 `validator.Struct`DTO MUST NOT 依赖 `validate:"required"` 覆盖路径字段而不回填。新增或修改的参数校验点 MUST 让校验失败返回 1001 且在 `msg` 中说明首个失败字段与规则,字段名取自该字段的中文 `description`;未触碰的既有 Handler 的通用提示按 As-Is 保留。
- **理由**`json:"-"` 的路径字段不参与 Body/Query 绑定,不回填则 `required` 恒失败,接口对任何合法请求都返回“参数不合法”,且原提示不指出字段,无法定位。
- **最小正例**`shopID, err := strconv.ParseUint(c.Params("shop_id"), 10, 64)``req.ShopID = uint(shopID)``validator.Struct(&req)`;失败时 `errors.New(errors.CodeInvalidParam, validationMessage("提现资料资格参数不合法", &req, err))` 产出“提现资料资格参数不合法:合同附件对象存储 Key 不能为空”。
- **最小反例**`c.BodyParser(&req)` 后直接 `validator.Struct(&req)` 并返回无字段信息的“XX参数不合法”。
- **机械检查/人工原因**:对每个被 `validator.Struct` 校验的 DTO核对携带 `path:"..."``validate``required` 的字段是否在调用点赋值;`go build ./cmd/api`。全仓同类 DTO 中存在未被校验的路径字段,不能只靠 grep 判定违规。
- **例外条件**:路径字段不带 `validate:"required"` 且调用方显式回填的 DTO 不受本规则约束;未纳入本次触碰范围的 Handler 通用提示不要求整改。
- **Owner**API 负责人
- **最后验证日期**2026-09-14
- **更新触发条件**DTO 绑定方式、校验消息约定或请求绑定工具变化
## ENG-RESP-001
- **状态**:生效
- **适用范围**HTTP Handler
- **规则**:成功响应 MUST 使用 `pkg/response`;错误 MUST 返回给 `internal/middleware.ErrorHandler`,不得自造响应壳。
- **理由**:统一 `{code,msg,data,timestamp}` 和 HTTP 映射。
- **最小正例**`return response.Success(c, data)`
- **最小反例**Handler 直接 `c.JSON` 返回另一套结构。
- **机械检查/人工原因**:审查新增 Handler`rg "response\.(Success|Error)" internal/handler internal/routes`;生成 OpenAPI 人工核对。
- **例外条件**:第三方回调必须返回渠道要求的字面协议时可使用专用响应。
- **Owner**API 负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:响应协议或第三方回调契约变化
## ENG-DTO-001
- **状态**:生效
- **适用范围**:进入 OpenAPI 的请求/响应 DTO
- **规则**:公开字段 MUST 有中文 description枚举描述 MUST 与 `pkg/constants` 当前值一致;状态响应按现有契约提供名称字段。
- **理由**:前端和生成文档依赖字段与枚举精确一致。
- **最小正例**:从 constants 原文复制枚举值并生成 OpenAPI 核对。
- **最小反例**:凭记忆写 description 或遗漏状态名称。
- **机械检查/人工原因**`go run cmd/gendocs/main.go` 后核对 OpenAPI暂为人工比对枚举与 constants单纯 grep 不足以证明完整。
- **例外条件**:仅内部、不进入接口契约的结构。
- **Owner**API 负责人
- **最后验证日期**2026-08-07
- **更新触发条件**DTO、枚举或 OpenAPI 生成变化
## ENG-MODEL-001
- **状态**:生效
- **适用范围**:新增/修改 `internal/model` 与当前根迁移
- **规则**:关联 MUST 保存 ID 并显式查询MUST NOT 新增 GORM `foreignKey`/`hasMany`/`belongsTo` 标签或数据库外键。
- **理由**:当前隔离与渐进迁移依赖显式关联。
- **最小正例**:保存 `ShopID`,由 Store 显式查询。
- **最小反例**:新增关联切片和 `foreignKey` 标签。
- **机械检查/人工原因**:只检查变更新增行:`git diff -- internal/model migrations | grep -E "^\+.*(foreignKey|hasMany|belongsTo|REFERENCES)"`,期望空。
- **例外条件**`migrations/archive` 的历史外键不作为新增违规。
- **Owner**:数据负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:模型或关联策略变化
## ENG-DB-001
- **状态**:生效
- **适用范围**:生产 Go 数据访问
- **规则**:生产数据访问 MUST 使用 GORMMUST NOT 新增 `database/sql` 直接访问。
- **理由**统一连接、事务、Callback 数据范围和错误处理。
- **最小正例**:通过 `*gorm.DB` 或 Store 查询。
- **最小反例**:业务 Service 新建 `sql.DB`
- **机械检查/人工原因**:检查变更新增 import`git diff -U0 -- "*.go" | grep -E "^\+.*database/sql"`,期望空。
- **例外条件**:第三方库内部实现不受本规则约束。
- **Owner**:数据负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:数据访问基础设施变化
## ENG-DB-002
- **状态**:生效
- **适用范围**Agent 对项目数据库的诊断、核对与只读查询。
- **规则**MUST 使用 dbhub MCP测试/本地库使用 `mcp__dbhub__execute_sql_main`,正式库使用 `mcp__dbhub__execute_sql_pro_main`MUST NOT 通过 `psql`、连接串、环境变量或其他命令行客户端直连数据库。
- **理由**dbhub 提供受控只读访问,避免命令历史、环境凭证和目标库选择漂移。
- **最小正例**:调用 `mcp__dbhub__execute_sql_main` 查询订单与佣金记录。
- **最小反例**`source .env && psql ...`
- **机械检查/人工原因**:审查 Agent 执行记录中的数据库访问工具;仓库业务 Go 代码不受本条约束,仍遵守 ENG-DB-001。
- **例外条件**维护者明确提供的、需执行写入或迁移的人工操作按生产运行说明执行Agent 不代执行。
- **Owner**:基础设施负责人
- **最后验证日期**2026-08-13
- **更新触发条件**dbhub MCP 名称、访问范围或数据库运维边界变化
## ENG-MIG-001
- **状态**:生效
- **适用范围**`migrations/` 当前根目录
- **规则**:新迁移 MUST 使用新顺序号并提供 `.up.sql`/`.down.sql` 配对MUST NOT 修改已发布迁移。
- **理由**:保证 Schema 可重放和回滚。
- **最小正例**:新增同号 up/down 文件并在隔离库执行。
- **最小反例**:改旧迁移或只提供 up。
- **机械检查/人工原因**:按 `migrations/*.up.sql``*.down.sql` basename 配对;当前根目录 89/89。隔离库使用 `scripts/migrate.sh` 执行 up/down/up。
- **例外条件**:不可逆数据清理须在 Change 说明恢复方式archive 历史资产不要求补配。
- **Owner**:数据负责人
- **最后验证日期**2026-08-07
- **更新触发条件**Schema 或迁移工具变化
## ENG-ROUTE-001
- **状态**:生效
- **适用范围**:新增或修改 Handler/路由
- **规则**:路由 MUST 经 `internal/routes.Register` 注册 RouteSpec新增 Handler MUST 同步 `cmd/api/docs.go``cmd/gendocs/main.go` 的占位装配。
- **理由**:运行路由与生成文档共享元数据,但两套 composition root 仍需一致。
- **最小正例**:路由文件注册并在两处 docs Handlers 增加字段。
- **最小反例**:只在 Fiber app 上挂载 Handler。
- **机械检查/人工原因**`go run cmd/gendocs/main.go`;对比两处 `bootstrap.Handlers` 字段;再对照真实 `internal/bootstrap` 装配。
- **例外条件**:健康/就绪路由在总入口内定义,但仍使用 RouteSpec。
- **Owner**API 负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增 Handler 或文档生成方式变化
## ENG-COMMENT-001
- **状态**:生效
- **适用范围**Go 生产代码
- **规则**:导出符号 MUST 有中文文档注释复杂逻辑说明原因Handler 注释 MUST 包含 HTTP 方法和路径。
- **理由**:保持 godoc、评审和真实路由可核对。
- **最小正例**:导出 Handler 注释写职责及 `POST /api/...`
- **最小反例**:注释复述赋值或路径过时。
- **机械检查/人工原因**`go vet` 只做基础检查;中文、原因和路径由变更 diff 人工核对真实路由。
- **例外条件**:显而易见且少于 15 行的未导出函数可无注释。
- **Owner**:后端负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增导出 API 或路由变化
## ENG-JSON-001
- **状态**:生效
- **适用范围**:新增 JSON 编解码代码
- **规则**:新增业务 JSON MUST 优先 sonic新增 `encoding/json` import MUST 记录兼容原因。
- **理由**Fiber 已配置 sonic但现有 SDK/OpenAPI/协议有标准库兼容需求。
- **最小正例**:业务载荷使用 `sonic.Marshal`
- **最小反例**:无理由新增 `encoding/json`
- **机械检查/人工原因**:只审查 diff 中新增 import当前仓库已有多处兼容使用不能以全仓 grep 直接失败。
- **例外条件**:标准库接口、第三方 SDK 或协议兼容明确要求。
- **Owner**:后端负责人
- **最后验证日期**2026-08-07
- **更新触发条件**JSON 库或兼容协议变化
## ENG-QUEUE-001
- **状态**:生效
- **适用范围**`queueClient.EnqueueTask` 调用
- **规则**payload MUST 是 struct 或 mapMUST NOT 传预序列化 `[]byte`
- **理由**:封装会统一校验并 sonic.Marshal字节切片会二次编码。
- **最小正例**`EnqueueTask(ctx, typ, Payload{ID:id})`
- **最小反例**Marshal 后把 `[]byte` 交给 EnqueueTask。
- **机械检查/人工原因**:核对全部 EnqueueTask 第三实参及 `pkg/queue/client.go` 的 ValidatePayload运行 `go build ./cmd/worker`
- **例外条件**:直接调用 `asynq.NewTask` 时由调用方序列化。
- **Owner**:异步任务负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:队列封装或载荷协议变化
## ENG-PAGE-001
- **状态**:生效
- **适用范围**:列表 API
- **规则**:列表 MUST 分页并执行各接口当前上限;默认 20/最大 100 仅适用于已明确采用该契约的列表。
- **理由**:避免无界查询,同时不把不同接口的既有分页强行合并。
- **最小正例**Handler/Query 对缺省值与上限显式归一化。
- **最小反例**:只写 OpenAPI tag不在运行时限制。
- **机械检查/人工原因**:从 DTO→Handler→Query 逐链核对 Limit暂为人工不以 tag 单独证明行为。
- **例外条件**:固定且有界的小型枚举集合。
- **Owner**API 负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:分页协议或查询性能变化
## ENG-STATE-001
- **状态**:生效
- **适用范围**:新增或变更状态/类型字段
- **规则**:新增生命周期状态 SHOULD 使用 int类型/方式 SHOULD 使用 string启停新增语义使用 0=禁用、1=启用。既有不一致 MUST 按 As-Is 保留,未经 Change 不得统一。
- **理由**:避免继续扩大状态语义漂移,同时保护兼容行为。
- **最小正例**:新 Status int新 PaymentMethod string。
- **最小反例**:基线任务顺手改既有状态值。
- **机械检查/人工原因**:比对 constants、DTO、模型与迁移既有例外必须在 Spec 记录。
- **例外条件**:第三方原始字段在 Adapter 边界转换;既有兼容状态保留。
- **Owner**:领域负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增状态机或兼容映射
## ENG-AUTHZ-001
- **状态**:生效
- **适用范围**:资源读取与写入
- **规则**:资源操作 MUST 在业务边界校验店铺/企业/个人客户数据范围MUST NOT 只依赖路由角色中间件。无权限与不存在不得形成可枚举差异。
- **理由**:路由只做粗粒度身份,资源归属随数据变化。
- **最小正例**Application/Service 在读写前调用当前数据范围检查。
- **最小反例**:知道资源 ID 即可更新。
- **机械检查/人工原因**:从 Handler 追踪 Application/Service 的资源校验及 GORM Callback暂为人工抽查权限失败响应。
- **例外条件**:公开健康、回调等不以资源归属认证,但必须执行自身信任校验。
- **Owner**:安全负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增资源入口或数据范围变化
## ENG-CONC-001
- **状态**:生效
- **适用范围**:状态机、余额、库存与并发领取
- **规则**:状态流转 MUST 使用 expected-status 条件更新;余额 MUST 使用 version/锁定策略MUST NOT 读后无条件写关键事实。
- **理由**:防止重复处理、负余额和并发覆盖。
- **最小正例**`WHERE status=expected` 并检查 RowsAffected。
- **最小反例**:先读状态再无条件 Updates。
- **机械检查/人工原因**:核对 `application/wallet``agentrecharge` 等现有模式;变更时人工检查条件、版本和 RowsAffected。
- **例外条件**:只读投影不适用。
- **Owner**:领域负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:并发写或状态机变化
## ENG-OUTBOX-001
- **状态**:生效
- **适用范围**:需要提交后可靠副作用的用例
- **规则**:业务事实与可靠异步副作用 MUST 在同一事务写 OutboxMUST NOT 用裸 goroutine 代替可靠投递;消费者 MUST 容忍重复。
- **理由**:进程崩溃和至少一次投递会造成丢失或重复。
- **最小正例**:事务写业务事实与 Outbox消费者条件更新。
- **最小反例**:提交后启动 goroutine 调第三方。
- **机械检查/人工原因**:追踪 producer→Outbox→Relay→consumer检查事件唯一键、租约和终态条件。
- **例外条件**:允许丢失的低价值遥测可不用 Outbox但须明确说明。
- **Owner**:可靠性负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增外部副作用或事件
## ENG-TX-001
- **状态**:生效
- **适用范围**:资金、状态与成功审计
- **规则**:资金/关键状态事实和要求成功必达的审计 MUST 在同一 GORM 事务;事务内 MUST NOT 持有不可回滚的长外部 I/O。
- **理由**:保证事实与审计一致,并控制锁时间。
- **最小正例**:事务写事实和 Audit Writer提交后由 Outbox 外发。
- **最小反例**:事务中等待第三方网络后再提交。
- **机械检查/人工原因**:逐用例人工核对 Transaction 闭包、Audit Writer 和外部调用位置。
- **例外条件**:业务回滚后的 failed/denied 审计,以及随之记录的回滚后失败状态事实,使用独立短事务;该短事务 MUST NOT 与已回滚的主事务共用连接或事务,且 MUST 以业务单仍处于允许该失败事实的状态为条件更新。
- **Owner**:架构与审计负责人
- **最后验证日期**2026-09-14
- **更新触发条件**:高风险写或外部调用变化
## ENG-AUDIT-001
- **状态**:机械检查阻塞
- **适用范围**:状态变更、资金、权限、关键配置、敏感读取与关键拒绝
- **规则**:用例 MUST 明确 Audit Event、Domain Ledger、Integration Log、Outbox 的使用决定或 N/A 理由;四类事实不得互相替代。
- **理由**Access Log 不能证明业务事实Integration Log 不能替代资金流水。
- **最小正例**:状态成功写 Audit外部尝试写 Integration Log。
- **最小反例**:只记录 access.log 后宣称已审计。
- **机械检查/人工原因**:当前 `cmd/audit-coverage` 写入已删除 `.scratch` 路径,不能作为通过门禁;修复前逐调用链人工核对并在仓库外记录证据。
- **例外条件**:低风险用例可登记 N/A 理由。
- **Owner**:审计负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:审计 CLI 输出路径修复或新增高风险用例
## ENG-LOG-001
- **状态**:生效
- **适用范围**:运行日志、审计、资金事实和外部交互
- **规则**Access Log、Audit Event、Domain Ledger、Integration Log 与 Outbox MUST 分责;日志 MUST 使用结构化 Zap 且不得记录密钥或完整敏感体。
- **理由**:不同保留期、查询维度与一致性要求不可混用。
- **最小正例**:外部失败写脱敏 Integration LogHTTP 请求写 Access Log。
- **最小反例**:把完整凭证写 app.log。
- **机械检查/人工原因**:人工核对 Logger 字段、Sanitizer 与事实存储;本地按 request_id/correlation_id 查询日志。
- **例外条件**:第三方要求回显的非敏感渠道码可保留。
- **Owner**:审计与安全负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:日志字段、保留策略或外部集成变化
## ENG-SECRET-001
- **状态**:生效
- **适用范围**:配置、代码、文档、日志和示例
- **规则**密钥、Token、证书和真实凭证 MUST 由生效配置/环境注入MUST NOT 新增到仓库或日志;示例只用占位符。
- **理由**:仓库历史和日志传播范围不可控。
- **最小正例**:文档使用 `<TOKEN>`,运行环境注入 Secret。
- **最小反例**:提交可用 AppSecret。
- **机械检查/人工原因**:对 git diff 做敏感模式人工扫描;核对 `pkg/config` 绑定和日志字段。当前历史凭证属于待决策存量,不得复制。
- **例外条件**:明确不可用的假值可用于文档占位;当前无测试夹具。
- **Owner**:安全负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:配置字段、凭证或集成变化
## ENG-CONFIG-001
- **状态**:生效
- **适用范围**:运行配置和依赖版本
- **规则**:依赖版本 MUST 以 `go.mod` 为准;运行参数 MUST 经 `pkg/config` 加载/校验;业务代码 MUST NOT 散落读取环境变量。
- **理由**:保证默认值、环境覆盖和启动校验只有一个入口。
- **最小正例**:在 Config 增字段并由 loader 绑定。
- **最小反例**:业务 Service 直接 `os.Getenv`
- **机械检查/人工原因**:检查变更新增 `os.Getenv`;运行 `go build` 并核对 `pkg/config/config.go` 校验。
- **例外条件**:启动脚本读取环境变量用于组装进程配置。
- **Owner**:基础设施负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:新增配置或依赖升级
## ENG-TEST-001
- **状态**:生效
- **适用范围**Agent 执行的迁移、Redis、API/Worker、部署与集成 Smoke 验证。
- **规则**:维护者指定的测试 PostgreSQL `junhong_cmp_test`、Redis DB 6、`Iteration/8-11` 测试部署和 `cmp-test` 日志主机构成唯一测试验证面MUST 使用该环境不额外要求独立数据库、Redis DB 或 namespace。迁移从本地工作区以明确 `DB_*` 参数执行;测试 fixture 仅可创建、删除当前 Change 自己的记录MUST NOT 重置整个测试库。测试部署通过 Gitea 工作流完成SSH 仅用于日志、容器状态与受控 Smoke。
- **理由**:同一可控测试面避免每个 Change 重复索取环境,且保留可复现的迁移、缓存、并发和部署证据。
- **最小正例**:当前 Change 在 `junhong_cmp_test` 执行迁移 up/down/up清理自己的 fixture并在测试部署后读取 `cmp-test` 日志。
- **最小反例**:以未提供额外“隔离环境”为由暂停,或重置整个测试数据库。
- **机械检查/人工原因**记录显式目标、迁移命令、fixture 清理范围、Redis 操作与测试部署 SHA通过 API/Worker 日志和可观察状态核对。
- **例外条件**:真实支付渠道和外部审批系统不作自动验证;第三方协议变更以契约文档和维护者提供的证据为准。生产仍按生产运行说明由维护者执行。
- **Owner**:基础设施负责人
- **最后验证日期**2026-09-08
- **更新触发条件**测试库、Redis DB、部署分支、测试主机或验证授权变化
## KNOWN-ISSUE-001
- **状态**:已知缺陷,待修复(当前不阻塞归档;标签功能未启用时无实际影响)
- **适用范围**`internal/service/exchange/migration.go` 的标签复制步骤(换货业务数据迁移的「资产标签」迁移项)
- **问题**:标签复制使用 `clause.OnConflict{Columns: [resource_type, resource_id, tag_id], DoNothing: true}`,未声明 `tb_resource_tag` 上部分唯一索引 `idx_resource_tag_unique``... WHERE deleted_at IS NULL`的谓词PostgreSQL 返回 `42P10`
- **理由**:旧资产存在任意 `tb_resource_tag` 行且换货请求要求迁移时,标签步骤必然失败,导致换货完成整体回滚、迁移状态落 `failed`,「已迁移」在该情形不可达。记录于此以便由独立变更修复,避免在其它任务中顺手改动迁移项。
- **证据**2026-09-14 在 `junhong_cmp_test``tb_audit_event` 实测 4 条 `action_code=exchange.card.complete``result=failed``error_code=1206``error_summary``复制资产标签失败 ... SQLSTATE 42P10``tb_resource_tag` 当前 0 行,故静态库状态下不可观测。该文件自 `add-exchange-data-migration-status` 起未修改md5 与 `git show HEAD` 一致)。
- **例外条件**:标签功能未启用(`tb_resource_tag` 为空)时无实际影响;不影响钱包余额、有效套餐使用记录、累计充值字段、资产归属与个人客户—资产绑定,也不影响无标签资产的换货完成。
- **修复方式**:为该 `OnConflict` 声明部分索引谓词(或调整索引),须另立 OpenSpec Change当前按维护者决策暂不修复仅登记待办。
- **Owner**:数据负责人
- **最后验证日期**2026-09-14
- **更新触发条件**:标签功能启用、换货迁移项变更或该缺陷修复
## KNOWN-ISSUE-002
- **状态**:已登记的边界,非缺陷(当前不阻塞归档)
- **适用范围**:卡轮询优先队列的三处实现边界:①「资产无有效套餐」的触发范围;②认领接缝探测失败的失败方向;③未被接缝使用的批量探测方法
- **问题**
- 边界一(无有效套餐触发范围):`no_valid_package` 只在既有判定点——`internal/service/iot_card/stop_resume_service.go:354` 的「条件B无有效套餐」被加入停机原因列表时——才会追加触发调用点 `internal/service/iot_card/stop_resume_service.go:123`,实现 `:209`)。已经处于停机状态的卡走 `EvaluateAndAct` 的离线分支,不再重新判定「无有效套餐」,因此停机卡不会被该场景加急。
- 边界二(认领接缝探测失败的失败方向):认领接缝的 `FindActive` 探测失败采用 fail-closed——四个轮询 Handler 按既有失败分支记为轮询失败并按既有间隔延后(`internal/task/polling_priority_claim.go:56` 返回错误;`internal/task/polling_realname_handler.go:47``internal/task/polling_carddata_handler.go:55``internal/task/polling_cardstatus_handler.go:56``internal/task/polling_package_handler.go:59` 各对应分支carddata 连同放弃当轮流量同步)。
- 边界三(未被使用的批量探测方法):`internal/store/postgres/polling_priority_item_store.go:148-149``ListActiveByCards` 当前没有任何调用方(`grep -rn "ListActiveByCards" internal/` 只命中定义本身)。
- **理由**:边界一——行为契约的 WHEN 以「普通套餐轮询判定资产无有效套餐」为前提(该 Scenario 见 `openspec/changes/archive/2026-09-17-add-priority-polling-queue/specs/priority-polling-queue/spec.md:31`停机卡不发生该判定故当前实现不违反契约把它扩展到停机卡属于新的产品口径。边界二——fail-open 会在数据库抖动时让同一卡同一任务类型同时出现两次上游调用,破坏本能力「至多一次上游调用」的 MUST失败方向必须偏保守。边界三——`tasks.md` 1.4 的执行契约要求存储层提供「按卡批量活动项查询」,先交付后使用。
- **证据**:见上述各边界的 `文件:行`;边界三的调用方检索当时结果为空(仅定义处命中)。
- **例外条件**:边界一——若产品要求停机卡也因「无有效套餐」获得加急,须先由产品确认收窄该 Scenario 的措辞,再以独立 Change 扩展触发范围。边界二——数据库连接正常时不会触发该失败方向,普通轮询的并发上限与无限重入队策略不受影响。
- **修复方式**:边界一——产品确认后另立 Change在此之前不改判定点。边界二——不修复刻意设计如未来引入可信的「探测能力不可用」信号可在该信号下单独放行并接受重复调用风险须重新评审。边界三——若认领接缝改为批量探测则启用否则按归档后的清理流程删除不长期保留未使用的方法。
- **Owner**:异步任务负责人
- **最后验证日期**2026-09-16
- **更新触发条件**:触发范围口径变化、认领策略变化、或接缝改为批量探测