ccpoad / internal /storage /sql /helpers.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
7.06 kB
package sql
import (
"context"
"fmt"
"log"
"strings"
"time"
"ccLoad/internal/model"
)
// ChannelInfo 渠道基本信息(用于批量查询)
type ChannelInfo struct {
Name string
Priority int
Type string
CostMultiplier float64
}
// fetchChannelInfoBatch 批量查询渠道信息(名称+优先级+类型)
// 消除 N+1:一次全表查询 + 内存过滤
// 设计原则(KISS):渠道总数<1000时,全表扫描比动态 IN 子查询更简单
// 输入:渠道ID集合 map[int64]bool
// 输出:ID→渠道信息映射 map[int64]ChannelInfo
func (s *SQLStore) fetchChannelInfoBatch(ctx context.Context, channelIDs map[int64]bool) (map[int64]ChannelInfo, error) {
if len(channelIDs) == 0 {
return make(map[int64]ChannelInfo), nil
}
// 查询所有渠道(全表扫描,渠道数<1000时比IN子查询更快)
// 优势:固定SQL(查询计划缓存)、无动态参数绑定、代码简单
rows, err := s.db.QueryContext(ctx, `
SELECT
id,
name,
priority,
LOWER(COALESCE(NULLIF(TRIM(channel_type), ''), 'anthropic')),
COALESCE(cost_multiplier, 1)
FROM channels
`)
if err != nil {
return nil, fmt.Errorf("query all channel info: %w", err)
}
defer func() { _ = rows.Close() }()
// 解析并过滤需要的渠道(内存过滤,O(N)但N<1000)
channelInfos := make(map[int64]ChannelInfo, len(channelIDs))
for rows.Next() {
var id int64
var name string
var priority int
var channelType string
var costMultiplier float64
if err := rows.Scan(&id, &name, &priority, &channelType, &costMultiplier); err != nil {
log.Printf("[WARN] 扫描渠道信息失败: %v", err)
continue // 跳过扫描错误的行
}
// 只保留需要的渠道
if channelIDs[id] {
channelInfos[id] = ChannelInfo{
Name: name,
Priority: priority,
Type: channelType,
CostMultiplier: normalizeCostMultiplier(costMultiplier),
}
}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate channel info rows: %w", err)
}
return channelInfos, nil
}
// fetchChannelNamesBatch 批量查询渠道名称(兼容旧接口)
// 输入:渠道ID集合 map[int64]bool
// 输出:ID→名称映射 map[int64]string
func (s *SQLStore) fetchChannelNamesBatch(ctx context.Context, channelIDs map[int64]bool) (map[int64]string, error) {
infos, err := s.fetchChannelInfoBatch(ctx, channelIDs)
if err != nil {
return nil, err
}
names := make(map[int64]string, len(infos))
for id, info := range infos {
names[id] = info.Name
}
return names, nil
}
// fetchAuthTokenDescriptionsBatch 批量查询API令牌描述
func (s *SQLStore) fetchAuthTokenDescriptionsBatch(ctx context.Context, tokenIDs map[int64]bool) (map[int64]string, error) {
if len(tokenIDs) == 0 {
return make(map[int64]string), nil
}
ids := make([]any, 0, len(tokenIDs))
placeholders := make([]string, 0, len(tokenIDs))
for id := range tokenIDs {
ids = append(ids, id)
placeholders = append(placeholders, "?")
}
query := "SELECT id, description FROM auth_tokens WHERE id IN (" +
strings.Join(placeholders, ",") + ")"
rows, err := s.db.QueryContext(ctx, query, ids...)
if err != nil {
return nil, fmt.Errorf("query auth token descriptions: %w", err)
}
defer func() { _ = rows.Close() }()
descriptions := make(map[int64]string, len(tokenIDs))
for rows.Next() {
var id int64
var desc string
if err := rows.Scan(&id, &desc); err != nil {
return nil, fmt.Errorf("scan auth token description: %w", err)
}
descriptions[id] = desc
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate auth token descriptions: %w", err)
}
return descriptions, nil
}
// fetchChannelIDsByNameFilter 根据精确/模糊名称获取渠道ID集合
func (s *SQLStore) fetchChannelIDsByNameFilter(ctx context.Context, exact string, like string) ([]int64, error) {
// 构建查询
var (
query string
args []any
)
if exact != "" {
query = "SELECT id FROM channels WHERE name = ?"
args = []any{exact}
} else if like != "" {
query = "SELECT id FROM channels WHERE name LIKE ?"
args = []any{"%" + like + "%"}
} else {
return nil, nil
}
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("query channel ids by name: %w", err)
}
defer func() { _ = rows.Close() }()
var ids []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("scan channel id: %w", err)
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ids, nil
}
// fetchChannelIDsByType 根据渠道类型获取渠道ID集合
// 目的:避免跨库JOIN,先解析为ID再过滤logs
func (s *SQLStore) fetchChannelIDsByType(ctx context.Context, channelType string) ([]int64, error) {
if channelType == "" {
return nil, nil
}
query := "SELECT id FROM channels WHERE channel_type = ?"
rows, err := s.db.QueryContext(ctx, query, channelType)
if err != nil {
return nil, fmt.Errorf("query channel ids by type: %w", err)
}
defer func() { _ = rows.Close() }()
var ids []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("scan channel id: %w", err)
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ids, nil
}
// applyChannelFilter 应用渠道类型或名称过滤(优先级:ChannelType > ChannelName/Like)
// 返回值:是否应用了过滤、是否为空结果、错误
// 注意:ChannelID 精确匹配不在此处处理,由 QueryBuilder.ApplyFilter 负责
func (s *SQLStore) applyChannelFilter(ctx context.Context, qb *QueryBuilder, filter *model.LogFilter) (bool, bool, error) {
channelIDs, isEmpty, err := s.resolveChannelFilter(ctx, filter)
if err != nil {
return false, false, err
}
if isEmpty {
return true, true, nil
}
if len(channelIDs) > 0 {
vals := make([]any, 0, len(channelIDs))
for _, id := range channelIDs {
vals = append(vals, id)
}
qb.WhereIn("channel_id", vals)
return true, false, nil
}
return false, false, nil
}
// intersectIDs 计算两个ID切片的交集
func intersectIDs(a, b []int64) []int64 {
set := make(map[int64]bool, len(a))
for _, id := range a {
set[id] = true
}
var result []int64
for _, id := range b {
if set[id] {
result = append(result, id)
}
}
return result
}
// timeToUnix 将时间转换为Unix时间戳(秒)
// SQLite和MySQL都存储为BIGINT类型的Unix时间戳
func timeToUnix(t time.Time) int64 {
return t.Unix()
}
// unixToTime 将Unix时间戳转换为时间
func unixToTime(ts int64) time.Time {
return time.Unix(ts, 0)
}
// boolToInt 将布尔值转换为整数
// SQLite和MySQL都使用 1=true, 0=false
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// normalizeCostMultiplier 规范化成本倍率:负数退化为 1;0 表示免费渠道,保持不变
func normalizeCostMultiplier(m float64) float64 {
if m < 0 {
return 1
}
return m
}