# 需求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`），禁用该按钮，展示"存在欠款，无法修改信用额度"。

### 页面：余额显示（全局）

代理管理列表的余额列：支持显示负数，负数红色标注。

### 平台员工余额（角色信用）

平台员工操作（如批量订购代理余额扣款）时，可用余额 = 当前余额 + 角色信用额度。  
在批量订购确认页展示可用额度。
