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:
84
openspec/specs/asset-wallet-query/spec.md
Normal file
84
openspec/specs/asset-wallet-query/spec.md
Normal 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`:资产数据库 ID(uint)
|
||||
- 无请求体
|
||||
- 返回字段:`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`
|
||||
@@ -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`:资源 ID(BIGINT,关联 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`:资源 ID(BIGINT)
|
||||
- `balance`:余额(BIGINT,单位:分,默认 0)
|
||||
- `frozen_balance`:冻结余额(BIGINT,单位:分,默认 0)
|
||||
- `currency`:币种(VARCHAR(10),默认 "CNY")
|
||||
- `status`:钱包状态(INT,1-正常 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** 物联网卡(ID 为 100)已有钱包,尝试再次创建钱包
|
||||
- **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`:卡钱包 ID(BIGINT,关联 tb_card_wallet.id)
|
||||
- `resource_type`:资源类型(VARCHAR(20),冗余字段,便于查询)
|
||||
- `resource_id`:资源 ID(BIGINT,冗余字段,便于查询)
|
||||
- `user_id`:操作人用户 ID(BIGINT,关联 tb_account.id)
|
||||
- `transaction_type`:交易类型(VARCHAR(20),枚举值:"recharge"-充值 | "deduct"-扣款 | "refund"-退款)
|
||||
- `amount`:变动金额(BIGINT,单位:分,正数为增加,负数为减少)
|
||||
- `balance_before`:变动前余额(BIGINT,单位:分)
|
||||
- `balance_after`:变动后余额(BIGINT,单位:分)
|
||||
- `status`:交易状态(INT,1-成功 2-失败 3-处理中,默认 1)
|
||||
- `reference_type`:关联业务类型(VARCHAR(50),如 "order" | "topup",可空)
|
||||
- `reference_id`:关联业务 ID(BIGINT,可空)
|
||||
- `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`:创建人 ID(BIGINT)
|
||||
- `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`:操作人用户 ID(BIGINT,关联 tb_account.id)
|
||||
- `card_wallet_id`:卡钱包 ID(BIGINT,关联 tb_card_wallet.id)
|
||||
- `resource_type`:资源类型(VARCHAR(20),冗余字段)
|
||||
- `resource_id`:资源 ID(BIGINT,冗余字段)
|
||||
- `recharge_no`:充值订单号(VARCHAR(50),唯一,格式:CRCH+时间戳+随机数)
|
||||
- `amount`:充值金额(BIGINT,单位:分,≥ 100)
|
||||
- `payment_method`:支付方式(VARCHAR(20),枚举值:"alipay"-支付宝 | "wechat"-微信)
|
||||
- `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,可空,软删除)
|
||||
#### 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 分)和 version(2)后重试
|
||||
|
||||
#### 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`,第一个请求获得锁,第二个请求等待或失败
|
||||
|
||||
Reference in New Issue
Block a user