本次提交完成 add-wallet-transfer-tag-models 提案的实施和归档: ## 新增功能模块 - 钱包系统:用户/代理钱包管理,支持充值、扣款、退款、乐观锁防并发 - 换卡记录:物联卡更换历史追溯,包含套餐快照(JSONB) - 标签系统:设备/IoT卡/号卡的统一标签管理 - 运营商渠道:四大运营商(CMCC/CUCC/CTCC/CBN)的渠道管理 ## 数据库变更 - 新增 6 张表:tb_wallet, tb_wallet_transaction, tb_recharge_record, tb_card_replacement_record, tb_tag, tb_resource_tag - 修改 2 张表:tb_carrier(新增渠道字段), tb_order(新增混合支付字段) - 迁移版本:v6 → v7(执行时间 282.5ms) ## 代码变更 - 新增 8 个 Go 模型(符合统一规范:gorm.Model + BaseModel) - 新增 40+ 个常量定义(含完整中文注释) - 新增 7 个 Redis Key 生成函数 - 修复模型规范:移除重复字段,统一使用 gorm.Model 嵌入 ## 文档变更 - 新增 3 个业务文档:数据模型设计、字段说明、迁移验证报告 - 更新 AGENTS.md:新增 Model 模型规范和常量注释规范 - 新增 4 个 OpenSpec 规范:wallet, carrier, card-replacement, tag - 更新 1 个 OpenSpec 规范:iot-order(支持混合支付) ## 验证通过 - ✅ LSP 诊断:所有模型和常量文件无错误 - ✅ OpenSpec 验证:openspec validate --strict 通过 - ✅ 迁移执行:表结构创建成功,索引正确 - ✅ 提案归档:2026-01-13-add-wallet-transfer-tag-models 变更文件统计:29 个文件,新增 3682 行
188 lines
7.5 KiB
Markdown
188 lines
7.5 KiB
Markdown
# card-replacement Specification
|
||
|
||
## Purpose
|
||
TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose after archive.
|
||
## Requirements
|
||
### Requirement: 换卡记录实体定义
|
||
|
||
系统 SHALL 定义换卡记录(CardReplacementRecord)实体,记录老卡到新卡的完整转移过程,包括套餐权益、代理关系、所有者信息等。
|
||
|
||
**核心概念**:
|
||
- **换卡场景**:老卡损坏、丢失或故障,需要更换新卡
|
||
- **权益转移**:老卡的套餐(含剩余流量)、代理关系、所有者信息等全部转移到新卡
|
||
- **套餐继续生效**:转移后套餐不作废,剩余流量继续可用
|
||
|
||
**实体字段**:
|
||
- `id`:换卡记录 ID(主键,BIGINT)
|
||
- `replacement_no`:换卡单号(VARCHAR(50),唯一)
|
||
- `old_card_id`:老卡 ID(BIGINT,关联 tb_iot_card.id)
|
||
- `old_iccid`:老卡 ICCID(VARCHAR(50),冗余存储,防止老卡被删除后无法追踪)
|
||
- `new_card_id`:新卡 ID(BIGINT,关联 tb_iot_card.id)
|
||
- `new_iccid`:新卡 ICCID(VARCHAR(50),冗余存储)
|
||
- `old_owner_type`:老卡所有者类型(VARCHAR(20))
|
||
- `old_owner_id`:老卡所有者 ID(BIGINT)
|
||
- `old_agent_id`:老卡代理 ID(BIGINT,可空)
|
||
- `new_owner_type`:新卡所有者类型(VARCHAR(20))
|
||
- `new_owner_id`:新卡所有者 ID(BIGINT)
|
||
- `new_agent_id`:新卡代理 ID(BIGINT,可空)
|
||
- `package_snapshot`:套餐快照(JSONB,记录转移时的套餐详情)
|
||
- `replacement_reason`:换卡原因(VARCHAR(20),枚举值:"damaged"-损坏 | "lost"-丢失 | "malfunction"-故障 | "upgrade"-升级 | "other"-其他)
|
||
- `remark`:备注(TEXT)
|
||
- `status`:换卡状态(INT,1-待审批 2-已通过 3-已拒绝 4-已完成)
|
||
- `approved_by`:审批人 ID(BIGINT,可空)
|
||
- `approved_at`:审批时间(TIMESTAMP,可空)
|
||
- `completed_at`:完成时间(TIMESTAMP,可空)
|
||
- `creator`:创建人 ID(BIGINT)
|
||
- `updater`:更新人 ID(BIGINT)
|
||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||
|
||
**套餐快照 JSON 格式示例**:
|
||
```json
|
||
{
|
||
"package_id": 3001,
|
||
"package_name": "月套餐 10GB",
|
||
"package_code": "PKG-M-001",
|
||
"data_limit_mb": 10240,
|
||
"data_usage_mb": 5120,
|
||
"real_data_usage_mb": 4000,
|
||
"virtual_data_usage_mb": 1120,
|
||
"data_remaining_mb": 5120,
|
||
"activated_at": "2026-01-01T00:00:00Z",
|
||
"expires_at": "2026-02-01T00:00:00Z",
|
||
"remaining_days": 15,
|
||
"order_id": 10001
|
||
}
|
||
```
|
||
|
||
#### Scenario: 创建换卡记录
|
||
|
||
- **WHEN** 用户(ID 为 2001)的老卡(ICCID 为 "8986001")损坏,需要换新卡(ICCID 为 "8986002")
|
||
- **THEN** 系统创建换卡记录,`old_card_id` 为老卡 ID,`new_card_id` 为新卡 ID,`replacement_reason` 为 "damaged",`status` 为 1(待审批)
|
||
|
||
#### Scenario: 审批通过换卡
|
||
|
||
- **WHEN** 运营人员(ID 为 999)审批通过换卡记录(ID 为 5001)
|
||
- **THEN** 系统将换卡记录状态从 1(待审批)变更为 2(已通过),记录 `approved_by` 为 999,`approved_at` 为当前时间
|
||
|
||
#### Scenario: 完成换卡
|
||
|
||
- **WHEN** 换卡记录(ID 为 5001)状态为 2(已通过),系统执行换卡操作
|
||
- **THEN** 系统将:
|
||
1. 记录老卡和新卡的快照信息(所有者、代理、套餐)
|
||
2. 将老卡的套餐权益转移到新卡(套餐使用记录的 `iot_card_id` 更新为新卡 ID)
|
||
3. 将新卡的 `owner_type` 和 `owner_id` 更新为老卡的值
|
||
4. 将新卡的代理关系更新为老卡的值(如有)
|
||
5. 将换卡记录状态变更为 4(已完成),记录 `completed_at` 为当前时间
|
||
|
||
#### Scenario: 拒绝换卡
|
||
|
||
- **WHEN** 运营人员(ID 为 999)拒绝换卡记录(ID 为 5001),原因为"新卡不符合要求"
|
||
- **THEN** 系统将换卡记录状态从 1(待审批)变更为 3(已拒绝),记录 `approved_by` 为 999,`approved_at` 为当前时间,`remark` 为拒绝原因
|
||
|
||
---
|
||
|
||
### Requirement: 套餐权益转移
|
||
|
||
系统 SHALL 在换卡完成后,将老卡的套餐权益(包括剩余流量、过期时间等)转移到新卡,套餐继续生效。
|
||
|
||
**转移内容**:
|
||
- 套餐使用记录(`tb_package_usage`)
|
||
- 剩余流量(`data_limit_mb - data_usage_mb`)
|
||
- 套餐过期时间(`expires_at`)
|
||
- 关联的订单信息
|
||
|
||
**转移规则**:
|
||
- 老卡的套餐使用记录的 `iot_card_id` 更新为新卡 ID
|
||
- 剩余流量完整保留
|
||
- 套餐过期时间不变
|
||
- 如果老卡有多个套餐(正式套餐 + 加油包),全部转移
|
||
|
||
#### Scenario: 套餐转移
|
||
|
||
- **WHEN** 老卡有月套餐(剩余 5120 MB 流量,还有 15 天过期)
|
||
- **THEN** 系统将套餐使用记录的 `iot_card_id` 从老卡 ID 更新为新卡 ID,流量和过期时间保持不变
|
||
|
||
#### Scenario: 多套餐转移
|
||
|
||
- **WHEN** 老卡有正式套餐和 2 个加油包
|
||
- **THEN** 系统将所有套餐使用记录的 `iot_card_id` 更新为新卡 ID,所有套餐继续生效
|
||
|
||
---
|
||
|
||
### Requirement: 代理关系转移
|
||
|
||
系统 SHALL 在换卡完成后,将老卡的代理关系转移到新卡。
|
||
|
||
**转移内容**:
|
||
- 新卡的 `owner_type` 更新为老卡的 `owner_type`
|
||
- 新卡的 `owner_id` 更新为老卡的 `owner_id`
|
||
- 如果老卡通过代理销售,新卡继承相同的代理关系
|
||
|
||
#### Scenario: 代理关系转移
|
||
|
||
- **WHEN** 老卡的 `owner_type` 为 "agent",`owner_id` 为 123
|
||
- **THEN** 系统将新卡的 `owner_type` 更新为 "agent",`owner_id` 更新为 123
|
||
|
||
---
|
||
|
||
### Requirement: 换卡记录查询
|
||
|
||
系统 SHALL 支持按老卡 ID、新卡 ID、用户 ID、换卡单号等条件查询换卡记录。
|
||
|
||
**查询条件**:
|
||
- 换卡单号(精确匹配)
|
||
- 老卡 ID(精确匹配)
|
||
- 新卡 ID(精确匹配)
|
||
- 老卡 ICCID(精确匹配或模糊匹配)
|
||
- 新卡 ICCID(精确匹配或模糊匹配)
|
||
- 换卡状态(单选或多选)
|
||
- 换卡原因(单选或多选)
|
||
- 创建时间范围
|
||
- 完成时间范围
|
||
|
||
**分页**:
|
||
- 默认每页 20 条,最大每页 100 条
|
||
- 返回总记录数和总页数
|
||
|
||
#### Scenario: 按老卡 ICCID 查询换卡记录
|
||
|
||
- **WHEN** 查询老卡 ICCID 为 "8986001" 的换卡记录
|
||
- **THEN** 系统返回所有 `old_iccid` 为 "8986001" 的换卡记录列表
|
||
|
||
#### Scenario: 按状态查询换卡记录
|
||
|
||
- **WHEN** 查询状态为 1(待审批)的换卡记录
|
||
- **THEN** 系统返回所有 `status` 为 1 的换卡记录列表,按创建时间倒序排列
|
||
|
||
---
|
||
|
||
### Requirement: 换卡数据校验
|
||
|
||
系统 SHALL 对换卡数据进行校验,确保数据完整性和一致性。
|
||
|
||
**校验规则**:
|
||
- `old_card_id`:必填,≥ 1,必须是有效的 IoT 卡 ID
|
||
- `new_card_id`:必填,≥ 1,必须是有效的 IoT 卡 ID,不能与 `old_card_id` 相同
|
||
- `old_iccid`:必填,长度 19-20 字符
|
||
- `new_iccid`:必填,长度 19-20 字符,不能与 `old_iccid` 相同
|
||
- `replacement_reason`:必填,枚举值 "damaged" | "lost" | "malfunction" | "upgrade" | "other"
|
||
- `status`:必填,枚举值 1-4
|
||
|
||
#### Scenario: 换卡时老卡和新卡相同
|
||
|
||
- **WHEN** 创建换卡记录,`old_card_id` 和 `new_card_id` 都为 1001
|
||
- **THEN** 系统拒绝创建,返回错误信息"新卡不能与老卡相同"
|
||
|
||
#### Scenario: 换卡时新卡 ICCID 无效
|
||
|
||
- **WHEN** 创建换卡记录,`new_iccid` 长度为 15(小于 19)
|
||
- **THEN** 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"
|
||
|
||
#### Scenario: 换卡时老卡不存在
|
||
|
||
- **WHEN** 创建换卡记录,`old_card_id` 为 99999(不存在的 IoT 卡)
|
||
- **THEN** 系统拒绝创建,返回错误信息"老卡不存在"
|
||
|