| package sql |
|
|
| import ( |
| "database/sql" |
| "encoding/json" |
| "fmt" |
| "log/slog" |
| "strconv" |
| "strings" |
| "time" |
|
|
| "ccLoad/internal/model" |
| ) |
|
|
| |
| type WhereBuilder struct { |
| conditions []string |
| args []any |
| } |
|
|
| |
| func NewWhereBuilder() *WhereBuilder { |
| return &WhereBuilder{ |
| conditions: make([]string, 0), |
| args: make([]any, 0), |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func (wb *WhereBuilder) AddCondition(condition string, args ...any) *WhereBuilder { |
| if condition == "" { |
| return wb |
| } |
|
|
| wb.conditions = append(wb.conditions, condition) |
| wb.args = append(wb.args, args...) |
| return wb |
| } |
|
|
| |
| func (wb *WhereBuilder) ApplyLogFilter(filter *model.LogFilter) *WhereBuilder { |
| if filter == nil { |
| wb.AddCondition("log_source = ?", model.LogSourceProxy) |
| return wb |
| } |
|
|
| if filter.ChannelID != nil { |
| wb.AddCondition("channel_id = ?", *filter.ChannelID) |
| } |
| |
| |
| if filter.Model != "" { |
| wb.AddCondition("model = ?", filter.Model) |
| } |
| if filter.ModelLike != "" { |
| wb.AddCondition("model LIKE ?", "%"+filter.ModelLike+"%") |
| } |
| if filter.StatusCode != nil { |
| wb.AddCondition("status_code = ?", *filter.StatusCode) |
| } |
| if filter.AuthTokenID != nil { |
| wb.AddCondition("auth_token_id = ?", *filter.AuthTokenID) |
| } |
| switch filter.LogSource { |
| case model.LogSourceAll: |
| case model.LogSourceDetection: |
| wb.AddCondition("log_source IN (?, ?)", model.LogSourceScheduledCheck, model.LogSourceManualTest) |
| case "": |
| wb.AddCondition("log_source = ?", model.LogSourceProxy) |
| default: |
| wb.AddCondition("log_source = ?", filter.LogSource) |
| } |
| return wb |
| } |
|
|
| |
| func (wb *WhereBuilder) Build() (string, []any) { |
| if len(wb.conditions) == 0 { |
| return "", wb.args |
| } |
| return strings.Join(wb.conditions, " AND "), wb.args |
| } |
|
|
| |
| func (wb *WhereBuilder) BuildWithPrefix(prefix string) (string, []any) { |
| whereClause, args := wb.Build() |
| if whereClause == "" { |
| return "", args |
| } |
| return prefix + " " + whereClause, args |
| } |
|
|
| |
| type ConfigScanner struct{} |
|
|
| |
| func NewConfigScanner() *ConfigScanner { |
| return &ConfigScanner{} |
| } |
|
|
| |
| func (cs *ConfigScanner) ScanConfig(scanner interface { |
| Scan(...any) error |
| }) (*model.Config, error) { |
| var c model.Config |
| var enabledInt int |
| var scheduledCheckEnabledInt int |
| var scheduledCheckModel string |
| var customRequestRules sql.NullString |
| var createdAtRaw, updatedAtRaw any |
|
|
| |
| |
| if err := scanner.Scan(&c.ID, &c.Name, &c.URL, &c.Priority, |
| &c.RPMLimit, &c.ChannelType, &c.ProtocolTransformMode, &enabledInt, &scheduledCheckEnabledInt, &scheduledCheckModel, |
| &c.CooldownUntil, &c.CooldownDurationMs, &c.DailyCostLimit, &c.CostMultiplier, &customRequestRules, &c.KeyCount, |
| &createdAtRaw, &updatedAtRaw); err != nil { |
| return nil, err |
| } |
|
|
| c.Enabled = enabledInt != 0 |
| c.ScheduledCheckEnabled = scheduledCheckEnabledInt != 0 |
| c.ScheduledCheckModel = scheduledCheckModel |
| c.CustomRequestRules = parseCustomRequestRules(c.ID, customRequestRules) |
| if c.CostMultiplier < 0 { |
| c.CostMultiplier = 1 |
| } |
|
|
| |
| now := time.Now() |
| c.CreatedAt = model.JSONTime{Time: cs.parseTimestampOrNow(createdAtRaw, now)} |
| c.UpdatedAt = model.JSONTime{Time: cs.parseTimestampOrNow(updatedAtRaw, now)} |
|
|
| |
| c.ModelEntries = nil |
|
|
| return &c, nil |
| } |
|
|
| |
| func (cs *ConfigScanner) ScanConfigs(rows interface { |
| Next() bool |
| Scan(...any) error |
| Err() error |
| }) ([]*model.Config, error) { |
| var configs []*model.Config |
|
|
| for rows.Next() { |
| config, err := cs.ScanConfig(rows) |
| if err != nil { |
| return nil, err |
| } |
| configs = append(configs, config) |
| } |
|
|
| if err := rows.Err(); err != nil { |
| return nil, err |
| } |
|
|
| return configs, nil |
| } |
|
|
| |
| |
| func (cs *ConfigScanner) parseTimestampOrNow(val any, fallback time.Time) time.Time { |
| switch v := val.(type) { |
| case int64: |
| if v > 0 { |
| return unixToTime(v) |
| } |
| case int: |
| if v > 0 { |
| return unixToTime(int64(v)) |
| } |
| case string: |
| |
| if ts, err := strconv.ParseInt(v, 10, 64); err == nil && ts > 0 { |
| return unixToTime(ts) |
| } |
| |
| if t, err := time.Parse(time.RFC3339, v); err == nil { |
| return t |
| } |
| |
| for _, layout := range []string{ |
| time.RFC3339Nano, |
| "2006-01-02T15:04:05.999999999Z07:00", |
| "2006-01-02 15:04:05.999999999 -07:00 MST", |
| } { |
| if t, err := time.Parse(layout, v); err == nil { |
| return t |
| } |
| } |
| } |
| |
| return fallback |
| } |
|
|
| |
| type QueryBuilder struct { |
| baseQuery string |
| wb *WhereBuilder |
| } |
|
|
| |
| func NewQueryBuilder(baseQuery string) *QueryBuilder { |
| return &QueryBuilder{ |
| baseQuery: baseQuery, |
| wb: NewWhereBuilder(), |
| } |
| } |
|
|
| |
| func (qb *QueryBuilder) Where(condition string, args ...any) *QueryBuilder { |
| qb.wb.AddCondition(condition, args...) |
| return qb |
| } |
|
|
| |
| func (qb *QueryBuilder) ApplyFilter(filter *model.LogFilter) *QueryBuilder { |
| qb.wb.ApplyLogFilter(filter) |
| return qb |
| } |
|
|
| |
| func (qb *QueryBuilder) WhereIn(column string, values []any) *QueryBuilder { |
| if len(values) == 0 { |
| |
| qb.wb.AddCondition("1=0") |
| return qb |
| } |
| |
| placeholders := make([]string, len(values)) |
| for i := range values { |
| placeholders[i] = "?" |
| } |
| cond := fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ",")) |
| qb.wb.AddCondition(cond, values...) |
| return qb |
| } |
|
|
| |
| func (qb *QueryBuilder) Build() (string, []any) { |
| whereClause, args := qb.wb.BuildWithPrefix("WHERE") |
|
|
| query := qb.baseQuery |
| if whereClause != "" { |
| query += " " + whereClause |
| } |
|
|
| return query, args |
| } |
|
|
| |
| func (qb *QueryBuilder) BuildWithSuffix(suffix string) (string, []any) { |
| query, args := qb.Build() |
| if suffix != "" { |
| query += " " + suffix |
| } |
| return query, args |
| } |
|
|
| |
| func parseCustomRequestRules(channelID int64, raw sql.NullString) *model.CustomRequestRules { |
| if !raw.Valid { |
| return nil |
| } |
| trimmed := strings.TrimSpace(raw.String) |
| if trimmed == "" || trimmed == "null" { |
| return nil |
| } |
| var rules model.CustomRequestRules |
| if err := json.Unmarshal([]byte(trimmed), &rules); err != nil { |
| slog.Warn("custom_request_rules: unmarshal failed, treated as empty", |
| "channel_id", channelID, "error", err.Error()) |
| return nil |
| } |
| if rules.IsEmpty() { |
| return nil |
| } |
| return &rules |
| } |
|
|
| |
| func marshalCustomRequestRules(rules *model.CustomRequestRules) (sql.NullString, error) { |
| if rules == nil || rules.IsEmpty() { |
| return sql.NullString{}, nil |
| } |
| data, err := json.Marshal(rules) |
| if err != nil { |
| return sql.NullString{}, fmt.Errorf("marshal custom_request_rules: %w", err) |
| } |
| return sql.NullString{String: string(data), Valid: true}, nil |
| } |
|
|