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,79 @@
## ADDED 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

@@ -0,0 +1,103 @@
## MODIFIED 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_wallet``tb_asset_wallet`
#### Scenario: 创建物联网卡钱包
- **WHEN** 个人客户通过 ICCID "8986001234567890" 登录,为该卡充值
- **THEN** 系统创建钱包记录写入 `tb_asset_wallet``resource_type` 为 "iot_card"`resource_id` 为卡 ID
#### Scenario: 创建设备钱包
- **WHEN** 个人客户通过设备号登录,为设备充值
- **THEN** 系统创建钱包记录写入 `tb_asset_wallet``resource_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`:关联业务类型(`recharge``order`,可空)
- `reference_no`:关联业务编号,存储充值单号(`CRCH…`)或订单号(`ORD…`VARCHAR(50),可空)— **原字段 `reference_id (bigint)` 改名并变更类型**
- `remark`备注TEXT可空
- `metadata`扩展信息JSONB可空
- `creator`:创建人 ID
- `shop_id_tag` / `enterprise_id_tag`:多租户标签
**表名变更**`tb_card_wallet_transaction``tb_asset_wallet_transaction`
**字段变更**`reference_id bigint``reference_no varchar(50)`
#### Scenario: 充值写入流水记录
- **WHEN** 个人客户完成充值(充值单号 CRCH20260309001金额 100 元),充值回调成功
- **THEN** 系统在 `tb_asset_wallet_transaction` 写入一条记录:`transaction_type="recharge"``amount=10000``reference_type="recharge"``reference_no="CRCH20260309001"`
#### Scenario: 钱包支付套餐写入扣款流水
- **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: 充值流水 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` 仍为 `123`JSON 字段名不变(仅 Go 内部字段名从 `CardWalletID` 改为 `AssetWalletID`