agent-skill-backend/账户冻结扣费计算逻辑说明.md

556 lines
15 KiB
Markdown
Raw Permalink 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.

# 账户冻结和扣费计算逻辑说明
## 📋 文档概述
本文档详细说明后端账户冻结、Token消费计算、扣费系数应用及余额扣减的完整逻辑。
---
## 🎯 核心概念定义
### 1. 计量单位体系
```
人民币(元) ←→ 积分
换算关系:
- 1 元 = 100 积分
- 数据库中的 balance、amount、frozen_amount 字段单位均为【积分】
```
### 2. 扣费系数
```yaml
# 配置文件application.yml
account:
deduction:
coefficient: 2 # 扣费系数默认2倍
```
**含义:**
- 实际消耗 1 积分,扣除 2 积分
- 计算公式:`实际扣费 = 基础费用 × 扣费系数`
### 3. 关键数据表字段
| 表名 | 字段 | 类型 | 单位 | 说明 |
|------|------|------|------|------|
| account | balance | decimal(10,2) | **积分** | 账户总余额 |
| account | frozen_amount | decimal(10,2) | **积分** | 冻结金额 |
| account_transaction | amount | decimal(10,2) | **积分** | 交易金额 |
| account_transaction | before_balance | decimal(10,2) | **积分** | 交易前余额 |
| account_transaction | after_balance | decimal(10,2) | **积分** | 交易后余额 |
| model_price | input_per_cent | bigint | **个/分** | 1分钱可购买的输入Token数 |
| model_price | output_per_cent | bigint | **个/分** | 1分钱可购买的输出Token数 |
---
## 💰 模型价格配置示例
| 厂商 | 模型名称 | 输入价格(元/百万Token) | 输出价格(元/百万Token) | input_per_cent | output_per_cent |
|------|----------|---------------------|---------------------|----------------|----------------|
| 阿里巴巴 | Qwen 3.5 Plus | 4.0000 | 12.0000 | 250000 | 83333 |
| 字节跳动 | 豆包 Lite | 0.6000 | 1.2000 | 1666667 | 833333 |
| OpenAI | GPT-4o | 18.0000 | 72.0000 | 55556 | 13889 |
**配置说明:**
- `input_per_cent`: 1分钱可以购买的输入Token数量
- `output_per_cent`: 1分钱可以购买的输出Token数量
**计算关系:**
```
input_per_cent = 1,000,000 / (input_price × 100)
output_per_cent = 1,000,000 / (output_price × 100)
例如 Qwen 3.5 Plus
- input_price = 4.0000 元/百万Token
- input_per_cent = 1,000,000 / (4.0000 × 100) = 250,000
```
---
## 🔢 完整计算逻辑
### 阶段一:创建冻结单(预扣费)
#### 接口:`POST /api/accountFrozen/frozen`
**输入参数:**
```json
{
"sessionId": "会话ID",
"modelName": "Qwen 3.5 Plus",
"estimatedInputTokens": 120000,
"estimatedOutputTokens": 200,
"frozenType": 1
}
```
**计算步骤:**
```java
// 1. 查询模型价格配置
ModelPrice modelPrice = queryByModelName("Qwen 3.5 Plus");
// 配置inputPerCent = 250000, outputPerCent = 83333
// 2. 计算输入Token费用向上取整到分
long inputFee = estimatedInputTokens / modelPrice.getInputPerCent();
if (estimatedInputTokens % modelPrice.getInputPerCent() > 0) {
inputFee += 1; // 不足1分按1分计算
}
// 示例120000 / 250000 = 0余数 120000 > 0所以 inputFee = 1 分
// 3. 计算输出Token费用向上取整到分
long outputFee = estimatedOutputTokens / modelPrice.getOutputPerCent();
if (estimatedOutputTokens % modelPrice.getOutputPerCent() > 0) {
outputFee += 1; // 不足1分按1分计算
}
// 示例200 / 83333 = 0余数 200 > 0所以 outputFee = 1 分
// 4. 计算基础费用(分)
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 示例1 + 1 = 2 分 = 2 积分
// 5. 应用扣费系数
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
BigDecimal coefficient = accountDeductionProperties.getCoefficient(); // 从配置文件获取默认2
BigDecimal finalFrozenAmount = baseAmount.multiply(coefficient);
// 示例2 × 2 = 4 积分
// 6. 检查可用余额是否足够
BigDecimal availableBalance = balance - frozenAmount;
if (availableBalance < finalFrozenAmount) {
throw new BizException("余额不足");
}
// 7. 更新账户冻结金额
account.frozenAmount += finalFrozenAmount;
```
**计算公式总结:**
```
冻结金额(积分)= [⌈输入Token / inputPerCent⌉ + ⌈输出Token / outputPerCent⌉] × 扣费系数
其中 ⌈x 表示向上取整
```
**实际案例(扣费系数=2**
```
输入Token: 120,000
输出Token: 200
Qwen 3.5 Plus: input_per_cent=250000, output_per_cent=83333
计算:
- inputFee = ⌈120000/250000⌉ = 1 积分
- outputFee = ⌈200/83333⌉ = 1 积分
- baseAmount = 2 积分
- finalFrozenAmount = 2 × 2 = 4 积分
```
---
### 阶段二:释放冻结单(实际扣费)
#### 接口:`POST /api/accountFrozen/release`
**输入参数:**
```json
{
"frozenId": "冻结单ID",
"usageInputTokens": 121067,
"usageOutputTokens": 209,
"usageTotalTokens": 121276
}
```
**计算步骤:**
```java
// 1. 查询冻结单
AccountFrozen frozen = getByFrozenId(frozenId);
// 获取 modelName: "Qwen 3.5 Plus"
// 2. 查询模型价格配置
ModelPrice modelPrice = queryByModelName("Qwen 3.5 Plus");
// 配置inputPerCent = 250000, outputPerCent = 83333
// 3. 计算实际输入Token费用向上取整到分
long inputFee = usageInputTokens / modelPrice.getInputPerCent();
if (usageInputTokens % modelPrice.getInputPerCent() > 0) {
inputFee += 1;
}
// 示例121067 / 250000 = 0余数 121067 > 0所以 inputFee = 1 分
// 4. 计算实际输出Token费用向上取整到分
long outputFee = usageOutputTokens / modelPrice.getOutputPerCent();
if (usageOutputTokens % modelPrice.getOutputPerCent() > 0) {
outputFee += 1;
}
// 示例209 / 83333 = 0余数 209 > 0所以 outputFee = 1 分
// 5. 计算基础费用(分)
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 示例1 + 1 = 2 分 = 2 积分
// 6. 应用扣费系数
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
BigDecimal coefficient = accountDeductionProperties.getCoefficient(); // 从配置文件获取默认2
BigDecimal finalAmount = baseAmount.multiply(coefficient);
// 示例2 × 2 = 4 积分
// 7. 释放全部冻结金额
account.frozenAmount -= frozen.frozenAmount;
// 8. 扣减实际消费金额
if (finalAmount > 0) {
if (account.balance < finalAmount) {
account.balance = 0; // 余额不足时设为0
} else {
account.balance -= finalAmount;
}
}
// 9. 生成交易记录
AccountTransaction transaction = {
userId: frozen.userId,
transactionType: 3, // 购买内容
amount: finalAmount, // 4 积分(已应用扣费系数)
beforeBalance: 原余额,
afterBalance: 扣减后余额,
status: 1, // 成功
payType: 3, // 余额支付
businessType: "frozen_release",
isExpense: 1, // 支出
inputToken: 121067,
outputToken: 209,
totalTokens: 121276,
modelName: "Qwen 3.5 Plus",
remark: "冻结单释放扣减: " + frozen.frozenId
};
// 10. 更新冻结单状态
frozen.status = "FINALIZED";
frozen.finalAmount = finalAmount;
```
**计算公式总结:**
```
实际扣费(积分)= [⌈实际输入Token / inputPerCent⌉ + 实际输出Token / outputPerCent⌉] × 扣费系数
```
---
## ✅ 完整案例验证
### 案例数据
**用户请求:**
- 模型Qwen 3.5 Plus
- 预估输入Token: 120,000
- 预估输出Token: 200
- 实际输入Token: 121,067
- 实际输出Token: 209
- 扣费系数2配置文件默认值
- 用户余额20 积分
### 阶段一:创建冻结单
```
计算过程:
1. inputFee = ⌈120000/250000⌉ = 1 积分
2. outputFee = ⌈200/83333⌉ = 1 积分
3. baseAmount = 1 + 1 = 2 积分
4. finalFrozenAmount = 2 × 2 = 4 积分(应用扣费系数)
账户变化:
- balance: 20 积分(不变)
- frozenAmount: 0 + 4 = 4 积分
- availableBalance: 20 - 4 = 16 积分
```
### 阶段二:释放冻结单
```
计算过程:
1. inputFee = ⌈121067/250000⌉ = 1 积分
2. outputFee = ⌈209/83333⌉ = 1 积分
3. baseAmount = 1 + 1 = 2 积分
4. finalAmount = 2 × 2 = 4 积分(应用扣费系数)
账户变化:
- frozenAmount: 4 - 4 = 0 积分(释放冻结)
- balance: 20 - 4 = 16 积分(扣减实际费用)
数据库交易记录:
- transaction_id: 145
- before_balance: 20.00 积分
- amount: 4.00 积分
- after_balance: 16.00 积分
- input_token: 121067
- output_token: 209
- model_name: "Qwen 3.5 Plus"
```
**验算:** 20.00 - 4.00 = 16.00 ✓ 完全正确!
---
## 📊 完整业务流程图
```
用户发起AI请求
【阶段1创建冻结单】
1. 预估Token数量输入+输出)
2. 查询模型价格配置inputPerCent, outputPerCent
3. 计算预估费用(向上取整到分)
baseAmount = ⌈estInput/inputPerCent⌉ + ⌈estOutput/outputPerCent⌉
4. 应用扣费系数
finalFrozenAmount = baseAmount × coefficient
5. 检查可用余额balance - frozenAmount >= finalFrozenAmount
6. 冻结金额frozenAmount += finalFrozenAmount
7. 创建冻结单status = "RESERVED"
AI模型执行完成
【阶段2释放冻结单】
1. 获取实际Token使用量
2. 查询模型价格配置
3. 计算实际费用(向上取整到分)
baseAmount = ⌈actualInput/inputPerCent⌉ + ⌈actualOutput/outputPerCent⌉
4. 应用扣费系数
finalAmount = baseAmount × coefficient
5. 释放冻结金额frozenAmount -= frozen.frozenAmount
6. 扣减实际消费balance -= finalAmount
7. 生成交易记录account_transaction
8. 更新冻结单状态status = "FINALIZED"
返回结果给用户
```
---
## ⚠️ 重要说明
### 1. 单位统一性
**所有涉及金额的字段在数据库中均以【积分】为单位存储:**
- `account.balance` - 积分
- `account.frozen_amount` - 积分
- `account_transaction.amount` - 积分
- `account_transaction.before_balance` - 积分
- `account_transaction.after_balance` - 积分
### 2. 向上取整规则
Token费用计算采用**向上取整到分**的策略:
```java
// 不足1分按1分计算
if (tokens % perCent > 0) {
fee += 1;
}
```
**原因:** 避免用户通过拆分请求来规避最小计费单位
### 3. 扣费系数应用
扣费系数在两个阶段都会应用:
- **创建冻结单**:预估费用 × 扣费系数
- **释放冻结单**:实际费用 × 扣费系数
**示例(扣费系数=2**
```
基础费用2 积分121,067输入Token + 209输出Token
实际扣费2 × 2 = 4 积分
```
### 4. 余额不足处理
```java
// 如果余额不够实际扣减将balance设置为0
if (balance.compareTo(finalAmount) < 0) {
account.setBalance(BigDecimal.ZERO);
} else {
account.setBalance(balance.subtract(finalAmount));
}
```
**原因:** 防止余额出现负数
### 5. 数据精度
数据库字段定义为 `decimal(10,2)`
- 最大值99,999,999.99 积分 = 999,999.9999 元
- 小数位数2位
- 精度损失:在极端情况下可能存在 0.01 积分的舍入误差
---
## 📝 前端展示建议
### 方案一:以"积分"为单位展示(推荐)
```javascript
// 后端返回的数据(积分)
const balance = 16; // 积分
// 前端显示
const displayBalance = balance.toFixed(2) + '积分'; // "16.00积分"
```
### 方案二:转换为"元"展示
```javascript
// 后端返回的数据(积分)
const balance = 16; // 积分
// 转换为元显示
const yuan = (balance / 100).toFixed(2); // "0.16" 元
const displayText = `${yuan}元`;
```
### 方案三:混合展示(推荐用于明细)
```javascript
// 同时显示元和积分
const yuan = (balance / 100).toFixed(2);
const points = balance.toFixed(2);
const displayText = `${yuan}元(${points}积分)`;
// 输出:"0.16元16.00积分)"
```
### 方案四:显示扣费明细
```javascript
// 显示完整的扣费信息
const baseFee = 2; // 基础费用
const coefficient = 2; // 扣费系数
const actualFee = 4; // 实际扣费
const displayText = `
基础费用:${baseFee}积分
扣费系数:×${coefficient}
实际扣费:${actualFee}积分(${(actualFee/100).toFixed(2)}元)
`;
```
---
## 🔧 如何调整扣费系数
### 修改配置文件
**开发环境** (`application-dev.yml`)
```yaml
account:
deduction:
coefficient: 1.5 # 1.5倍扣费(测试用)
```
**生产环境** (`application-prod.yml`)
```yaml
account:
deduction:
coefficient: 2 # 2倍扣费
```
### 配置说明
- 修改后重启服务即可生效
- 无需修改代码
- 支持小数(如 1.5、2.5
- 默认值为 2
### 不同场景的扣费系数建议
| 场景 | 扣费系数 | 说明 |
|------|----------|------|
| 测试环境 | 1.0 | 不额外扣费,方便测试 |
| 开发环境 | 1.5 | 适度扣费,接近真实场景 |
| 生产环境-普惠模型 | 1.5 | 降低用户成本 |
| 生产环境-高级模型 | 2.0 | 标准扣费 |
| 生产环境-旗舰模型 | 2.5 | 高端服务额外扣费 |
---
## ✅ 验证清单
前端可以通过以下方式验证后端计算的正确性:
- [ ] 交易记录的 `before_balance - amount = after_balance`
- [ ] 根据模型价格配置,手动计算 Token 基础费用
- [ ] 验证向上取整逻辑不足1分按1分计算
- [ ] 检查冻结单的预估费用是否应用了扣费系数
- [ ] 确认释放后的实际费用是否应用了扣费系数
- [ ] 验证账户的 `frozen_amount` 在释放后正确减少
- [ ] 核对扣费系数是否与配置文件一致
### 计算验证公式
```
预期费用(积分)= [⌈输入Token/inputPerCent⌉ + 输出Token/outputPerCent] × coefficient
验证:
- before_balance - after_balance = amount ✓
- amount = 预期费用 ✓
```
---
## 🔍 关键代码位置
### 后端实现文件
1. **扣费配置属性类**
- 文件:`src/main/java/com/kexue/skills/config/AccountDeductionProperties.java`
- 作用:读取配置文件中的扣费系数
2. **冻结单控制器**
- 文件:`src/main/java/com/kexue/skills/controller/AccountFrozenController.java`
- 接口:`POST /api/accountFrozen/frozen`
- 接口:`POST /api/accountFrozen/release`
3. **冻结单服务实现**
- 文件:`src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java`
- 方法:`createFrozen()` - 第 57-141 行
- 方法:`releaseFrozen()` - 第 148-283 行
- 扣费系数应用:第 110-111 行、第 209-210 行
4. **配置文件**
- 文件:`src/main/resources/application.yml`
- 配置项:`account.deduction.coefficient`
---
## 📞 联系方式
如有疑问,请联系后端开发团队。
**文档版本:** v3.0
**更新时间:** 2026-04-13
**作者:** 后端开发团队
**更新说明:**
- v1.0: 初始版本,基于"元"为单位
- v2.0: 根据模型价格配置重新验证,发现单位问题
- v3.0: 引入积分单位和扣费系数,完整说明当前逻辑