Files
junhong_cmp_fiber/openspec/specs/card-wallet/spec.md

13 KiB
Raw Blame History

card-wallet Specification

Purpose

资产钱包系统,提供物联网卡和设备级别的钱包管理,支持充值、套餐扣费、余额查询等操作。与代理钱包完全隔离,独立的数据表和代码实现。

Requirements

Requirement: 资产钱包实体定义

系统 SHALL 定义资产钱包AssetWallet实体管理物联网卡和设备级别的钱包支持资源转手场景。原 CardWallet / tb_card_wallet 全量改名为 AssetWallet / tb_asset_wallet

核心概念

  • 物联网卡钱包:归属单张物联网卡,卡转手时钱包跟着卡走
  • 设备钱包归属设备含1-4张卡设备的多张卡共享钱包设备转手时钱包跟着设备走

实体字段(与原 CardWallet 完全一致,仅表名改变)

  • id:钱包 ID主键BIGINT自增
  • resource_type资源类型VARCHAR(20),枚举值:"iot_card" | "device"
  • resource_id:资源 IDBIGINT
  • balance余额BIGINT单位默认 0
  • frozen_balance冻结余额BIGINT单位默认 0
  • currency币种VARCHAR(10),默认 "CNY"
  • status钱包状态INT1-正常 2-冻结 3-关闭,默认 1
  • version版本号INT乐观锁
  • shop_id_tag:店铺 ID 标签(多租户过滤)
  • enterprise_id_tag:企业 ID 标签(可空)
  • created_at / updated_at / deleted_at

表名变更tb_card_wallettb_asset_wallet

唯一约束(resource_type, resource_id)deleted_at IS NULL 条件下唯一

可用余额计算:可用余额 = balance - frozen_balance

Scenario: 创建物联网卡钱包

  • WHEN 个人客户通过 ICCID "8986001234567890" 登录,为该卡充值
  • THEN 系统创建钱包记录写入 tb_asset_walletresource_type 为 "iot_card"resource_id 为卡 ID

Scenario: 创建设备钱包

  • WHEN 个人客户通过设备号登录,为设备充值
  • THEN 系统创建钱包记录写入 tb_asset_walletresource_type 为 "device",设备的所有卡共享该钱包

Scenario: 计算可用余额

  • WHEN 钱包余额 10000 分,冻结余额 3000 分
  • THEN 可用余额 = 7000 分

Scenario: 防止同一资源重复创建钱包

  • WHEN 物联网卡ID=100已有钱包尝试再次创建
  • THEN 系统拒绝,返回错误"该资源已存在钱包"

Requirement: 资产钱包交易记录

系统 SHALL 记录所有资产钱包余额变动,包括充值、套餐扣费、退款,确保完整收支审计追踪。原 CardWalletTransaction / tb_card_wallet_transaction 全量改名为 AssetWalletTransaction / tb_asset_wallet_transaction,同时 reference_id (bigint) 字段改为 reference_no (varchar 50)

实体字段

  • id:交易记录 ID主键
  • asset_wallet_id:资产钱包 ID关联 tb_asset_wallet.id,原 card_wallet_id
  • resource_type:资源类型(冗余字段)
  • resource_id:资源 ID冗余字段
  • user_id:操作人用户 ID
  • transaction_type:交易类型(recharge / deduct / refund
  • amount:变动金额(分,充值为正,扣款/退款为负)
  • balance_before:变动前余额(分)
  • balance_after:变动后余额(分)
  • status交易状态1-成功 2-失败 3-处理中)
  • reference_type:关联业务类型(rechargeorder,可空)
  • reference_no:关联业务编号,存储充值单号(CRCH…)或订单号(ORD…VARCHAR(50),可空)— 原字段 reference_id (bigint) 改名并变更类型
  • remark备注TEXT可空
  • metadata扩展信息JSONB可空
  • creator:创建人 ID
  • shop_id_tag / enterprise_id_tag:多租户标签

表名变更tb_card_wallet_transactiontb_asset_wallet_transaction

字段变更reference_id bigintreference_no varchar(50)

Scenario: 充值写入流水记录

  • WHEN 个人客户完成充值(充值单号 CRCH20260309001金额 100 元),充值回调成功
  • THEN 系统在 tb_asset_wallet_transaction 写入一条记录:transaction_type="recharge"amount=10000reference_type="recharge"reference_no="CRCH20260309001"

Scenario: 钱包支付套餐写入扣款流水

  • WHEN 个人客户使用钱包支付套餐订单(订单号 ORD20260310001金额 30 元),WalletPay 执行成功
  • THEN 系统在同一事务内向 tb_asset_wallet_transaction 写入一条记录:transaction_type="deduct"amount=-3000reference_type="order"reference_no="ORD20260310001"balance_before 为扣款前余额,balance_after = balance_before - 3000

Scenario: 充值流水 reference_no 格式

  • WHEN 系统写入充值流水
  • THEN reference_no 存储充值单号(格式:CRCH + 时间戳 + 随机数),而非数据库主键 ID

Scenario: 扣款流水 reference_no 格式

  • WHEN 系统写入扣款流水
  • THEN reference_no 存储订单号(格式:ORD + 时间戳 + 6位随机数而非数据库主键 ID

Requirement: 充值记录表改名

系统 SHALL 将原 tb_card_recharge_record 表重命名为 tb_asset_recharge_record,对应 Go 类型由 CardRechargeRecord 改名为 AssetRechargeRecord。H5 充值接口 JSON 响应字段 wallet_id 不变(保持向后兼容)。

Scenario: H5 充值接口字段不变

  • WHEN 前端调用 GET /api/h5/wallets/recharges/:id,充值记录关联的钱包 ID 为 123
  • THEN 响应 JSON 中 wallet_id 仍为 123JSON 字段名不变(仅 Go 内部字段名从 CardWalletID 改为 AssetWalletID

Requirement: 资产钱包余额操作

系统 SHALL 支持资产钱包余额的充值、扣款、退款等操作,使用乐观锁防止并发问题。

操作类型

  • 充值:增加钱包余额
  • 扣款:减少钱包余额(如购买套餐)
  • 退款:增加钱包余额(如订单退款)

并发控制

  • 使用 version 字段实现乐观锁
  • 每次更新余额时,检查 version 是否匹配
  • 如果 version 不匹配,说明有并发更新,操作失败并重试

操作约束

  • 扣款时检查可用余额balance - frozen_balance是否充足
  • 所有余额变动必须创建交易记录

Scenario: 资产钱包充值

  • WHEN 资产钱包当前余额为 10000 分,充值 5000 分
  • THEN 系统将钱包余额更新为 15000 分,version 从 1 变更为 2创建交易记录transaction_type 为 "recharge"amount 为 5000

Scenario: 资产钱包扣款

  • WHEN 资产钱包当前余额为 15000 分,购买套餐扣款 3000 分
  • THEN 系统检查可用余额15000 - 0 = 15000≥ 3000将钱包余额更新为 12000 分,version 从 2 变更为 3创建交易记录transaction_type 为 "deduct"amount 为 -3000

Scenario: 余额不足扣款失败

  • WHEN 资产钱包当前余额为 2000 分,购买套餐需要扣款 3000 分
  • THEN 系统检查可用余额2000 - 0 = 2000< 3000拒绝扣款返回错误信息"余额不足"

Scenario: 并发扣款乐观锁生效

  • WHEN 资产钱包当前余额为 10000 分version 为 1两个并发请求同时扣款 3000 分和 5000 分
  • THEN 第一个请求成功,余额变为 7000 分version 变为 2第二个请求因 version 不匹配失败需重新读取最新余额7000 分)和 version2后重试

Scenario: 订单退款

  • WHEN 资产钱包当前余额为 7000 分,订单退款 3000 分
  • THEN 系统将钱包余额更新为 10000 分,version 增加 1创建交易记录transaction_type 为 "refund"amount 为 3000

Requirement: 资产钱包数据校验

系统 SHALL 对资产钱包数据进行校验,确保数据完整性和一致性。

校验规则

  • resource_type:必填,枚举值 "iot_card" | "device"
  • resource_id:必填,≥ 1必须是有效的资源 ID
  • balance:必填,≥ 0
  • frozen_balance:必填,≥ 0≤ balance
  • currency:必填,长度 1-10 字符,默认 "CNY"
  • status:必填,枚举值 1-3
  • version:必填,≥ 0

Scenario: 创建钱包时 resource_type 无效

  • WHEN 创建资产钱包,resource_type 为 "invalid"
  • THEN 系统拒绝创建,返回错误信息"资源类型无效,必须是 iot_card 或 device"

Scenario: 创建钱包时 resource_id 无效

  • WHEN 创建资产钱包,resource_type 为 "iot_card"resource_id 为 0
  • THEN 系统拒绝创建,返回错误信息"资源 ID 无效,必须 ≥ 1"

Scenario: 冻结余额超过总余额

  • WHEN 资产钱包余额为 10000 分,尝试冻结 15000 分
  • THEN 系统拒绝操作,返回错误信息"冻结余额不能超过总余额"

Scenario: 余额为负数

  • WHEN 尝试将资产钱包余额设置为 -10000 分
  • THEN 系统拒绝操作,返回错误信息"余额不能为负数"

Requirement: 资产钱包归属资源转手规则

系统 SHALL 支持资产钱包随资源(物联网卡、设备)转手,新用户登录后可以看到钱包余额。

归属规则

资源类型 ResourceType 适用场景 转手规则
物联网卡 iot_card 个人客户购买单卡 钱包归属卡,卡转手时钱包跟着卡走
设备 device 个人客户购买设备含1-4张卡 钱包归属设备,设备的多张卡共享钱包,设备转手时钱包跟着设备走

资源转手场景

  • 物联网卡转手:新用户通过 ICCID 登录后可以看到卡的钱包余额
  • 设备转手:新用户通过设备号登录后可以看到设备的钱包余额(包含绑定的所有卡)

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 张卡共享该钱包,balance 为 20000

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 设备(设备号 "DEV-001")绑定 3 张卡ICCID 为 "111"、"222"、"333"),设备钱包余额为 20000 分
  • THEN 用户通过任意一张卡的 ICCID 登录,查询钱包余额都是 20000 分(设备级别钱包)

Requirement: 资产钱包 Redis 缓存策略

系统 SHALL 使用 Redis 缓存资产钱包余额,提升查询性能,并使用 Redis 分布式锁防止并发操作冲突。

缓存 Key 定义

  • 余额缓存:asset_wallet:balance:{resource_type}:{resource_id}
  • 分布式锁:asset_wallet:lock:{resource_type}:{resource_id}

缓存 TTL

  • 余额缓存180 秒3 分钟)
  • 分布式锁10 秒

缓存更新策略

  • 余额变动时删除缓存Cache-Aside 模式)
  • 下次查询时重新加载到缓存

常量定义位置pkg/constants/redis.go

func RedisAssetWalletBalanceKey(resourceType string, resourceID uint) string
func RedisAssetWalletLockKey(resourceType string, resourceID uint) string

Scenario: 查询余额时使用缓存

  • WHEN 查询物联网卡ICCID "8986001234567890")钱包余额,缓存中存在该余额
  • THEN 系统直接从 Redis 返回余额,不查询数据库

Scenario: 余额变动后删除缓存

  • WHEN 物联网卡ID 为 100钱包余额增加 5000 分
  • THEN 系统删除 Redis 缓存 Key asset_wallet:balance:iot_card:100,下次查询时重新加载

Scenario: 使用分布式锁防止并发扣款

  • WHEN 两个并发请求同时尝试从物联网卡ID 为 100钱包扣款
  • THEN 系统使用 Redis 分布式锁 asset_wallet:lock:iot_card:100,第一个请求获得锁,第二个请求等待或失败