Files
huang 2570269c8d feat(wallet,tag): 钱包和标签系统多租户改造
核心变更:
- 钱包表:删除 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/
2026-01-13 16:52:37 +08:00

12 KiB
Raw Permalink Blame History

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:用户 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 为 2001wallet_type 为 "user"balance 为 0status 为 1正常

Scenario: 创建代理钱包

  • WHEN 代理商ID 为 123首次充值
  • THEN 系统创建钱包记录,user_id 为 123wallet_type 为 "agent"balance 为 0status 为 1正常

Scenario: 计算可用余额

  • WHEN 用户钱包余额为 10000 分100 元),冻结余额为 3000 分30 元)
  • THEN 系统计算可用余额为 7000 分70 元)

Requirement: 钱包明细记录

系统 SHALL 记录所有钱包余额变动,包括充值、扣款、退款、分佣、提现等操作,确保完整的审计追踪。

实体字段

  • 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 为 10000balance_before 为 0balance_after 为 10000status 为 1成功

Scenario: 购买套餐扣款创建明细记录

  • WHEN 用户ID 为 2001使用钱包支付购买套餐金额 3000 分30 元)
  • THEN 系统创建钱包明细记录,transaction_type 为 "deduct"amount 为 -3000balance_before 为 10000balance_after 为 7000reference_type 为 "order"reference_id 为订单 ID

Scenario: 分佣发放创建明细记录

  • WHEN 代理ID 为 123的分佣 5000 分50 元)审批通过并发放
  • THEN 系统创建钱包明细记录,transaction_type 为 "commission"amount 为 5000balance_before 为 20000balance_after 为 25000reference_type 为 "commission"reference_id 为分佣记录 ID

Requirement: 充值记录管理

系统 SHALL 记录所有充值操作,包括充值订单号、金额、支付方式、支付状态等信息。

实体字段

  • 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_noamount 为 10000payment_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:必填,≥ 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 为卡 IDbalance 为 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 为 10balance 增加 50000 分

Scenario: 代理商店铺的多个员工账号共享钱包

  • WHEN 代理商店铺(店铺 ID 为 10有 3 个员工账号(账号 ID 为 201、202、203店铺钱包余额为 50000 分
  • THEN 3 个员工账号登录后查询店铺钱包,余额都是 50000 分,可以共享使用