1
0
Files
nex/openspec/specs/usage-statistics/spec.md
lanyuanxiaoyao 1522c87c74 fix: 修复 statsRepo 并发竞态条件,使用 upsert 保证原子性
- 使用 GORM clause.OnConflict 替代事务包装
- Record 和 BatchUpdate 方法改用 upsert 模式
- 修复 UsageStats 的 GORM struct tag,确保 AutoMigrate 创建正确的 UNIQUE 约束
- 更新 usage-statistics spec 以反映 upsert 操作

MySQL 并发测试验证:10 并发调用 → request_count = 10
2026-04-23 15:54:56 +08:00

200 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Usage Statistics
## Purpose
定义 API 使用统计规范,支持请求统计记录、按供应商/模型/日期范围查询统计、统计聚合以及并发安全的统计记录。
## Requirements
### Requirement: 记录请求统计
网关 SHALL 为每次 API 调用记录请求统计。
#### Scenario: 记录成功请求
- **WHEN** 请求成功转发到供应商
- **THEN** 网关 SHALL 通过 StatsBuffer 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 异步批量写入数据库(定时或阈值触发)
- **THEN** 网关 SHALL 不阻塞响应
#### Scenario: 记录流式请求
- **WHEN** 流式请求成功完成
- **THEN** 网关 SHALL 通过 StatsBuffer 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 在流结束后异步记录统计
### Requirement: 使用统计记录统一模型标识
系统 SHALL 使用 providerID 和 modelName上游模型名记录使用统计。
#### Scenario: 代理请求统计记录
- **WHEN** 代理请求成功完成
- **THEN** SHALL 记录 provider_id 和 model_name 到 usage_stats 表(参数来自路由结果)
- **THEN** SHALL 异步执行,不阻塞响应
#### Scenario: 查询统计
- **WHEN** 查询统计数据
- **THEN** 支持按 provider_id 和 model_name 过滤
### Requirement: 按供应商查询统计
网关 SHALL 允许按供应商过滤查询统计。
#### Scenario: 查询特定供应商的统计
- **WHEN** 向 `/api/stats?provider_id=<provider_id>` 发送 GET 请求
- **THEN** 网关 SHALL 仅返回指定供应商的统计
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 按模型查询统计
网关 SHALL 允许按模型过滤查询统计。
#### Scenario: 查询特定模型的统计
- **WHEN** 向 `/api/stats?model_name=<model_name>` 发送 GET 请求
- **THEN** 网关 SHALL 仅返回指定模型的统计
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 按日期范围查询统计
网关 SHALL 允许在日期范围内查询统计。
#### Scenario: 使用日期范围查询统计
- **WHEN** 向 `/api/stats?start=<start_date>&end=<end_date>` 发送 GET 请求
- **THEN** 网关 SHALL 仅返回指定范围内的日期统计
- **THEN** 日期格式 SHALL 为 YYYY-MM-DD
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 聚合统计
网关 SHALL 按日期聚合统计。
#### Scenario: 同一天多次请求
- **WHEN** 同一天对同一供应商和模型发起多次请求
- **THEN** 网关 SHALL 为该天维护单条统计记录
- **THEN** 请求计数 SHALL 为所有请求的总和
**变更说明:** 聚合逻辑在 StatsRepository 中实现。API 接口保持不变。
### Requirement: 支持并发统计记录
网关 SHALL 支持并发请求统计记录而无冲突。
#### Scenario: 并发请求
- **WHEN** 同时处理多个并发请求
- **THEN** 网关 SHALL 使用原子操作正确增加每个请求的计数
- **THEN** 不 SHALL 因并发写入而丢失统计
- **THEN** SHALL 使用 upsert 操作保证原子性
#### Scenario: 并发调用 Record 方法
- **WHEN** 多个 goroutine 并发调用 StatsRepository.Record
- **THEN** SHALL 使用 INSERT ... ON DUPLICATE KEY UPDATE (MySQL) 或 INSERT ... ON CONFLICT DO UPDATE (SQLite)
- **THEN** SHALL 保证所有并发调用的计数都被正确累加
- **THEN** 不 SHALL 因 UNIQUE 约束冲突而丢失数据
#### Scenario: 并发调用 BatchUpdate 方法
- **WHEN** 多个 goroutine 并发调用 StatsRepository.BatchUpdate
- **THEN** SHALL 使用 upsert 操作保证原子性
- **THEN** SHALL 正确累加所有 delta 值
- **THEN** 不 SHALL 因并发写入而丢失统计
### Requirement: 使用 service 层处理业务逻辑
Handler SHALL 通过 StatsService 处理业务逻辑。
#### Scenario: 调用 service 方法
- **WHEN** handler 收到统计查询请求
- **THEN** SHALL 调用对应的 StatsService 方法Get、Aggregate
- **THEN** SHALL 使用 domain.UsageStats 类型
#### Scenario: 异步记录统计
- **WHEN** 请求完成需要记录统计
- **THEN** SHALL 异步调用 StatsService.Record()
- **THEN** SHALL 不阻塞响应返回
### Requirement: 使用 repository 层访问数据
Service SHALL 通过 StatsRepository 访问数据。
#### Scenario: 调用 repository 方法
- **WHEN** service 处理业务逻辑
- **THEN** SHALL 调用对应的 StatsRepository 方法
- **THEN** SHALL 使用 domain.UsageStats 类型
#### Scenario: 批量更新统计
- **WHEN** StatsBuffer 刷新统计
- **THEN** SHALL 调用 StatsRepository.BatchUpdate
- **THEN** SHALL 使用 upsert 操作更新或创建统计记录
- **THEN** SHALL 支持增量更新request_count + delta
#### Scenario: upsert 操作
- **WHEN** 记录统计
- **THEN** SHALL 在 repository 层使用 upsert 操作
- **THEN** SHALL 保证原子性和并发安全
### Requirement: 统计查询优化
统计查询 SHALL 使用索引优化性能。
#### Scenario: 使用索引
- **WHEN** 查询统计
- **THEN** SHALL 使用 (provider_id, model_name, date) 复合索引
- **THEN** SHALL 优化查询性能
### Requirement: 统计数据可接受少量丢失
统计记录方式改为内存缓冲,可接受少量丢失。
#### Scenario: 进程崩溃丢失统计
- **WHEN** 进程崩溃
- **THEN** MAY 丢失最近 flushInterval 内的统计
- **THEN** 统计数据用于监控,可接受少量丢失
#### Scenario: 优雅关闭保证不丢失
- **WHEN** 服务优雅关闭
- **THEN** SHALL 调用 StatsBuffer.Stop
- **THEN** SHALL 等待最后一次刷新完成
- **THEN** SHALL 保证统计数据不丢失
### Requirement: StatsRepository 支持批量更新
StatsRepository SHALL 新增 BatchUpdate 方法支持批量增量更新。
#### Scenario: BatchUpdate 更新现有记录
- **WHEN** 调用 BatchUpdate 且当日记录存在
- **THEN** SHALL 使用 upsert 操作更新 request_count = request_count + delta
- **THEN** SHALL 保证原子性,无竞态条件
- **THEN** SHALL 不创建新记录
#### Scenario: BatchUpdate 创建新记录
- **WHEN** 调用 BatchUpdate 且当日记录不存在
- **THEN** SHALL 创建新记录request_count = delta
- **THEN** SHALL 使用 upsert 操作保证原子性
#### Scenario: BatchUpdate 并发安全
- **WHEN** 多个 BatchUpdate 调用同时执行
- **THEN** SHALL 保证所有 delta 都被正确累加
- **THEN** SHALL 不因并发冲突而丢失数据