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

7.5 KiB
Raw Permalink Blame History

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 格式示例

{
  "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 为老卡 IDnew_card_id 为新卡 IDreplacement_reason 为 "damaged"status 为 1待审批

Scenario: 审批通过换卡

  • WHEN 运营人员ID 为 999审批通过换卡记录ID 为 5001
  • THEN 系统将换卡记录状态从 1待审批变更为 2已通过记录 approved_by 为 999approved_at 为当前时间

Scenario: 完成换卡

  • WHEN 换卡记录ID 为 5001状态为 2已通过系统执行换卡操作
  • THEN 系统将:
    1. 记录老卡和新卡的快照信息(所有者、代理、套餐)
    2. 将老卡的套餐权益转移到新卡(套餐使用记录的 iot_card_id 更新为新卡 ID
    3. 将新卡的 owner_typeowner_id 更新为老卡的值
    4. 将新卡的代理关系更新为老卡的值(如有)
    5. 将换卡记录状态变更为 4已完成记录 completed_at 为当前时间

Scenario: 拒绝换卡

  • WHEN 运营人员ID 为 999拒绝换卡记录ID 为 5001原因为"新卡不符合要求"
  • THEN 系统将换卡记录状态从 1待审批变更为 3已拒绝记录 approved_by 为 999approved_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_idnew_card_id 都为 1001
  • THEN 系统拒绝创建,返回错误信息"新卡不能与老卡相同"

Scenario: 换卡时新卡 ICCID 无效

  • WHEN 创建换卡记录,new_iccid 长度为 15小于 19
  • THEN 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"

Scenario: 换卡时老卡不存在

  • WHEN 创建换卡记录,old_card_id 为 99999不存在的 IoT 卡)
  • THEN 系统拒绝创建,返回错误信息"老卡不存在"