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

15 KiB
Raw Blame History

账户冻结和扣费计算逻辑说明

📋 文档概述

本文档详细说明后端账户冻结、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 = 预期费用 ✓

🔍 关键代码位置

后端实现文件

  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: 引入积分单位和扣费系数,完整说明当前逻辑