收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -0,0 +1,139 @@
# 七月迭代套餐接续恢复
## 故障基线
生产只读诊断结果为 `scanned_count=100``skipped_as_occupied=100``waiting_realname=0`。旧扫描先截取全库前 100 条待生效套餐,再逐条排除仍有占位套餐的载体,导致窗口之外的真实孤儿永久饥饿。
## 候选查询
以下 SQL 只读取真实孤儿载体的稳定队首,`EXPLAIN` 版本仅在可访问目标数据库时执行:
```sql
EXPLAIN (ANALYZE, BUFFERS)
WITH pending_queue AS (
SELECT pending.id,
pending.priority,
pending.created_at,
ROW_NUMBER() OVER (
PARTITION BY
CASE WHEN COALESCE(pending.iot_card_id, 0) > 0 THEN 'iot_card' ELSE 'device' END,
CASE WHEN COALESCE(pending.iot_card_id, 0) > 0 THEN pending.iot_card_id ELSE pending.device_id END
ORDER BY pending.priority ASC, pending.created_at ASC, pending.id ASC
) AS queue_position
FROM tb_package_usage AS pending
WHERE pending.status = 0
AND pending.master_usage_id IS NULL
AND pending.deleted_at IS NULL
AND (COALESCE(pending.iot_card_id, 0) > 0 OR COALESCE(pending.device_id, 0) > 0)
AND NOT EXISTS (
SELECT 1
FROM tb_package_usage AS occupied
WHERE occupied.status IN (1, 2)
AND occupied.master_usage_id IS NULL
AND occupied.deleted_at IS NULL
AND (
(COALESCE(pending.iot_card_id, 0) > 0 AND occupied.iot_card_id = pending.iot_card_id)
OR (COALESCE(pending.iot_card_id, 0) = 0 AND pending.device_id > 0 AND occupied.device_id = pending.device_id)
)
)
)
SELECT usage.*
FROM pending_queue AS candidate
JOIN tb_package_usage AS usage ON usage.id = candidate.id
WHERE candidate.queue_position = 1
ORDER BY candidate.priority ASC, candidate.created_at ASC, candidate.id ASC
LIMIT 100;
```
去掉首行 `EXPLAIN (ANALYZE, BUFFERS)` 可查看实际候选。没有查询计划证据前不新增索引。
## 实现边界
- 旧主套餐及关联加油包先在原事务提交,随后调用 `ActivateNextPendingMainPackage`
- 孤儿扫描对每个真实孤儿直接调用同一应用能力,不投递排队激活任务。
- 应用能力在新事务内重新检查占位状态、队首与实名条件,并使用载体级 Redis 锁收敛并发。
- 激活状态与稳定的 `card.observation.series.requested` Outbox 在同一事务提交;事件 ID 为 `card-observation:package-usage:{usage_id}:activated`
- Audit Event、Domain Ledger 新增项和 Integration Log 均为 N/A`tb_package_usage` 继续作为套餐状态权威事实。
## 部署前后只读核验
记录真实孤儿数量和最老等待时间:
```sql
WITH pending_queue AS (
SELECT pending.id,
pending.created_at,
ROW_NUMBER() OVER (
PARTITION BY
CASE WHEN COALESCE(pending.iot_card_id, 0) > 0 THEN 'iot_card' ELSE 'device' END,
CASE WHEN COALESCE(pending.iot_card_id, 0) > 0 THEN pending.iot_card_id ELSE pending.device_id END
ORDER BY pending.priority ASC, pending.created_at ASC, pending.id ASC
) AS queue_position
FROM tb_package_usage AS pending
WHERE pending.status = 0
AND pending.master_usage_id IS NULL
AND pending.deleted_at IS NULL
AND (COALESCE(pending.iot_card_id, 0) > 0 OR COALESCE(pending.device_id, 0) > 0)
AND NOT EXISTS (
SELECT 1
FROM tb_package_usage AS occupied
WHERE occupied.status IN (1, 2)
AND occupied.master_usage_id IS NULL
AND occupied.deleted_at IS NULL
AND (
(COALESCE(pending.iot_card_id, 0) > 0 AND occupied.iot_card_id = pending.iot_card_id)
OR (COALESCE(pending.iot_card_id, 0) = 0 AND pending.device_id > 0 AND occupied.device_id = pending.device_id)
)
)
)
SELECT COUNT(*) AS orphan_count,
MIN(created_at) AS oldest_created_at,
NOW() - MIN(created_at) AS oldest_waiting
FROM pending_queue
WHERE queue_position = 1;
```
检查同一载体是否存在多条占位主套餐:
```sql
SELECT 'iot_card' AS carrier_type, iot_card_id AS carrier_id, COUNT(*) AS occupied_count
FROM tb_package_usage
WHERE status IN (1, 2)
AND master_usage_id IS NULL
AND deleted_at IS NULL
AND COALESCE(iot_card_id, 0) > 0
GROUP BY iot_card_id
HAVING COUNT(*) > 1
UNION ALL
SELECT 'device', device_id, COUNT(*)
FROM tb_package_usage
WHERE status IN (1, 2)
AND master_usage_id IS NULL
AND deleted_at IS NULL
AND COALESCE(iot_card_id, 0) = 0
AND COALESCE(device_id, 0) > 0
GROUP BY device_id
HAVING COUNT(*) > 1;
```
检查卡观测 Outbox 待投递状态:
```sql
SELECT status, COUNT(*) AS event_count, MIN(created_at) AS oldest_created_at
FROM tb_outbox_event
WHERE event_type = 'card.observation.series.requested'
GROUP BY status
ORDER BY status;
```
部署后至少观察两个轮询周期。停止观察的条件是:真实孤儿持续收敛、重复占位查询为 0、同步接续成功日志与对应套餐 `status=1` 一致、卡观测 Outbox 正常进入已投递状态。当前环境未连接目标数据库,以上 SQL 的实际结果需在部署窗口记录。
## 回滚与人工恢复
本次没有数据库迁移。发布后如需回滚,则回退本修复代码并重新部署 Worker已经正确激活的套餐保持业务事实不执行反向 SQL。
单卡人工恢复只允许通过 `ActivateNextPendingMainPackage` 按载体类型和 ID 调用,由应用服务重新检查 `status IN (1,2)` 占位、队首、实名条件、Redis 锁及 Outbox 原子写入;禁止直接把 `tb_package_usage.status` 更新为 `1`,避免遗漏激活时间、到期时间、重置时间和卡观测 Outbox。
## 验证声明
按用户要求,本修复不新增、修改或运行自动化测试。代码验证使用格式化、差异检查、静态检查和全量构建;数据库验证仅使用上述只读 SQL。

View File

@@ -0,0 +1,17 @@
# 批量套餐失效订单生成脚本功能总结
## 功能说明
新增 `scripts/batch_package_purchase/generate_invalidate_orders.py`,读取单列 ICCID CSV通过现有后台资产套餐接口查询状态为待生效、生效中或已用完的套餐按订单号去重后生成现有订单套餐批量失效功能要求的单列 CSV。
## 边界与安全
- 仅接受 19 或 20 位 ICCID并复用批量购买脚本的表头、重复值和单列校验。
- 只读取现有受权后台接口,不直连数据库,不新增依赖。
- 已过期和已失效套餐不进入结果;无订单号的套餐记录跳过。
- 任一 ICCID 查询失败时整批终止,不生成不完整的订单号文件。
- 脚本不触发套餐失效,实际状态变更继续使用已有批量失效功能。
## 输出
输出文件首行为 `order_no`,后续每行一个去重后的订单号,可直接上传到已有订单套餐批量失效功能。

View File

@@ -0,0 +1,18 @@
# 批量套餐过期时间修改脚本功能总结
## 功能说明
新增 `scripts/batch_package_purchase/batch_update_package_expiry.py`,读取单列资产标识 CSV复用现有资产套餐查询及过期时间修改接口将每个资产全部生效中套餐统一修改为指定过期时间。
## 安全控制
- 默认仅查询和预演,显式增加 `--execute` 后才会修改。
- 真实执行前先完成整批资产查询,避免查询阶段错误造成半批修改。
- 只处理套餐状态 `1`,不修改待生效、已用完、已过期或已失效套餐。
- 每条修改继续由现有接口校验资产归属,并记录 `asset_package_expires_at` 前后值审计。
- 接口权限沿用现状,仅后台账号 ID `41``127` 可调用。
- PATCH 请求不自动重试,结果逐条刷新到 CSV中断时已完成记录不会丢失。
## 输出
结果 CSV 记录资产标识、套餐使用记录 ID、套餐名称、原过期时间、目标过期时间、HTTP 状态码、业务错误码和接口消息。