feat: 钱包系统分离 - 代理钱包与卡钱包完全隔离
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s

## 变更概述
将统一钱包系统拆分为代理钱包和卡钱包两个独立系统,实现数据表和代码层面的完全隔离。

## 数据库变更
- 新增 6 张表:tb_agent_wallet、tb_agent_wallet_transaction、tb_agent_recharge_record、tb_card_wallet、tb_card_wallet_transaction、tb_card_recharge_record
- 删除 3 张旧表:tb_wallet、tb_wallet_transaction、tb_recharge_record
- 代理钱包:按 (shop_id, wallet_type) 唯一标识,支持主钱包和分佣钱包
- 卡钱包:按 (resource_type, resource_id) 唯一标识,支持物联网卡和设备

## 代码变更
- Model 层:新增 AgentWallet、AgentWalletTransaction、AgentRechargeRecord、CardWallet、CardWalletTransaction、CardRechargeRecord 模型
- Store 层:新增 6 个独立 Store,支持事务、乐观锁、Redis 缓存
- Service 层:重构 commission_calculation、commission_withdrawal、order、recharge 等 8 个服务
- Bootstrap 层:更新 Store 和 Service 依赖注入
- 常量层:按钱包类型重新组织常量和 Redis Key 生成函数

## 技术特性
- 乐观锁:使用 version 字段防止并发冲突
- 多租户:支持 shop_id_tag 和 enterprise_id_tag 过滤
- 事务管理:所有余额变动使用事务保证 ACID
- 缓存策略:Cache-Aside 模式,余额变动后删除缓存

## 业务影响
- 代理钱包和卡钱包业务完全隔离,互不影响
- 为独立监控、优化、扩展打下基础
- 提升代理钱包的稳定性和独立性

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 09:51:00 +08:00
parent f32d32cd36
commit 18daeae65a
66 changed files with 4420 additions and 1090 deletions

View File

@@ -1,254 +1,88 @@
# wallet Specification
# wallet Specification (DEPRECATED)
## Purpose
TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose after archive.
## Requirements
**⚠️ 此规范已废弃**
钱包系统已重构,废弃统一钱包设计,拆分为 `agent-wallet`(代理钱包)和 `card-wallet`(卡钱包)两个完全独立的系统,实现数据层和代码层的完全隔离。
**请参阅新规范:**
- 代理钱包系统:[agent-wallet/spec.md](../agent-wallet/spec.md)
- 卡钱包系统:[card-wallet/spec.md](../card-wallet/spec.md)
---
## REMOVED Requirements
### Requirement: 钱包实体定义
系统 SHALL 定义钱包(Wallet)实体,统一管理用户钱包和代理钱包,支持余额管理、充值、扣款等操作
**⚠️ 已废弃** - 废弃统一钱包设计拆分为代理钱包AgentWallet和卡钱包CardWallet两个独立实体使用独立的数据表
**核心概念**
- **用户钱包**:普通用户和企业用户的钱包,用于购买套餐
- **代理钱包**:代理商的钱包,支持预充值,可用成本价购买套餐
**实体字段**
- `id`:钱包 ID主键BIGINT
- `user_id`:用户 IDBIGINT关联 tb_account.id
- `wallet_type`钱包类型VARCHAR(20),枚举值:"user"-用户钱包 | "agent"-代理钱包)
- `balance`余额BIGINT单位默认 0
- `frozen_balance`冻结余额BIGINT单位默认 0用于订单待支付、提现申请中等场景
- `currency`币种VARCHAR(10),默认 "CNY"
- `status`钱包状态INT1-正常 2-冻结 3-关闭)
- `version`版本号INT默认 0乐观锁字段用于防止并发扣款
- `creator`:创建人 IDBIGINT
- `updater`:更新人 IDBIGINT
- `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 元)
**迁移指南**
- 代理钱包shop 类型)→ `tb_agent_wallet` 表,参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡钱包iot_card 和 device 类型)→ `tb_card_wallet` 表,参见 [card-wallet spec](../card-wallet/spec.md)
- 代码层使用新的 Model`model.AgentWallet``model.CardWallet`
- 代码层使用新的 Store`AgentWalletStore``CardWalletStore`
---
### Requirement: 钱包明细记录
系统 SHALL 记录所有钱包余额变动,包括充值、扣款、退款、分佣、提现等操作,确保完整的审计追踪
**⚠️ 已废弃** - 废弃统一交易记录表拆分为代理钱包交易记录tb_agent_wallet_transaction和卡钱包交易记录tb_card_wallet_transaction两个独立表
**实体字段**
- `id`:明细 ID主键BIGINT
- `wallet_id`:钱包 IDBIGINT关联 tb_wallet.id
- `user_id`:用户 IDBIGINT关联 tb_account.id
- `transaction_type`交易类型VARCHAR(20),枚举值:"recharge"-充值 | "deduct"-扣款 | "refund"-退款 | "commission"-分佣 | "withdrawal"-提现)
- `amount`变动金额BIGINT单位正数为增加负数为减少
- `balance_before`变动前余额BIGINT单位
- `balance_after`变动后余额BIGINT单位
- `status`交易状态INT1-成功 2-失败 3-处理中)
- `reference_type`关联业务类型VARCHAR(50),如 "order" | "commission" | "withdrawal" | "topup"
- `reference_id`:关联业务 IDBIGINT
- `remark`备注TEXT
- `metadata`扩展信息JSONB如手续费、支付方式等
- `creator`:创建人 IDBIGINT
- `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
**迁移指南**
- 代理钱包交易记录 → `tb_agent_wallet_transaction` 表,参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡钱包交易记录 → `tb_card_wallet_transaction` 表,参见 [card-wallet spec](../card-wallet/spec.md)
- 代码层使用新的 Model`model.AgentWalletTransaction``model.CardWalletTransaction`
---
### Requirement: 充值记录管理
系统 SHALL 记录所有充值操作,包括充值订单号、金额、支付方式、支付状态等信息
**⚠️ 已废弃** - 废弃统一充值记录表拆分为代理充值记录tb_agent_recharge_record和卡充值记录tb_card_recharge_record两个独立表
**实体字段**
- `id`:充值记录 ID主键BIGINT
- `user_id`:用户 IDBIGINT关联 tb_account.id
- `wallet_id`:钱包 IDBIGINT关联 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`充值状态INT1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款)
- `paid_at`支付时间TIMESTAMP可空
- `completed_at`完成时间TIMESTAMP可空
- `creator`:创建人 IDBIGINT
- `updater`:更新人 IDBIGINT
- `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` 时间
**迁移指南**
- 代理充值记录 → `tb_agent_recharge_record` 表,参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡充值记录 → `tb_card_recharge_record` 表,参见 [card-wallet spec](../card-wallet/spec.md)
- 代码层使用新的 Model`model.AgentRechargeRecord``model.CardRechargeRecord`
- 充值服务拆分为独立的代理充值和卡充值逻辑
---
### Requirement: 钱包余额操作
系统 SHALL 支持钱包余额的充值、扣款、退款、冻结、解冻等操作,使用乐观锁防止并发问题
**⚠️ 已废弃** - 余额操作逻辑拆分到代理钱包和卡钱包两个独立系统,使用各自的 Store 实现
**操作类型**
- **充值**:增加钱包余额
- **扣款**:减少钱包余额(如购买套餐)
- **退款**:增加钱包余额(如订单退款)
- **冻结**:将部分余额转为冻结状态(如订单待支付)
- **解冻**:将冻结余额转回可用余额(如订单取消)
**并发控制**
- 使用 `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 分
**迁移指南**
- 代理钱包余额操作 → 使用 `AgentWalletStore`,参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡钱包余额操作 → 使用 `CardWalletStore`,参见 [card-wallet spec](../card-wallet/spec.md)
- 并发控制(乐观锁)机制保持不变,继续使用 `version` 字段
---
### Requirement: 钱包数据校验
系统 SHALL 对钱包数据进行校验,确保数据完整性和一致性
**⚠️ 已废弃** - 数据校验规则拆分到代理钱包和卡钱包两个独立系统,针对各自的字段设计优化
**校验规则变更**
- ~~`user_id`~~~~必填,≥ 1~~**已删除**
- `resource_type`:必填,枚举值 "iot_card" | "device" | "shop"**新增**
- `resource_id`:必填,≥ 1必须是有效的资源 ID**新增**
- `wallet_type`:必填,枚举值 "main" | "commission"
- `balance`:必填,≥ 0
- `frozen_balance`:必填,≥ 0≤ balance
- `currency`:必填,长度 1-10 字符
- `status`:必填,枚举值 1-3
- `version`:必填,≥ 0
#### Scenario: 创建钱包时 resource_type 无效
- **WHEN** 创建钱包,`resource_type` 为 "invalid"
- **THEN** 系统拒绝创建,返回错误信息"资源类型无效,必须是 iot_card、device 或 shop"
#### Scenario: 创建钱包时 resource_id 无效
- **WHEN** 创建钱包,`resource_type` 为 "iot_card"`resource_id` 为 0
- **THEN** 系统拒绝创建,返回错误信息"资源 ID 无效,必须 ≥ 1"
#### Scenario: 冻结余额超过总余额
- **WHEN** 钱包余额为 10000 分,尝试冻结 15000 分
- **THEN** 系统拒绝操作,返回错误信息"冻结余额不能超过总余额"
### Requirement: 钱包归属资源规则
系统 SHALL 根据资源类型管理钱包归属,支持个人客户卡/设备转手和代理商店铺级别管理。
**归属规则**
| 资源类型 | ResourceType | 适用场景 | 说明 |
|---------|-------------|---------|------|
| 物联网卡 | iot_card | 个人客户购买单卡 | 钱包归属卡,卡转手时钱包跟着卡走 |
| 设备 | device | 个人客户购买设备含1-4张卡 | 钱包归属设备,设备的多张卡共享钱包 |
| 店铺 | shop | 代理商预存款 | 钱包归属店铺,店铺的多个员工账号共享钱包 |
**资源转手规则**
- 物联网卡转手:新用户登录后可以看到卡的钱包余额
- 设备转手:新用户登录后可以看到设备的钱包余额(包含绑定的所有卡)
- 店铺钱包:不支持转手,归属店铺不变
#### Scenario: 个人客户购买单卡并充值
- **WHEN** 个人客户通过 ICCID "8986001234567890" 登录(首次登录),为该卡充值 10000 分
- **THEN** 系统创建钱包记录,`resource_type` 为 "iot_card"`resource_id` 为卡 ID`balance` 为 10000
#### Scenario: 个人客户购买设备并充值
- **WHEN** 个人客户通过设备号 "DEV-001" 登录(首次登录),该设备绑定 3 张卡,为设备充值 20000 分
- **THEN** 系统创建钱包记录,`resource_type` 为 "device"`resource_id` 为设备 ID设备的 3 张卡共享该钱包
#### Scenario: 卡转手后新用户查询余额
- **WHEN** 个人客户 A微信 OpenID 为 "wx_a"的卡ICCID 为 "8986001234567890")转手给个人客户 B微信 OpenID 为 "wx_b"),卡钱包余额为 5000 分
- **THEN** 个人客户 B 通过 ICCID "8986001234567890" 登录后查询钱包,余额为 5000 分,可以继续使用
#### Scenario: 设备转手后新用户查询余额
- **WHEN** 个人客户 A 的设备(设备号 "DEV-001",绑定 3 张卡)转手给个人客户 B设备钱包余额为 15000 分
- **THEN** 个人客户 B 通过设备号 "DEV-001" 登录后查询钱包,余额为 15000 分3 张卡共享该余额
#### Scenario: 代理商店铺钱包充值
- **WHEN** 代理商(店铺 ID 为 10充值 50000 分
- **THEN** 系统创建或更新钱包记录,`resource_type` 为 "shop"`resource_id` 为 10`balance` 增加 50000 分
#### Scenario: 代理商店铺的多个员工账号共享钱包
- **WHEN** 代理商店铺(店铺 ID 为 10有 3 个员工账号(账号 ID 为 201、202、203店铺钱包余额为 50000 分
- **THEN** 3 个员工账号登录后查询店铺钱包,余额都是 50000 分,可以共享使用
**迁移指南**
- 代理钱包数据校验:使用 `shop_id` + `wallet_type`,参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡钱包数据校验:使用 `resource_type` + `resource_id`,参见 [card-wallet spec](../card-wallet/spec.md)
---
### Requirement: 钱包归属资源规则
**⚠️ 已废弃** - 归属规则拆分到代理钱包和卡钱包两个独立系统,业务语义更清晰。
**迁移指南**
- 代理钱包归属店铺shop_id不支持转手参见 [agent-wallet spec](../agent-wallet/spec.md)
- 卡钱包归属资源iot_card / device支持转手参见 [card-wallet spec](../card-wallet/spec.md)
---
## 变更历史
- **2026-02-25**: 钱包系统重构,废弃统一钱包设计,拆分为 agent-wallet 和 card-wallet 两个独立系统
- 旧的 3 张表tb_wallet、tb_wallet_transaction、tb_recharge_record已删除
- 新的 6 张表已创建并投入使用
---