Files
junhong_cmp_fiber/docs/7月迭代/需求17-信用额度.md
2026-07-13 12:01:18 +09:00

265 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 需求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`),禁用该按钮,展示"存在欠款,无法修改信用额度"。
### 页面:余额显示(全局)
代理管理列表的余额列:支持显示负数,负数红色标注。
### 平台员工余额(角色信用)
平台员工操作(如批量订购代理余额扣款)时,可用余额 = 当前余额 + 角色信用额度。
在批量订购确认页展示可用额度。