亮点原子计费机制
原子计费机制
基于数据库事务的原子计费:积分扣费与任务执行强一致,失败自动回滚,绝不重复扣费。
基于数据库事务的原子计费机制,确保积分扣费与任务执行的一致性,绝不丢失积分、绝不重复扣费。
💡 核心承诺:任务失败自动退款,扣费与执行保证原子性,完整的交易审计日志。
1. 什么是原子计费
1.1 原子性(Atomicity)概念
原子性是数据库 ACID 特性之一,指一组操作要么全部成功,要么全部失败,不存在中间状态。
应用到计费场景:
- ✅ 扣费成功 + 任务执行成功 → 用户付费,获得服务
- ✅ 扣费失败 → 任务不执行
- ✅ 任务执行失败 → 自动退款(回滚扣费)
- ❌ 绝不允许:扣费成功但任务未执行(用户损失)
- ❌ 绝不允许:任务执行成功但未扣费(系统损失)
1.2 为什么需要原子计费
问题场景 1:网络故障
传统方案:
1. 扣除 100 积分 → 成功
2. 调用 LLM API → 网络超时,失败
3. 结果:用户损失 100 积分,但没有获得服务
原子计费方案:
1. 开启事务
2. 扣除 100 积分(暂未提交)
3. 调用 LLM API → 失败
4. 事务回滚,积分退回
5. 结果:用户积分无损失 ✅问题场景 2:系统崩溃
传统方案:
1. 扣除 100 积分 → 成功
2. 开始生成报告 → 服务器崩溃
3. 结果:用户损失 100 积分,报告未生成
原子计费方案:
1. 开启事务
2. 扣除 100 积分(暂未提交)
3. 开始生成报告 → 服务器崩溃
4. 事务自动回滚(数据库保证)
5. 结果:用户积分无损失 ✅问题场景 3:重复扣费
传统方案:
1. 用户点击"生成报告"
2. 扣除 100 积分
3. 网络延迟,前端超时
4. 用户再次点击
5. 再次扣除 100 积分
6. 结果:重复扣费 200 积分 ❌
原子计费方案:
1. 检查是否有进行中的任务
2. 如果有 → 拒绝重复提交
3. 如果没有 → 开启事务
4. 扣费 + 创建任务记录(原子操作)
5. 结果:避免重复扣费 ✅2. 原子计费的技术实现
2.1 数据库事务机制
技术栈:PostgreSQL + Prisma ORM
事务示例:
// 伪代码示例
async function generateReportWithBilling(userId, templateId) {
// 1. 开启数据库事务
return await prisma.$transaction(async (tx) => {
// 2. 检查积分余额
const user = await tx.user.findUnique({ where: { id: userId } })
if (user.credits < estimatedCost) {
throw new Error('Insufficient credits')
}
// 3. 扣除积分(暂未提交)
await tx.user.update({
where: { id: userId },
data: { credits: user.credits - estimatedCost }
})
// 4. 创建积分交易记录(暂未提交)
await tx.creditTransaction.create({
data: {
userId,
amount: -estimatedCost,
type: 'REPORT_GENERATION',
status: 'PENDING'
}
})
// 5. 创建报告任务(暂未提交)
const report = await tx.report.create({
data: { userId, templateId, status: 'PENDING' }
})
// 6. 事务提交(所有操作一起成功或失败)
return report
})
// 7. 异步执行报告生成
// 如果失败,通过 Worker 回调退款
}关键点:
- ✅ 所有操作在同一事务中(扣费、创建记录、更新状态)
- ✅ 任何一步失败,整个事务回滚
- ✅ 提交后才真正扣费(不可逆)
2.2 失败自动退款机制
退款流程:
- 任务执行失败(Worker 检测到异常)
- Worker 回调(通知系统任务失败)
- 查询扣费记录(根据任务 ID 查找对应的积分交易)
- 创建退款交易(新增一条
amount > 0的交易记录) - 更新用户积分(
credits += refundAmount) - 更新任务状态(
status = FAILED)
退款示例:
// 伪代码
async function refundOnFailure(taskId, reason) {
return await prisma.$transaction(async (tx) => {
// 1. 查找任务和扣费记录
const task = await tx.report.findUnique({ where: { id: taskId } })
const transaction = await tx.creditTransaction.findFirst({
where: {
type: 'REPORT_GENERATION',
reportId: taskId,
amount: { lt: 0 } // 扣费记录
}
})
// 2. 创建退款记录
await tx.creditTransaction.create({
data: {
userId: task.userId,
amount: Math.abs(transaction.amount), // 正数(退款)
type: 'REFUND',
description: `任务失败自动退款:${reason}`
}
})
// 3. 更新用户积分
await tx.user.update({
where: { id: task.userId },
data: {
credits: { increment: Math.abs(transaction.amount) }
}
})
// 4. 更新任务状态
await tx.report.update({
where: { id: taskId },
data: { status: 'FAILED', error: reason }
})
})
}2.3 重复提交防护
防护机制:
- 幂等性检查:提交前检查是否有相同任务在执行中
- 唯一约束:数据库层面防止重复插入
- 状态机:任务状态只能单向流转(PENDING → PROCESSING → SUCCESS/FAILED)
检查示例:
// 伪代码
async function createTaskWithCheck(userId, templateId) {
// 1. 检查是否有相同任务在执行
const existingTask = await prisma.report.findFirst({
where: {
userId,
templateId,
status: { in: ['PENDING', 'PROCESSING'] }
}
})
if (existingTask) {
throw new Error('任务已在执行中,请勿重复提交')
}
// 2. 创建新任务(原子计费)
return await createReportWithBilling(userId, templateId)
}3. 与传统计费方案对比
| 特性 | 传统计费 | 原子计费 |
|---|---|---|
| 扣费与执行一致性 | ❌ 不保证 | ✅ 事务保证 |
| 任务失败退款 | ❌ 手动申请 | ✅ 自动退款 |
| 重复扣费风险 | ⚠️ 存在风险 | ✅ 幂等性检查 |
| 积分丢失风险 | ⚠️ 系统故障时可能丢失 | ✅ 事务回滚保护 |
| 审计追踪 | ⚠️ 可能不完整 | ✅ 完整交易记录 |
| 用户信任度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
3.1 真实案例对比
案例 1:报告生成失败
| 步骤 | 传统计费 | 原子计费 |
|---|---|---|
| 1. 点击"生成报告" | 扣除 200 积分 | 开启事务,暂扣 200 积分 |
| 2. 执行生成任务 | LLM API 失败 | LLM API 失败 |
| 3. 处理失败 | 任务标记失败 | 事务回滚 + 自动退款 |
| 4. 用户操作 | 提交退款申请 | 无需操作(已退款) |
| 5. 最终结果 | 3-7天后退款 | 立即退款 ✅ |
案例 2:网络延迟重复提交
| 步骤 | 传统计费 | 原子计费 |
|---|---|---|
| 1. 首次点击 | 扣除 200 积分 | 扣除 200 积分 |
| 2. 网络延迟 | 前端超时 | 前端超时 |
| 3. 用户再次点击 | 再次扣除 200 积分 | 检测到任务进行中,拒绝 |
| 4. 最终结果 | 重复扣费 400 积分 ❌ | 只扣费 200 积分 ✅ |
4. 交易审计日志
4.1 完整的交易记录
记录内容:
id: 交易唯一IDuserId: 用户IDamount: 金额(正数=充值/退款,负数=扣费)type: 交易类型(REPORT_GENERATION, CONTENT_COLLECTION, CHAT, REFUND 等)description: 交易描述createdAt: 交易时间metadata: 关联的任务ID、报告ID等
查询示例:
// 用户积分消费明细
积分管理 → 消费明细
示例记录:
┌─────────────────────┬────────┬──────────┬───────────────────────┐
│ 时间 │ 金额 │ 类型 │ 描述 │
├─────────────────────┼────────┼──────────┼───────────────────────┤
│ 2025-10-27 10:00 │ -200 │ 报告生成 │ 每日新闻摘要 │
│ 2025-10-27 10:05 │ +200 │ 退款 │ 任务失败自动退款 │
│ 2025-10-27 11:00 │ -50 │ 数据收集 │ RSS订阅源执行 │
│ 2025-10-27 12:00 │ -20 │ 问答 │ 知识库问答 │
│ 2025-10-27 14:00 │ +10000 │ 兑换码 │ 充值码:ABC123 │
└─────────────────────┴────────┴──────────┴───────────────────────┘4.2 审计追踪能力
支持的查询:
- 按时间范围查询(如"最近 7 天")
- 按交易类型筛选(如"只看报告生成")
- 按金额筛选(如"只看扣费"或"只看退款")
- 导出为 CSV(用于财务审计)
数据一致性保证:
- ✅ 每笔扣费都有对应的任务记录
- ✅ 失败任务必有退款记录
- ✅ 积分余额 = 初始积分 + 所有交易金额之和
5. 用户保障措施
5.1 积分安全承诺
✅ 我们的承诺:
- 任务失败,100% 自动退款,无需申请
- 系统故障,积分绝不丢失(数据库事务保护)
- 重复提交,自动拒绝,避免重复扣费
- 所有交易,完整记录,随时可查
5.2 余额不足保护
检查机制:
- 提交前检查:前端显示预估成本,余额不足时禁用按钮
- 事务中检查:扣费前再次检查余额,不足时回滚事务
- 并发控制:使用数据库行级锁,避免超支
示例场景:
用户当前余额:100 积分
情况 1:单个任务
- 预估成本:80 积分
- 检查结果:余额充足 ✅
- 操作:允许提交
情况 2:并发任务
- 用户同时提交 2 个任务,每个 60 积分
- 事务 A:扣除 60 积分 → 余额 40 → 提交成功
- 事务 B:尝试扣除 60 积分 → 余额不足 → 回滚
- 结果:只有 1 个任务成功,避免超支 ✅
情况 3:实际成本超出预估
- 预估成本:100 积分
- 实际成本:120 积分(LLM token 超出预估)
- 处理:任务失败 + 退款 100 积分
- 提示:请充值后重试5.3 争议解决机制
如果你发现异常扣费:
- 查看"消费明细"(积分管理 → 消费明细)
- 找到异常交易记录
- 查看关联的任务日志(点击交易记录查看详情)
- 如果确认异常,联系技术支持(提供交易ID)
- 技术支持会根据审计日志核实并处理
争议处理时效:
- ✅ 系统自动退款:实时(任务失败后立即退款)
- ✅ 人工审核退款:1-3 个工作日
6. 技术优势总结
| 技术特性 | 实现方式 | 用户收益 |
|---|---|---|
| 事务原子性 | PostgreSQL 事务 + Prisma ORM | 扣费与执行一致,绝不丢失积分 |
| 自动退款 | Worker 回调 + 事务处理 | 任务失败立即退款,无需等待 |
| 重复提交防护 | 幂等性检查 + 状态机 | 避免重复扣费,保护资金安全 |
| 审计日志 | 完整的交易记录表 | 随时查询,争议可追溯 |
| 并发控制 | 数据库行级锁 | 避免超支,余额准确 |
7. 常见问题
Q1: 任务失败后,积分多久会退回?
A:立即退回。当 Worker 检测到任务失败时,会自动触发退款流程(通常在任务失败后 1-3 秒内完成)。你可以在"消费明细"中看到退款记录。
Q2: 如果我重复点击"生成报告",会重复扣费吗?
A:不会。系统会检测是否有相同任务在执行中,如果有,会拒绝重复提交并提示"任务已在执行中"。
Q3: 预估成本和实际成本不一致怎么办?
A:预估成本是基于历史数据计算的参考值。如果实际成本超出预估:
- 余额不足 → 任务失败 + 退款预扣的积分
- 余额充足 → 按实际成本扣费
- 建议:保留一定余额缓冲(如预估成本的 1.5 倍)
Q4: 我可以查看所有的历史交易吗?
A:可以。进入"积分管理 → 消费明细",可以查看所有交易记录,支持:
- 按时间范围筛选(如"最近 30 天")
- 按交易类型筛选(如"只看退款")
- 按金额筛选(如"只看大于 100 积分的交易")
- 导出为 CSV(用于个人记账)
Q5: 原子计费是否会影响系统性能?
A:影响极小。事务开销通常在 1-5ms,对用户体验几乎无感知。我们的优化措施:
- 使用数据库连接池(减少连接开销)
- 异步执行耗时任务(报告生成、内容收集)
- 合理的事务粒度(只锁必要的行)