This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
@@ -0,0 +1,57 @@
|
||||
## Context
|
||||
|
||||
见 proposal.md。现有 `scripts/migration/` 已使用 Python 加载 YAML/CSV 并生成由维护者以 `psql -1` 执行的 SQL;店铺导入也采用该模式。CSV 固定为 988 条合法且唯一记录,其中 8 条引用同 CSV 内的上级代理。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 复用现有迁移目录的 Python、YAML、CSV 和 SQL 审核模式。
|
||||
- 在 SQL 写入前发现本地输入错误,并在 SQL 事务起点阻断目标库冲突。
|
||||
- 生成稳定编号、父级优先的店铺、初始账号、角色、双钱包和成功审计 SQL。
|
||||
- 让生成器和执行过程都不直接写入目标库。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不新增 HTTP API、Go CLI、Application 用例、数据表、迁移或运行配置字段。
|
||||
- 不修改已有店铺、账号、角色或钱包;目标冲突一律阻断本批。
|
||||
- 不导入套餐、资产、余额、佣金历史或 CSV 备注以外的业务事实。
|
||||
- 不自动执行 SQL、发布生产或代替维护者审核。
|
||||
|
||||
## Decisions
|
||||
|
||||
### Python 生成 SQL,维护者手工单事务执行
|
||||
新增 `scripts/migration/import_shops.py` 及最小 `lib/shop_*` 模块,沿用 `migrate_assets.py` 的参数、配置目录和输出目录约定。生成器只读取本地 CSV 与 YAML,输出 `shop_bulk_import_<batch>.sql`、结果 CSV、错误 CSV 和摘要;不连接目标 PostgreSQL。
|
||||
|
||||
生成器不新增数据库配置。目标库依赖角色、操作者、业务员和冲突的校验被写入 SQL 的开头,在任何业务 INSERT 之前执行。维护者仅能在审核后以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行,事务中的任意 `RAISE EXCEPTION` 或 INSERT 失败均回滚整批。
|
||||
|
||||
备选的 Go CLI 会偏离现有迁移执行模式;Python 直连 PostgreSQL 会扩大凭据与写入边界,均不采用。
|
||||
|
||||
### 本地输入预检与稳定排序
|
||||
CSV 读取器要求“奇成代理名称、店铺名(新卡管)、用户名、联系方式、业务员、是否有上级代理、上级代理名称”列,清理空白后校验必填项、11 位 ASCII 手机号、店铺名/用户名/手机号同批唯一性以及上级代理唯一解析。店铺编号为 `<prefix>-<四位序号>`,序号按原始 CSV 数据行从 1 开始。
|
||||
|
||||
结果 SQL 的创建顺序为无上级记录按 CSV 顺序在前、子记录在其父记录之后;父级可以出现在 CSV 的后续行。循环引用或无法唯一解析的父级属于本地错误,阻止 SQL 生成。
|
||||
|
||||
### SQL 直接构造完整初始事实与审计
|
||||
SQL Builder 使用字符串安全转义、`DO` 守卫、`INSERT ... SELECT` 和 `RETURNING`/CTE 关联新 ID。它创建 `tb_shop`、`tb_account`、`tb_account_role`、`tb_shop_role`、两条 `tb_agent_wallet`,并按当前 `tb_audit_event` 与 `tb_audit_event_resource` 表结构、动作注册事实生成成功审计和业务员归属审计。
|
||||
|
||||
默认角色必须是启用客户角色;每个映射业务员必须是启用平台账号;迁移操作者必须是启用超级管理员。店铺编号、用户名和手机号的有效记录冲突由 SQL 守卫按源 CSV 行号报错。SQL 必须在 `BEGIN` 后首先执行所有守卫,再进行任何业务写入。
|
||||
|
||||
### 初始密码哈希写入 SQL
|
||||
Python 使用新增的 `bcrypt` 依赖,以 `adm@` 加每条手机号后四位在内存中构造初始密码并生成 bcrypt 哈希。哈希直接作为 `tb_account.password` 的 SQL 值写入;这是经确认允许保存的受控 SQL 产物。明文密码不得出现在控制台、结果 CSV、错误 CSV、摘要或日志。
|
||||
|
||||
### 审核产物和 Git 忽略
|
||||
结果 CSV 记录源行号、店铺编号、店铺名称、层级、上级解析和业务员映射;错误 CSV 记录源行号和原因;摘要记录输入数、计划数、父子关系数、错误数及待执行 SQL 路径。实际 YAML、生成 SQL 和所有审核产物均受 `scripts/migration/.gitignore` 保护。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [SQL 绕过 Go Application 语义] → Builder 以现有 `CreateService`、模型、迁移和审计 Writer 的当前事实为基准,隔离库核对所有初始事实与审计数量。
|
||||
- [bcrypt 哈希位于 SQL 文件] → SQL 仅保存在 Git 忽略的受控目录;不得复制到日志、文档或结果产物。
|
||||
- [预生成到执行之间目标库变化] → SQL 在同一事务的第一阶段重新校验所有可变目标事实。
|
||||
- [长事务处理 988 家店铺] → 事务不包含网络 I/O;如锁等待不可接受,停止执行并调整批次设计,不降级为部分提交。
|
||||
- [CSV 后续插行改变后续编号] → 审核时将输入 CSV 与同批审核产物一起留存;重跑同一文件保持编号稳定。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 维护者将审核通过的 CSV 和实际 `shop_bulk_import.yaml` 放到受控本地目录,运行 Python 生成器并审核 SQL、结果、错误和摘要。
|
||||
2. 在隔离环境以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行 SQL,核对店铺、账号、角色、钱包和审计数量,以及冲突和中途失败时的整批回滚。
|
||||
3. 维护者按相同流程在生产环境手工执行;Agent 不连接生产主机或执行写入。
|
||||
4. SQL 执行失败由单一事务自动回滚;成功后的业务回退不由工具自动执行,需由维护者另行制定删除或禁用方案。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
现有店铺创建入口一次只能创建一家店铺,而奇成代理商清单已整理为 988 家合法且唯一的店铺数据。手工创建会遗漏账号、角色、钱包、上下级关系或审计事实,且无法安全重跑。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增受控的 Python 店铺导入 SQL 生成器:读取本地 CSV 和本地导入配置,先执行全量本地预检,再生成单个可审核的 PostgreSQL 事务 SQL。
|
||||
- 生成的 SQL 在写入前校验目标库角色、业务员、店铺编号、用户名和手机号冲突;通过后创建店铺主账号、账号角色、店铺默认角色、主钱包、佣金钱包和成功审计事实。
|
||||
- 支持父店铺优先、业务员账号映射、稳定店铺编码和 `adm@` 加手机号后四位的初始密码规则;Python 生成 bcrypt 哈希并写入受 Git 忽略保护的 SQL 文件。
|
||||
- 输出不含明文密码的逐行结果清单、错误清单和摘要;维护者审核后以 `psql -1` 手工执行生成的 SQL,任一校验或写入失败均回滚整批。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `shop-bulk-import`: 受控生成可审核、可回滚的店铺批量导入 SQL。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 在 `scripts/migration/` 新增 Python 入口和最小公共模块,复用现有 YAML、CSV、SQL 生成器的目录与执行模式。
|
||||
- 新增 Python `bcrypt` 依赖、本地导入配置示例和受 Git 忽略的 SQL/审核产物。
|
||||
- 不新增 HTTP API、Go CLI、数据库 Schema 或运行配置字段;最终 SQL 由维护者在隔离环境和生产环境手工执行。
|
||||
@@ -0,0 +1,52 @@
|
||||
## Purpose
|
||||
|
||||
定义受控离线迁移工具从已核对的代理商 CSV 生成店铺初始事实 SQL,使维护者可以在执行前审核输入解析、目标库守卫和逐行计划,并以单事务手工执行。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 导入输入必须在生成 SQL 前完成全量预检
|
||||
系统 SHALL 在生成 SQL 前校验 CSV 必备列、店铺名称/初始用户名/初始手机号同批唯一性、11 位手机号格式、上下级代理引用、业务员映射、默认角色、迁移操作者和稳定生成的店铺编号。上级代理必须按 CSV 的“奇成代理名称”唯一解析。
|
||||
|
||||
#### Scenario: 输入或映射错误被发现
|
||||
- **WHEN** 工具发现任一 CSV 行、配置项或同批关系错误
|
||||
- **THEN** 工具输出带 CSV 行号和原因的错误清单,不生成可执行导入 SQL
|
||||
|
||||
#### Scenario: 上级代理按 CSV 标识解析
|
||||
- **WHEN** 子店铺的上级代理名称在同一 CSV 中唯一对应一个代理记录
|
||||
- **THEN** 工具将该代理记录排在子店铺之前,并在结果清单中记录上级解析结果
|
||||
|
||||
### Requirement: 生成的 SQL 必须在写入前校验目标库状态
|
||||
系统 SHALL 在单事务 SQL 的任何店铺、账号、角色、钱包或审计写入前,校验配置指定的默认角色为启用客户角色、业务员映射为启用平台账号、迁移操作者为可用超级管理员,且目标库不存在同店铺编号、用户名或手机号的有效记录。
|
||||
|
||||
#### Scenario: 目标库冲突被发现
|
||||
- **WHEN** 维护者执行的 SQL 发现角色、账号映射或任一目标记录冲突
|
||||
- **THEN** SQL 以包含对应 CSV 行号和原因的错误终止,且不写入本批店铺、账号、角色、钱包或成功审计事实
|
||||
|
||||
### Requirement: 批量导入 SQL 必须创建完整的店铺初始事实
|
||||
系统 SHALL 为每个通过预检和目标库守卫的 CSV 记录创建启用的店铺、一个启用的店铺主账号、该账号和店铺的默认客户角色关联、主钱包和佣金钱包,并按配置写入平台业务员归属和成功审计。店铺默认角色必须是配置指定的启用客户角色。
|
||||
|
||||
#### Scenario: 成功导入店铺
|
||||
- **WHEN** 维护者以单事务执行通过审核的导入 SQL
|
||||
- **THEN** 每家店铺具有稳定生成的店铺编号、正确的上级层级和业务员归属,并拥有一个主账号、默认角色关联、主钱包、佣金钱包及成功创建审计
|
||||
|
||||
#### Scenario: SQL 中保存初始账号密码哈希
|
||||
- **WHEN** 工具根据配置的初始密码规则生成店铺主账号 SQL
|
||||
- **THEN** SQL 将该账号的 bcrypt 密码哈希写入数据库,结果清单、错误清单、摘要和控制台输出不包含初始密码明文
|
||||
|
||||
### Requirement: 导入执行必须由维护者审核后以单事务手工确认
|
||||
系统 SHALL 只生成 SQL 和审核产物,不直接连接或写入目标 PostgreSQL。维护者 SHALL 在审核通过后以 PostgreSQL 单事务模式执行生成的 SQL;任一记录创建失败时回滚整批。
|
||||
|
||||
#### Scenario: 仅运行生成器
|
||||
- **WHEN** 操作者运行 Python 导入生成器
|
||||
- **THEN** 工具仅生成审核产物和 SQL,不写入目标 PostgreSQL
|
||||
|
||||
#### Scenario: SQL 执行期间发生创建失败
|
||||
- **WHEN** 维护者以单事务模式执行 SQL,且任一记录无法创建
|
||||
- **THEN** PostgreSQL 回滚整批,且本批次不保留任何店铺、账号、角色关联、钱包或成功创建审计事实
|
||||
|
||||
### Requirement: 导入结果必须可审核和重跑定位
|
||||
系统 SHALL 为每次 SQL 生成输出不含明文密码的结果清单与摘要,至少包含源 CSV 行号、稳定店铺编号、店铺名称、层级、上级解析结果、业务员映射结果、计划状态和成功/失败统计。生成的 SQL、结果清单、错误清单和摘要 SHALL 使用同一批次标识以便审核和定位。
|
||||
|
||||
#### Scenario: 生成完成后审核
|
||||
- **WHEN** 工具成功完成 SQL 生成
|
||||
- **THEN** 操作者可通过结果清单核对每条源记录及其生成的店铺编号、层级和业务员归属,并通过摘要确认总数和待执行 SQL 文件
|
||||
@@ -0,0 +1,22 @@
|
||||
## 1. 导入契约与本地配置
|
||||
|
||||
- [x] 1.1 新增店铺批量导入的版本化配置示例和 Git 忽略规则,配置项覆盖店铺编号前缀、默认角色、操作者、初始密码规则及业务员账号映射。
|
||||
- [x] 1.2 在 `scripts/migration/` 实现 Python 配置和 CSV 加载,校验必备列、必填字段、11 位手机号、同批唯一性、父代理唯一解析、稳定店铺编号及父级优先排序。
|
||||
- [x] 1.3 增加 Python `bcrypt` 依赖,并实现不含明文密码的错误清单、结果清单和摘要输出。
|
||||
|
||||
## 2. 单事务 SQL 生成
|
||||
|
||||
- [x] 2.1 实现店铺、主账号、账号角色、店铺角色、双钱包和 bcrypt 密码哈希的 SQL 构造;SQL 按父店铺优先顺序创建并写入操作者和业务员归属。
|
||||
- [x] 2.2 在 SQL 事务起点校验默认客户角色、迁移超级管理员、业务员平台账号及目标库店铺编号/用户名/手机号冲突;错误必须包含 CSV 行号并在写入前中止。
|
||||
- [x] 2.3 按当前审计表结构和动作注册事实生成店铺创建、业务员归属的成功审计 SQL,并确保任一 SQL 失败回滚整批。
|
||||
|
||||
## 3. 迁移入口与运行说明
|
||||
|
||||
- [x] 3.1 新增 `scripts/migration/import_shops.py`,提供 CSV、配置和输出目录参数,只生成 SQL 与审核产物且不连接或写入目标 PostgreSQL。
|
||||
- [x] 3.2 补充 `scripts/migration/README.md` 的准备、SQL 生成、审核、隔离环境 `psql -1` 执行、生产人工执行和回退说明。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- [x] 4.1 使用最终 CSV 和不含生产凭据的本地配置运行生成器,核对 988 条计划记录、8 条父子关系、SQL 含 bcrypt 哈希且结果/错误/摘要/控制台不含初始密码明文。
|
||||
- [ ] 4.2 在隔离数据库以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行 SQL,核对店铺、主账号、角色关联、双钱包及成功审计数量,并验证冲突或中途失败时整批回滚。
|
||||
- [ ] 4.3 运行 Python 语法检查、生成器 dry-run、`openspec validate --all` 和 `./scripts/context-health.sh`。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,51 @@
|
||||
## Context
|
||||
|
||||
现有 step2 依据旧套餐的类型、状态、生效时间和到期时间分类生命周期;分类后只要同一资产有多条 `active` 就阻断。奇成数据中的少量续费记录缺少生效时间且仍标为正常,无法从通用字段推断先后关系。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 用映射配置表达经运营确认的逐卡裁决。
|
||||
- 在生成 SQL 前验证裁决完整性,确保不静默丢失或重复迁移套餐。
|
||||
- 将覆盖后的状态写入现有审核产物,保持可追溯性。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改奇成老库记录。
|
||||
- 不依据到期时间、创建时间或套餐时长引入全局自动裁决规则。
|
||||
- 不改变未覆盖资产的现有分类和阻断逻辑。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 使用 ICCID 与生命周期 ID 的显式覆盖
|
||||
在 `mapping.yaml` 增加 `package_lifecycle_overrides`。每项包含完整 ICCID、一个 `active_life_id`、可选的 `pending_life_ids` 与 `skipped_life_ids`。
|
||||
|
||||
生命周期 ID 是老库主键,避免到期时间存在 UTC/本地时区展示差异或同日期重复时的歧义。完整 ICCID 与 step2 的资产键一致。
|
||||
|
||||
未采用“最早到期记录为当前”的规则:已确认的第三张异常卡表明未来记录并不必然均应作为待生效迁移。
|
||||
|
||||
### 覆盖在冲突检测前重分类
|
||||
先获取老库原始生命周期及既有通用分类,再按 ICCID 应用覆盖,最后执行现有映射、唯一 active 校验与 SQL 生成。覆盖只改变配置明确列出的记录状态;同卡其他通用分类为 `active` 或 `pending` 的正式套餐必须也被覆盖明确裁决,否则报配置错误并阻断该资产。
|
||||
|
||||
### 配置与运行时双重校验
|
||||
加载时校验字段格式、同一覆盖内 ID 不重复且状态集合不重叠。读取老库后校验每个被引用 ID 都属于该 ICCID 的 `tbl_card_life` 记录、active 恰好一个、覆盖未遗漏其他原本可迁移记录。错误沿用 step2 的错误 CSV,且不生成该资产套餐 SQL。
|
||||
|
||||
### 本批已确认裁决
|
||||
实现时将下列裁决写入迁移配置:
|
||||
|
||||
| ICCID | active | pending | skipped |
|
||||
|---|---|---|---|
|
||||
| `89860624630055027529` | `DD74BA52EC8242FF94815532DC19389B` | `AB77C853EEF444E2AF20A8475F61CFA7` | 无 |
|
||||
| `89860624630055035589` | `4ACDBC19027A4E90BF500F515093E0A3` | `2D247EF7877C4CE18E74EF9B139B7FCD` | 无 |
|
||||
| `89860624590009246403` | `186B9062F3904AACB4A231041B6B5E98` | 无 | `EB006800000D49EB9A18024922657680` |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [运营裁决填写错误] → 覆盖按老库主键校验,并在审核 CSV 输出覆盖后的每条记录状态。
|
||||
- [老库后续新增套餐记录] → 完整性校验拒绝遗漏的可迁移记录,要求重新确认配置。
|
||||
- [覆盖配置被误用于常规数据] → 不提供全局或通配符规则,覆盖仅作用于单一完整 ICCID。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 发布包含覆盖能力与上述三项配置的迁移脚本。
|
||||
2. 在隔离环境重新生成 step2,核对三张卡各一条 active,前两张各一条 pending,第三张无额外套餐。
|
||||
3. 线上仅执行经审核的生成 SQL;若需回退,移除对应覆盖后重新生成,不写老库。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
奇成 `tbl_card_life` 的少量卡会同时存在多条 `status=1`、未来到期且缺少生效时间的正式套餐记录。迁移脚本无法安全判断当前套餐,因而阻断这些资产的套餐迁移;直接以到期时间做全局裁决会误判其他历史数据。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在奇成迁移配置中增加按 ICCID 和旧套餐生命周期记录 ID 指定 `active`、`pending`、`skipped` 状态的覆盖能力。
|
||||
- 在检测多个当前生效正式套餐前应用覆盖;覆盖外继续维持现有严格阻断行为。
|
||||
- 对覆盖的完整性和引用的生命周期记录进行校验,并在审核产物中保留实际裁决结果。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `qicheng-migration-package-lifecycle-overrides`: 为异常旧套餐生命周期提供可审计、逐卡的迁移状态覆盖。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- `scripts/migration/config/mapping.yaml`
|
||||
- `scripts/migration/lib/mapping_loader.py`
|
||||
- `scripts/migration/lib/sql_builder.py`
|
||||
- 奇成迁移 step2 生成的 SQL 与审核 CSV;不写入或修改奇成老库。
|
||||
@@ -0,0 +1,34 @@
|
||||
## Purpose
|
||||
|
||||
为奇成迁移中状态字段无法唯一表达套餐先后关系的少量资产,提供逐卡、可审计且不影响其他资产的套餐生命周期裁决能力。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 迁移配置支持逐卡套餐生命周期覆盖
|
||||
系统 SHALL 支持在奇成迁移映射配置中,以完整 ICCID 和 `tbl_card_life` 生命周期记录 ID 指定唯一当前生效套餐,以及待生效或跳过的套餐记录。
|
||||
|
||||
#### Scenario: 指定当前与待生效套餐
|
||||
- **WHEN** 某 ICCID 的覆盖指定一个当前生效记录和一个待生效记录
|
||||
- **THEN** step2 生成结果 SHALL 将前者迁移为生效套餐、后者迁移为待生效套餐,且不再以多个当前生效套餐阻断该资产
|
||||
|
||||
#### Scenario: 指定跳过套餐
|
||||
- **WHEN** 某 ICCID 的覆盖将一个未来正式套餐记录指定为跳过
|
||||
- **THEN** step2 SHALL 不为该记录生成套餐使用数据,并在审核产物中记录其跳过裁决
|
||||
|
||||
### Requirement: 覆盖必须完整且可验证
|
||||
系统 MUST 拒绝生命周期记录不存在、重复引用、状态互相冲突、未指定唯一当前生效记录,或未明确裁决其他原本可迁移正式套餐的覆盖。
|
||||
|
||||
#### Scenario: 覆盖引用不存在的记录
|
||||
- **WHEN** 覆盖中的生命周期记录 ID 不属于对应 ICCID 的 `tbl_card_life` 记录
|
||||
- **THEN** step2 SHALL 将该 ICCID 记入错误产物且不生成该资产的套餐迁移数据
|
||||
|
||||
#### Scenario: 覆盖遗漏另一条可迁移套餐
|
||||
- **WHEN** 覆盖指定当前生效记录但遗漏同卡另一条原本会迁移的正式套餐
|
||||
- **THEN** step2 SHALL 将该 ICCID 记入错误产物且不猜测其状态
|
||||
|
||||
### Requirement: 未覆盖资产维持严格冲突阻断
|
||||
系统 SHALL 对未配置生命周期覆盖的资产维持现有套餐状态分类和多个当前生效正式套餐阻断行为。
|
||||
|
||||
#### Scenario: 未覆盖资产存在多个当前生效套餐
|
||||
- **WHEN** 未配置覆盖的 ICCID 仍有多条当前生效正式套餐
|
||||
- **THEN** step2 SHALL 输出 `multiple_active_packages` 错误且不迁移该资产的套餐
|
||||
@@ -0,0 +1,16 @@
|
||||
## 1. 覆盖配置与校验
|
||||
|
||||
- [x] 1.1 在映射加载模型中增加逐卡套餐生命周期覆盖,并校验 ICCID、生命周期 ID、唯一 active 及状态集合不重叠。
|
||||
- [x] 1.2 在 `mapping.yaml` 写入已确认的三张异常卡生命周期裁决。
|
||||
|
||||
## 2. Step2 生命周期裁决
|
||||
|
||||
- [x] 2.1 在套餐队列解析中,于多 active 冲突检查前应用逐卡覆盖。
|
||||
- [x] 2.2 校验覆盖记录归属对应 ICCID、覆盖未遗漏原本可迁移的正式套餐;校验失败时写入错误 CSV 并阻断该资产套餐 SQL。
|
||||
- [x] 2.3 使审核产物展示覆盖后的 active、pending、skipped 裁决,同时保留未覆盖资产的既有阻断语义。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- [x] 3.1 运行映射加载校验及 Python 语法检查,确认覆盖配置可加载。
|
||||
- [x] 3.2 在隔离迁移输入上重新生成 step2,核对三张确认卡的套餐状态、优先级、SQL 行和审核 CSV。
|
||||
- [x] 3.3 复核其他未覆盖的多 active 资产仍输出 `multiple_active_packages`,并记录验证结果。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,29 @@
|
||||
## Context
|
||||
|
||||
step2 套餐使用 SQL 已从 `tb_package` 读取流量、名称及价格配置快照,但遗漏计时条款快照列;新记录因此使用表默认空值和零值,触发数据库完整性校验失败。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 从 SQL 已连接的目标套餐行直接写入四个计时条款快照。
|
||||
- 保持现有套餐选择、状态、优先级和冲突幂等键不变。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改套餐使用表、触发器或历史记录。
|
||||
- 不推断或修正目标套餐自身的计时条款配置。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 在 `INSERT` 列表和对应 `SELECT` 中加入 `p.expiry_base`、`p.calendar_type` 及按周期类型规范化后的时长快照:`natural_month` 保留 `p.duration_months` 并写零天数,`by_day` 保留 `p.duration_days` 并写零月数。这与正常下单的领域快照规则一致;目标套餐的非适用时长字段不进入使用记录快照。
|
||||
- 不在 SQL 使用 `COALESCE` 或常量兜底有效时长。目标套餐配置无效时应由数据库完整性约束拒绝,不能静默写入错误快照。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [目标套餐存在无效计时配置] → SQL 将被触发器拒绝;在正式执行前用隔离数据库检查生成 SQL,并修正目标套餐配置。
|
||||
- [执行旧生成文件] → 仍会失败;必须重新生成并替换 `step2_02_package_usages.sql`。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 修改生成器并重新生成 step2 输出。
|
||||
2. 在隔离环境执行新的套餐使用 SQL,确认无快照触发器错误。
|
||||
3. 维护者在生产环境按事务执行新生成文件;失败时事务自动回滚,无需数据回滚。
|
||||
@@ -0,0 +1,22 @@
|
||||
## Why
|
||||
|
||||
奇成迁移 step2 生成的套餐使用记录未写入计时条款快照,生产库的完整性触发器因此拒绝全部新记录,导致套餐使用数据无法迁移。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 使 step2 套餐使用 SQL 从目标正式套餐读取并写入生效基准、周期类型、月数和天数快照。
|
||||
- 保持迁移套餐的其他状态、流量和幂等语义不变。
|
||||
- 生成后校验 SQL 包含全部计时快照字段,且可满足快照完整性约束。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `qicheng-migration-package-usage`: 奇成迁移创建套餐使用记录时的计时条款快照行为。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- `scripts/migration/lib/sql_builder.py` 的 step2 套餐使用 SQL 生成。
|
||||
- 重新生成并人工执行 `step2_02_package_usages.sql`;不修改数据库迁移、触发器或既有数据。
|
||||
@@ -0,0 +1,23 @@
|
||||
## Purpose
|
||||
|
||||
确保奇成迁移创建的套餐使用记录保存目标套餐在迁移时的完整计时条款快照,并能通过数据库完整性校验。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 迁移套餐使用记录保存计时条款快照
|
||||
系统 SHALL 在 step2 为奇成迁移套餐创建使用记录时,写入所选正式套餐的生效基准、周期类型、月数和天数快照。
|
||||
|
||||
#### Scenario: 自然月正式套餐迁移
|
||||
- **WHEN** step2 为周期类型为 `natural_month` 的目标正式套餐生成使用记录
|
||||
- **THEN** 使用记录 SHALL 保存该套餐的生效基准、`natural_month`、正月数和零天数快照
|
||||
|
||||
#### Scenario: 按天正式套餐迁移
|
||||
- **WHEN** step2 为周期类型为 `by_day` 的目标正式套餐生成使用记录
|
||||
- **THEN** 使用记录 SHALL 保存该套餐的生效基准、`by_day`、零月数和正天数快照
|
||||
|
||||
### Requirement: 迁移套餐使用 SQL 满足快照完整性约束
|
||||
系统 MUST 生成满足套餐使用记录计时快照完整性约束的 SQL,且不得依赖空值或默认值填充迁移记录的快照。
|
||||
|
||||
#### Scenario: 执行生成的套餐使用 SQL
|
||||
- **WHEN** 在启用套餐使用计时快照校验的数据库中执行 step2 套餐使用 SQL
|
||||
- **THEN** 数据库 SHALL 不因计时快照不完整或无效而拒绝迁移记录
|
||||
@@ -0,0 +1,10 @@
|
||||
## 1. SQL 生成修复
|
||||
|
||||
- [x] 1.1 在 step2 套餐使用记录 INSERT 及 SELECT 中写入四个目标套餐计时条款快照字段。
|
||||
- [x] 1.2 保持现有套餐使用状态、流量快照与冲突幂等语义不变。
|
||||
|
||||
## 2. 验证与交付
|
||||
|
||||
- [x] 2.1 生成 step2 输出并校验套餐使用 SQL 包含四个快照列及来自目标套餐的取值。
|
||||
- [x] 2.2 在隔离数据库或等价的 SQL 约束检查中验证自然月和按天快照均满足完整性条件。
|
||||
- [x] 2.3 执行 Python 语法检查与 OpenSpec 严格校验,并记录重新生成生产执行文件的要求。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,69 @@
|
||||
## Context
|
||||
|
||||
见 proposal.md。当前 `PollingCarddataHandler` 与 `PollingCardStatusHandler` 在调用 Gateway 前创建 pending Integration Log,成功后再终结该记录;Integration Log 创建还会创建统一审计关联。因此一次无变化成功轮询至少造成审计事件、审计资源、Integration Log 创建和终结等多次 PostgreSQL 写入。生产轮询并发配置的数值远高于单机磁盘可承受范围,现有按任务类型的 Redis 信号量不能限制不同任务类型的合计压力。`AuditRetentionCleanupEnabled=false` 仅关闭物理删除,不阻止归档和留存扫描。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 让无业务变化的成功轮询不产生 PostgreSQL 可调查记录。
|
||||
- 保留支付、审批、入站回调和失败/未知外部调用的可靠持久化语义。
|
||||
- 用跨 Worker 的总量与分类限流阻止轮询积压同时执行。
|
||||
- 让归档和留存默认不参与日常生产负载,并能低峰限量推进。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不把支付、回调或审批的可靠幂等状态迁移到 Redis。
|
||||
- 不删除历史审计或 Integration Log,不修改表结构或既有迁移。
|
||||
- 不实现复杂自适应限流;先使用可验证的固定安全上限。
|
||||
- 不改变轮询产生业务状态变化时的领域规则、Outbox 语义或渠道调用协议。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 轮询完成后按结果决定是否记录
|
||||
|
||||
轮询路径先在内存中生成稳定的关联标识并调用渠道,再应用观测结果。只有失败、无效/未知响应或观测应用产生业务变化时,才持久化一个终态的轮询外部交互记录;成功且无变化直接进入下一次调度。
|
||||
|
||||
审计 Writer 只在观测业务变化或失败路径被调用。轮询专用的终态记录路径不得复用“调用前先写 pending”的通用可靠调用路径,以免重新引入无变化成功的两次写入和审计关联。
|
||||
|
||||
备选方案是在写入后异步删除无变化日志;它仍消耗 WAL 和索引 I/O,不能解决问题,故不采用。
|
||||
|
||||
### 2. Redis 仅承担协调与聚合
|
||||
|
||||
继续使用 Redis 保存轮询并发计数、短期去重和可选成功计数。原子获取脚本同时获取“全部轮询”令牌与“任务类型”令牌;任一令牌不足时不调用 Gateway,按现有重入队流程延后。释放和 TTL 修复必须同时覆盖两个计数,避免重启或 Worker 异常后令牌永久泄漏。
|
||||
|
||||
Redis 丢失后只会丢失短期协调或统计,轮询可以保守重试;可靠业务状态、支付/回调幂等和外部恢复继续在 PostgreSQL 中维持。
|
||||
|
||||
备选方案是让 Redis 保存所有 Integration Log 或回调幂等状态;Redis 不是权威持久化存储,故不采用。
|
||||
|
||||
### 3. 采用固定且受校验的并发预算
|
||||
|
||||
为全部轮询设置一个 Worker 配置总上限,并为每个任务类型保留现有动态上限。动态上限和总上限都必须校验为有限正整数;代码层硬上限防止生产误设为数千或数万。初始生产值由维护者在低峰期设置为保守值,并根据数据库 I/O、队列积压和渠道延迟逐项增加。
|
||||
|
||||
备选方案是只依赖 Asynq Worker `Concurrency`;它不能区分轮询和其他任务,也不能限制多实例或不同任务类型合计压力,故不采用。
|
||||
|
||||
### 4. 归档与留存采用默认关闭的总开关和单日预算
|
||||
|
||||
新增 Worker 配置总开关,默认关闭。关闭时不注册定时调度;处理器仍以安全 no-op 方式接住已入队任务,避免旧任务重试或扫描。开启时,各归档/留存执行只处理一个已结束的上海自然日;留存服务用“下一待处理日”替换跨历史日期循环。物理清理开关继续只决定是否删除,不改变新的任务总开关。
|
||||
|
||||
备选方案是只降低任务并发;现有跨历史日期循环单任务即可长时间占用数据库,故不采用。
|
||||
|
||||
### 5. 以可操作指标验证降载
|
||||
|
||||
轮询处理记录结构化计数:无变化跳过持久化、状态变化持久化、失败持久化、因总量/分类令牌延后。归档任务记录开关状态、处理日期和耗时。维护者据此结合 PostgreSQL I/O wait、活跃会话与 Asynq 队列深度决定是否提高预算。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [正常轮询不再可逐次调查] → 调查接口明确只展示变化、失败和人工事实;运行次数依赖聚合指标和应用日志。
|
||||
- [失败后才建立轮询外部交互记录] → 使用同一内存关联标识写入终态失败记录,保留资源、场景、请求关联和恢复摘要。
|
||||
- [Redis 令牌异常泄漏或丢失] → 原子双令牌脚本配合 TTL;丢失时按保守重试处理,不承担权威状态。
|
||||
- [过低限流造成队列延迟] → 先保护数据库;监控积压、I/O 和渠道延迟后逐项提高,不允许绕过代码硬上限。
|
||||
- [关闭归档导致历史在线数据继续增长] → 低峰期按单日预算受控推进,不能通过重新开启无界补偿来解决。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 维护者先使用现有轮询并发控制将所有任务类型降至保守值,并保持归档/留存 Worker 停止,记录 PostgreSQL 基线。
|
||||
2. 发布新配置与代码时,新归档/留存总开关保持关闭,轮询总并发使用保守值。
|
||||
3. 验证无变化轮询不新增三张日志表记录,失败和状态变化仍可调查,支付/回调幂等行为不变。
|
||||
4. 观察稳定窗口内 I/O wait、WAL 等待、队列积压和渠道延迟;每次只调整一类轮询预算。
|
||||
5. 低峰期手动开启归档/留存总开关,以单日预算推进;异常时关闭开关并停止相关 Worker。
|
||||
|
||||
回滚时恢复上一版二进制和原有配置;已跳过的正常无变化日志不补写,已积压的归档日期仍按受控任务处理。
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
生产环境异常重启后的轮询积压与日志留存任务同时执行,使 PostgreSQL 出现高 I/O 等待和锁等待。当前每次正常轮询都会持久化审计事件、资源快照与外部交互日志,即使观测结果没有任何业务变化,导致轮询频率直接放大为数据库写入量。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 正常且无业务变化的轮询不再创建逐次 `tb_audit_event`、`tb_audit_event_resource` 或 `tb_integration_log`;轮询仍更新必要的短期去重、指标和调度状态。
|
||||
- 网络状态、实名状态、套餐状态、有效流量增量等业务事实变化,以及外部调用失败、未知结果和人工触发,继续持久化业务事实与可调查记录。
|
||||
- 支付、审批、入站回调等需要可靠幂等、恢复或渠道裁决的外部交互继续使用 PostgreSQL 持久化;不得以 Redis 替代其权威状态。
|
||||
- 为轮询增加全局硬并发上限与背压语义,阻止积压任务在恢复或重启后同时压垮数据库和渠道。
|
||||
- **BREAKING** 正常无变化的轮询不再出现在外部交互日志列表或审计调查中;查询仅展示状态变化、异常、人工操作和其他需可靠保留的记录。
|
||||
- 将 Audit 日归档、Integration Log 日归档和日志日留存改为独立的受控任务:可整体停用、每次执行受日期或工作量预算约束,且不得与核心轮询争抢无上限资源。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `polling-load-control`: 轮询任务的全局并发上限、背压和正常无变化观测的低写入处理。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `operations-audit`: 调整审计事实的保留边界,并使审计归档与日留存任务受控、限量执行。
|
||||
- `external-integration`: 调整高频正常轮询的外部交互日志保留边界,并使 Integration Log 归档与留存任务受控、限量执行。
|
||||
|
||||
## Impact
|
||||
|
||||
- 轮询任务、卡观测和 Gateway 调用路径。
|
||||
- 审计 Writer、Integration Log Repository 与调查查询的记录策略。
|
||||
- Redis 轮询并发控制、Asynq Worker 队列与定时任务注册。
|
||||
- Worker 配置、生产运维说明和观测指标。
|
||||
- 不新增数据库 Schema;不修改既有迁移。
|
||||
@@ -0,0 +1,31 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 高频轮询外部交互日志保留边界
|
||||
系统 SHALL 不为成功且无业务变化的自动轮询逐次创建 `tb_integration_log`。支付、审批、入站回调及其他依赖外部交互记录实现可靠幂等、结果恢复或渠道裁决的调用 MUST 继续持久化其外部交互记录。轮询外部调用失败、响应无效或结果未知时,系统 MUST 持久化可调查的失败或恢复记录。
|
||||
|
||||
#### Scenario: Gateway 轮询成功且状态未变化
|
||||
- **GIVEN** 一次 Gateway 自动轮询成功,且结果没有引起业务事实变化
|
||||
- **WHEN** 系统完成该轮询
|
||||
- **THEN** 系统不创建该次轮询的 Integration Log
|
||||
|
||||
#### Scenario: Gateway 轮询调用失败
|
||||
- **WHEN** 一次 Gateway 自动轮询超时、连接失败或返回无效响应
|
||||
- **THEN** 系统创建可调查的失败或恢复记录,并按既有策略处理后续轮询
|
||||
|
||||
#### Scenario: 入站支付回调
|
||||
- **GIVEN** 支付渠道发送入站回调
|
||||
- **WHEN** 系统处理该回调
|
||||
- **THEN** 系统继续使用可靠持久化的外部交互记录保证既有幂等和恢复语义
|
||||
|
||||
### Requirement: 外部交互日志归档与留存受控执行
|
||||
系统 SHALL 在审计归档与日留存任务总开关关闭时停止 Integration Log 的日归档和日留存处理,并安全跳过已入队的相关任务。总开关开启后,每次相关任务执行 MUST 至多处理一个已结束的上海自然日,且必须继续满足既有归档校验和 pending 记录保留规则。
|
||||
|
||||
#### Scenario: Integration Log 归档任务被停用
|
||||
- **GIVEN** 审计归档与日留存任务总开关关闭
|
||||
- **WHEN** Integration Log 日归档或日留存任务被调度或消费
|
||||
- **THEN** 系统不扫描、归档、删除或更新在线 Integration Log
|
||||
|
||||
#### Scenario: Integration Log 积压受控推进
|
||||
- **GIVEN** 任务总开关开启且存在多个满足既有归档条件的日期
|
||||
- **WHEN** 系统执行一次 Integration Log 归档或留存任务
|
||||
- **THEN** 系统仅处理一个自然日,并继续保留未处理日期以供后续执行
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 轮询审计事实保留边界
|
||||
系统 SHALL 仅为产生业务事实变化、轮询失败、结果未知或人工触发的轮询操作持久化统一审计事件及资源快照。成功且无业务变化的自动轮询 MUST 不创建 `tb_audit_event` 或 `tb_audit_event_resource`,审计调查接口仅返回实际已保留的历史事实。
|
||||
|
||||
#### Scenario: 自动轮询成功但无业务变化
|
||||
- **GIVEN** 一次自动轮询成功且没有改变任何业务状态、有效流量或风险结论
|
||||
- **WHEN** 系统结束该轮询处理
|
||||
- **THEN** 审计事件和审计资源表不新增该次轮询记录
|
||||
|
||||
#### Scenario: 自动轮询改变业务事实
|
||||
- **GIVEN** 一次自动轮询产生可应用的业务状态或有效流量变化
|
||||
- **WHEN** 系统提交该变化
|
||||
- **THEN** 系统保留对应的审计事实与资源快照
|
||||
|
||||
### Requirement: 审计归档与日留存受控执行
|
||||
系统 SHALL 提供独立的审计归档与日留存任务总开关。总开关关闭时,系统 MUST 不调度、不消费且安全跳过已入队的 Audit 日归档、Integration Log 日归档和日志日留存任务,不扫描或修改在线日志表。总开关开启时,每次任务执行 MUST 至多处理一个已结束的上海自然日;维护者可在低峰期重复执行以推进历史积压。
|
||||
|
||||
#### Scenario: 归档与留存总开关关闭
|
||||
- **GIVEN** 审计归档与日留存任务总开关关闭
|
||||
- **WHEN** 定时器触发或 Worker 取得已入队的归档或留存任务
|
||||
- **THEN** 系统跳过该任务,不扫描 `tb_audit_event`、`tb_audit_event_resource` 或 `tb_integration_log`,且不创建归档或清理记录
|
||||
|
||||
#### Scenario: 总开关开启时处理积压
|
||||
- **GIVEN** 审计归档与日留存任务总开关开启,且存在多个未处理的已结束自然日
|
||||
- **WHEN** Worker 执行一次归档或留存任务
|
||||
- **THEN** 系统至多处理一个自然日,并保留其余日期供后续受控执行
|
||||
@@ -0,0 +1,47 @@
|
||||
## Purpose
|
||||
|
||||
使高频卡轮询在多 Worker、重启积压和渠道延迟场景下保持受控吞吐,避免正常无变化观测将数据库写入量放大为不可接受的 I/O 负载。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 轮询全局并发与背压
|
||||
系统 SHALL 同时对全部轮询任务以及每一种轮询任务应用跨 Worker 实例共享的全局并发上限。达到任一上限的任务 MUST 不调用外部渠道、不创建审计或外部交互记录,并按既有调度语义延后执行。配置的上限 MUST 受安全范围约束,不能以零、负数或不受约束的大值绕过背压。
|
||||
|
||||
#### Scenario: 多 Worker 达到同类轮询上限
|
||||
- **GIVEN** 多个 Worker 正在执行同一种轮询,且该任务的全局并发已达到配置上限
|
||||
- **WHEN** 又一个该类型轮询任务开始处理
|
||||
- **THEN** 系统不发起外部调用、不写入 PostgreSQL 观测记录,并将任务延后处理
|
||||
|
||||
#### Scenario: 达到全部轮询任务总上限
|
||||
- **GIVEN** 不同种类的轮询任务合计已达到全局总并发上限
|
||||
- **WHEN** 任意一种新的轮询任务开始处理
|
||||
- **THEN** 系统不发起外部调用、不写入 PostgreSQL 观测记录,并将任务延后处理
|
||||
|
||||
#### Scenario: 轮询上限配置非法
|
||||
- **WHEN** 维护者提交超出允许范围或非正数的轮询并发上限
|
||||
- **THEN** 系统拒绝该配置并保留原有有效上限
|
||||
|
||||
### Requirement: 正常无变化观测的低写入处理
|
||||
系统 SHALL 将“渠道调用成功且未产生业务事实变化”的轮询视为短期运行观测,而非持久化审计事实。该结果 MUST 不创建逐次 Audit Event、Audit Event Resource 或 Integration Log;系统仍 MUST 维护下一次调度所需状态,并可保留短期去重或聚合指标。
|
||||
|
||||
#### Scenario: 网络状态轮询无变化
|
||||
- **GIVEN** 一张卡的网络状态轮询成功,且规范化后的状态与当前业务事实一致
|
||||
- **WHEN** 系统应用该轮询结果
|
||||
- **THEN** 系统不新增审计事件、审计资源或外部交互日志,并按配置继续后续轮询
|
||||
|
||||
#### Scenario: 流量读数无有效增量
|
||||
- **GIVEN** 一张卡的流量轮询成功,且读数未形成有效流量增量或跨周期业务变化
|
||||
- **WHEN** 系统应用该轮询结果
|
||||
- **THEN** 系统不新增审计事件、审计资源或外部交互日志,并保留后续调度能力
|
||||
|
||||
### Requirement: 业务变化与轮询异常仍可追溯
|
||||
系统 SHALL 在轮询产生网络、实名、套餐或有效流量等业务事实变化时持久化必要的业务事实与审计记录。外部调用失败、响应无效或结果未知时,系统 MUST 保留可调查的失败或恢复信息,且不得把失败静默降级为正常无变化观测。
|
||||
|
||||
#### Scenario: 轮询产生网络状态变化
|
||||
- **GIVEN** 渠道返回的网络状态与当前业务事实不同
|
||||
- **WHEN** 系统成功应用该状态变化
|
||||
- **THEN** 系统持久化业务状态变化及相应审计事实
|
||||
|
||||
#### Scenario: 轮询渠道调用失败
|
||||
- **WHEN** 轮询调用渠道超时、失败或返回无效响应
|
||||
- **THEN** 系统保留可调查的失败或恢复信息,并按既有策略安排后续处理
|
||||
@@ -0,0 +1,26 @@
|
||||
## 1. 轮询背压配置
|
||||
|
||||
- [x] 1.1 在 Worker/轮询配置中加入默认保守且受范围校验的全部轮询总并发上限,并保留现有分类轮询上限。
|
||||
- [x] 1.2 扩展 Redis 轮询令牌获取、释放和 TTL 修复,使其原子地同时限制总量与任务类型;令牌不足时沿用延后入队语义。
|
||||
- [x] 1.3 收紧轮询并发配置入口的参数校验,拒绝零、负数和超过代码安全上限的配置,并记录令牌不足的结构化指标日志。
|
||||
|
||||
## 2. 轮询记录策略
|
||||
|
||||
- [x] 2.1 盘点所有 Gateway 自动轮询处理器及其预调用 Integration Log、审计 Writer 调用,区分正常无变化、业务变化、失败和未知结果路径。
|
||||
- [x] 2.2 为轮询增加按终态记录外部交互的最小持久化路径:失败、无效/未知结果和业务变化保留可调查记录;成功无变化不创建 Integration Log。
|
||||
- [x] 2.3 修改所有自动轮询处理器,使成功无变化路径不创建 Audit Event、Audit Event Resource 或 Integration Log,且不影响下一次调度、缓存失效和领域观测判断。
|
||||
- [x] 2.4 确保轮询业务变化和失败仍写入必要的业务事实、审计或恢复记录;支付、审批、入站回调及其他非轮询可靠外部交互路径保持不变。
|
||||
- [x] 2.5 为无变化跳过、变化持久化、失败持久化及按总量/分类限流延后的轮询输出可聚合结构化日志。
|
||||
|
||||
## 3. 归档与留存降载
|
||||
|
||||
- [x] 3.1 新增默认关闭的审计归档与日留存任务总开关,并同步配置加载、生产运行说明和 Worker 启动日志。
|
||||
- [x] 3.2 总开关关闭时停止注册相关定时任务,并让 Audit 日归档、Integration 日归档和日留存处理器安全跳过已入队任务。
|
||||
- [x] 3.3 将留存服务从一次跨全部历史日期的循环改为一次最多处理一个已结束上海自然日,保留既有归档校验、pending 保留和物理清理语义。
|
||||
|
||||
## 4. 验证与运维交接
|
||||
|
||||
- [x] 4.1 在隔离环境核验:成功无变化轮询不新增三张日志表记录;状态变化和失败仍保留可调查记录;入站回调幂等与恢复路径不受影响。
|
||||
- [x] 4.2 在隔离环境核验:总量或分类轮询令牌耗尽时不调用渠道且任务延后;非法并发配置被拒绝。
|
||||
- [x] 4.3 在隔离环境核验:归档/留存总开关关闭时已入队任务不扫描在线日志表;开启时单次仅处理一个日期。
|
||||
- [x] 4.4 执行 `gofmt`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate --all` 与 `./scripts/context-health.sh`,并记录生产低峰发布、并发预算逐项调整和回滚步骤。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,50 @@
|
||||
## Context
|
||||
|
||||
见 proposal.md。当前代理开放接口的查询 Module 持有读取型观测分发能力;其 Interface 只暴露一次分发调用,却隐藏了三阶梯任务、Redis 协调记录、Gateway 请求和 Integration Log。该能力与由稳定 Outbox 事实触发的可靠观测复用同一实现,导致高频查询获得了不属于它的可靠副作用。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 让代理开放接口查询的 Interface 只包含认证、数据范围校验和本地事实读取。
|
||||
- 在查询 Module 与可靠观测 Module 之间移除读取型触发的 Seam,阻断查询量向 Redis/Asynq 放大的路径。
|
||||
- 保持持久化业务事实经 Outbox 触发可靠观测的现有行为不变。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不改变查询响应字段、认证、数据范围或把查询改为同步 Gateway 查询。
|
||||
- 不修改 Redis `maxmemory`、淘汰策略或手工删除可靠协调、队列、流量事实 key。
|
||||
- 不迁移个人客户、后台资产读取或设备控制入口;这些入口在本变更后仍需单独审查其是否应持有读取型观测能力。
|
||||
- 不在本变更中缩短可靠业务事实的 24 小时协调保留时间。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 删除查询后的观测触发,而非增加节流层
|
||||
|
||||
代理开放接口的查询在组装响应前读取的是本地持久化事实,随后才异步提交观测;该提交不会改善当前响应。删除查询成功后的卡与设备绑定卡分发调用,能使一次查询不产生 Redis 写入、Asynq 任务或 Gateway 请求。
|
||||
|
||||
不采用按卡节流、开关或另一套短 TTL 协调 Module。它们会保留“读取隐式发起外部副作用”的错误 Interface,并继续让调用方承担隐藏的资源成本。若未来确有查询新鲜度承诺,应以独立 Change 定义新鲜度语义和有界实现。
|
||||
|
||||
### 将可靠观测的触发 Seam 收回到持久化业务事实
|
||||
|
||||
可靠观测继续由业务事务写入 Outbox、Worker 消费稳定事件并触发。稳定事件键、三阶梯任务和 Redis 协调 Adapter 都留在可靠观测 Module 的实现内;代理开放接口查询不再接触该 Module。
|
||||
|
||||
这保持可靠业务写的幂等语义,同时让查询 Module 的 Interface 不再拥有无界任务创建能力。修改集中在代理开放接口 Module 及其 bootstrap 装配,获得更好的 Locality。
|
||||
|
||||
### 本次不全局缩短 24 小时协调保留
|
||||
|
||||
`cardsync:schedule`、`cardsync:attempt` 和完成标记目前共同使用 24 小时保留。它们同时覆盖三阶梯任务执行、Outbox 重投和迟到重复投递;全局缩短会改变可靠业务事实的重复消费语义。
|
||||
|
||||
自动 Outbox 重试上限为 10 次,指数退避累计约 73 分钟,但人工重放和任务积压的可接受窗口尚未形成业务契约。本变更先消除产生 95% key 的读取源头;后续应根据可靠事件的重放窗口,将不同协调记录拆分为各自有明确上限的保留策略。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [调用方曾依赖查询后最终刷新本地事实] → 当前开放接口 Spec 未承诺该行为,且查询不等待刷新结果;继续由既有轮询、回调和业务事实驱动更新。若确认存在新鲜度承诺,另建 Change 定义该承诺。
|
||||
- [旧版 API 二进制回滚] → 回滚会恢复高频查询触发;发布后优先观察 Redis key 数、OOM 错误和开放接口调用量,只有 API 可用性故障才回滚。
|
||||
- [其他读取入口仍保留同类能力] → 本变更只切断已证实的生产主来源;后续按入口的真实新鲜度契约审查,不在事故修复中猜测性改动。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 发布包含本变更的 API 二进制;Worker、Redis 配置和现有队列不作迁移或清理。
|
||||
2. 发布后以只读方式核对开放接口成功查询不再增长 `cardsync:*`,并持续观察 `DBSIZE`、`INFO memory` 和 `INFO errorstats`。
|
||||
3. 已有协调 key 按现有 TTL 自然过期。若 API 功能不可用,恢复前一 API 二进制;不得通过清空 Redis 处理回滚。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
代理开放接口的高频查询复用了可靠卡观测序列:过去 24 小时约 70 万次查询产生约 35 万条序列,并留下 230 多万个 `cardsync:*` Redis 协调 key。Redis 已达到 2GB 上限并出现大量 OOM 写入失败;查询本身返回本地事实,读后异步观测既不改变本次响应,也没有当前对外契约要求。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 停止代理开放接口的卡流量、卡状态、实名状态和设备流量查询在成功响应后创建后台卡观测序列,包括设备查询展开到绑定卡的路径。
|
||||
- 移除代理开放接口模块对读取型观测分发能力的依赖,使查询只读取并返回本地业务事实,不再隐式提交 Redis/Asynq/Gateway 副作用。
|
||||
- 保留停复机、购包、套餐激活等既有持久化业务事实经 Outbox 触发观测的语义和当前可靠协调实现;本变更不迁移其他读取入口或设备控制入口。
|
||||
- 在代理开放接口能力中建立可观察的约束,防止其后续高频查询再次接入可靠序列。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- 无。
|
||||
|
||||
### Modified Capabilities
|
||||
- `agent-open-api`: 代理开放接口的查询操作只返回当前本地事实,不再提交后台卡观测序列。
|
||||
|
||||
## Impact
|
||||
|
||||
- 代码:`internal/service/agent_open_api/service.go`、`internal/bootstrap/services.go`,以及卡观测触发模块的装配和可达调用链。
|
||||
- Redis/Asynq:不再由代理开放接口查询持续创建 `cardsync:*` 和对应任务数据;已存在 key 按其既有 TTL 自然过期。
|
||||
- 外部行为:查询响应字段、认证和数据范围不变;查询不再附带非契约化的后台 Gateway 刷新。
|
||||
- 不调整 Redis `maxmemory`、淘汰策略,也不批量清理业务协调、队列或流量事实 key。
|
||||
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理开放接口查询不触发可靠卡观测
|
||||
系统 SHALL 对卡流量、卡状态、实名状态和设备流量查询返回当前本地业务事实,且 MUST NOT 因查询成功而提交卡观测序列、展开设备绑定卡观测或安排 Gateway 后台刷新任务。
|
||||
|
||||
#### Scenario: 查询单卡本地事实
|
||||
- **WHEN** 已通过认证和数据范围校验的代理查询单卡流量、状态或实名状态
|
||||
- **THEN** 系统返回当前本地业务事实,且不创建卡观测序列或后台观测任务
|
||||
|
||||
#### Scenario: 查询设备流量
|
||||
- **WHEN** 已通过认证和数据范围校验的代理查询设备流量,或以设备标识查询卡流量
|
||||
- **THEN** 系统返回当前本地业务事实,且不展开该设备的绑定卡来创建后台观测任务
|
||||
|
||||
#### Scenario: 查询后本地事实仍可能由既有机制更新
|
||||
- **WHEN** 代理完成上述查询后发生既有轮询、回调或可靠业务事件驱动的本地事实更新
|
||||
- **THEN** 系统按对应既有机制更新本地事实,查询请求本身不承担触发该更新的责任
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. 剥离代理开放接口查询副作用
|
||||
|
||||
- [x] 1.1 删除卡流量、卡状态、实名状态和设备流量查询成功路径中的卡观测分发,保留原有本地事实查询、响应和错误语义。
|
||||
- [x] 1.2 删除代理开放接口 Module 中仅为查询观测保留的分发字段、注入方法和辅助调用,并移除 bootstrap 对该能力的装配。
|
||||
- [x] 1.3 静态追踪代理开放接口 Module 的可达调用链,确认其查询路径不再引用读取型观测分发能力,也不经设备绑定卡展开创建观测任务。
|
||||
|
||||
## 2. 构建与契约核对
|
||||
|
||||
- [x] 2.1 对修改的 Go 文件执行 gofmt,并运行 `go build ./cmd/api ./cmd/worker`。
|
||||
- [x] 2.2 运行 `go run cmd/gendocs/main.go`,核对开放接口路由、认证和响应文档没有非预期变化。
|
||||
- [ ] 2.3 运行 `openspec validate --all`,确认 delta spec 和 Change 工件一致。
|
||||
|
||||
## 3. 人工发布后只读核验
|
||||
|
||||
- [x] 3.1 由维护者按生产运行说明发布 API 二进制;不修改 Redis 配置、不清空 Redis、不中断 Worker。
|
||||
- [x] 3.2 发布前后以只读方式记录开放接口调用量、`cardsync:*` 分类 key 数、`DBSIZE`、`INFO memory` 和 `INFO errorstats`,确认查询不再持续创建观测协调记录且 OOM 计数不再增长。
|
||||
- [x] 3.3 在既有 key 最长 TTL 到期后复核 Redis key 数和内存趋势,确认剩余可靠业务观测量与业务事件量相称。
|
||||
Reference in New Issue
Block a user