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>
15 KiB
15 KiB
agent-wallet Specification
Purpose
代理钱包系统,提供店铺级别的主钱包和分佣钱包管理,支持充值、扣款、冻结、提现等操作。与卡钱包完全隔离,独立的数据表和代码实现。
ADDED Requirements
Requirement: 代理钱包实体定义
系统 SHALL 定义代理钱包(AgentWallet)实体,管理店铺级别的钱包,支持主钱包和分佣钱包两种类型。
核心概念:
- 主钱包(main):店铺的主要资金账户,用于预充值和购买套餐
- 分佣钱包(commission):店铺的佣金账户,用于接收分佣和提现
实体字段:
id:钱包 ID(主键,BIGINT,自增)shop_id:店铺 ID(BIGINT,关联 tb_shop.id,唯一约束之一)wallet_type:钱包类型(VARCHAR(20),枚举值:"main"-主钱包 | "commission"-分佣钱包,唯一约束之一)balance:余额(BIGINT,单位:分,默认 0,≥ 0)frozen_balance:冻结余额(BIGINT,单位:分,默认 0,≥ 0)currency:币种(VARCHAR(10),默认 "CNY")status:钱包状态(INT,1-正常 2-冻结 3-关闭,默认 1)version:版本号(INT,默认 0,乐观锁字段,用于防止并发扣款)shop_id_tag:店铺 ID 标签(BIGINT,多租户过滤用,与 shop_id 相同)enterprise_id_tag:企业 ID 标签(BIGINT,多租户过滤用,可空)created_at:创建时间(TIMESTAMP,自动填充)updated_at:更新时间(TIMESTAMP,自动填充)deleted_at:删除时间(TIMESTAMP,可空,软删除)
唯一约束:(shop_id, wallet_type) 在 deleted_at IS NULL 条件下唯一
可用余额计算:可用余额 = balance - frozen_balance
表名:tb_agent_wallet
Scenario: 创建店铺主钱包
- WHEN 店铺(ID 为 10)首次充值
- THEN 系统创建代理钱包记录,
shop_id为 10,wallet_type为 "main",balance为 0,status为 1(正常),shop_id_tag为 10
Scenario: 创建店铺分佣钱包
- WHEN 店铺(ID 为 10)首次获得佣金
- THEN 系统创建代理钱包记录,
shop_id为 10,wallet_type为 "commission",balance为 0,status为 1(正常)
Scenario: 计算可用余额
- WHEN 代理钱包余额为 100000 分(1000 元),冻结余额为 30000 分(300 元)
- THEN 系统计算可用余额为 70000 分(700 元)
Scenario: 防止同一店铺创建重复钱包类型
- WHEN 店铺(ID 为 10)已有 wallet_type 为 "main" 的钱包,尝试再次创建 wallet_type 为 "main" 的钱包
- THEN 系统拒绝创建,返回错误信息"该店铺已存在主钱包"
Requirement: 代理钱包交易记录
系统 SHALL 记录所有代理钱包余额变动,包括充值、扣款、退款、分佣、提现等操作,确保完整的审计追踪。
实体字段:
id:交易记录 ID(主键,BIGINT,自增)agent_wallet_id:代理钱包 ID(BIGINT,关联 tb_agent_wallet.id)shop_id:店铺 ID(BIGINT,冗余字段,便于按店铺查询)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-处理中,默认 1)reference_type:关联业务类型(VARCHAR(50),如 "order" | "commission" | "withdrawal" | "topup",可空)reference_id:关联业务 ID(BIGINT,可空)remark:备注(TEXT,可空)metadata:扩展信息(JSONB,如手续费、支付方式等,可空)creator:创建人 ID(BIGINT)shop_id_tag:店铺 ID 标签(BIGINT,多租户过滤用)enterprise_id_tag:企业 ID 标签(BIGINT,多租户过滤用,可空)created_at:创建时间(TIMESTAMP,自动填充)updated_at:更新时间(TIMESTAMP,自动填充)deleted_at:删除时间(TIMESTAMP,可空,软删除)
表名:tb_agent_wallet_transaction
索引:
idx_agent_tx_wallet (agent_wallet_id, created_at):按钱包查询交易历史idx_agent_tx_shop (shop_id, created_at):按店铺汇总交易idx_agent_tx_ref (reference_type, reference_id):按关联业务查询idx_agent_tx_type (transaction_type, created_at):按交易类型统计
Scenario: 充值创建交易记录
- WHEN 店铺(ID 为 10)主钱包充值 100000 分(1000 元)
- THEN 系统创建代理钱包交易记录,
transaction_type为 "recharge",amount为 100000,balance_before为 0,balance_after为 100000,status为 1(成功),shop_id为 10
Scenario: 分佣发放创建交易记录
- WHEN 店铺(ID 为 10)的分佣钱包收到佣金 50000 分(500 元)
- THEN 系统创建代理钱包交易记录,
transaction_type为 "commission",amount为 50000,balance_before为 200000,balance_after为 250000,reference_type为 "commission",reference_id为分佣记录 ID
Scenario: 提现创建交易记录
- WHEN 店铺(ID 为 10)从分佣钱包提现 30000 分(300 元)
- THEN 系统创建代理钱包交易记录,
transaction_type为 "withdrawal",amount为 -30000,balance_before为 250000,balance_after为 220000,reference_type为 "withdrawal",reference_id为提现申请 ID
Scenario: 按店铺查询交易历史
- WHEN 管理员查询店铺(ID 为 10)的所有钱包交易记录,按时间倒序
- THEN 系统使用索引
idx_agent_tx_shop查询,返回该店铺的主钱包和分佣钱包的所有交易记录,按created_at降序排序
Requirement: 代理充值记录管理
系统 SHALL 记录所有代理充值操作,包括充值订单号、金额、支付方式、支付状态等信息。
实体字段:
id:充值记录 ID(主键,BIGINT,自增)user_id:操作人用户 ID(BIGINT,关联 tb_account.id)agent_wallet_id:代理钱包 ID(BIGINT,关联 tb_agent_wallet.id)shop_id:店铺 ID(BIGINT,冗余字段,便于查询)recharge_no:充值订单号(VARCHAR(50),唯一,格式:ARCH+时间戳+随机数)amount:充值金额(BIGINT,单位:分,≥ 10000)payment_method:支付方式(VARCHAR(20),枚举值:"alipay"-支付宝 | "wechat"-微信 | "bank"-银行转账 | "offline"-线下)payment_channel:支付渠道(VARCHAR(50),可空)payment_transaction_id:第三方支付交易号(VARCHAR(100),可空)status:充值状态(INT,1-待支付 2-已支付 3-已完成 4-已关闭 5-已退款,默认 1)paid_at:支付时间(TIMESTAMP,可空)completed_at:完成时间(TIMESTAMP,可空)shop_id_tag:店铺 ID 标签(BIGINT,多租户过滤用)enterprise_id_tag:企业 ID 标签(BIGINT,多租户过滤用,可空)created_at:创建时间(TIMESTAMP,自动填充)updated_at:更新时间(TIMESTAMP,自动填充)deleted_at:删除时间(TIMESTAMP,可空,软删除)
表名:tb_agent_recharge_record
充值金额限制:
- 最小充值金额:10000 分(100 元)
- 最大充值金额:100000000 分(1000000 元)
索引:
idx_agent_recharge_user (user_id, created_at):按用户查询充值记录idx_agent_recharge_shop (shop_id, created_at):按店铺查询充值记录idx_agent_recharge_status (status, created_at):按状态过滤充值记录idx_agent_recharge_no (recharge_no):按订单号查询
Scenario: 创建代理充值订单
- WHEN 店铺(ID 为 10)的管理员发起充值 100000 分(1000 元),选择支付宝支付
- THEN 系统创建代理充值记录,生成唯一的
recharge_no(如 "ARCH20260224123456789012"),amount为 100000,payment_method为 "alipay",status为 1(待支付),shop_id为 10
Scenario: 充值金额低于最小限制
- WHEN 店铺管理员尝试充值 5000 分(50 元)
- THEN 系统拒绝创建充值订单,返回错误信息"充值金额不能低于 100 元"
Scenario: 充值支付完成
- WHEN 店铺管理员完成支付宝支付
- THEN 系统将充值记录状态从 1(待支付)变更为 2(已支付),记录
paid_at时间和payment_transaction_id
Scenario: 充值到账
- WHEN 充值记录状态为 2(已支付),系统处理充值到账
- THEN 系统将代理钱包余额增加 100000 分,创建代理钱包交易记录,将充值记录状态变更为 3(已完成),记录
completed_at时间
Requirement: 代理钱包余额操作
系统 SHALL 支持代理钱包余额的充值、扣款、退款、冻结、解冻等操作,使用乐观锁防止并发问题。
操作类型:
- 充值:增加钱包余额
- 扣款:减少钱包余额(如购买套餐)
- 退款:增加钱包余额(如订单退款)
- 冻结:将部分余额转为冻结状态(如提现申请中)
- 解冻:将冻结余额转回可用余额(如提现取消)
并发控制:
- 使用
version字段实现乐观锁 - 每次更新余额时,检查
version是否匹配 - 如果
version不匹配,说明有并发更新,操作失败并重试
操作约束:
- 扣款时,检查可用余额(balance - frozen_balance)是否充足
- 冻结时,检查可用余额是否充足
- 所有余额变动必须创建交易记录
Scenario: 代理钱包充值
- WHEN 店铺主钱包当前余额为 100000 分,充值 50000 分
- THEN 系统将钱包余额更新为 150000 分,
version从 1 变更为 2,创建交易记录(transaction_type为 "recharge",amount为 50000)
Scenario: 代理钱包扣款
- WHEN 店铺主钱包当前余额为 150000 分,购买套餐扣款 30000 分
- THEN 系统检查可用余额(150000 - 0 = 150000)≥ 30000,将钱包余额更新为 120000 分,
version从 2 变更为 3,创建交易记录(transaction_type为 "deduct",amount为 -30000)
Scenario: 余额不足扣款失败
- WHEN 店铺主钱包当前余额为 20000 分,购买套餐需要扣款 30000 分
- THEN 系统检查可用余额(20000 - 0 = 20000)< 30000,拒绝扣款,返回错误信息"余额不足"
Scenario: 并发扣款乐观锁生效
- WHEN 店铺主钱包当前余额为 100000 分,version 为 1,两个并发请求同时扣款 30000 分和 50000 分
- THEN 第一个请求成功,余额变为 70000 分,version 变为 2;第二个请求因 version 不匹配失败,需重新读取最新余额(70000 分)和 version(2)后重试
Scenario: 冻结余额用于提现
- WHEN 店铺分佣钱包余额为 100000 分,申请提现 30000 分
- THEN 系统将钱包的
frozen_balance增加 30000 分,可用余额减少 30000 分,version增加 1
Scenario: 解冻余额(提现取消)
- WHEN 店铺分佣钱包冻结余额为 30000 分,用户取消提现申请
- THEN 系统将钱包的
frozen_balance减少 30000 分,可用余额增加 30000 分,version增加 1
Requirement: 代理钱包数据校验
系统 SHALL 对代理钱包数据进行校验,确保数据完整性和一致性。
校验规则:
shop_id:必填,≥ 1,必须是有效的店铺 IDwallet_type:必填,枚举值 "main" | "commission"balance:必填,≥ 0frozen_balance:必填,≥ 0,≤ balancecurrency:必填,长度 1-10 字符,默认 "CNY"status:必填,枚举值 1-3version:必填,≥ 0
Scenario: 创建钱包时 shop_id 无效
- WHEN 创建代理钱包,
shop_id为 0 - THEN 系统拒绝创建,返回错误信息"店铺 ID 无效,必须 ≥ 1"
Scenario: 创建钱包时 wallet_type 无效
- WHEN 创建代理钱包,
wallet_type为 "invalid" - THEN 系统拒绝创建,返回错误信息"钱包类型无效,必须是 main 或 commission"
Scenario: 冻结余额超过总余额
- WHEN 代理钱包余额为 100000 分,尝试冻结 150000 分
- THEN 系统拒绝操作,返回错误信息"冻结余额不能超过总余额"
Scenario: 余额为负数
- WHEN 尝试将代理钱包余额设置为 -10000 分
- THEN 系统拒绝操作,返回错误信息"余额不能为负数"
Requirement: 代理钱包归属店铺规则
系统 SHALL 确保代理钱包归属店铺,不支持转手,店铺的多个员工账号共享钱包。
归属规则:
- 代理钱包归属店铺(shop_id),不归属个人用户
- 同一店铺的所有员工账号共享该店铺的主钱包和分佣钱包
- 店铺钱包不支持转手,归属关系固定
Scenario: 店铺的多个员工账号共享钱包
- WHEN 店铺(ID 为 10)有 3 个员工账号(账号 ID 为 201、202、203),店铺主钱包余额为 500000 分
- THEN 3 个员工账号登录后查询店铺主钱包,余额都是 500000 分,可以共享使用
Scenario: 员工账号只能访问自己店铺的钱包
- WHEN 员工账号(ID 为 201,归属店铺 10)尝试访问店铺 20 的钱包
- THEN 系统拒绝访问,返回错误信息"无权限访问该店铺的钱包"
Requirement: 代理钱包 Redis 缓存策略
系统 SHALL 使用 Redis 缓存代理钱包余额,提升查询性能,并使用 Redis 分布式锁防止并发操作冲突。
缓存 Key 定义:
- 余额缓存:
agent_wallet:balance:{shop_id}:{wallet_type} - 分布式锁:
agent_wallet:lock:{shop_id}:{wallet_type}
缓存 TTL:
- 余额缓存:300 秒(5 分钟)
- 分布式锁:10 秒
缓存更新策略:
- 余额变动时,删除缓存(Cache-Aside 模式)
- 下次查询时重新加载到缓存
常量定义位置:pkg/constants/wallet.go
func RedisAgentWalletBalanceKey(shopID uint, walletType string) string
func RedisAgentWalletLockKey(shopID uint, walletType string) string
Scenario: 查询余额时使用缓存
- WHEN 查询店铺(ID 为 10)主钱包余额,缓存中存在该余额
- THEN 系统直接从 Redis 返回余额,不查询数据库
Scenario: 余额变动后删除缓存
- WHEN 店铺(ID 为 10)主钱包余额增加 50000 分
- THEN 系统删除 Redis 缓存 Key
agent_wallet:balance:10:main,下次查询时重新加载
Scenario: 使用分布式锁防止并发冻结
- WHEN 两个并发请求同时尝试冻结店铺(ID 为 10)主钱包的余额
- THEN 系统使用 Redis 分布式锁
agent_wallet:lock:10:main,第一个请求获得锁,第二个请求等待或失败