feat(usage): 使用记录增加计费模式字段 — 记录/展示/筛选 token/按次/图片

- DB: usage_logs 表新增 billing_mode VARCHAR(20) 列
- 后端: RecordUsage 写入时根据 image_count 判定计费模式
- 前端: 使用记录表格新增计费模式 badge 列 + 筛选下拉
This commit is contained in:
erio
2026-03-30 22:13:16 +08:00
parent 726730bb0e
commit a51e0047b7
25 changed files with 1495 additions and 47 deletions

View File

@@ -28,7 +28,7 @@ import (
gocache "github.com/patrickmn/go-cache"
)
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, requested_model, upstream_model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, request_type, stream, openai_ws_mode, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, media_type, service_tier, reasoning_effort, inbound_endpoint, upstream_endpoint, cache_ttl_overridden, channel_id, model_mapping_chain, billing_tier, created_at"
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, requested_model, upstream_model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, request_type, stream, openai_ws_mode, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, media_type, service_tier, reasoning_effort, inbound_endpoint, upstream_endpoint, cache_ttl_overridden, channel_id, model_mapping_chain, billing_tier, billing_mode, created_at"
// usageLogInsertArgTypes must stay in the same order as:
// 1. prepareUsageLogInsert().args
@@ -80,6 +80,7 @@ var usageLogInsertArgTypes = [...]string{
"bigint", // channel_id
"text", // model_mapping_chain
"text", // billing_tier
"text", // billing_mode
"timestamptz", // created_at
}
@@ -356,6 +357,7 @@ func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor,
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7,
@@ -363,7 +365,7 @@ func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor,
$10, $11, $12, $13,
$14, $15,
$16, $17, $18, $19, $20, $21,
$22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43
$22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44
)
ON CONFLICT (request_id, api_key_id) DO NOTHING
RETURNING id, created_at
@@ -791,10 +793,11 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
) AS (VALUES `)
args := make([]any, 0, len(keys)*44)
args := make([]any, 0, len(keys)*45)
argPos := 1
for idx, key := range keys {
if idx > 0 {
@@ -865,6 +868,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
)
SELECT
@@ -910,6 +914,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
FROM input
ON CONFLICT (request_id, api_key_id) DO NOTHING
@@ -995,10 +1000,11 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
) AS (VALUES `)
args := make([]any, 0, len(preparedList)*43)
args := make([]any, 0, len(preparedList)*44)
argPos := 1
for idx, prepared := range preparedList {
if idx > 0 {
@@ -1066,6 +1072,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
)
SELECT
@@ -1111,6 +1118,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
FROM input
ON CONFLICT (request_id, api_key_id) DO NOTHING
@@ -1164,6 +1172,7 @@ func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared
channel_id,
model_mapping_chain,
billing_tier,
billing_mode,
created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7,
@@ -1171,7 +1180,7 @@ func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared
$10, $11, $12, $13,
$14, $15,
$16, $17, $18, $19, $20, $21,
$22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43
$22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44
)
ON CONFLICT (request_id, api_key_id) DO NOTHING
`, prepared.args...)
@@ -1206,6 +1215,7 @@ func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
channelID := nullInt64(log.ChannelID)
modelMappingChain := nullString(log.ModelMappingChain)
billingTier := nullString(log.BillingTier)
billingMode := nullString(log.BillingMode)
requestedModel := strings.TrimSpace(log.RequestedModel)
if requestedModel == "" {
requestedModel = strings.TrimSpace(log.Model)
@@ -1265,6 +1275,7 @@ func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
channelID,
modelMappingChain,
billingTier,
billingMode,
createdAt,
},
}
@@ -2597,8 +2608,8 @@ type UsageLogFilters = usagestats.UsageLogFilters
// ListWithFilters lists usage logs with optional filters (for admin)
func (r *usageLogRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
conditions := make([]string, 0, 8)
args := make([]any, 0, 8)
conditions := make([]string, 0, 9)
args := make([]any, 0, 9)
if filters.UserID > 0 {
conditions = append(conditions, fmt.Sprintf("user_id = $%d", len(args)+1))
@@ -2622,6 +2633,10 @@ func (r *usageLogRepository) ListWithFilters(ctx context.Context, params paginat
conditions = append(conditions, fmt.Sprintf("billing_type = $%d", len(args)+1))
args = append(args, int16(*filters.BillingType))
}
if filters.BillingMode != "" {
conditions = append(conditions, fmt.Sprintf("billing_mode = $%d", len(args)+1))
args = append(args, filters.BillingMode)
}
if filters.StartTime != nil {
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)+1))
args = append(args, *filters.StartTime)
@@ -3289,6 +3304,10 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us
conditions = append(conditions, fmt.Sprintf("billing_type = $%d", len(args)+1))
args = append(args, int16(*filters.BillingType))
}
if filters.BillingMode != "" {
conditions = append(conditions, fmt.Sprintf("billing_mode = $%d", len(args)+1))
args = append(args, filters.BillingMode)
}
if filters.StartTime != nil {
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)+1))
args = append(args, *filters.StartTime)
@@ -3995,6 +4014,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
channelID sql.NullInt64
modelMappingChain sql.NullString
billingTier sql.NullString
billingMode sql.NullString
createdAt time.Time
)
@@ -4042,6 +4062,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
&channelID,
&modelMappingChain,
&billingTier,
&billingMode,
&createdAt,
); err != nil {
return nil, err
@@ -4136,6 +4157,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
if billingTier.Valid {
log.BillingTier = &billingTier.String
}
if billingMode.Valid {
log.BillingMode = &billingMode.String
}
return log, nil
}