# 账户冻结和扣费计算逻辑说明 ## 📋 文档概述 本文档详细说明后端账户冻结、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: 引入积分单位和扣费系数,完整说明当前逻辑