15 KiB
15 KiB
账户冻结和扣费计算逻辑说明
📋 文档概述
本文档详细说明后端账户冻结、Token消费计算、扣费系数应用及余额扣减的完整逻辑。
🎯 核心概念定义
1. 计量单位体系
人民币(元) ←→ 积分
换算关系:
- 1 元 = 100 积分
- 数据库中的 balance、amount、frozen_amount 字段单位均为【积分】
2. 扣费系数
# 配置文件: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
输入参数:
{
"sessionId": "会话ID",
"modelName": "Qwen 3.5 Plus",
"estimatedInputTokens": 120000,
"estimatedOutputTokens": 200,
"frozenType": 1
}
计算步骤:
// 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
输入参数:
{
"frozenId": "冻结单ID",
"usageInputTokens": 121067,
"usageOutputTokens": 209,
"usageTotalTokens": 121276
}
计算步骤:
// 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费用计算采用向上取整到分的策略:
// 不足1分按1分计算
if (tokens % perCent > 0) {
fee += 1;
}
原因: 避免用户通过拆分请求来规避最小计费单位
3. 扣费系数应用
扣费系数在两个阶段都会应用:
- 创建冻结单:预估费用 × 扣费系数
- 释放冻结单:实际费用 × 扣费系数
示例(扣费系数=2):
基础费用:2 积分(121,067输入Token + 209输出Token)
实际扣费:2 × 2 = 4 积分
4. 余额不足处理
// 如果余额不够实际扣减,将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 积分的舍入误差
📝 前端展示建议
方案一:以"积分"为单位展示(推荐)
// 后端返回的数据(积分)
const balance = 16; // 积分
// 前端显示
const displayBalance = balance.toFixed(2) + '积分'; // "16.00积分"
方案二:转换为"元"展示
// 后端返回的数据(积分)
const balance = 16; // 积分
// 转换为元显示
const yuan = (balance / 100).toFixed(2); // "0.16" 元
const displayText = `${yuan}元`;
方案三:混合展示(推荐用于明细)
// 同时显示元和积分
const yuan = (balance / 100).toFixed(2);
const points = balance.toFixed(2);
const displayText = `${yuan}元(${points}积分)`;
// 输出:"0.16元(16.00积分)"
方案四:显示扣费明细
// 显示完整的扣费信息
const baseFee = 2; // 基础费用
const coefficient = 2; // 扣费系数
const actualFee = 4; // 实际扣费
const displayText = `
基础费用:${baseFee}积分
扣费系数:×${coefficient}
实际扣费:${actualFee}积分(${(actualFee/100).toFixed(2)}元)
`;
🔧 如何调整扣费系数
修改配置文件
开发环境 (application-dev.yml):
account:
deduction:
coefficient: 1.5 # 1.5倍扣费(测试用)
生产环境 (application-prod.yml):
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 = 预期费用 ✓
🔍 关键代码位置
后端实现文件
-
扣费配置属性类
- 文件:
src/main/java/com/kexue/skills/config/AccountDeductionProperties.java - 作用:读取配置文件中的扣费系数
- 文件:
-
冻结单控制器
- 文件:
src/main/java/com/kexue/skills/controller/AccountFrozenController.java - 接口:
POST /api/accountFrozen/frozen - 接口:
POST /api/accountFrozen/release
- 文件:
-
冻结单服务实现
- 文件:
src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java - 方法:
createFrozen()- 第 57-141 行 - 方法:
releaseFrozen()- 第 148-283 行 - 扣费系数应用:第 110-111 行、第 209-210 行
- 文件:
-
配置文件
- 文件:
src/main/resources/application.yml - 配置项:
account.deduction.coefficient
- 文件:
📞 联系方式
如有疑问,请联系后端开发团队。
文档版本: v3.0
更新时间: 2026-04-13
作者: 后端开发团队
更新说明:
- v1.0: 初始版本,基于"元"为单位
- v2.0: 根据模型价格配置重新验证,发现单位问题
- v3.0: 引入积分单位和扣费系数,完整说明当前逻辑