Files
huang 6e2dc325d7 新增钱包、换卡、标签系统的数据模型和规范
本次提交完成 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 行
2026-01-13 15:47:32 +08:00

188 lines
7.5 KiB
Markdown
Raw Permalink 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.
# 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`:老卡 IDBIGINT关联 tb_iot_card.id
- `old_iccid`:老卡 ICCIDVARCHAR(50),冗余存储,防止老卡被删除后无法追踪)
- `new_card_id`:新卡 IDBIGINT关联 tb_iot_card.id
- `new_iccid`:新卡 ICCIDVARCHAR(50),冗余存储)
- `old_owner_type`老卡所有者类型VARCHAR(20)
- `old_owner_id`:老卡所有者 IDBIGINT
- `old_agent_id`:老卡代理 IDBIGINT可空
- `new_owner_type`新卡所有者类型VARCHAR(20)
- `new_owner_id`:新卡所有者 IDBIGINT
- `new_agent_id`:新卡代理 IDBIGINT可空
- `package_snapshot`套餐快照JSONB记录转移时的套餐详情
- `replacement_reason`换卡原因VARCHAR(20),枚举值:"damaged"-损坏 | "lost"-丢失 | "malfunction"-故障 | "upgrade"-升级 | "other"-其他)
- `remark`备注TEXT
- `status`换卡状态INT1-待审批 2-已通过 3-已拒绝 4-已完成)
- `approved_by`:审批人 IDBIGINT可空
- `approved_at`审批时间TIMESTAMP可空
- `completed_at`完成时间TIMESTAMP可空
- `creator`:创建人 IDBIGINT
- `updater`:更新人 IDBIGINT
- `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** 系统拒绝创建,返回错误信息"老卡不存在"