Files
junhong_cmp_fiber/docs/7月迭代/需求17-信用额度.md
2026-07-16 15:07:59 +08:00

11 KiB
Raw Blame History

需求17信用额度

状态:待评审 DDD 范围:仅迁移代理主钱包的复杂写用例,资金列表和统计继续走 Query 关联需求BPO-009~012、需求19批量订购


一、评审结论

代理信用额度在现有 AgentWallet 基础上落地,属于典型资金聚合:余额、冻结金额、信用额度、版本和流水必须在同一事务内保持不变量。

平台员工信用额度不实施,原因如下:

  • 平台员工是操作主体,不是订单结算主体;实际付款方只能是代理钱包或线下支付主体。
  • 角色表达权限范围,不表达资产、余额和债务,给角色配置资金会混淆 RBAC 与财务账本。
  • 系统不存在员工钱包、员工充值、员工还款和离职债务交接链路,负余额无法对账和追责。
  • 批量订购已经明确由代理钱包扣款或使用线下支付,不存在必须从员工个人额度扣款的业务场景。
  • 引入员工信用会与代理钱包形成两套资金来源,增加订单归属、退款去向和审计解释成本,但不产生实际业务价值。

因此信用额度只属于代理主钱包,平台员工仅通过权限决定是否可以查看或调整代理额度。


二、已确认范围:代理主钱包授信

2.1 业务规则

  • 信用额度只作用于代理主钱包,不作用于佣金钱包和资产钱包。
  • 可用金额:balance - frozen_balance + effective_credit_limit
  • credit_enabled=false 时,effective_credit_limit=0
  • 余额可以为负,最低不能小于 -(credit_limit - frozen_balance)
  • 扣款、冻结、解冻、充值和调额都必须维护同一钱包不变量。
  • 存在欠款或冻结金额导致可用金额不足时,禁止降低额度或关闭信用。
  • 金额统一使用分,禁止浮点数入库。
flowchart TD
    Debit[请求扣款] --> Lock[按钱包ID和version加载]
    Lock --> Calc[计算 balance - frozen + effective_credit]
    Calc --> Enough{可用金额足够?}
    Enough -->|否| Reject[拒绝:可用余额不足]
    Enough -->|是| Update[条件更新余额和version]
    Update --> Tx[同事务写资金流水]
    Tx --> Success[返回扣款后余额]

2.2 信用开关

  • 创建代理时可以提交 enable_creditcredit_limit
  • 关闭开关时额度必须为 0。
  • 打开开关时额度必须大于 0。
  • 修改额度前必须校验修改后的可用金额不为负,而不是只判断 balance >= 0

三、数据库变更

信用额度属于钱包,不在 tb_shoptb_agent_wallet 各保存一份,避免双写失真。创建/编辑代理接口可以接收信用参数,但最终权威数据写入代理主钱包。

ALTER TABLE tb_agent_wallet
    ADD COLUMN credit_enabled BOOLEAN NOT NULL DEFAULT FALSE,
    ADD COLUMN credit_limit BIGINT NOT NULL DEFAULT 0;

COMMENT ON COLUMN tb_agent_wallet.credit_enabled IS '是否启用信用额度,仅主钱包有效';
COMMENT ON COLUMN tb_agent_wallet.credit_limit IS '信用额度上限(分),仅主钱包有效';

ALTER TABLE tb_agent_wallet
    DROP CONSTRAINT IF EXISTS chk_agent_wallet_frozen_balance;

ALTER TABLE tb_agent_wallet
    ADD CONSTRAINT chk_agent_wallet_frozen_nonnegative
        CHECK (frozen_balance >= 0),
    ADD CONSTRAINT chk_agent_wallet_credit_nonnegative
        CHECK (credit_limit >= 0),
    ADD CONSTRAINT chk_agent_wallet_available_nonnegative
        CHECK (
            balance - frozen_balance +
            CASE WHEN credit_enabled THEN credit_limit ELSE 0 END >= 0
        );

必须先检查生产库真实约束名;DROP CONSTRAINT IF EXISTS 不能替代迁移前核对。历史钱包默认关闭信用,行为不变。


四、领域模型

// AgentWallet 代理主钱包聚合根
type AgentWallet struct {
    ID            uint
    WalletType    string
    Balance       int64
    FrozenBalance int64
    CreditEnabled bool
    CreditLimit   int64
    Version       int64
}

// AvailableBalance 返回当前可用金额。
func (w *AgentWallet) AvailableBalance() int64 {
    credit := int64(0)
    if w.CreditEnabled {
        credit = w.CreditLimit
    }
    return w.Balance - w.FrozenBalance + credit
}

// Debit 执行钱包扣款并维护信用边界。
func (w *AgentWallet) Debit(amount int64) error {
    if amount <= 0 {
        return ErrInvalidAmount
    }
    if w.AvailableBalance() < amount {
        return ErrInsufficientAvailableBalance
    }
    w.Balance -= amount
    return nil
}

// ChangeCredit 修改信用配置。
func (w *AgentWallet) ChangeCredit(enabled bool, limit int64) error {
    if limit < 0 || (!enabled && limit != 0) || (enabled && limit == 0) {
        return ErrInvalidCreditConfig
    }
    nextCredit := int64(0)
    if enabled {
        nextCredit = limit
    }
    if w.Balance-w.FrozenBalance+nextCredit < 0 {
        return ErrCreditLimitBelowDebt
    }
    w.CreditEnabled = enabled
    w.CreditLimit = limit
    return nil
}

领域层只维护资金不变量,不查询角色、店铺名称或页面权限。角色能否授信由 Application 在调用聚合前校验。


五、应用用例与持久化

5.1 修改代理信用额度

Handler
  → ChangeShopCreditUseCase
      → 校验操作人和代理数据权限
      → 加载代理主钱包
      → 调用 wallet.ChangeCredit()
      → 按 version 条件更新钱包
      → 写操作日志和信用变更流水

条件更新示例:

UPDATE tb_agent_wallet
SET credit_enabled = ?,
    credit_limit = ?,
    version = version + 1,
    updated_at = NOW()
WHERE id = ?
  AND wallet_type = 'main'
  AND version = ?
  AND balance - frozen_balance +
      CASE WHEN ? THEN ? ELSE 0 END >= 0;

受影响行数为 0 时,重新读取钱包以区分并发冲突和额度低于当前欠款。

5.2 钱包扣款

所有现有主钱包扣款语句必须从:

balance - frozen_balance >= amount

统一改为:

balance - frozen_balance +
CASE WHEN credit_enabled THEN credit_limit ELSE 0 END >= amount

扣款、版本递增和钱包流水必须在同一事务。禁止只修改 GetAvailableBalance() 而遗漏 Store 中的 SQL 条件,否则页面显示可用但实际仍无法扣款。

5.3 受影响用例

  • 后台订单和批量订购的代理钱包支付。
  • C端/代理端使用代理主钱包的订单支付。
  • 钱包冻结与解冻。
  • 退款回充和员工线下代充值。
  • 资金概况、钱包详情和导出 Query。

实施时只迁移这些被信用额度触碰的完整资金用例,不主动改造佣金钱包和资产钱包。


六、查询方案

资金概况、钱包详情、列表和导出使用 Query 直接读取:

balance - frozen_balance +
CASE WHEN credit_enabled THEN credit_limit ELSE 0 END AS available_balance

响应统一增加:

CreditEnabled   bool  `json:"credit_enabled" description:"是否启用信用额度"`
CreditLimit     int64 `json:"credit_limit" description:"信用额度(分)"`
AvailableBalance int64 `json:"available_balance" description:"可用金额(分)"`
IsInDebt        bool  `json:"is_in_debt" description:"余额是否为负"`
DebtAmount      int64 `json:"debt_amount" description:"欠款金额(分)"`

debt_amount = max(-balance, 0),不包含冻结金额。


七、API 设计

7.1 创建代理

现有:

POST /api/admin/shops

新增参数:

EnableCredit bool  `json:"enable_credit" description:"是否开启信用额度"`
CreditLimit  int64 `json:"credit_limit" validate:"min=0" description:"信用额度(分)"`

Application 创建店铺和主钱包时,将信用配置写入钱包;任何一步失败整笔事务回滚。

7.2 修改信用额度

PUT /api/admin/shops/{id}/credit-limit
type UpdateCreditLimitRequest struct {
    EnableCredit bool  `json:"enable_credit" description:"是否开启信用额度"`
    CreditLimit  int64 `json:"credit_limit" validate:"min=0" description:"信用额度(分)"`
    Version      int64 `json:"version" validate:"min=0" description:"钱包版本,用于并发控制"`
}

7.3 查询展示

现有 GET /api/admin/shops/fund-summary 和代理详情响应增加信用字段;不新增不存在的 /agent-wallets/{shop_id} 路由。


八、前端技术方案

8.1 创建/编辑代理

信用额度
[开关] 允许使用信用额度
授信上限 [金额输入,单位元]
  • 开关关闭时清空输入并提交 credit_limit=0
  • 金额输入使用分/元安全转换,不允许负数和小数精度超过两位。
  • 编辑代理基础资料不自动覆盖信用额度;信用调整使用独立权限和独立接口。

8.2 资金概况和详情

账面余额:-¥200.00
冻结金额¥0.00
信用额度¥1,000.00
可用金额¥800.00
  • balance < 0 时账面余额显示欠款样式。
  • 可用金额以接口值为准,不在前端自行重复计算。
  • 修改额度弹框展示当前余额、冻结金额、额度和修改后可用金额预览;提交后仍以后端校验为准。
  • 后端返回并发冲突时刷新钱包版本和最新金额,不保留旧计算结果。

平台员工端不展示个人余额或个人信用额度。拥有信用额度管理权限的员工,只能在代理资金页面查看和调整代理主钱包额度。


九、审计与可观测性

每次信用配置变更记录:

  • 操作人、店铺、钱包 ID。
  • 变更前后开关、额度、余额、冻结金额和版本。
  • request_id、IP、设备信息和时间。

关键日志:

  • 扣款因信用额度不足被拒绝。
  • 钱包 version 冲突。
  • 数据库信用边界约束失败。
  • 信用额度调整失败和旧值/新值。

资金流水必须能够通过订单号、批量任务号或充值/退款业务号反查,不以普通操作日志替代钱包流水。


十、发布与回滚

本需求随七月迭代停机发布:

  1. 维护窗口内先核对并调整钱包现有 CHECK 约束,再增加信用字段,历史数据默认关闭。
  2. 同时发布钱包聚合、全部主钱包扣款条件、查询字段和管理端信用配置页面,禁止只改余额展示而遗漏真实扣款 SQL。
  3. 开放访问前保持所有代理信用开关关闭,人工验证普通余额、信用扣款、并发冲突和额度调整。
  4. 系统开放后再由有权限的管理员对指定代理逐个开启额度。

尚未开启任何信用额度时可以回滚应用版本和可逆迁移。额度启用并产生负余额后,不能直接关闭信用或删除字段;必须先完成还款或保留当前资金逻辑,数据库字段和资金流水不做破坏性回滚。