# 需求17:信用额度 > DDD 结构:`internal/domain/wallet/` --- ## 业务规则 ### 两类主体 | 主体 | 信用额度来源 | 修改条件 | |------|------------|---------| | 代理(店铺) | 创建店铺时设置 `credit_limit` | 修改前必须先清零欠款(`balance >= 0`)| | 平台员工 | 基于**角色**,不是账号 | 角色权限表里配置 | ### 可用余额公式 ``` 可用额度 = balance + credit_limit - frozen_balance ``` - `balance` 可以为负(表示欠款) - `balance < 0` 时表示已在使用信用额度 - `balance < -credit_limit` 表示超额使用(理论上不可达,系统保护) ### BPO-009 "是否可授权额度"开关 - 代理创建时,新增 `enable_credit` 开关 - 开关关闭 = `credit_limit` 强制为 0,不允许使用信用额度 - 平台员工账号通过角色自动拥有额度,无需开关 ### BPO-012 负余额显示 - 代理前台(AgentWallet):`balance` 正常展示(可为负) - 前端不做"不能为负"的前端校验,由后端控制 --- ## 数据库变更 ### 1. Shop 表加信用额度字段 ```sql ALTER TABLE tb_shop ADD COLUMN enable_credit BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否可使用信用额度', ADD COLUMN credit_limit BIGINT NOT NULL DEFAULT 0 COMMENT '授信额度上限(分)'; ``` ### 2. AgentWallet 表加信用额度字段 ```sql ALTER TABLE tb_agent_wallet ADD COLUMN credit_limit BIGINT NOT NULL DEFAULT 0 COMMENT '信用额度(分),与 shop.credit_limit 同步,余额可到 -credit_limit'; ``` > 钱包上冗余一份,是为了避免每次扣款都要 join shop 表。通过 Shop 修改额度时同步更新钱包。 ### 3. 角色信用额度配置(平台员工) ```sql ALTER TABLE tb_role ADD COLUMN credit_limit BIGINT NOT NULL DEFAULT 0 COMMENT '该角色的信用额度(分),仅对平台员工角色有效'; ``` --- ## Model 变更 ```go // internal/model/shop.go type Shop struct { // ...原有字段... EnableCredit bool `gorm:"column:enable_credit;not null;default:false;comment:是否可使用信用额度" json:"enable_credit"` CreditLimit int64 `gorm:"column:credit_limit;type:bigint;not null;default:0;comment:授信额度上限(分)" json:"credit_limit"` } // internal/model/agent_wallet.go type AgentWallet struct { // ...原有字段(Balance, FrozenBalance 已存在)... CreditLimit int64 `gorm:"column:credit_limit;type:bigint;not null;default:0;comment:信用额度(分)" json:"credit_limit"` } // GetAvailableBalance 更新:可用余额 = 余额 - 冻结 + 信用额度 func (w *AgentWallet) GetAvailableBalance() int64 { return w.Balance - w.FrozenBalance + w.CreditLimit } ``` --- ## 领域层(DDD) ```go // internal/domain/wallet/wallet.go // AgentWallet 钱包聚合根(扩展现有模型) type AgentWalletAggregate struct { model.AgentWallet domainEvents []DomainEvent `gorm:"-"` } // Debit 扣款(含信用额度) func (w *AgentWalletAggregate) Debit(amountCents int64, refType string, refID uint) error { available := w.Balance - w.FrozenBalance + w.CreditLimit if available < amountCents { return ErrInsufficientBalance } before := w.Balance w.Balance -= amountCents w.recordEvent(WalletDebitedEvent{ WalletID: w.ID, ShopID: w.ShopID, Amount: amountCents, BalanceBefore: before, BalanceAfter: w.Balance, RefType: refType, RefID: refID, }) return nil } ``` --- ## 应用层 ```go // internal/application/wallet/grant_credit.go // GrantCreditCommand 授信命令 type GrantCreditCommand struct { ShopID uint CreditLimit int64 // 分 OperatorID uint } // GrantCreditUseCase 设置代理信用额度 type GrantCreditUseCase struct { shopStore ShopStore walletStore WalletStore txManager TxManager } func (uc *GrantCreditUseCase) Execute(ctx context.Context, cmd GrantCreditCommand) error { return uc.txManager.RunInTx(ctx, func(tx *gorm.DB) error { shop, err := uc.shopStore.GetByID(ctx, cmd.ShopID) if err != nil { return err } // 修改信用额度前,必须当前余额 >= 0(无欠款) wallet, err := uc.walletStore.GetMainWallet(ctx, cmd.ShopID) if err != nil { return err } if wallet.Balance < 0 { return errors.New(errors.CodeForbidden, "存在欠款,请先清零后再修改信用额度") } // 同步更新 shop 和 wallet if err := uc.shopStore.UpdateCreditLimit(ctx, tx, cmd.ShopID, cmd.CreditLimit); err != nil { return err } return uc.walletStore.UpdateCreditLimit(ctx, tx, wallet.ID, cmd.CreditLimit) }) } ``` --- ## API 设计 ### 1. 创建店铺(新增字段) ``` POST /admin/shops ``` 新增参数: ```go EnableCredit bool `json:"enable_credit" description:"是否开启信用额度"` CreditLimit int64 `json:"credit_limit" description:"授信额度(分),enable_credit=true 时有效"` ``` ### 2. 修改信用额度 ``` PUT /admin/shops/{id}/credit-limit ``` ```go type UpdateCreditLimitRequest struct { CreditLimit int64 `json:"credit_limit" validate:"min=0" description:"新授信额度(分),修改前需余额>=0"` } ``` 错误场景:余额为负时,返回 `CodeForbidden`,msg="存在欠款(-XX元),请先清零后再修改信用额度" ### 3. 角色信用额度配置(平台员工) ``` PUT /admin/roles/{id}/credit-limit ``` ```go type UpdateRoleCreditLimitRequest struct { CreditLimit int64 `json:"credit_limit" validate:"min=0" description:"该角色的信用额度(分)"` } ``` ### 4. 查询钱包(余额展示更新) 现有 `GET /admin/agent-wallets/{shopID}` 响应新增字段: ```go type AgentWalletResponse struct { // ...原有字段... CreditLimit int64 `json:"credit_limit" description:"信用额度(分)"` AvailableBalance int64 `json:"available_balance" description:"可用余额 = balance - frozen + credit_limit(分),可为负"` IsInDebt bool `json:"is_in_debt" description:"是否有欠款(balance < 0)"` DebtAmount int64 `json:"debt_amount" description:"欠款金额(分),balance<0时有值"` } ``` --- ## 前端对接 ### 页面:创建/编辑代理 新增"信用额度"区块: ``` 信用额度: [☑ 开启信用额度] 授信上限:[____] 元 (enable_credit=true 时展示) ``` 提交时:`enable_credit: true, credit_limit: 100000`(单位:分,前端转换) ### 页面:代理详情 > 钱包信息 ``` 钱包余额:-¥200.00 (红色显示) 信用额度:¥1,000.00 可用余额:¥800.00 ``` 当 `is_in_debt=true` 时,余额展示红色,并加"欠款"标签。 ### 页面:修改信用额度 入口:代理详情 > 钱包 > "修改信用额度"按钮 弹框:输入新额度 → `PUT /admin/shops/{id}/credit-limit` 如果当前有欠款(`is_in_debt=true`),禁用该按钮,展示"存在欠款,无法修改信用额度"。 ### 页面:余额显示(全局) 代理管理列表的余额列:支持显示负数,负数红色标注。 ### 平台员工余额(角色信用) 平台员工操作(如批量订购代理余额扣款)时,可用余额 = 当前余额 + 角色信用额度。 在批量订购确认页展示可用额度。