docs: 记录换货迁移导致主套餐双生效的生产数据修正

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-11-04 13:19:04 +09:00
parent 7c1026d65e
commit ee5772be2c

View File

@@ -0,0 +1,133 @@
# 2026-11-04 换货迁移导致卡上两个主套餐同时生效的数据修正
## 现象
卡 `89860624630055040571`(`tb_iot_card.id = 93937`)在后台的预计套餐到期时间显示"数据异常"(`expiry_estimate_status = invalid_data`)。
## 原因
### 1. 换货迁移把老卡的生效中套餐原样挂到了新卡上
2026-11-03 17:42,换货单 `EXC20261103174239616885`(`tb_exchange_order.id = 1094`)完成直接换货:
- 老卡:`89860624630055027198`(`id = 93846`),停机、未实名,从未产生流量。
- 新卡:`89860624630055040571`(`id = 93937`)。
- 选了迁移数据。换货原因是"未实名卡,给老卡续费"。
`internal/service/exchange/migration.go` 的 `migratePackageUsageWithTx` 只把老卡套餐使用记录的 `iot_card_id` 改成新卡,状态原样保留,也不检查新卡是否已有生效中的主套餐。迁移后新卡上同时有两个生效中(`status = 1`)的主套餐:
| 套餐使用记录 | 套餐 | 订单号 | 修正前状态 | 到期时间 |
|---|---|---|---|---|
| 47051 | Mifi-U省内版(12个月省内流量每月3000G),`package_id = 272` | `MIG-89860624630055040571-1` | 生效中 | 2026-12-14 |
| 46981 | 12个月省内流量每月1000G,`package_id = 230` | `MIG-89860624630055027198-1` | 生效中,`activated_at` 为空 | 2027-12-14 |
46981 的到期时间正好是 47051 到期后再加一年,说明运营的本意是"续费,接在现有套餐后面"。
`internal/query/packageexpiry/query.go` 的 `Calculate` 要求一个资产只能有一个生效中或已用完的主套餐。出现两个时返回 `invalid_data`。
### 2. 流量扣到了续费的套餐上
换货后,新卡的流量全部扣到了 46981 上,47051 从 11-03 17:40 起没有再被扣过:
| 日期 | 47051 日用量 | 46981 日用量 |
|---|---|---|
| 11-01 | 941 MB | |
| 11-02 | 255 MB | |
| 11-03 | 5,393 MB | 52 MB |
| 11-04 | | 2,707 MB |
合计 9,348 MB,和新卡 11 月的 `current_month_usage_mb = 9348.33` 一致。老卡本月和上月的用量都是 0,所以 46981 上的用量全部来自新卡。
### 3. 第一次修正后仍显示数据异常:排序问题
46981 改成待生效后,页面仍然显示数据异常。原因是两条记录的 `priority` 都是 1,`created_at` 也完全相同(同一次迁移写入,都是 `2026-09-02 16:40:32.81965`),排序只能按 `id`,46981 就排在了 47051 前面。`Calculate` 不允许待生效套餐排在当前套餐之前,所以仍然判成 `invalid_data`。把 46981 的 `priority` 改成 2 后,它排到了 47051 后面。
## 已执行的修正(生产库,2026-11-04)
两步都在单个事务中执行:先锁行,改完后自动校验,校验不通过就整体回滚。
### 第一步:用量转回 47051,46981 改为待生效
执行前:46981 的 `data_usage_mb = 2759`,47051 的 `data_usage_mb = 112102`。
```sql
BEGIN;
SELECT id, status, data_usage_mb, iot_card_id FROM tb_package_usage WHERE id IN (46981, 47051) ORDER BY id FOR UPDATE;
-- 46981 的已用量转回 47051
UPDATE tb_package_usage a
SET data_usage_mb = a.data_usage_mb + b.data_usage_mb, updated_at = now()
FROM tb_package_usage b
WHERE a.id = 47051 AND b.id = 46981 AND a.status = 1 AND b.status = 1 AND a.iot_card_id = 93937 AND b.iot_card_id = 93937;
-- 日用量记录并到 47051
INSERT INTO tb_package_usage_daily_record (package_usage_id, date, daily_usage_mb, cumulative_usage_mb, created_at, updated_at)
SELECT 47051, d.date, d.daily_usage_mb, 0, now(), now()
FROM tb_package_usage_daily_record d WHERE d.package_usage_id = 46981
ON CONFLICT (package_usage_id, date) DO UPDATE
SET daily_usage_mb = tb_package_usage_daily_record.daily_usage_mb + EXCLUDED.daily_usage_mb, updated_at = now();
-- 以 11-02 的累计值为起点,重算 47051 从 11-03 起的累计用量
UPDATE tb_package_usage_daily_record r SET cumulative_usage_mb = x.cum, updated_at = now()
FROM (
SELECT id,
(SELECT cumulative_usage_mb FROM tb_package_usage_daily_record WHERE package_usage_id = 47051 AND date = '2026-11-02')
+ sum(daily_usage_mb) OVER (ORDER BY date) AS cum
FROM tb_package_usage_daily_record
WHERE package_usage_id = 47051 AND date >= '2026-11-03'
) x
WHERE r.id = x.id;
DELETE FROM tb_package_usage_daily_record WHERE package_usage_id = 46981;
-- 46981 改为待生效,和现有待生效套餐的数据形态一致;激活时不会把已用量清零,所以这里必须清零
UPDATE tb_package_usage
SET status = 0, data_usage_mb = 0, activated_at = NULL, expires_at = NULL,
last_reset_at = NULL, next_reset_at = NULL, updated_at = now()
WHERE id = 46981 AND status = 1 AND iot_card_id = 93937;
-- 校验项:47051 的已用量等于两者之和;46981 是待生效、已用量为 0;
-- 47051 最后一天的累计用量等于它的已用量;46981 已经没有日用量记录
COMMIT;
```
### 第二步:调整排队顺序
```sql
BEGIN;
UPDATE tb_package_usage SET priority = 2, updated_at = now()
WHERE id = 46981 AND status = 0 AND iot_card_id = 93937 AND priority = 1;
COMMIT;
```
## 修正后的状态
| 套餐使用记录 | 状态 | 优先级 | 已用量 | 生效时间 | 到期时间 | 下次重置 |
|---|---|---|---|---|---|---|
| 47051 | 生效中 | 1 | 114,861 MB | 2025-12-14 | 2026-12-14 | 2026-11-09 |
| 46981 | 待生效 | 2 | 0 | 空 | 空 | 空 |
47051 的日用量记录:11-03 为 5,445 MB,累计 112,154 MB;11-04 为 2,707 MB,累计 114,861 MB。
预期效果:
- 后续流量扣到 47051 上。
- 预计到期时间推算为约 2027-12-14:47051 到期后,46981 由排队激活接上 365 天。
## 回滚
一般不需要回滚。如果必须恢复到修正前的状态:
1. 46981 恢复为 `status = 1`、`priority = 1`、`data_usage_mb = 2759`、`expires_at = '2027-12-14 00:00:00'`、`last_reset_at = '2026-10-18 01:23:14.86183'`、`next_reset_at = '2026-11-17 00:00:00'`。
2. 47051 的 `data_usage_mb` 减去 2759。
3. 日用量记录拆回原样:
- 46981:11-03 为 52/52,11-04 为 2707/2759;
- 47051:11-03 为 5393/112102,并删除 47051 的 11-04 记录。
回滚前,要先确认修正之后又扣进 47051 的新用量。
## 遗留问题
- **代码漏洞未修。** 换货迁移不处理新资产已有生效中主套餐的情况,以后同类换货还会再出现两个主套餐同时生效。可选的修法:
- 迁移过来的主套餐改为待生效,排在新资产现有套餐之后(`priority` 递增);
- 或者在换货前检查,直接拒绝,提示运营先处理。
- **同类问题未排查。** 生产中可能还有其他卡或设备同时挂着多个生效中的主套餐,尚未全量排查。