核心变更: - 钱包表:删除 user_id,添加 resource_type/resource_id(绑定资源而非用户) - 标签表:添加 enterprise_id/shop_id(实现三级隔离:全局/企业/店铺) - GORM Callback:自动数据权限过滤 - 迁移脚本:可重复执行,已验证回滚功能 钱包归属重构原因: - 旧设计:钱包绑定用户账号,个人客户卡/设备转手后新用户无法使用余额 - 新设计:钱包绑定资源(卡/设备/店铺),余额随资源流转 标签三级隔离: - 平台全局标签:所有用户可见 - 企业标签:仅该企业可见(企业内唯一) - 店铺标签:该店铺及下级可见(店铺内唯一) 测试覆盖: - 9 个单元测试验证标签多租户过滤(全部通过) - 迁移和回滚功能测试通过(测试环境) - OpenSpec 验证通过 变更 ID: fix-wallet-tag-multi-tenant 迁移版本: 000008 参考: openspec/changes/archive/2026-01-13-fix-wallet-tag-multi-tenant/
12 KiB
12 KiB
wallet Specification
Purpose
TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose after archive.
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(已删除)resource_type:必填,枚举值 "iot_card" | "device" | "shop"(新增)resource_id:必填,≥ 1,必须是有效的资源 ID(新增)wallet_type:必填,枚举值 "main" | "commission"balance:必填,≥ 0frozen_balance:必填,≥ 0,≤ balancecurrency:必填,长度 1-10 字符status:必填,枚举值 1-3version:必填,≥ 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 分,可以共享使用