docs: 归档 asset-wallet-interface OpenSpec 提案,更新卡钱包 spec

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-16 23:30:48 +08:00
parent 63ca12393b
commit f3297f0529
8 changed files with 927 additions and 148 deletions

View File

@@ -0,0 +1,84 @@
# asset-wallet-query Specification
## Purpose
Admin 端资产钱包查询,允许平台用户和代理账号查询指定物联网卡或设备的钱包余额概况及收支流水,流水包含可跳转的来源编号(充值单号 / 订单号)。
## Requirements
### Requirement: Admin 端查询资产钱包概况
系统 SHALL 提供 `GET /api/admin/assets/:asset_type/:id/wallet` 接口,允许平台用户和代理账号查询指定卡或设备的钱包余额概况。
**接口规格**
- 路径参数 `asset_type``card``device`
- 路径参数 `id`:资产数据库 IDuint
- 无请求体
- 返回字段:`wallet_id``resource_type``resource_id``balance``frozen_balance``available_balance``currency``status``status_text``created_at``updated_at`
**权限规则**
- 平台用户/超级管理员:可查询所有资产钱包
- 代理账号:只能查询 `shop_id_tag IN (当前店铺及下级店铺)` 的资产钱包(由 `ApplyShopTagFilter` 自动过滤)
- 企业账号Handler 层直接返回 403禁止访问
#### Scenario: 平台用户查询卡钱包概况
- **WHEN** 平台用户请求 `GET /api/admin/assets/card/456/wallet`,该卡存在钱包记录,余额 100 元,冻结 0 元
- **THEN** 系统返回 200`balance=10000``frozen_balance=0``available_balance=10000``status=1``status_text="正常"`
#### Scenario: 代理账号查询下级资产钱包
- **WHEN** 代理账号shop_id=10请求 `GET /api/admin/assets/device/789/wallet`,该设备的 `shop_id_tag` 在该代理的下级店铺范围内
- **THEN** 系统返回 200返回该设备的钱包详情
#### Scenario: 代理账号查询越权资产钱包
- **WHEN** 代理账号shop_id=10请求 `GET /api/admin/assets/card/999/wallet`,该卡的 `shop_id_tag` 不在该代理的下级店铺范围内
- **THEN** 系统返回 404错误消息为"该资产暂无钱包记录"(不区分"无权"与"不存在"
#### Scenario: 企业账号请求被拒绝
- **WHEN** 企业账号请求 `GET /api/admin/assets/card/456/wallet`
- **THEN** 系统返回 403错误消息为"企业账号无权查看钱包信息"
#### Scenario: 资产无钱包记录
- **WHEN** 平台用户请求 `GET /api/admin/assets/card/456/wallet`,该卡尚未创建钱包(未充值过)
- **THEN** 系统返回 404错误消息为"该资产暂无钱包记录"
---
### Requirement: Admin 端查询资产钱包流水列表
系统 SHALL 提供 `GET /api/admin/assets/:asset_type/:id/wallet/transactions` 接口,允许平台用户和代理账号分页查询指定资产的钱包收支流水,每条流水包含可跳转的来源编号。
**接口规格**
- 路径参数:同上
- 查询参数:`page`(默认 1`page_size`(默认 20最大 100`transaction_type`(可选过滤)、`start_time`(可选)、`end_time`(可选)
- 流水按 `created_at` 倒序排列
- 每条流水返回:`id``transaction_type``transaction_type_text``amount``balance_before``balance_after``reference_type``reference_no``remark``created_at`
**来源编号跳转规则**
- `reference_type = "recharge"``reference_no` 为充值单号(`CRCH…`),前端可跳转至充值单详情
- `reference_type = "order"``reference_no` 为订单号(`ORD…`),前端可跳转至订单详情
**权限规则**:与钱包概况接口相同
#### Scenario: 查询充值和扣款流水
- **WHEN** 平台用户请求 `GET /api/admin/assets/card/456/wallet/transactions?page=1&page_size=20`,该卡有 1 条充值流水100 元)和 1 条扣款流水(-30 元)
- **THEN** 系统返回 200`total=2`,按时间倒序返回两条记录,充值流水 `amount=10000``reference_type="recharge"``reference_no="CRCH20260309001"`;扣款流水 `amount=-3000``reference_type="order"``reference_no="ORD20260310001"`
#### Scenario: 按交易类型过滤
- **WHEN** 平台用户请求 `GET /api/admin/assets/card/456/wallet/transactions?transaction_type=recharge`
- **THEN** 系统只返回 `transaction_type="recharge"` 的流水记录
#### Scenario: 分页超出范围
- **WHEN** 请求 `page_size=200`(超过最大值 100
- **THEN** 系统返回 400错误消息为参数验证失败
#### Scenario: 资产无流水记录
- **WHEN** 平台用户请求某资产的流水列表,该资产钱包存在但尚无任何流水
- **THEN** 系统返回 200`list=[]``total=0`

View File

@@ -1,178 +1,121 @@
# card-wallet Specification
## Purpose
钱包系统,提供物联网卡和设备级别的钱包管理,支持充值、套餐扣费、余额查询等操作。与代理钱包完全隔离,独立的数据表和代码实现。
资产钱包系统,提供物联网卡和设备级别的钱包管理,支持充值、套餐扣费、余额查询等操作。与代理钱包完全隔离,独立的数据表和代码实现。
## ADDED Requirements
## Requirements
### Requirement: 钱包实体定义
### Requirement: 资产钱包实体定义
系统 SHALL 定义钱包(CardWallet实体管理物联网卡和设备级别的钱包支持资源转手场景。
系统 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,关联 tb_iot_card.id 或 tb_device.id唯一约束之一
- `balance`余额BIGINT单位默认 0 0
- `frozen_balance`冻结余额BIGINT单位默认 0 0
- `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默认 0乐观锁字段用于防止并发扣款
- `shop_id_tag`:店铺 ID 标签(BIGINT多租户过滤
- `enterprise_id_tag`:企业 ID 标签(BIGINT多租户过滤用可空)
- `created_at`创建时间TIMESTAMP自动填充
- `updated_at`更新时间TIMESTAMP自动填充
- `deleted_at`删除时间TIMESTAMP可空软删除
- `version`版本号INT乐观锁
- `shop_id_tag`:店铺 ID 标签(多租户过滤)
- `enterprise_id_tag`:企业 ID 标签(可空)
- `created_at` / `updated_at` / `deleted_at`
**表名变更**`tb_card_wallet``tb_asset_wallet`
**唯一约束**`(resource_type, resource_id)``deleted_at IS NULL` 条件下唯一
**可用余额计算**:可用余额 = balance - frozen_balance
**表名**`tb_card_wallet`
#### Scenario: 创建物联网卡钱包
- **WHEN** 个人客户通过 ICCID "8986001234567890" 登录(首次登录),为该卡充值
- **THEN** 系统创建钱包记录,`resource_type` 为 "iot_card"`resource_id` 为卡 ID`balance` 为 0`status` 为 1正常
- **WHEN** 个人客户通过 ICCID "8986001234567890" 登录,为该卡充值
- **THEN** 系统创建钱包记录写入 `tb_asset_wallet``resource_type` 为 "iot_card"`resource_id` 为卡 ID
#### Scenario: 创建设备钱包
- **WHEN** 个人客户通过设备号 "DEV-001" 登录(首次登录),该设备绑定 3 张卡,为设备充值
- **THEN** 系统创建钱包记录,`resource_type` 为 "device"`resource_id` 为设备 ID设备的 3 张卡共享该钱包
- **WHEN** 个人客户通过设备号登录,为设备充值
- **THEN** 系统创建钱包记录写入 `tb_asset_wallet``resource_type` 为 "device"设备的所有卡共享该钱包
#### Scenario: 计算可用余额
- **WHEN** 钱包余额 10000 分100 元),冻结余额 3000 分30 元)
- **THEN** 系统计算可用余额 7000 分70 元)
- **WHEN** 钱包余额 10000 分,冻结余额 3000 分
- **THEN** 可用余额 = 7000 分
#### Scenario: 防止同一资源创建重复钱包
#### Scenario: 防止同一资源重复创建钱包
- **WHEN** 物联网卡ID100已有钱包尝试再次创建钱包
- **THEN** 系统拒绝创建,返回错误信息"该资源已存在钱包"
- **WHEN** 物联网卡ID=100已有钱包尝试再次创建
- **THEN** 系统拒绝,返回错误"该资源已存在钱包"
---
### Requirement: 钱包交易记录
### Requirement: 资产钱包交易记录
系统 SHALL 记录所有钱包余额变动,包括充值、套餐扣费、退款等操作,确保完整审计追踪。
系统 SHALL 记录所有资产钱包余额变动,包括充值、套餐扣费、退款,确保完整收支审计追踪。`CardWalletTransaction` / `tb_card_wallet_transaction` 全量改名为 `AssetWalletTransaction` / `tb_asset_wallet_transaction`,同时 `reference_id (bigint)` 字段改为 `reference_no (varchar 50)`
**实体字段**
- `id`:交易记录 ID主键BIGINT自增
- `card_wallet_id`钱包 IDBIGINT关联 tb_card_wallet.id
- `resource_type`:资源类型(VARCHAR(20),冗余字段,便于查询
- `resource_id`:资源 IDBIGINT冗余字段便于查询
- `user_id`:操作人用户 IDBIGINT关联 tb_account.id
- `transaction_type`:交易类型(VARCHAR(20),枚举值:"recharge"-充值 | "deduct"-扣款 | "refund"-退款
- `amount`:变动金额(BIGINT单位正数为增加负数为减少
- `balance_before`:变动前余额(BIGINT单位分)
- `balance_after`:变动后余额(BIGINT单位分)
- `status`:交易状态(INT1-成功 2-失败 3-处理中,默认 1
- `reference_type`:关联业务类型(VARCHAR(50),如 "order" | "topup",可空)
- `reference_id`:关联业务 IDBIGINT可空
- `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`:关联业务类型(`recharge``order`,可空)
- `reference_no`:关联业务编号,存储充值单号(`CRCH…`)或订单号(`ORD…`VARCHAR(50),可空)— **原字段 `reference_id (bigint)` 改名并变更类型**
- `remark`备注TEXT可空
- `metadata`扩展信息JSONB如套餐信息、支付方式等,可空)
- `creator`:创建人 IDBIGINT
- `shop_id_tag`:店铺 ID 标签BIGINT多租户过滤用
- `enterprise_id_tag`:企业 ID 标签BIGINT多租户过滤用可空
- `created_at`创建时间TIMESTAMP自动填充
- `updated_at`更新时间TIMESTAMP自动填充
- `deleted_at`删除时间TIMESTAMP可空软删除
- `metadata`扩展信息JSONB可空
- `creator`:创建人 ID
- `shop_id_tag` / `enterprise_id_tag`:多租户标签
**表名**`tb_card_wallet_transaction`
**表名变更**`tb_card_wallet_transaction``tb_asset_wallet_transaction`
**索引**
- `idx_card_tx_wallet (card_wallet_id, created_at)`:按钱包查询交易历史
- `idx_card_tx_resource (resource_type, resource_id, created_at)`:按资源查询交易
- `idx_card_tx_ref (reference_type, reference_id)`:按关联业务查询
- `idx_card_tx_type (transaction_type, created_at)`:按交易类型统计
**字段变更**`reference_id bigint``reference_no varchar(50)`
#### Scenario: 充值创建交易记录
#### Scenario: 充值写入流水记录
- **WHEN** 物联网卡ICCID "8986001234567890")充值 10000 分100 元)
- **THEN** 系统创建卡钱包交易记录`transaction_type`"recharge"`amount`10000`balance_before` 为 0`balance_after` 为 10000`status` 为 1成功
- **WHEN** 个人客户完成充值(充值单号 CRCH20260309001金额 100 元),充值回调成功
- **THEN** 系统`tb_asset_wallet_transaction` 写入一条记录`transaction_type="recharge"``amount=10000``reference_type="recharge"``reference_no="CRCH20260309001"`
#### Scenario: 套餐扣费创建交易记录
#### Scenario: 钱包支付套餐写入扣款流水
- **WHEN** 物联网卡ICCID "8986001234567890")购买套餐,钱包支付扣款 3000 分30 元)
- **THEN** 系统创建卡钱包交易记录`transaction_type`"deduct"`amount`-3000`balance_before` 为 10000`balance_after` 为 7000`reference_type` 为 "order"`reference_id` 为订单 ID
- **WHEN** 个人客户使用钱包支付套餐订单(订单号 ORD20260310001金额 30 元),`WalletPay` 执行成功
- **THEN** 系统在同一事务内向 `tb_asset_wallet_transaction` 写入一条记录`transaction_type="deduct"``amount=-3000``reference_type="order"``reference_no="ORD20260310001"``balance_before` 为扣款前余额,`balance_after` = `balance_before - 3000`
#### Scenario: 订单退款创建交易记录
#### Scenario: 充值流水 reference_no 格式
- **WHEN** 物联网卡订单ID 为 1001退款 3000 分30 元)
- **THEN** 系统创建卡钱包交易记录,`transaction_type` 为 "refund"`amount` 为 3000`balance_before` 为 7000`balance_after` 为 10000`reference_type` 为 "order"`reference_id` 为 1001
- **WHEN** 系统写入充值流水
- **THEN** `reference_no` 存储充值单号(格式:`CRCH` + 时间戳 + 随机数),而非数据库主键 ID
#### Scenario: 按资源查询交易历史
#### Scenario: 扣款流水 reference_no 格式
- **WHEN** 个人客户查询物联网卡ICCID "8986001234567890")的交易历史
- **THEN** 系统使用索引 `idx_card_tx_resource` 查询,返回该卡的所有钱包交易记录,按 `created_at` 降序排序
- **WHEN** 系统写入扣款流水
- **THEN** `reference_no` 存储订单号(格式:`ORD` + 时间戳 + 6位随机数而非数据库主键 ID
---
### Requirement: 充值记录管理
### Requirement: 充值记录表改名
系统 SHALL 记录所有卡充值操作,包括充值订单号、金额、支付方式、支付状态等信息
系统 SHALL 将原 `tb_card_recharge_record` 表重命名为 `tb_asset_recharge_record`,对应 Go 类型由 `CardRechargeRecord` 改名为 `AssetRechargeRecord`。H5 充值接口 JSON 响应字段 `wallet_id` 不变(保持向后兼容)
**实体字段**
- `id`:充值记录 ID主键BIGINT自增
- `user_id`:操作人用户 IDBIGINT关联 tb_account.id
- `card_wallet_id`:卡钱包 IDBIGINT关联 tb_card_wallet.id
- `resource_type`资源类型VARCHAR(20),冗余字段)
- `resource_id`:资源 IDBIGINT冗余字段
- `recharge_no`充值订单号VARCHAR(50)唯一格式CRCH+时间戳+随机数)
- `amount`充值金额BIGINT单位≥ 100
- `payment_method`支付方式VARCHAR(20),枚举值:"alipay"-支付宝 | "wechat"-微信)
- `payment_channel`支付渠道VARCHAR(50),可空)
- `payment_transaction_id`第三方支付交易号VARCHAR(100),可空)
- `status`充值状态INT1-待支付 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可空软删除
#### Scenario: H5 充值接口字段不变
**表名**`tb_card_recharge_record`
**充值金额限制**
- 最小充值金额100 分1 元)
- 最大充值金额10000000 分100000 元)
**索引**
- `idx_card_recharge_user (user_id, created_at)`:按用户查询充值记录
- `idx_card_recharge_resource (resource_type, resource_id, created_at)`:按资源查询充值记录
- `idx_card_recharge_status (status, created_at)`:按状态过滤充值记录
- `idx_card_recharge_no (recharge_no)`:按订单号查询
#### Scenario: 创建卡充值订单
- **WHEN** 个人客户为物联网卡ICCID "8986001234567890")发起充值 10000 分100 元),选择微信支付
- **THEN** 系统创建卡充值记录,生成唯一的 `recharge_no`(如 "CRCH20260224123456789012"`amount` 为 10000`payment_method` 为 "wechat"`status` 为 1待支付`resource_type` 为 "iot_card"
#### Scenario: 充值金额低于最小限制
- **WHEN** 个人客户尝试充值 50 分0.5 元)
- **THEN** 系统拒绝创建充值订单,返回错误信息"充值金额不能低于 1 元"
#### Scenario: 充值支付完成
- **WHEN** 个人客户完成微信支付
- **THEN** 系统将充值记录状态从 1待支付变更为 2已支付记录 `paid_at` 时间和 `payment_transaction_id`
#### Scenario: 充值到账
- **WHEN** 充值记录状态为 2已支付系统处理充值到账
- **THEN** 系统将卡钱包余额增加 10000 分,创建卡钱包交易记录,将充值记录状态变更为 3已完成记录 `completed_at` 时间
- **WHEN** 前端调用 `GET /api/h5/wallets/recharges/:id`,充值记录关联的钱包 ID 为 123
- **THEN** 响应 JSON 中 `wallet_id` 仍为 `123`JSON 字段名不变(仅 Go 内部字段名从 `CardWalletID` 改为 `AssetWalletID`
---
### Requirement: 钱包余额操作
### Requirement: 资产钱包余额操作
系统 SHALL 支持钱包余额的充值、扣款、退款等操作,使用乐观锁防止并发问题。
系统 SHALL 支持资产钱包余额的充值、扣款、退款等操作,使用乐观锁防止并发问题。
**操作类型**
- **充值**:增加钱包余额
@@ -188,36 +131,36 @@
- 扣款时检查可用余额balance - frozen_balance是否充足
- 所有余额变动必须创建交易记录
#### Scenario: 钱包充值
#### Scenario: 资产钱包充值
- **WHEN** 钱包当前余额为 10000 分,充值 5000 分
- **WHEN** 资产钱包当前余额为 10000 分,充值 5000 分
- **THEN** 系统将钱包余额更新为 15000 分,`version` 从 1 变更为 2创建交易记录`transaction_type` 为 "recharge"`amount` 为 5000
#### Scenario: 钱包扣款
#### Scenario: 资产钱包扣款
- **WHEN** 钱包当前余额为 15000 分,购买套餐扣款 3000 分
- **WHEN** 资产钱包当前余额为 15000 分,购买套餐扣款 3000 分
- **THEN** 系统检查可用余额15000 - 0 = 15000≥ 3000将钱包余额更新为 12000 分,`version` 从 2 变更为 3创建交易记录`transaction_type` 为 "deduct"`amount` 为 -3000
#### Scenario: 余额不足扣款失败
- **WHEN** 钱包当前余额为 2000 分,购买套餐需要扣款 3000 分
- **WHEN** 资产钱包当前余额为 2000 分,购买套餐需要扣款 3000 分
- **THEN** 系统检查可用余额2000 - 0 = 2000< 3000拒绝扣款返回错误信息"余额不足"
#### Scenario: 并发扣款乐观锁生效
- **WHEN** 钱包当前余额为 10000 分version 为 1两个并发请求同时扣款 3000 分和 5000 分
- **WHEN** 资产钱包当前余额为 10000 分version 为 1两个并发请求同时扣款 3000 分和 5000 分
- **THEN** 第一个请求成功,余额变为 7000 分version 变为 2第二个请求因 version 不匹配失败需重新读取最新余额7000 分)和 version2后重试
#### Scenario: 订单退款
- **WHEN** 钱包当前余额为 7000 分,订单退款 3000 分
- **WHEN** 资产钱包当前余额为 7000 分,订单退款 3000 分
- **THEN** 系统将钱包余额更新为 10000 分,`version` 增加 1创建交易记录`transaction_type` 为 "refund"`amount` 为 3000
---
### Requirement: 钱包数据校验
### Requirement: 资产钱包数据校验
系统 SHALL 对钱包数据进行校验,确保数据完整性和一致性。
系统 SHALL 对资产钱包数据进行校验,确保数据完整性和一致性。
**校验规则**
- `resource_type`:必填,枚举值 "iot_card" | "device"
@@ -230,29 +173,29 @@
#### Scenario: 创建钱包时 resource_type 无效
- **WHEN** 创建钱包,`resource_type` 为 "invalid"
- **WHEN** 创建资产钱包,`resource_type` 为 "invalid"
- **THEN** 系统拒绝创建,返回错误信息"资源类型无效,必须是 iot_card 或 device"
#### Scenario: 创建钱包时 resource_id 无效
- **WHEN** 创建钱包,`resource_type` 为 "iot_card"`resource_id` 为 0
- **WHEN** 创建资产钱包,`resource_type` 为 "iot_card"`resource_id` 为 0
- **THEN** 系统拒绝创建,返回错误信息"资源 ID 无效,必须 ≥ 1"
#### Scenario: 冻结余额超过总余额
- **WHEN** 钱包余额为 10000 分,尝试冻结 15000 分
- **WHEN** 资产钱包余额为 10000 分,尝试冻结 15000 分
- **THEN** 系统拒绝操作,返回错误信息"冻结余额不能超过总余额"
#### Scenario: 余额为负数
- **WHEN** 尝试将钱包余额设置为 -10000 分
- **WHEN** 尝试将资产钱包余额设置为 -10000 分
- **THEN** 系统拒绝操作,返回错误信息"余额不能为负数"
---
### Requirement: 钱包归属资源转手规则
### Requirement: 资产钱包归属资源转手规则
系统 SHALL 支持钱包随资源(物联网卡、设备)转手,新用户登录后可以看到钱包余额。
系统 SHALL 支持资产钱包随资源(物联网卡、设备)转手,新用户登录后可以看到钱包余额。
**归属规则**
@@ -268,16 +211,16 @@
#### Scenario: 个人客户购买单卡并充值
- **WHEN** 个人客户通过 ICCID "8986001234567890" 登录(首次登录),为该卡充值 10000 分
- **THEN** 系统创建钱包记录,`resource_type` 为 "iot_card"`resource_id` 为卡 ID`balance` 为 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 张卡共享该钱包,`balance` 为 20000
- **THEN** 系统创建资产钱包记录,`resource_type` 为 "device"`resource_id` 为设备 ID设备的 3 张卡共享该钱包,`balance` 为 20000
#### Scenario: 卡转手后新用户查询余额
- **WHEN** 个人客户 A微信 OpenID 为 "wx_a"的卡ICCID 为 "8986001234567890")转手给个人客户 B微信 OpenID 为 "wx_b"钱包余额为 5000 分
- **WHEN** 个人客户 A微信 OpenID 为 "wx_a"的卡ICCID 为 "8986001234567890")转手给个人客户 B微信 OpenID 为 "wx_b"),钱包余额为 5000 分
- **THEN** 个人客户 B 通过 ICCID "8986001234567890" 登录后查询钱包,余额为 5000 分,可以继续使用
#### Scenario: 设备转手后新用户查询余额
@@ -292,13 +235,13 @@
---
### Requirement: 钱包 Redis 缓存策略
### Requirement: 资产钱包 Redis 缓存策略
系统 SHALL 使用 Redis 缓存钱包余额,提升查询性能,并使用 Redis 分布式锁防止并发操作冲突。
系统 SHALL 使用 Redis 缓存资产钱包余额,提升查询性能,并使用 Redis 分布式锁防止并发操作冲突。
**缓存 Key 定义**
- 余额缓存:`card_wallet:balance:{resource_type}:{resource_id}`
- 分布式锁:`card_wallet:lock:{resource_type}:{resource_id}`
- 余额缓存:`asset_wallet:balance:{resource_type}:{resource_id}`
- 分布式锁:`asset_wallet:lock:{resource_type}:{resource_id}`
**缓存 TTL**
- 余额缓存180 秒3 分钟)
@@ -308,11 +251,11 @@
- 余额变动时删除缓存Cache-Aside 模式)
- 下次查询时重新加载到缓存
**常量定义位置**`pkg/constants/wallet.go`
**常量定义位置**`pkg/constants/redis.go`
```go
func RedisCardWalletBalanceKey(resourceType string, resourceID uint) string
func RedisCardWalletLockKey(resourceType string, resourceID uint) string
func RedisAssetWalletBalanceKey(resourceType string, resourceID uint) string
func RedisAssetWalletLockKey(resourceType string, resourceID uint) string
```
#### Scenario: 查询余额时使用缓存
@@ -323,11 +266,9 @@ func RedisCardWalletLockKey(resourceType string, resourceID uint) string
#### Scenario: 余额变动后删除缓存
- **WHEN** 物联网卡ID 为 100钱包余额增加 5000 分
- **THEN** 系统删除 Redis 缓存 Key `card_wallet:balance:iot_card:100`,下次查询时重新加载
- **THEN** 系统删除 Redis 缓存 Key `asset_wallet:balance:iot_card:100`,下次查询时重新加载
#### Scenario: 使用分布式锁防止并发扣款
- **WHEN** 两个并发请求同时尝试从物联网卡ID 为 100钱包扣款
- **THEN** 系统使用 Redis 分布式锁 `card_wallet:lock:iot_card:100`,第一个请求获得锁,第二个请求等待或失败
---
- **THEN** 系统使用 Redis 分布式锁 `asset_wallet:lock:iot_card:100`,第一个请求获得锁,第二个请求等待或失败