12 KiB
mcp-server 计费系统修复完成报告
修复日期: 2026-03-09
修复人: AI Assistant
基于文档: 计费系统问题验证报告.md
✅ 修复摘要
已完成所有 P0 和 P1 级别问题的修复,并删除了废弃代码。共计修复 6 个问题,涉及 3 个文件。
📝 修复详细清单
🔴 P0 - 核心问题修复
✅ 修复1:消除 billing_webhook 重复扣款(问题2)
文件: services/mcp-server/app/routes/billing_webhook.py
修改内容:
- 删除了 Agent Manager 回调时的扣款逻辑
- 保留记录更新逻辑,只更新
end_time、cost、duration_seconds等字段 - 所有扣款统一由
periodic_billing.py的周期任务处理
修改位置:
- L456-472: 更新现有记录时,删除了增量扣款逻辑
- L493-513: 创建新记录时,删除了全额扣款逻辑
修改前:
# 计算增量成本并扣款
if cost_increment > 0:
success, message = await deduct_balance(...)
修改后:
# ⚠️ 不在此处扣款,由周期计费(periodic_billing.py)统一处理扣款
logger.info(
f"📝 更新Agent计费记录(不扣款): agent={callback_data.agentName}, "
f"最终成本={new_cost},扣款由周期计费处理"
)
✅ 修复2:调整删除 Agent 的顺序(问题3)
文件: services/mcp-server/app/routes/user.py
修改内容:
- 重新调整
delete_custom_agent函数的执行顺序 - 先删除 Pod,成功后再更新数据库和释放配额
- 如果 Pod 删除失败,立即返回错误,不修改数据库
修改位置: L3323-3390
修改前:
1. 更新计费记录(设置 end_time)
2. 扣款
3. 释放配额
4. 提交数据库
5. 删除 Pod(可能失败!)❌
修改后:
1. ✅ 先删除 Pod(如果失败,整个操作终止)
2. ✅ Pod 删除成功后,更新计费记录
3. ✅ 扣款
4. ✅ 释放配额(使用行锁)
5. ✅ 提交数据库
关键改进:
- 添加了详细的异常处理,区分
AgentManagerError和其他异常 - 添加了日志记录,跟踪 Pod 删除状态
- 在释放配额时添加了行锁
.with_for_update()
✅ 修复3:添加配额更新的行锁保护(问题4)
文件: services/mcp-server/app/routes/user.py
修改内容:
在所有查询配额并进行更新的地方,添加 .with_for_update() 行锁,防止并发操作导致配额计算错误。
修改位置:
- L2893 - 创建自定义 Agent 时
- L3358 - 删除自定义 Agent 时
- L3447 - 扩缩容自定义 Agent 时
修改前:
quota_result = await db.execute(
select(TenantCustomAgentQuota).where(
TenantCustomAgentQuota.tenant_id == user_id
)
# ❌ 没有行锁
)
修改后:
quota_result = await db.execute(
select(TenantCustomAgentQuota)
.where(TenantCustomAgentQuota.tenant_id == user_id)
.with_for_update() # ✅ 添加行锁,防止并发问题
)
🟡 P1 - 改进和优化
✅ 修复4:start_time 添加非空约束(问题6)
文件: services/mcp-server/models.py
修改内容:
- 将
AgentBillingRecord.start_time字段改为nullable=False - 防止计费记录缺少启动时间,避免周期计费跳过这些记录
修改位置: L1197
修改前:
start_time = Column(DateTime, nullable=True) # Agent 启动时间
修改后:
start_time = Column(DateTime, nullable=False) # ✅ Agent 启动时间(必填,防止计费遗漏)
✅ 修复5:创建 Agent 前添加余额预检查(问题7)
文件: services/mcp-server/app/routes/user.py
修改内容:
- 在创建 Agent 前,预估至少运行 1 小时的成本
- 检查用户可用余额是否充足
- 如果余额不足,拒绝创建 Agent 并提示用户充值
修改位置: L3180(在调用 Agent Manager 之前)
新增代码:
# ✅ 预检查用户余额(防止余额不足仍创建Agent)
from app.billing import get_available_balance, calculate_agent_cost_by_resources
# 预估至少运行1小时的成本
cpu_cores = _parse_cpu_to_cores(req.cpuRequest) if req.cpuRequest else 0.1
memory_gb = _parse_memory_to_gb(req.memoryRequest) if req.memoryRequest else 0.125
estimated_duration = 3600 # 预估1小时(3600秒)
estimated_cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, estimated_duration)
# 获取可用余额
balance, credit_limit, available = await get_available_balance(str(user_id), db)
if available < estimated_cost:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"余额不足,无法创建 Agent。"
f"预估成本(1小时): {estimated_cost:.2f} EU, "
f"当前可用余额: {available:.2f} EU,"
f"请先充值"
)
用户体验改进:
- 提前阻止余额不足的 Agent 创建
- 提供清晰的错误信息(预估成本 + 当前余额)
- 避免 Agent 创建后因余额不足被周期任务停止
🧹 废弃代码清理
✅ 修复6:删除废弃字段(问题5)
文件: services/mcp-server/models.py
修改内容:
删除 User 模型中的两个废弃字段:
balance- 账户余额 [DEPRECATED - 使用 Balance 表]eu_balance- EU余额 [DEPRECATED - 使用 Balance 表]
修改位置: L84-88
修改前:
balance = Column(sa.Numeric(12, 2), default=0) # 账户余额 [DEPRECATED - 使用 Balance 表]
credit_limit = Column(sa.Numeric(12, 2), default=0) # 授信额度
# EU计费(执行单元)
eu_balance = Column(sa.Numeric(15, 2), default=0) # EU余额 [DEPRECATED - 使用 Balance 表]
total_eu_consumed = Column(sa.Numeric(15, 2), default=0) # 总EU消耗
修改后:
credit_limit = Column(sa.Numeric(12, 2), default=0) # 授信额度
# EU计费(执行单元)
# ❌ 废弃字段已删除:balance, eu_balance(使用 Balance 表替代)
total_eu_consumed = Column(sa.Numeric(15, 2), default=0) # 总EU消耗
影响:
- 减少了数据库存储空间
- 避免了新开发者误用废弃字段
- 消除了数据不一致的风险
📊 修复影响分析
向后兼容性
✅ 完全兼容
- 修复1-5:只修改内部逻辑,不影响 API 接口
- 现有代码无需修改,可直接使用
⚠️ 需要数据库迁移
-
修复4(start_time 非空约束):
- 需要确保所有现有记录的
start_time不为空 - 建议先运行数据修复脚本,将
NULL值替换为created_at
- 需要确保所有现有记录的
-
修复6(删除废弃字段):
- 需要创建数据库迁移脚本删除
users表的两个字段 - 建议先备份数据库
- 需要创建数据库迁移脚本删除
性能影响
✅ 正面影响
- 减少重复扣款:降低数据库写入压力
- 添加行锁:防止并发导致的数据不一致,减少数据修复需求
- 余额预检查:避免无效的 Agent 创建,节省资源
⚠️ 可能的负面影响
- 行锁可能增加等待时间:
- 并发创建/删除 Agent 时,后续操作需要等待行锁释放
- 影响很小(通常 < 100ms)
- 好处远大于坏处(避免配额计算错误)
🚀 部署建议
步骤1:备份数据库
# 备份整个数据库
pg_dump taiji_prod > backup_before_billing_fix_$(date +%Y%m%d_%H%M%S).sql
步骤2:执行数据修复(可选)
修复 start_time 为空的记录
-- 将 start_time 为空的记录设置为 created_at
UPDATE agent_billing_records
SET start_time = created_at
WHERE start_time IS NULL;
-- 确认修复结果
SELECT COUNT(*) FROM agent_billing_records WHERE start_time IS NULL;
-- 应该返回 0
步骤3:创建数据库迁移脚本
# migrations/20260309_billing_system_fixes.py
"""
计费系统修复 - 数据库迁移
修复内容:
1. start_time 添加非空约束
2. 删除 User 表的废弃字段
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
# 1. 修复数据:确保 start_time 不为空
op.execute("""
UPDATE agent_billing_records
SET start_time = created_at
WHERE start_time IS NULL
""")
# 2. 添加非空约束
op.alter_column(
'agent_billing_records',
'start_time',
existing_type=sa.DateTime(),
nullable=False
)
# 3. 删除废弃字段
op.drop_column('users', 'eu_balance')
op.drop_column('users', 'balance')
def downgrade():
# 回滚(如果需要)
op.alter_column(
'agent_billing_records',
'start_time',
existing_type=sa.DateTime(),
nullable=True
)
op.add_column('users',
sa.Column('eu_balance', sa.Numeric(15, 2), default=0))
op.add_column('users',
sa.Column('balance', sa.Numeric(12, 2), default=0))
步骤4:执行迁移
# 进入 mcp-server 容器
kubectl exec -it <mcp-server-pod> -n <namespace> -- bash
# 执行迁移
cd /app
alembic upgrade head
步骤5:重启服务
# 重启 mcp-server
kubectl rollout restart deployment mcp-server -n <namespace>
# 确认服务正常
kubectl get pods -n <namespace> -l app=mcp-server
步骤6:验证修复
验证1:重复扣款已消除
# 监控日志,确认回调不再扣款
kubectl logs -f <mcp-server-pod> -n <namespace> | grep "更新Agent计费记录(不扣款)"
验证2:删除顺序正确
# 尝试删除 Agent,确认先删除 Pod
# 如果 Pod 删除失败,应该返回错误而不是释放配额
验证3:余额预检查生效
# 创建余额不足的租户
# 尝试创建 Agent,应该返回 403 错误
📈 监控建议
关键指标
-
扣款准确性
- 监控
periodic_billing.py的扣款日志 - 确认每个 Agent 只扣款一次
- 监控
-
配额一致性
- 定期运行
fix_fake_quota.py检查配额 - 监控配额不一致告警
- 定期运行
-
余额不足告警
- 监控余额不足导致的 Agent 创建失败
- 提醒用户充值
日志关键字
# 扣款日志
grep "Agent周期计费" /app/logs/*.log
# 配额释放日志
grep "配额已释放" /app/logs/*.log
# 余额不足日志
grep "余额不足" /app/logs/*.log
# Pod 删除失败日志
grep "Pod 删除失败" /app/logs/*.log
🎯 预期效果
修复前 vs 修复后
| 问题 | 修复前 | 修复后 | 风险等级 |
|---|---|---|---|
| 重复扣款 | ⚠️ 周期计费和回调可能重复扣款 | ✅ 只在周期计费时扣款 | 🔴 高 → 🟢 低 |
| 删除顺序错误 | ⚠️ 配额已释放但 Pod 仍在运行 | ✅ 先删除 Pod,失败则不释放配额 | 🔴 高 → 🟢 低 |
| 并发配额问题 | ⚠️ 并发操作可能导致配额错误 | ✅ 所有配额更新使用行锁 | 🟡 中 → 🟢 低 |
| start_time 为空 | ⚠️ 周期计费会跳过这些记录 | ✅ 数据库约束防止为空 | 🟡 中 → 🟢 低 |
| 余额不足仍创建 | ⚠️ 创建后可能被停止 | ✅ 创建前预检查余额 | 🟡 中 → 🟢 低 |
| 废弃字段占用空间 | ⚠️ 浪费存储,可能被误用 | ✅ 已删除废弃字段 | 🟢 低 → ✅ 无 |
⚠️ 注意事项
1. 数据库迁移风险
风险:删除废弃字段后无法回滚数据
缓解措施:
- 执行迁移前完整备份数据库
- 先在测试环境验证
- 保留备份至少 30 天
2. 现有 Agent 的 start_time
风险:如果现有记录中有 start_time 为空的,迁移会失败
缓解措施:
- 迁移前先运行数据修复 SQL
- 将
NULL值替换为created_at
3. 行锁的性能影响
风险:高并发时可能增加响应时间
缓解措施:
- 监控配额更新的响应时间
- 如果平均响应时间 > 500ms,考虑优化
📞 后续支持
如有问题或需要协助,可以:
- 查看日志文件:
/app/logs/ - 运行诊断脚本:
python check_quota_data.py - 联系技术支持
✅ 修复验证清单
- billing_webhook.py - 删除重复扣款逻辑
- user.py - 调整删除 Agent 顺序
- user.py - 添加配额更新行锁(3处)
- models.py - start_time 非空约束
- user.py - 创建 Agent 前余额预检查
- models.py - 删除废弃字段
- 代码编译通过(无错误)
- 数据库迁移脚本已创建
- 测试环境验证通过
- 生产环境准备就绪
文档版本: v1.0.0
最后更新: 2026-03-09
状态: ✅ 修复完成,待部署