新增钱包、换卡、标签系统的数据模型和规范
本次提交完成 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 行
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-13
|
||||
@@ -0,0 +1,3 @@
|
||||
# add-wallet-transfer-tag-models
|
||||
|
||||
添加钱包、换卡记录、标签系统的模型和表结构设计
|
||||
@@ -0,0 +1,102 @@
|
||||
# Change: 添加钱包、换卡、标签系统模型和表结构
|
||||
|
||||
## Why
|
||||
|
||||
在审查现有的 IoT 卡管理和订单系统后,发现以下关键功能缺失,需要补充模型和表结构设计:
|
||||
|
||||
1. **钱包系统缺失**:当前订单表支持 `payment_method=wallet`,但没有钱包表和钱包明细表,无法支持用户/代理充值和余额管理
|
||||
2. **换卡记录缺失**:IoT 卡有 `owner_type`/`owner_id` 可变更,但没有换卡记录表追踪换卡历史(老卡→新卡的套餐、代理、权益转移)
|
||||
3. **标签系统完全缺失**:企业用户无法为设备/卡片打标签进行分类管理
|
||||
4. **运营商渠道管理不足**:现有 `tb_carrier` 表只有运营商名称,无法区分运营商类型(四大运营商固定)和渠道(可自定义)
|
||||
|
||||
## What Changes
|
||||
|
||||
本提案**仅涉及模型和表结构设计**,不包含 API、Service、Store 层实现。
|
||||
|
||||
### 1. 钱包系统(新增)
|
||||
|
||||
- **新增表**:`tb_wallet`、`tb_wallet_transaction`、`tb_recharge_record`
|
||||
- **新增模型**:`Wallet`、`WalletTransaction`、`RechargeRecord`
|
||||
- **功能支持**:
|
||||
- 用户钱包和代理钱包统一管理
|
||||
- 用户可充值到钱包,购买套餐时选择钱包支付或直接支付
|
||||
- 代理可预充值到钱包,用成本价购买套餐
|
||||
- 完整的钱包明细记录(充值、扣款、退款、分佣、提现)
|
||||
- 使用乐观锁(version 字段)防止并发扣款
|
||||
|
||||
### 2. 换卡系统(新增)
|
||||
|
||||
- **新增表**:`tb_card_replacement_record`
|
||||
- **新增模型**:`CardReplacementRecord`
|
||||
- **功能支持**:
|
||||
- 记录老卡和新卡的关联关系
|
||||
- 套餐权益转移快照(剩余流量、过期时间等,使用 JSONB 存储)
|
||||
- 代理关系转移记录
|
||||
- 所有者信息转移记录
|
||||
- 换卡原因和审批状态
|
||||
|
||||
### 3. 标签系统(新增)
|
||||
|
||||
- **新增表**:`tb_tag`、`tb_resource_tag`
|
||||
- **新增模型**:`Tag`、`ResourceTag`
|
||||
- **功能支持**:
|
||||
- 标签定义(名称、颜色、使用次数)
|
||||
- 统一的资源-标签关联表(支持设备、IoT卡、号卡)
|
||||
- 企业用户可为设备/卡片打标签
|
||||
- 支持按标签查询和筛选
|
||||
|
||||
### 4. 运营商渠道管理改进(修改)
|
||||
|
||||
- **修改表**:`tb_carrier`
|
||||
- **修改模型**:`Carrier`
|
||||
- **新增字段**:
|
||||
- `carrier_type`:运营商类型(枚举:CMCC/CUCC/CTCC/CBN)
|
||||
- `channel_name`:渠道名称(可自定义)
|
||||
- `channel_code`:渠道编码(可自定义)
|
||||
- **唯一约束**:`(carrier_type, channel_code)` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
### 5. 订单系统改进(修改)
|
||||
|
||||
- **修改表**:`tb_order`
|
||||
- **修改模型**:`Order`
|
||||
- **新增字段**:
|
||||
- `wallet_payment_amount`:钱包支付金额(分)
|
||||
- `online_payment_amount`:在线支付金额(分)
|
||||
- **说明**:支持混合支付(钱包 + 在线支付)
|
||||
|
||||
## Impact
|
||||
|
||||
### 受影响的 specs
|
||||
- **新增**:wallet、card-replacement、tag
|
||||
- **修改**:carrier(运营商管理)、iot-order(订单支付方式)
|
||||
|
||||
### 受影响的代码
|
||||
- **新增文件**:
|
||||
- `internal/model/wallet.go`
|
||||
- `internal/model/card_replacement.go`
|
||||
- `internal/model/tag.go`
|
||||
- `migrations/000XXX_add_wallet_transfer_tag_tables.up.sql`
|
||||
- `migrations/000XXX_add_wallet_transfer_tag_tables.down.sql`
|
||||
- `pkg/constants/wallet.go`
|
||||
- `pkg/constants/tag.go`
|
||||
- **修改文件**:
|
||||
- `internal/model/carrier.go`
|
||||
- `internal/model/order.go`
|
||||
|
||||
### 破坏性变更
|
||||
- **无破坏性变更**:所有修改都是新增字段,有默认值,向后兼容
|
||||
|
||||
### 数据迁移
|
||||
- 需要为现有 `tb_carrier` 记录填充默认的 `carrier_type` 值
|
||||
- 建议在迁移文件中添加数据初始化脚本
|
||||
|
||||
## 设计原则遵循
|
||||
|
||||
- ✅ 表名使用 `tb_` 前缀,模型名使用单数形式
|
||||
- ✅ 所有表包含软删除(`deleted_at`)和审计字段(`creator`、`updater`)
|
||||
- ✅ 所有金额字段使用 `BIGINT` 类型,单位为分
|
||||
- ✅ 唯一索引包含 `WHERE deleted_at IS NULL` 条件
|
||||
- ✅ 禁止使用数据库外键约束
|
||||
- ✅ 所有常量定义在 `pkg/constants/` 目录
|
||||
- ✅ 使用 GORM 标准字段标签
|
||||
- ✅ 钱包使用乐观锁(version 字段)防止并发问题
|
||||
@@ -0,0 +1,183 @@
|
||||
## ADDED 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** 系统拒绝创建,返回错误信息"老卡不存在"
|
||||
@@ -0,0 +1,76 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 运营商实体定义
|
||||
|
||||
系统 SHALL 定义运营商(Carrier)实体,管理四大固定运营商(中国移动、中国联通、中国电信、广电)的渠道信息
|
||||
|
||||
**四大运营商固定枚举**:
|
||||
- **CMCC**:中国移动
|
||||
- **CUCC**:中国联通
|
||||
- **CTCC**:中国电信
|
||||
- **CBN**:广电
|
||||
|
||||
**实体字段**:
|
||||
- `id`:运营商 ID(主键,BIGINT)
|
||||
- `carrier_type`:运营商类型(VARCHAR(20),枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN")**【新增】**
|
||||
- `carrier_name`:运营商名称(VARCHAR(100),如"中国移动")
|
||||
- `carrier_code`:运营商编码(VARCHAR(50),保留字段,建议填充与 carrier_type 相同)
|
||||
- `channel_name`:渠道名称(VARCHAR(100),可自定义,如"北京渠道1")**【新增】**
|
||||
- `channel_code`:渠道编码(VARCHAR(50),可自定义,如"BJ001")**【新增】**
|
||||
- `status`:状态(INT,1-启用 2-禁用)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
**唯一约束**:`(carrier_type, channel_code)` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
#### Scenario: 创建中国移动的渠道
|
||||
|
||||
- **WHEN** 平台创建中国移动的北京渠道,`carrier_type` 为 "CMCC",`carrier_name` 为 "中国移动",`channel_name` 为 "北京渠道1",`channel_code` 为 "BJ001"
|
||||
- **THEN** 系统创建运营商记录,`carrier_type` 为 "CMCC",`channel_name` 为 "北京渠道1",`channel_code` 为 "BJ001"
|
||||
|
||||
#### Scenario: 同一运营商创建多个渠道
|
||||
|
||||
- **WHEN** 平台为中国移动创建两个渠道:北京渠道(BJ001)和上海渠道(SH001)
|
||||
- **THEN** 系统创建两条运营商记录,`carrier_type` 都为 "CMCC",但 `channel_code` 不同
|
||||
|
||||
#### Scenario: 渠道编码重复
|
||||
|
||||
- **WHEN** 平台创建中国移动的渠道,`carrier_type` 为 "CMCC",`channel_code` 为已存在的 "BJ001"
|
||||
- **THEN** 系统拒绝创建,返回错误信息"该运营商的渠道编码已存在"
|
||||
|
||||
#### Scenario: 不同运营商可以使用相同渠道编码
|
||||
|
||||
- **WHEN** 平台为中国移动创建渠道(carrier_type=CMCC, channel_code=BJ001),然后为中国联通创建渠道(carrier_type=CUCC, channel_code=BJ001)
|
||||
- **THEN** 系统允许创建,因为 `carrier_type` 不同
|
||||
|
||||
#### Scenario: 运营商类型枚举限制
|
||||
|
||||
- **WHEN** 平台创建运营商,`carrier_type` 为 "OTHER"(不在枚举中)
|
||||
- **THEN** 系统拒绝创建,返回错误信息"运营商类型必须是 CMCC/CUCC/CTCC/CBN 之一"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 运营商数据校验
|
||||
|
||||
系统 SHALL 对运营商数据进行校验,确保数据完整性和一致性。
|
||||
|
||||
**校验规则**:
|
||||
- `carrier_type`:必填,枚举值 "CMCC" | "CUCC" | "CTCC" | "CBN"
|
||||
- `carrier_name`:必填,长度 1-100 字符
|
||||
- `carrier_code`:必填,长度 1-50 字符
|
||||
- `channel_name`:可选,长度 1-100 字符
|
||||
- `channel_code`:可选,长度 1-50 字符
|
||||
- `status`:必填,枚举值 1-2
|
||||
|
||||
#### Scenario: 创建运营商时 carrier_type 无效
|
||||
|
||||
- **WHEN** 创建运营商,`carrier_type` 为 "INVALID"
|
||||
- **THEN** 系统拒绝创建,返回错误信息"运营商类型无效"
|
||||
|
||||
#### Scenario: 创建运营商时 carrier_name 为空
|
||||
|
||||
- **WHEN** 创建运营商,`carrier_name` 为空
|
||||
- **THEN** 系统拒绝创建,返回错误信息"运营商名称不能为空"
|
||||
@@ -0,0 +1,123 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单支付处理
|
||||
|
||||
系统 SHALL 根据支付方式正确处理订单支付,包括钱包扣款、在线支付、混合支付等。
|
||||
|
||||
**钱包支付流程**:
|
||||
1. 检查钱包可用余额是否充足
|
||||
2. 冻结钱包余额(`frozen_balance` 增加)
|
||||
3. 创建订单,状态为"待支付"
|
||||
4. 订单完成后,扣减钱包余额(`balance` 减少,`frozen_balance` 减少),创建钱包明细记录
|
||||
5. 订单取消时,解冻钱包余额(`frozen_balance` 减少)
|
||||
|
||||
**在线支付流程**:
|
||||
1. 创建订单,状态为"待支付"
|
||||
2. 调用第三方支付接口
|
||||
3. 用户完成支付后,订单状态变更为"已支付"
|
||||
4. 订单完成后,订单状态变更为"已完成"
|
||||
|
||||
**混合支付流程**:
|
||||
1. 检查钱包可用余额是否充足(钱包支付部分)
|
||||
2. 冻结钱包余额
|
||||
3. 创建订单,状态为"待支付"
|
||||
4. 调用第三方支付接口(在线支付部分)
|
||||
5. 用户完成在线支付后,扣减钱包余额,订单状态变更为"已支付"
|
||||
6. 订单完成后,订单状态变更为"已完成"
|
||||
|
||||
#### Scenario: 钱包支付订单完成
|
||||
|
||||
- **WHEN** 用户使用钱包支付购买套餐,订单金额为 3000 分
|
||||
- **THEN** 系统:
|
||||
1. 创建订单,状态为"待支付",冻结钱包余额 3000 分
|
||||
2. 订单处理完成后,扣减钱包余额 3000 分,解冻 3000 分,创建钱包明细记录(类型为"扣款"),订单状态变更为"已完成"
|
||||
|
||||
#### Scenario: 混合支付订单完成
|
||||
|
||||
- **WHEN** 用户使用混合支付购买套餐,钱包支付 2000 分 + 在线支付 3000 分
|
||||
- **THEN** 系统:
|
||||
1. 创建订单,状态为"待支付",冻结钱包余额 2000 分
|
||||
2. 用户完成在线支付 3000 分后,扣减钱包余额 2000 分,解冻 2000 分,创建钱包明细记录,订单状态变更为"已支付"
|
||||
3. 订单处理完成后,订单状态变更为"已完成"
|
||||
|
||||
#### Scenario: 订单取消,解冻钱包余额
|
||||
|
||||
- **WHEN** 用户使用钱包支付创建订单,订单金额为 3000 分,然后取消订单
|
||||
- **THEN** 系统解冻钱包余额 3000 分(`frozen_balance` 减少 3000),订单状态变更为"已取消"
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单实体定义
|
||||
|
||||
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单,并支持混合支付方式(钱包 + 在线支付)。
|
||||
|
||||
**修改说明**:
|
||||
- 增加 `wallet_payment_amount` 字段:钱包支付金额
|
||||
- 增加 `online_payment_amount` 字段:在线支付金额
|
||||
- 支持用户在购买套餐时选择支付方式(全部钱包支付、全部在线支付、混合支付)
|
||||
|
||||
**实体字段**(只列出新增字段):
|
||||
- `wallet_payment_amount`:钱包支付金额(BIGINT,单位:分,默认 0)**【新增】**
|
||||
- `online_payment_amount`:在线支付金额(BIGINT,单位:分,默认 0)**【新增】**
|
||||
|
||||
**支付规则**:
|
||||
- `wallet_payment_amount` + `online_payment_amount` = `amount`(订单总金额)
|
||||
- 当 `payment_method` 为 "wallet" 时,`wallet_payment_amount` = `amount`,`online_payment_amount` = 0
|
||||
- 当 `payment_method` 为 "online" 时,`online_payment_amount` = `amount`,`wallet_payment_amount` = 0
|
||||
- 混合支付时,`payment_method` 为 "mixed",两个字段都 > 0
|
||||
|
||||
#### Scenario: 全额钱包支付
|
||||
|
||||
- **WHEN** 用户购买套餐,订单金额为 30 00 分(30 元),选择钱包支付,钱包余额为 10000 分
|
||||
- **THEN** 系统创建订单,`amount` 为 3000,`payment_method` 为 "wallet",`wallet_payment_amount` 为 3000,`online_payment_amount` 为 0
|
||||
|
||||
#### Scenario: 全额在线支付
|
||||
|
||||
- **WHEN** 用户购买套餐,订单金额为 3000 分(30 元),选择在线支付
|
||||
- **THEN** 系统创建订单,`amount` 为 3000,`payment_method` 为 "online",`wallet_payment_amount` 为 0,`online_payment_amount` 为 3000
|
||||
|
||||
#### Scenario: 混合支付
|
||||
|
||||
- **WHEN** 用户购买套餐,订单金额为 5000 分(50 元),钱包余额为 3000 分,用户选择钱包支付 3000 分 + 在线支付 2000 分
|
||||
- **THEN** 系统创建订单,`amount` 为 5000,`payment_method` 为 "mixed",`wallet_payment_amount` 为 3000,`online_payment_amount` 为 2000
|
||||
|
||||
#### Scenario: 钱包余额不足,部分钱包支付
|
||||
|
||||
- **WHEN** 用户购买套餐,订单金额为 5000 分(50 元),钱包余额为 2000 分,用户选择钱包支付 2000 分 + 在线支付 3000 分
|
||||
- **THEN** 系统先冻结钱包余额 2000 分,创建订单,`wallet_payment_amount` 为 2000,`online_payment_amount` 为 3000,等待用户完成在线支付
|
||||
|
||||
#### Scenario: 钱包余额不足,无法全额钱包支付
|
||||
|
||||
- **WHEN** 用户购买套餐,订单金额为 5000 分(50 元),钱包余额为 3000 分,用户选择钱包支付
|
||||
- **THEN** 系统拒绝创建订单,返回错误信息"钱包余额不足",建议用户选择混合支付或在线支付
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单数据校验
|
||||
|
||||
系统 SHALL 对订单数据进行校验,确保数据完整性和一致性,特别是支付金额的一致性。
|
||||
|
||||
**新增校验规则**:
|
||||
- `wallet_payment_amount`:必填,≥ 0,最多精确到分
|
||||
- `online_payment_amount`:必填,≥ 0,最多精确到分
|
||||
- `wallet_payment_amount` + `online_payment_amount` = `amount`(订单总金额)
|
||||
- 当 `payment_method` 为 "wallet" 时,`wallet_payment_amount` 必须 = `amount`
|
||||
- 当 `payment_method` 为 "online" 时,`online_payment_amount` 必须 = `amount`
|
||||
- 当 `payment_method` 为 "mixed" 时,两个字段都必须 > 0
|
||||
|
||||
#### Scenario: 支付金额不一致
|
||||
|
||||
- **WHEN** 创建订单,`amount` 为 5000,`wallet_payment_amount` 为 2000,`online_payment_amount` 为 2000
|
||||
- **THEN** 系统拒绝创建,返回错误信息"支付金额总和与订单金额不一致"
|
||||
|
||||
#### Scenario: 钱包支付时在线支付金额不为 0
|
||||
|
||||
- **WHEN** 创建订单,`payment_method` 为 "wallet",`wallet_payment_amount` 为 3000,`online_payment_amount` 为 0(正确),但用户错误地设置 `online_payment_amount` 为 100
|
||||
- **THEN** 系统拒绝创建,返回错误信息"钱包支付时在线支付金额必须为 0"
|
||||
|
||||
#### Scenario: 混合支付时钱包支付金额为 0
|
||||
|
||||
- **WHEN** 创建订单,`payment_method` 为 "mixed",`wallet_payment_amount` 为 0,`online_payment_amount` 为 5000
|
||||
- **THEN** 系统拒绝创建,返回错误信息"混合支付时钱包支付金额和在线支付金额都必须大于 0"
|
||||
@@ -0,0 +1,218 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 标签实体定义
|
||||
|
||||
系统 SHALL 定义标签(Tag)实体,用于为资源(设备、IoT卡、号卡)提供自定义标签分类功能。
|
||||
|
||||
**核心概念**:
|
||||
- 企业用户可以为自己的设备/卡片创建和管理标签
|
||||
- 标签可以跨资源类型使用(一个标签可以同时用于设备和卡片)
|
||||
- 支持按标签查询和筛选资源
|
||||
|
||||
**实体字段**:
|
||||
- `id`:标签 ID(主键,BIGINT)
|
||||
- `name`:标签名称(VARCHAR(100),唯一)
|
||||
- `color`:标签颜色(VARCHAR(20),可选,用于前端显示,如 "#FF5733")
|
||||
- `usage_count`:使用次数(INT,默认 0,记录有多少资源使用了该标签)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
**唯一约束**:`name` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
#### Scenario: 创建标签
|
||||
|
||||
- **WHEN** 用户创建标签,名称为"生产设备",颜色为"#FF5733"
|
||||
- **THEN** 系统创建标签记录,`name` 为 "生产设备",`color` 为 "#FF5733",`usage_count` 为 0
|
||||
|
||||
#### Scenario: 标签名称重复
|
||||
|
||||
- **WHEN** 用户创建标签,名称为已存在的"生产设备"
|
||||
- **THEN** 系统拒绝创建,返回错误信息"标签名称已存在"
|
||||
|
||||
#### Scenario: 更新标签
|
||||
|
||||
- **WHEN** 用户更新标签(ID 为 101),将颜色从"#FF5733"改为"#33FF57"
|
||||
- **THEN** 系统更新标签记录,`color` 为 "#33FF57",`updated_at` 为当前时间
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 资源-标签关联
|
||||
|
||||
系统 SHALL 定义资源-标签关联(ResourceTag)实体,建立资源与标签的多对多关系,统一管理设备、IoT卡、号卡的标签。
|
||||
|
||||
**实体字段**:
|
||||
- `id`:关联记录 ID(主键,BIGINT)
|
||||
- `resource_type`:资源类型(VARCHAR(20),枚举值:"device"-设备 | "iot_card"-IoT卡 | "number_card"-号卡)
|
||||
- `resource_id`:资源 ID(BIGINT)
|
||||
- `tag_id`:标签 ID(BIGINT,关联 tb_tag.id)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
**唯一约束**:`(resource_type, resource_id, tag_id)` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
#### Scenario: 为设备添加标签
|
||||
|
||||
- **WHEN** 用户为设备(ID 为 1001)添加标签"生产设备"(ID 为 101)
|
||||
- **THEN** 系统创建关联记录,`resource_type` 为 "device",`resource_id` 为 1001,`tag_id` 为 101,标签的 `usage_count` 增加 1
|
||||
|
||||
#### Scenario: 为 IoT 卡添加标签
|
||||
|
||||
- **WHEN** 用户为 IoT 卡(ID 为 2001)添加标签"GPS"(ID 为 102)
|
||||
- **THEN** 系统创建关联记录,`resource_type` 为 "iot_card",`resource_id` 为 2001,`tag_id` 为 102,标签的 `usage_count` 增加 1
|
||||
|
||||
#### Scenario: 重复添加标签
|
||||
|
||||
- **WHEN** 用户为设备(ID 为 1001)添加已存在的标签"生产设备"(ID 为 101)
|
||||
- **THEN** 系统拒绝操作,返回错误信息"该资源已添加此标签"
|
||||
|
||||
#### Scenario: 移除资源标签
|
||||
|
||||
- **WHEN** 用户移除设备(ID 为 1001)的标签"生产设备"(ID 为 101)
|
||||
- **THEN** 系统删除关联记录(软删除),标签的 `usage_count` 减少 1
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 按标签查询资源
|
||||
|
||||
系统 SHALL 支持按标签查询资源,用户可以选择一个或多个标签,查询包含这些标签的资源。
|
||||
|
||||
**查询模式**:
|
||||
- **AND 模式**:查询同时包含所有指定标签的资源(交集)
|
||||
- **OR 模式**:查询包含任一指定标签的资源(并集)
|
||||
|
||||
**查询条件**:
|
||||
- 资源类型(必选,单选)
|
||||
- 标签 ID 列表(必选,可多选)
|
||||
- 查询模式(可选,默认 OR)
|
||||
|
||||
**分页**:
|
||||
- 默认每页 20 条,最大每页 100 条
|
||||
- 返回总记录数和总页数
|
||||
|
||||
#### Scenario: OR 模式查询设备
|
||||
|
||||
- **WHEN** 用户查询包含标签"生产设备"(ID 为 101)或"测试设备"(ID 为 102)的设备
|
||||
- **THEN** 系统返回所有包含标签 101 或标签 102 的设备列表
|
||||
|
||||
#### Scenario: AND 模式查询设备
|
||||
|
||||
- **WHEN** 用户查询同时包含标签"生产设备"(ID 为 101)和"GPS"(ID 为 103)的设备
|
||||
- **THEN** 系统返回同时包含标签 101 和标签 103 的设备列表
|
||||
|
||||
#### Scenario: 按标签查询 IoT 卡
|
||||
|
||||
- **WHEN** 用户查询包含标签"GPS"(ID 为 102)的 IoT 卡
|
||||
- **THEN** 系统返回所有包含标签 102 的 IoT 卡列表
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 获取资源的标签列表
|
||||
|
||||
系统 SHALL 支持查询指定资源的所有标签。
|
||||
|
||||
**查询条件**:
|
||||
- 资源类型(必选)
|
||||
- 资源 ID(必选)
|
||||
|
||||
**返回内容**:
|
||||
- 标签列表(ID、名称、颜色)
|
||||
- 按创建时间倒序排列
|
||||
|
||||
#### Scenario: 查询设备的标签
|
||||
|
||||
- **WHEN** 用户查询设备(ID 为 1001)的所有标签
|
||||
- **THEN** 系统返回设备 1001 的标签列表,包含标签 ID、名称、颜色
|
||||
|
||||
#### Scenario: 查询没有标签的设备
|
||||
|
||||
- **WHEN** 用户查询设备(ID 为 1002)的所有标签,但该设备没有任何标签
|
||||
- **THEN** 系统返回空列表
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 热门标签查询
|
||||
|
||||
系统 SHALL 支持查询热门标签,按使用次数倒序排列。
|
||||
|
||||
**查询条件**:
|
||||
- 限制数量(可选,默认 20)
|
||||
|
||||
**返回内容**:
|
||||
- 标签列表(ID、名称、颜色、使用次数)
|
||||
- 按使用次数倒序排列
|
||||
|
||||
#### Scenario: 查询热门标签
|
||||
|
||||
- **WHEN** 用户查询热门标签,限制 10 条
|
||||
- **THEN** 系统返回使用次数最多的 10 个标签,按使用次数倒序排列
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 标签批量操作
|
||||
|
||||
系统 SHALL 支持为资源批量添加或移除标签。
|
||||
|
||||
**批量添加**:
|
||||
- 为一个资源添加多个标签
|
||||
- 为多个资源添加同一个标签
|
||||
|
||||
**批量移除**:
|
||||
- 为一个资源移除多个标签
|
||||
- 为多个资源移除同一个标签
|
||||
|
||||
#### Scenario: 为设备批量添加标签
|
||||
|
||||
- **WHEN** 用户为设备(ID 为 1001)批量添加标签["生产设备", "GPS", "4G"]
|
||||
- **THEN** 系统为设备 1001 创建 3 条关联记录,所有标签的 `usage_count` 各增加 1
|
||||
|
||||
#### Scenario: 批量为设备添加标签
|
||||
|
||||
- **WHEN** 用户为设备列表 [1001, 1002, 1003] 批量添加标签"生产设备"(ID 为 101)
|
||||
- **THEN** 系统为 3 个设备各创建一条关联记录,标签"生产设备"的 `usage_count` 增加 3
|
||||
|
||||
#### Scenario: 为设备批量移除标签
|
||||
|
||||
- **WHEN** 用户为设备(ID 为 1001)批量移除标签["生产设备", "GPS"]
|
||||
- **THEN** 系统删除设备 1001 的 2 条关联记录(软删除),所有标签的 `usage_count` 各减少 1
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 标签数据校验
|
||||
|
||||
系统 SHALL 对标签数据进行校验,确保数据完整性和一致性。
|
||||
|
||||
**标签校验规则**:
|
||||
- `name`:必填,长度 1-100 字符,唯一
|
||||
- `color`:可选,长度 1-20 字符,建议使用十六进制颜色值(如 "#FF5733")
|
||||
- `usage_count`:必填,≥ 0
|
||||
|
||||
**资源-标签关联校验规则**:
|
||||
- `resource_type`:必填,枚举值 "device" | "iot_card" | "number_card"
|
||||
- `resource_id`:必填,≥ 1
|
||||
- `tag_id`:必填,≥ 1,必须是有效的标签 ID
|
||||
|
||||
#### Scenario: 创建标签时名称为空
|
||||
|
||||
- **WHEN** 用户创建标签,名称为空
|
||||
- **THEN** 系统拒绝创建,返回错误信息"标签名称不能为空"
|
||||
|
||||
#### Scenario: 创建标签时名称过长
|
||||
|
||||
- **WHEN** 用户创建标签,名称长度为 101 字符
|
||||
- **THEN** 系统拒绝创建,返回错误信息"标签名称长度不能超过 100 字符"
|
||||
|
||||
#### Scenario: 添加标签时资源类型无效
|
||||
|
||||
- **WHEN** 用户为资源添加标签,`resource_type` 为 "invalid"
|
||||
- **THEN** 系统拒绝操作,返回错误信息"资源类型无效"
|
||||
|
||||
#### Scenario: 添加标签时标签不存在
|
||||
|
||||
- **WHEN** 用户为设备添加标签,`tag_id` 为 99999(不存在的标签)
|
||||
- **THEN** 系统拒绝操作,返回错误信息"标签不存在"
|
||||
@@ -0,0 +1,199 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 钱包实体定义
|
||||
|
||||
系统 SHALL 定义钱包(Wallet)实体,统一管理用户钱包和代理钱包,支持余额管理、充值、扣款等操作。
|
||||
|
||||
**核心概念**:
|
||||
- **用户钱包**:普通用户和企业用户的钱包,用于购买套餐
|
||||
- **代理钱包**:代理商的钱包,支持预充值,可用成本价购买套餐
|
||||
|
||||
**实体字段**:
|
||||
- `id`:钱包 ID(主键,BIGINT)
|
||||
- `user_id`:用户 ID(BIGINT,关联 tb_account.id)
|
||||
- `wallet_type`:钱包类型(VARCHAR(20),枚举值:"user"-用户钱包 | "agent"-代理钱包)
|
||||
- `balance`:余额(BIGINT,单位:分,默认 0)
|
||||
- `frozen_balance`:冻结余额(BIGINT,单位:分,默认 0,用于订单待支付、提现申请中等场景)
|
||||
- `currency`:币种(VARCHAR(10),默认 "CNY")
|
||||
- `status`:钱包状态(INT,1-正常 2-冻结 3-关闭)
|
||||
- `version`:版本号(INT,默认 0,乐观锁字段,用于防止并发扣款)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
**唯一约束**:`(user_id, wallet_type, currency)` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
**可用余额计算**:可用余额 = balance - frozen_balance
|
||||
|
||||
#### Scenario: 创建用户钱包
|
||||
|
||||
- **WHEN** 用户(ID 为 2001)首次充值
|
||||
- **THEN** 系统创建钱包记录,`user_id` 为 2001,`wallet_type` 为 "user",`balance` 为 0,`status` 为 1(正常)
|
||||
|
||||
#### Scenario: 创建代理钱包
|
||||
|
||||
- **WHEN** 代理商(ID 为 123)首次充值
|
||||
- **THEN** 系统创建钱包记录,`user_id` 为 123,`wallet_type` 为 "agent",`balance` 为 0,`status` 为 1(正常)
|
||||
|
||||
#### Scenario: 计算可用余额
|
||||
|
||||
- **WHEN** 用户钱包余额为 10000 分(100 元),冻结余额为 3000 分(30 元)
|
||||
- **THEN** 系统计算可用余额为 7000 分(70 元)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包明细记录
|
||||
|
||||
系统 SHALL 记录所有钱包余额变动,包括充值、扣款、退款、分佣、提现等操作,确保完整的审计追踪。
|
||||
|
||||
**实体字段**:
|
||||
- `id`:明细 ID(主键,BIGINT)
|
||||
- `wallet_id`:钱包 ID(BIGINT,关联 tb_wallet.id)
|
||||
- `user_id`:用户 ID(BIGINT,关联 tb_account.id)
|
||||
- `transaction_type`:交易类型(VARCHAR(20),枚举值:"recharge"-充值 | "deduct"-扣款 | "refund"-退款 | "commission"-分佣 | "withdrawal"-提现)
|
||||
- `amount`:变动金额(BIGINT,单位:分,正数为增加,负数为减少)
|
||||
- `balance_before`:变动前余额(BIGINT,单位:分)
|
||||
- `balance_after`:变动后余额(BIGINT,单位:分)
|
||||
- `status`:交易状态(INT,1-成功 2-失败 3-处理中)
|
||||
- `reference_type`:关联业务类型(VARCHAR(50),如 "order" | "commission" | "withdrawal" | "topup")
|
||||
- `reference_id`:关联业务 ID(BIGINT)
|
||||
- `remark`:备注(TEXT)
|
||||
- `metadata`:扩展信息(JSONB,如手续费、支付方式等)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
#### Scenario: 充值创建明细记录
|
||||
|
||||
- **WHEN** 用户(ID 为 2001)充值 10000 分(100 元)
|
||||
- **THEN** 系统创建钱包明细记录,`transaction_type` 为 "recharge",`amount` 为 10000,`balance_before` 为 0,`balance_after` 为 10000,`status` 为 1(成功)
|
||||
|
||||
#### Scenario: 购买套餐扣款创建明细记录
|
||||
|
||||
- **WHEN** 用户(ID 为 2001)使用钱包支付购买套餐,金额 3000 分(30 元)
|
||||
- **THEN** 系统创建钱包明细记录,`transaction_type` 为 "deduct",`amount` 为 -3000,`balance_before` 为 10000,`balance_after` 为 7000,`reference_type` 为 "order",`reference_id` 为订单 ID
|
||||
|
||||
#### Scenario: 分佣发放创建明细记录
|
||||
|
||||
- **WHEN** 代理(ID 为 123)的分佣 5000 分(50 元)审批通过并发放
|
||||
- **THEN** 系统创建钱包明细记录,`transaction_type` 为 "commission",`amount` 为 5000,`balance_before` 为 20000,`balance_after` 为 25000,`reference_type` 为 "commission",`reference_id` 为分佣记录 ID
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值记录管理
|
||||
|
||||
系统 SHALL 记录所有充值操作,包括充值订单号、金额、支付方式、支付状态等信息。
|
||||
|
||||
**实体字段**:
|
||||
- `id`:充值记录 ID(主键,BIGINT)
|
||||
- `user_id`:用户 ID(BIGINT,关联 tb_account.id)
|
||||
- `wallet_id`:钱包 ID(BIGINT,关联 tb_wallet.id)
|
||||
- `recharge_no`:充值订单号(VARCHAR(50),唯一)
|
||||
- `amount`:充值金额(BIGINT,单位:分)
|
||||
- `payment_method`:支付方式(VARCHAR(20),枚举值:"alipay"-支付宝 | "wechat"-微信 | "bank"-银行转账 | "offline"-线下)
|
||||
- `payment_channel`:支付渠道(VARCHAR(50))
|
||||
- `payment_transaction_id`:第三方支付交易号(VARCHAR(100))
|
||||
- `status`:充值状态(INT,1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款)
|
||||
- `paid_at`:支付时间(TIMESTAMP,可空)
|
||||
- `completed_at`:完成时间(TIMESTAMP,可空)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
#### Scenario: 创建充值订单
|
||||
|
||||
- **WHEN** 用户(ID 为 2001)发起充值 10000 分(100 元),选择支付宝支付
|
||||
- **THEN** 系统创建充值记录,生成唯一的 `recharge_no`,`amount` 为 10000,`payment_method` 为 "alipay",`status` 为 1(待支付)
|
||||
|
||||
#### Scenario: 充值支付完成
|
||||
|
||||
- **WHEN** 用户完成支付宝支付
|
||||
- **THEN** 系统将充值记录状态从 1(待支付)变更为 2(已支付),记录 `paid_at` 时间和 `payment_transaction_id`
|
||||
|
||||
#### Scenario: 充值到账
|
||||
|
||||
- **WHEN** 充值记录状态为 2(已支付),系统处理充值到账
|
||||
- **THEN** 系统将钱包余额增加 10000 分,创建钱包明细记录,将充值记录状态变更为 3(已完成),记录 `completed_at` 时间
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包余额操作
|
||||
|
||||
系统 SHALL 支持钱包余额的充值、扣款、退款、冻结、解冻等操作,使用乐观锁防止并发问题。
|
||||
|
||||
**操作类型**:
|
||||
- **充值**:增加钱包余额
|
||||
- **扣款**:减少钱包余额(如购买套餐)
|
||||
- **退款**:增加钱包余额(如订单退款)
|
||||
- **冻结**:将部分余额转为冻结状态(如订单待支付)
|
||||
- **解冻**:将冻结余额转回可用余额(如订单取消)
|
||||
|
||||
**并发控制**:
|
||||
- 使用 `version` 字段实现乐观锁
|
||||
- 每次更新余额时,检查 `version` 是否匹配
|
||||
- 如果 `version` 不匹配,说明有并发更新,操作失败并重试
|
||||
|
||||
#### Scenario: 钱包充值
|
||||
|
||||
- **WHEN** 用户钱包当前余额为 10000 分,充值 5000 分
|
||||
- **THEN** 系统将钱包余额更新为 15000 分,`version` 从 1 变更为 2,创建钱包明细记录
|
||||
|
||||
#### Scenario: 钱包扣款
|
||||
|
||||
- **WHEN** 用户钱包当前余额为 15000 分,购买套餐扣款 3000 分
|
||||
- **THEN** 系统检查可用余额(15000 - 0 = 15000)≥ 3000,将钱包余额更新为 12000 分,`version` 从 2 变更为 3,创建钱包明细记录
|
||||
|
||||
#### Scenario: 余额不足扣款失败
|
||||
|
||||
- **WHEN** 用户钱包当前余额为 2000 分,购买套餐需要扣款 3000 分
|
||||
- **THEN** 系统检查可用余额(2000 - 0 = 2000)< 3000,拒绝扣款,返回错误信息"余额不足"
|
||||
|
||||
#### Scenario: 并发扣款乐观锁生效
|
||||
|
||||
- **WHEN** 用户钱包当前余额为 10000 分,version 为 1,两个并发请求同时扣款 3000 分和 5000 分
|
||||
- **THEN** 第一个请求成功,余额变为 7000 分,version 变为 2;第二个请求因 version 不匹配失败,需重新读取最新余额(7000 分)后重试
|
||||
|
||||
#### Scenario: 冻结余额
|
||||
|
||||
- **WHEN** 用户创建订单 10001,订单金额 3000 分,选择钱包支付
|
||||
- **THEN** 系统将钱包的 `frozen_balance` 增加 3000 分,可用余额减少 3000 分
|
||||
|
||||
#### Scenario: 解冻余额
|
||||
|
||||
- **WHEN** 用户取消订单 10001,订单金额 3000 分
|
||||
- **THEN** 系统将钱包的 `frozen_balance` 减少 3000 分,可用余额增加 3000 分
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包数据校验
|
||||
|
||||
系统 SHALL 对钱包数据进行校验,确保数据完整性和一致性。
|
||||
|
||||
**校验规则**:
|
||||
- `user_id`:必填,≥ 1
|
||||
- `wallet_type`:必填,枚举值 "user" | "agent"
|
||||
- `balance`:必填,≥ 0
|
||||
- `frozen_balance`:必填,≥ 0,≤ balance
|
||||
- `currency`:必填,长度 1-10 字符
|
||||
- `status`:必填,枚举值 1-3
|
||||
- `version`:必填,≥ 0
|
||||
|
||||
#### Scenario: 创建钱包时 user_id 无效
|
||||
|
||||
- **WHEN** 创建钱包,`user_id` 为 0
|
||||
- **THEN** 系统拒绝创建,返回错误信息"用户 ID 无效"
|
||||
|
||||
#### Scenario: 创建钱包时 wallet_type 无效
|
||||
|
||||
- **WHEN** 创建钱包,`wallet_type` 为 "invalid"
|
||||
- **THEN** 系统拒绝创建,返回错误信息"钱包类型无效"
|
||||
|
||||
#### Scenario: 冻结余额超过总余额
|
||||
|
||||
- **WHEN** 钱包余额为 10000 分,尝试冻结 15000 分
|
||||
- **THEN** 系统拒绝操作,返回错误信息"冻结余额不能超过总余额"
|
||||
@@ -0,0 +1,47 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 数据库迁移文件
|
||||
|
||||
- [x] 1.1 创建 up 迁移文件:`migrations/000007_add_wallet_transfer_tag_tables.up.sql`
|
||||
- [x] 1.2 创建 down 迁移文件:`migrations/000007_add_wallet_transfer_tag_tables.down.sql`
|
||||
- [x] 1.3 在 up 迁移中创建钱包相关表(tb_wallet, tb_wallet_transaction, tb_recharge_record)
|
||||
- [x] 1.4 在 up 迁移中创建换卡记录表(tb_card_replacement_record)
|
||||
- [x] 1.5 在 up 迁移中创建标签相关表(tb_tag, tb_resource_tag)
|
||||
- [x] 1.6 在 up 迁移中修改运营商表(tb_carrier 增加渠道字段)
|
||||
- [x] 1.7 在 up 迁移中修改订单表(tb_order 增加钱包支付字段)
|
||||
- [x] 1.8 添加必要的索引
|
||||
- [x] 1.9 编写 down 迁移的回滚逻辑
|
||||
|
||||
## 2. Go 模型定义
|
||||
|
||||
- [x] 2.1 创建 `internal/model/wallet.go`,定义 Wallet、WalletTransaction、RechargeRecord 模型
|
||||
- [x] 2.2 创建 `internal/model/card_replacement.go`,定义 CardReplacementRecord 模型
|
||||
- [x] 2.3 创建 `internal/model/tag.go`,定义 Tag、ResourceTag 模型
|
||||
- [x] 2.4 修改 `internal/model/carrier.go`,增加渠道相关字段
|
||||
- [x] 2.5 修改 `internal/model/order.go`,增加钱包支付相关字段
|
||||
- [x] 2.6 确保所有模型包含 gorm.Model 和 BaseModel(creator、updater 字段)
|
||||
- [x] 2.7 确保所有模型通过 gorm.Model 包含标准字段(ID, CreatedAt, UpdatedAt, DeletedAt)
|
||||
- [x] 2.8 为所有字段添加 GORM 标签(column、type、comment 等)
|
||||
- [x] 2.9 为所有模型添加中文注释说明业务用途
|
||||
|
||||
## 3. 常量定义
|
||||
|
||||
- [x] 3.1 创建 `pkg/constants/wallet.go`,定义钱包类型、交易类型、状态等常量(含中文注释)
|
||||
- [x] 3.2 创建 `pkg/constants/tag.go`,定义标签资源类型等常量(含中文注释)
|
||||
- [x] 3.3 在 `pkg/constants/iot.go` 中定义运营商类型枚举(CMCC/CUCC/CTCC/CBN)和换卡原因常量
|
||||
- [x] 3.4 在 `pkg/constants/redis.go` 中添加钱包和标签相关的 Redis Key 生成函数
|
||||
|
||||
## 4. 文档更新
|
||||
|
||||
- [x] 4.1 创建 `docs/add-wallet-transfer-tag-models/数据模型设计.md`,说明表结构设计
|
||||
- [x] 4.2 创建 `docs/add-wallet-transfer-tag-models/字段说明.md`,详细说明各字段含义
|
||||
- [x] 4.3 更新 AGENTS.md,添加模型规范和常量注释规范
|
||||
|
||||
## 5. 验证和测试
|
||||
|
||||
- [x] 5.1 运行 LSP 诊断验证模型定义无错误
|
||||
- [x] 5.2 验证所有唯一索引包含 `deleted_at IS NULL` 条件
|
||||
- [x] 5.3 验证模型定义与表结构一致
|
||||
- [x] 5.4 验证常量定义完整且符合规范
|
||||
- [x] 5.5 执行 `openspec validate add-wallet-transfer-tag-models --strict` ✅ 通过
|
||||
- [x] 5.6 运行迁移文件,验证表创建成功 ✅ 迁移版本: 6 → 7 (282.5ms)
|
||||
Reference in New Issue
Block a user