Files
junhong_cmp_fiber/openspec/changes/archive/2026-03-16-asset-wallet-interface/design.md

17 KiB
Raw Blame History

Context

asset-detail-refactor 建立了 /api/admin/assets/ 路径下的完整资产体系(解析、状态、套餐、停复机),但缺少钱包维度。现有钱包体系命名混乱:CardWallet / tb_card_wallet 实际同时承载 iot_carddevice 两种资产名不副实。此外H5 端个人客户用钱包支付套餐时(WalletPay)代码直接 UPDATE 余额,从未写入 CardWalletTransaction 流水记录,导致流水表只有充值记录、没有扣款记录。

本次设计目标:改名 + 补流水 + 新增 Admin 查询接口,三件事合并在一个变更里。

Goals / Non-Goals

Goals:

  • 数据表改名:tb_card_wallet*tb_asset_wallet*,代码全量跟进
  • 流水表字段变更:reference_id (bigint)reference_no (varchar 50),存储可读业务编号
  • 补写 WalletPay 卡钱包路径的 deduct 流水,填补现有数据空白
  • 新增 Admin 端资产钱包概况接口 GET /api/admin/assets/:asset_type/:id/wallet
  • 新增 Admin 端资产钱包流水列表接口 GET /api/admin/assets/:asset_type/:id/wallet/transactions
  • 企业账号禁止访问钱包接口;代理账号通过 shop_id_tag 自动过滤

Non-Goals:

  • H5 端不新增/修改任何接口(现有充值 / 订单接口 JSON 字段名不变)
  • 不新增 Admin 端充值单列表接口(充值记录通过流水的 reference_no 跳转即可)
  • 不做历史数据回填(WalletPay 修复只对新数据有效)
  • 代理主钱包(AgentWallet)不在本次范围

Decisions

决策 1三张表全部改名保持 tb_asset_* 前缀统一

tb_card_wallet              → tb_asset_wallet
tb_card_wallet_transaction  → tb_asset_wallet_transaction
tb_card_recharge_record     → tb_asset_recharge_record

代码层 Go 类型名同步改名:

CardWallet              → AssetWallet
CardWalletTransaction   → AssetWalletTransaction
CardRechargeRecord      → AssetRechargeRecord
CardWalletStore         → AssetWalletStore
CardWalletTransactionStore → AssetWalletTransactionStore
CardRechargeStore       → AssetRechargeStore

Redis Key 常量更名:RedisCardWalletBalanceKeyRedisAssetWalletBalanceKey

决策 2reference_id (bigint)reference_no (varchar 50)

原来存主键 ID前端无法直接用于展示或跳转改为存业务编号

  • 充值场景:reference_no = recharge.RechargeNo(格式:CRCH…
  • 扣款场景:reference_no = order.OrderNo(格式:ORD…

现有流水数据全部在开发阶段写入(无生产数据),直接 ALTER COLUMN,不需要数据迁移。

决策 3WalletPay 卡钱包路径补写 deduct 流水

在卡钱包扣款成功后,在同一事务内写入 AssetWalletTransaction

transaction := &model.AssetWalletTransaction{
    AssetWalletID:   wallet.ID,
    ResourceType:    resourceType,           // "iot_card" 或 "device"
    ResourceID:      resourceID,
    UserID:          buyerID,
    TransactionType: "deduct",
    Amount:          -order.TotalAmount,     // 负数
    BalanceBefore:   walletBalanceBefore,
    BalanceAfter:    walletBalanceBefore - order.TotalAmount,
    Status:          1,
    ReferenceType:   strPtr("order"),
    ReferenceNo:     &order.OrderNo,
    Remark:          strPtr("钱包支付套餐"),
    ShopIDTag:       wallet.ShopIDTag,
    EnterpriseIDTag: wallet.EnterpriseIDTag,
}

决策 4权限控制复用 ApplyShopTagFilter

tb_asset_wallettb_asset_wallet_transaction 都有 shop_id_tag 字段,已有 ApplyShopTagFilter 机制:

  • 平台/超管:不添加过滤条件
  • 代理用户:WHERE shop_id_tag IN (当前店铺及下级店铺IDs)
  • 企业账号:在 Handler 层检查 user_type == UserTypeEnterprise,直接返回 403

决策 5新接口挂载在现有 /assets/ 路径下,由 AssetWalletHandler 承载

独立的 Handler 文件 internal/handler/admin/asset_wallet.go,通过 AssetWalletService 提供服务逻辑。路由注册追加到 internal/routes/asset.go


API 合约

接口一:查询资产钱包概况

GET /api/admin/assets/:asset_type/:id/wallet

路径参数

参数 类型 说明
asset_type string 资产类型:carddevice
id uint 资产数据库 ID

请求体:无

成功响应 200 OK

{
  "code": 0,
  "msg": "success",
  "data": {
    "wallet_id":        123,
    "resource_type":    "iot_card",
    "resource_id":      456,
    "balance":          10000,
    "frozen_balance":   0,
    "available_balance": 10000,
    "currency":         "CNY",
    "status":           1,
    "status_text":      "正常",
    "created_at":       "2026-03-10T00:00:00Z",
    "updated_at":       "2026-03-10T00:00:00Z"
  },
  "timestamp": 1741564800
}

响应字段说明

字段 类型 说明
wallet_id uint 钱包数据库 ID
resource_type string iot_carddevice
resource_id uint 对应卡或设备的数据库 ID
balance int64 总余额(分)
frozen_balance int64 冻结余额(分)
available_balance int64 可用余额 = balance - frozen_balance
currency string 币种,目前固定 CNY
status int 钱包状态1-正常 2-冻结 3-关闭
status_text string 状态文本
created_at string 创建时间RFC3339
updated_at string 更新时间RFC3339

错误响应

场景 HTTP 状态码 错误码 错误消息
asset_type 非法 400 CodeInvalidParam 无效的资产类型
id 非法 400 CodeInvalidParam 无效的资产ID
资产不存在 404 CodeNotFound 资产不存在
钱包不存在 404 CodeNotFound 该资产暂无钱包记录
企业账号调用 403 CodeForbidden 企业账号无权查看钱包信息

接口二:查询资产钱包流水列表

GET /api/admin/assets/:asset_type/:id/wallet/transactions

路径参数:同上

查询参数

参数 类型 必填 说明
page int 页码,默认 1
page_size int 每页数量,默认 20最大 100
transaction_type string 类型过滤:recharge / deduct / refund
start_time string 开始时间RFC3339
end_time string 结束时间RFC3339

成功响应 200 OK

{
  "code": 0,
  "msg": "success",
  "data": {
    "list": [
      {
        "id":                   1,
        "transaction_type":     "deduct",
        "transaction_type_text": "扣款",
        "amount":               -3000,
        "balance_before":       10000,
        "balance_after":        7000,
        "reference_type":       "order",
        "reference_no":         "ORD20260310001",
        "remark":               "钱包支付套餐",
        "created_at":           "2026-03-10T14:20:00Z"
      },
      {
        "id":                   2,
        "transaction_type":     "recharge",
        "transaction_type_text": "充值",
        "amount":               10000,
        "balance_before":       0,
        "balance_after":        10000,
        "reference_type":       "recharge",
        "reference_no":         "CRCH20260309001",
        "remark":               "钱包充值",
        "created_at":           "2026-03-09T09:15:00Z"
      }
    ],
    "total":       2,
    "page":        1,
    "page_size":   20,
    "total_pages": 1
  },
  "timestamp": 1741564800
}

响应字段说明(单条流水)

字段 类型 说明
id uint 流水记录 ID
transaction_type string 交易类型:recharge/deduct/refund
transaction_type_text string 交易类型文本:充值/扣款/退款
amount int64 变动金额(分),充值为正数,扣款/退款为负数
balance_before int64 变动前余额(分)
balance_after int64 变动后余额(分)
reference_type string 关联业务类型:rechargeorder(可空)
reference_no string 关联业务编号充值单号CRCH…或订单号ORD…可空
remark string 备注(可空)
created_at string 流水创建时间RFC3339

错误响应:同接口一


接口调用流程图

场景一Admin 查看资产钱包详情(典型调用链)

前端Admin 页面)                API 服务                       数据库
       │                            │                              │
       │── ① 解析资产 ─────────────→│                              │
       │  GET /assets/resolve/:identifier                          │
       │                            │── AssetService.Resolve() ──→ │
       │                            │   SELECT iot_card/device     │
       │← 返回 {asset_type, asset_id}│←────────────────────────────│
       │                            │                              │
       │── ② 查询钱包概况 ──────────→│                              │
       │  GET /assets/card/456/wallet                              │
       │                            │── AssetWalletService         │
       │                            │   .GetWallet()              │
       │                            │── SELECT tb_asset_wallet ──→ │
       │                            │   WHERE resource_type='iot_card'
       │                            │   AND resource_id=456        │
       │← 返回 {balance, available…}│←────────────────────────────│
       │                            │                              │
       │── ③ 查询流水列表 ──────────→│                              │
       │  GET /assets/card/456/wallet/transactions?page=1          │
       │                            │── AssetWalletService         │
       │                            │   .ListTransactions()       │
       │                            │── SELECT tb_asset_wallet_transaction
       │                            │   WHERE resource_type='iot_card'
       │                            │   AND resource_id=456        │
       │                            │   ORDER BY created_at DESC   │
       │← 返回流水列表 ─────────────│←────────────────────────────│
       │  [{type:deduct, ref:ORD…},  │                              │
       │   {type:recharge, ref:CRCH…}]                             │
       │                            │                              │
       │── ④(可选)前端用 reference_no 跳转到充值单或订单详情页   │

场景二H5 个人客户钱包支付(补写流水后的完整事务)

H5 前端                    order.Service.WalletPay()            数据库(事务)
    │                              │                                  │
    │── POST /orders/:id/wallet-pay→│                                  │
    │                              │                                  │
    │                              │── ① 查询订单 ─────────────────→ │
    │                              │   SELECT tb_order WHERE id=:id   │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ② 查询 AssetWallet ─────────→ │
    │                              │   SELECT tb_asset_wallet         │
    │                              │   WHERE resource_type+resource_id│
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ③ 开启事务 ─────────────────→ │
    │                              │   BEGIN                          │
    │                              │                                  │
    │                              │── ④ 更新订单支付状态 ─────────→ │
    │                              │   UPDATE tb_order                │
    │                              │   SET payment_status=paid        │
    │                              │   WHERE id=:id AND status=pending│
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑤ 扣减钱包余额(乐观锁)────→ │
    │                              │   UPDATE tb_asset_wallet         │
    │                              │   SET balance=balance-amount     │
    │                              │   WHERE id=:id AND version=:v    │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑥ ★ 写入扣款流水(新增)────→ │
    │                              │   INSERT tb_asset_wallet_transaction
    │                              │   (transaction_type="deduct",    │
    │                              │    amount=-totalAmount,          │
    │                              │    reference_type="order",       │
    │                              │    reference_no=order.OrderNo)   │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑦ 激活套餐 ─────────────────→ │
    │                              │   activatePackage()              │
    │                              │←──────────────────────────────── │
    │                              │                                  │
    │                              │── ⑧ 提交事务 ─────────────────→ │
    │                              │   COMMIT                         │
    │← 200 OK ──────────────────── │                                  │

字段变更对现有接口的影响分析

H5 充值接口(无 breaking change

以下 H5 接口 JSON 响应字段名保持不变,前端零感知:

接口 JSON 字段 Go 字段改名 影响
POST /api/h5/wallets/recharge 响应 wallet_id CardRechargeRecord.CardWalletIDAssetRechargeRecord.AssetWalletID JSON tag 不变,无影响
GET /api/h5/wallets/recharges 响应 wallet_id 同上 JSON tag 不变,无影响
GET /api/h5/wallets/recharges/:id 响应 wallet_id 同上 JSON tag 不变,无影响

新增接口中 reference_no 字段(全新字段,无旧调用方

tb_asset_wallet_transaction.reference_no 是首次通过 API 暴露。变更前该字段叫 reference_iduint从未被任何接口返回不存在已有调用方无 breaking change


Risks / Trade-offs

[风险 1] WalletPay 中余额快照时机

扣款前需记录 balance_before,但乐观锁可能因并发重试导致实际余额与快照不符。缓解:在事务内先查询钱包余额作为快照,再执行乐观锁更新;balance_before = 查询值,balance_after = 查询值 - amount若乐观锁失败则整个事务回滚流水不写入。

[风险 2] 表改名期间的零停机

开发阶段无生产数据,直接 migration 改名,无需双写策略。

[Trade-off] 不回填历史扣款流水

WalletPay 修复前的历史订单无对应 deduct 流水。接受:开发阶段,测试数据可清空重来,不引入回填复杂度。