2026-01-09 20:52:57 +08:00
package repository
import (
"context"
"database/sql"
2026-02-12 16:27:29 +08:00
"encoding/json"
2026-01-09 20:52:57 +08:00
"fmt"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/lib/pq"
)
type opsRepository struct {
db * sql . DB
}
2026-03-11 17:32:00 +08:00
const insertOpsErrorLogSQL = `
2026-01-09 20:52:57 +08:00
INSERT INTO ops_error_logs (
request_id ,
client_request_id ,
user_id ,
api_key_id ,
account_id ,
group_id ,
client_ip ,
platform ,
model ,
request_path ,
stream ,
user_agent ,
error_phase ,
error_type ,
severity ,
status_code ,
is_business_limited ,
2026-01-12 16:50:41 +08:00
is_count_tokens ,
2026-01-09 20:52:57 +08:00
error_message ,
error_body ,
error_source ,
error_owner ,
upstream_status_code ,
upstream_error_message ,
upstream_error_detail ,
2026-01-11 15:30:27 +08:00
upstream_errors ,
2026-02-12 09:41:37 +08:00
auth_latency_ms ,
routing_latency_ms ,
upstream_latency_ms ,
response_latency_ms ,
2026-01-09 20:52:57 +08:00
time_to_first_token_ms ,
request_body ,
request_body_truncated ,
request_body_bytes ,
request_headers ,
is_retryable ,
retry_count ,
created_at
) VALUES (
2026-02-12 09:41:37 +08:00
$ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 , $ 8 , $ 9 , $ 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
2026-03-11 17:32:00 +08:00
) `
func NewOpsRepository ( db * sql . DB ) service . OpsRepository {
return & opsRepository { db : db }
}
func ( r * opsRepository ) InsertErrorLog ( ctx context . Context , input * service . OpsInsertErrorLogInput ) ( int64 , error ) {
if r == nil || r . db == nil {
return 0 , fmt . Errorf ( "nil ops repository" )
}
if input == nil {
return 0 , fmt . Errorf ( "nil input" )
}
2026-01-09 20:52:57 +08:00
var id int64
err := r . db . QueryRowContext (
ctx ,
2026-03-11 17:32:00 +08:00
insertOpsErrorLogSQL + " RETURNING id" ,
opsInsertErrorLogArgs ( input ) ... ,
) . Scan ( & id )
if err != nil {
return 0 , err
}
return id , nil
}
func ( r * opsRepository ) BatchInsertErrorLogs ( ctx context . Context , inputs [ ] * service . OpsInsertErrorLogInput ) ( int64 , error ) {
if r == nil || r . db == nil {
return 0 , fmt . Errorf ( "nil ops repository" )
}
if len ( inputs ) == 0 {
return 0 , nil
}
tx , err := r . db . BeginTx ( ctx , nil )
if err != nil {
return 0 , err
}
defer func ( ) {
if err != nil {
_ = tx . Rollback ( )
}
} ( )
stmt , err := tx . PrepareContext ( ctx , insertOpsErrorLogSQL )
if err != nil {
return 0 , err
}
defer func ( ) {
_ = stmt . Close ( )
} ( )
var inserted int64
for _ , input := range inputs {
if input == nil {
continue
}
if _ , err = stmt . ExecContext ( ctx , opsInsertErrorLogArgs ( input ) ... ) ; err != nil {
return inserted , err
}
inserted ++
}
if err = tx . Commit ( ) ; err != nil {
return inserted , err
}
return inserted , nil
}
func opsInsertErrorLogArgs ( input * service . OpsInsertErrorLogInput ) [ ] any {
return [ ] any {
2026-01-09 20:52:57 +08:00
opsNullString ( input . RequestID ) ,
opsNullString ( input . ClientRequestID ) ,
opsNullInt64 ( input . UserID ) ,
opsNullInt64 ( input . APIKeyID ) ,
opsNullInt64 ( input . AccountID ) ,
opsNullInt64 ( input . GroupID ) ,
opsNullString ( input . ClientIP ) ,
opsNullString ( input . Platform ) ,
opsNullString ( input . Model ) ,
opsNullString ( input . RequestPath ) ,
input . Stream ,
opsNullString ( input . UserAgent ) ,
input . ErrorPhase ,
input . ErrorType ,
opsNullString ( input . Severity ) ,
opsNullInt ( input . StatusCode ) ,
input . IsBusinessLimited ,
2026-01-12 16:50:41 +08:00
input . IsCountTokens ,
2026-01-09 20:52:57 +08:00
opsNullString ( input . ErrorMessage ) ,
opsNullString ( input . ErrorBody ) ,
opsNullString ( input . ErrorSource ) ,
opsNullString ( input . ErrorOwner ) ,
opsNullInt ( input . UpstreamStatusCode ) ,
opsNullString ( input . UpstreamErrorMessage ) ,
opsNullString ( input . UpstreamErrorDetail ) ,
2026-01-11 15:30:27 +08:00
opsNullString ( input . UpstreamErrorsJSON ) ,
2026-02-12 09:41:37 +08:00
opsNullInt64 ( input . AuthLatencyMs ) ,
opsNullInt64 ( input . RoutingLatencyMs ) ,
opsNullInt64 ( input . UpstreamLatencyMs ) ,
opsNullInt64 ( input . ResponseLatencyMs ) ,
2026-01-09 20:52:57 +08:00
opsNullInt64 ( input . TimeToFirstTokenMs ) ,
opsNullString ( input . RequestBodyJSON ) ,
input . RequestBodyTruncated ,
opsNullInt ( input . RequestBodyBytes ) ,
opsNullString ( input . RequestHeadersJSON ) ,
input . IsRetryable ,
input . RetryCount ,
input . CreatedAt ,
}
}
func ( r * opsRepository ) ListErrorLogs ( ctx context . Context , filter * service . OpsErrorLogFilter ) ( * service . OpsErrorLogList , error ) {
if r == nil || r . db == nil {
return nil , fmt . Errorf ( "nil ops repository" )
}
if filter == nil {
filter = & service . OpsErrorLogFilter { }
}
page := filter . Page
if page <= 0 {
page = 1
}
pageSize := filter . PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 500 {
pageSize = 500
}
where , args := buildOpsErrorLogsWhere ( filter )
2026-01-14 12:40:34 +08:00
countSQL := "SELECT COUNT(*) FROM ops_error_logs e " + where
2026-01-09 20:52:57 +08:00
var total int
if err := r . db . QueryRowContext ( ctx , countSQL , args ... ) . Scan ( & total ) ; err != nil {
return nil , err
}
offset := ( page - 1 ) * pageSize
argsWithLimit := append ( args , pageSize , offset )
selectSQL := `
SELECT
2026-01-14 12:40:34 +08:00
e . id ,
e . created_at ,
e . error_phase ,
e . error_type ,
COALESCE ( e . error_owner , ' ' ) ,
COALESCE ( e . error_source , ' ' ) ,
e . severity ,
COALESCE ( e . upstream_status_code , e . status_code , 0 ) ,
COALESCE ( e . platform , ' ' ) ,
COALESCE ( e . model , ' ' ) ,
COALESCE ( e . is_retryable , false ) ,
COALESCE ( e . retry_count , 0 ) ,
COALESCE ( e . resolved , false ) ,
e . resolved_at ,
e . resolved_by_user_id ,
COALESCE ( u2 . email , ' ' ) ,
e . resolved_retry_id ,
COALESCE ( e . client_request_id , ' ' ) ,
COALESCE ( e . request_id , ' ' ) ,
COALESCE ( e . error_message , ' ' ) ,
e . user_id ,
COALESCE ( u . email , ' ' ) ,
e . api_key_id ,
e . account_id ,
COALESCE ( a . name , ' ' ) ,
e . group_id ,
COALESCE ( g . name , ' ' ) ,
CASE WHEN e . client_ip IS NULL THEN NULL ELSE e . client_ip : : text END ,
COALESCE ( e . request_path , ' ' ) ,
e . stream
FROM ops_error_logs e
LEFT JOIN accounts a ON e . account_id = a . id
LEFT JOIN groups g ON e . group_id = g . id
LEFT JOIN users u ON e . user_id = u . id
LEFT JOIN users u2 ON e . resolved_by_user_id = u2 . id
2026-01-09 20:52:57 +08:00
` + where + `
2026-01-14 12:40:34 +08:00
ORDER BY e . created_at DESC
2026-01-09 20:52:57 +08:00
LIMIT $ ` + itoa(len(args)+1) + ` OFFSET $ ` + itoa ( len ( args ) + 2 )
rows , err := r . db . QueryContext ( ctx , selectSQL , argsWithLimit ... )
if err != nil {
return nil , err
}
2026-01-11 23:26:29 +08:00
defer func ( ) { _ = rows . Close ( ) } ( )
2026-01-09 20:52:57 +08:00
out := make ( [ ] * service . OpsErrorLog , 0 , pageSize )
for rows . Next ( ) {
var item service . OpsErrorLog
var statusCode sql . NullInt64
var clientIP sql . NullString
var userID sql . NullInt64
var apiKeyID sql . NullInt64
var accountID sql . NullInt64
2026-01-14 12:40:34 +08:00
var accountName string
2026-01-09 20:52:57 +08:00
var groupID sql . NullInt64
2026-01-14 12:40:34 +08:00
var groupName string
var userEmail string
2026-01-14 09:03:01 +08:00
var resolvedAt sql . NullTime
var resolvedBy sql . NullInt64
2026-01-14 12:40:34 +08:00
var resolvedByName string
2026-01-14 09:03:01 +08:00
var resolvedRetryID sql . NullInt64
2026-01-09 20:52:57 +08:00
if err := rows . Scan (
& item . ID ,
& item . CreatedAt ,
& item . Phase ,
& item . Type ,
2026-01-14 09:03:01 +08:00
& item . Owner ,
& item . Source ,
2026-01-09 20:52:57 +08:00
& item . Severity ,
& statusCode ,
& item . Platform ,
& item . Model ,
2026-01-14 09:03:01 +08:00
& item . IsRetryable ,
& item . RetryCount ,
& item . Resolved ,
& resolvedAt ,
& resolvedBy ,
2026-01-14 12:40:34 +08:00
& resolvedByName ,
2026-01-14 09:03:01 +08:00
& resolvedRetryID ,
2026-01-09 20:52:57 +08:00
& item . ClientRequestID ,
& item . RequestID ,
& item . Message ,
& userID ,
2026-01-14 12:40:34 +08:00
& userEmail ,
2026-01-09 20:52:57 +08:00
& apiKeyID ,
& accountID ,
2026-01-14 12:40:34 +08:00
& accountName ,
2026-01-09 20:52:57 +08:00
& groupID ,
2026-01-14 12:40:34 +08:00
& groupName ,
2026-01-09 20:52:57 +08:00
& clientIP ,
& item . RequestPath ,
& item . Stream ,
) ; err != nil {
return nil , err
}
2026-01-14 09:03:01 +08:00
if resolvedAt . Valid {
t := resolvedAt . Time
item . ResolvedAt = & t
}
if resolvedBy . Valid {
v := resolvedBy . Int64
item . ResolvedByUserID = & v
}
2026-01-14 12:40:34 +08:00
item . ResolvedByUserName = resolvedByName
2026-01-14 09:03:01 +08:00
if resolvedRetryID . Valid {
v := resolvedRetryID . Int64
item . ResolvedRetryID = & v
}
2026-01-09 20:52:57 +08:00
item . StatusCode = int ( statusCode . Int64 )
if clientIP . Valid {
s := clientIP . String
item . ClientIP = & s
}
if userID . Valid {
v := userID . Int64
item . UserID = & v
}
2026-01-14 12:40:34 +08:00
item . UserEmail = userEmail
2026-01-09 20:52:57 +08:00
if apiKeyID . Valid {
v := apiKeyID . Int64
item . APIKeyID = & v
}
if accountID . Valid {
v := accountID . Int64
item . AccountID = & v
}
2026-01-14 12:40:34 +08:00
item . AccountName = accountName
2026-01-09 20:52:57 +08:00
if groupID . Valid {
v := groupID . Int64
item . GroupID = & v
}
2026-01-14 12:40:34 +08:00
item . GroupName = groupName
2026-01-09 20:52:57 +08:00
out = append ( out , & item )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return & service . OpsErrorLogList {
Errors : out ,
Total : total ,
Page : page ,
PageSize : pageSize ,
} , nil
}
func ( r * opsRepository ) GetErrorLogByID ( ctx context . Context , id int64 ) ( * service . OpsErrorLogDetail , error ) {
if r == nil || r . db == nil {
return nil , fmt . Errorf ( "nil ops repository" )
}
if id <= 0 {
return nil , fmt . Errorf ( "invalid id" )
}
q := `
SELECT
2026-01-14 23:52:00 +08:00
e . id ,
e . created_at ,
e . error_phase ,
e . error_type ,
COALESCE ( e . error_owner , ' ' ) ,
COALESCE ( e . error_source , ' ' ) ,
e . severity ,
COALESCE ( e . upstream_status_code , e . status_code , 0 ) ,
COALESCE ( e . platform , ' ' ) ,
COALESCE ( e . model , ' ' ) ,
COALESCE ( e . is_retryable , false ) ,
COALESCE ( e . retry_count , 0 ) ,
COALESCE ( e . resolved , false ) ,
e . resolved_at ,
e . resolved_by_user_id ,
e . resolved_retry_id ,
COALESCE ( e . client_request_id , ' ' ) ,
COALESCE ( e . request_id , ' ' ) ,
COALESCE ( e . error_message , ' ' ) ,
COALESCE ( e . error_body , ' ' ) ,
e . upstream_status_code ,
COALESCE ( e . upstream_error_message , ' ' ) ,
COALESCE ( e . upstream_error_detail , ' ' ) ,
COALESCE ( e . upstream_errors : : text , ' ' ) ,
e . is_business_limited ,
e . user_id ,
2026-01-14 23:56:45 +08:00
COALESCE ( u . email , ' ' ) ,
2026-01-14 23:52:00 +08:00
e . api_key_id ,
e . account_id ,
COALESCE ( a . name , ' ' ) ,
e . group_id ,
COALESCE ( g . name , ' ' ) ,
CASE WHEN e . client_ip IS NULL THEN NULL ELSE e . client_ip : : text END ,
COALESCE ( e . request_path , ' ' ) ,
e . stream ,
COALESCE ( e . user_agent , ' ' ) ,
e . auth_latency_ms ,
e . routing_latency_ms ,
e . upstream_latency_ms ,
e . response_latency_ms ,
e . time_to_first_token_ms ,
COALESCE ( e . request_body : : text , ' ' ) ,
e . request_body_truncated ,
e . request_body_bytes ,
COALESCE ( e . request_headers : : text , ' ' )
FROM ops_error_logs e
2026-01-14 23:56:45 +08:00
LEFT JOIN users u ON e . user_id = u . id
2026-01-14 23:52:00 +08:00
LEFT JOIN accounts a ON e . account_id = a . id
LEFT JOIN groups g ON e . group_id = g . id
WHERE e . id = $ 1
2026-01-09 20:52:57 +08:00
LIMIT 1 `
var out service . OpsErrorLogDetail
var statusCode sql . NullInt64
2026-01-11 15:30:27 +08:00
var upstreamStatusCode sql . NullInt64
2026-01-14 09:03:01 +08:00
var resolvedAt sql . NullTime
var resolvedBy sql . NullInt64
var resolvedRetryID sql . NullInt64
2026-01-09 20:52:57 +08:00
var clientIP sql . NullString
var userID sql . NullInt64
var apiKeyID sql . NullInt64
var accountID sql . NullInt64
var groupID sql . NullInt64
var authLatency sql . NullInt64
var routingLatency sql . NullInt64
var upstreamLatency sql . NullInt64
var responseLatency sql . NullInt64
var ttft sql . NullInt64
var requestBodyBytes sql . NullInt64
err := r . db . QueryRowContext ( ctx , q , id ) . Scan (
& out . ID ,
& out . CreatedAt ,
& out . Phase ,
& out . Type ,
2026-01-14 09:03:01 +08:00
& out . Owner ,
& out . Source ,
2026-01-09 20:52:57 +08:00
& out . Severity ,
& statusCode ,
& out . Platform ,
& out . Model ,
2026-01-14 09:03:01 +08:00
& out . IsRetryable ,
& out . RetryCount ,
& out . Resolved ,
& resolvedAt ,
& resolvedBy ,
& resolvedRetryID ,
2026-01-09 20:52:57 +08:00
& out . ClientRequestID ,
& out . RequestID ,
& out . Message ,
& out . ErrorBody ,
2026-01-11 15:30:27 +08:00
& upstreamStatusCode ,
& out . UpstreamErrorMessage ,
& out . UpstreamErrorDetail ,
& out . UpstreamErrors ,
2026-01-09 20:52:57 +08:00
& out . IsBusinessLimited ,
& userID ,
2026-01-14 23:56:45 +08:00
& out . UserEmail ,
2026-01-09 20:52:57 +08:00
& apiKeyID ,
& accountID ,
2026-01-14 23:52:00 +08:00
& out . AccountName ,
2026-01-09 20:52:57 +08:00
& groupID ,
2026-01-14 23:52:00 +08:00
& out . GroupName ,
2026-01-09 20:52:57 +08:00
& clientIP ,
& out . RequestPath ,
& out . Stream ,
& out . UserAgent ,
& authLatency ,
& routingLatency ,
& upstreamLatency ,
& responseLatency ,
& ttft ,
& out . RequestBody ,
& out . RequestBodyTruncated ,
& requestBodyBytes ,
& out . RequestHeaders ,
)
if err != nil {
return nil , err
}
out . StatusCode = int ( statusCode . Int64 )
2026-01-14 09:03:01 +08:00
if resolvedAt . Valid {
t := resolvedAt . Time
out . ResolvedAt = & t
}
if resolvedBy . Valid {
v := resolvedBy . Int64
out . ResolvedByUserID = & v
}
if resolvedRetryID . Valid {
v := resolvedRetryID . Int64
out . ResolvedRetryID = & v
}
2026-01-09 20:52:57 +08:00
if clientIP . Valid {
s := clientIP . String
out . ClientIP = & s
}
2026-01-11 15:30:27 +08:00
if upstreamStatusCode . Valid && upstreamStatusCode . Int64 > 0 {
v := int ( upstreamStatusCode . Int64 )
out . UpstreamStatusCode = & v
}
2026-01-09 20:52:57 +08:00
if userID . Valid {
v := userID . Int64
out . UserID = & v
}
if apiKeyID . Valid {
v := apiKeyID . Int64
out . APIKeyID = & v
}
if accountID . Valid {
v := accountID . Int64
out . AccountID = & v
}
if groupID . Valid {
v := groupID . Int64
out . GroupID = & v
}
if authLatency . Valid {
v := authLatency . Int64
out . AuthLatencyMs = & v
}
if routingLatency . Valid {
v := routingLatency . Int64
out . RoutingLatencyMs = & v
}
if upstreamLatency . Valid {
v := upstreamLatency . Int64
out . UpstreamLatencyMs = & v
}
if responseLatency . Valid {
v := responseLatency . Int64
out . ResponseLatencyMs = & v
}
if ttft . Valid {
v := ttft . Int64
out . TimeToFirstTokenMs = & v
}
if requestBodyBytes . Valid {
v := int ( requestBodyBytes . Int64 )
out . RequestBodyBytes = & v
}
// Normalize request_body to empty string when stored as JSON null.
out . RequestBody = strings . TrimSpace ( out . RequestBody )
if out . RequestBody == "null" {
out . RequestBody = ""
}
// Normalize request_headers to empty string when stored as JSON null.
out . RequestHeaders = strings . TrimSpace ( out . RequestHeaders )
if out . RequestHeaders == "null" {
out . RequestHeaders = ""
}
2026-01-11 15:30:27 +08:00
// Normalize upstream_errors to empty string when stored as JSON null.
out . UpstreamErrors = strings . TrimSpace ( out . UpstreamErrors )
if out . UpstreamErrors == "null" {
out . UpstreamErrors = ""
}
2026-01-09 20:52:57 +08:00
return & out , nil
}
func ( r * opsRepository ) InsertRetryAttempt ( ctx context . Context , input * service . OpsInsertRetryAttemptInput ) ( int64 , error ) {
if r == nil || r . db == nil {
return 0 , fmt . Errorf ( "nil ops repository" )
}
if input == nil {
return 0 , fmt . Errorf ( "nil input" )
}
if input . SourceErrorID <= 0 {
return 0 , fmt . Errorf ( "invalid source_error_id" )
}
if strings . TrimSpace ( input . Mode ) == "" {
return 0 , fmt . Errorf ( "invalid mode" )
}
q := `
INSERT INTO ops_retry_attempts (
requested_by_user_id ,
source_error_id ,
mode ,
pinned_account_id ,
status ,
started_at
) VALUES (
$ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6
) RETURNING id `
var id int64
err := r . db . QueryRowContext (
ctx ,
q ,
opsNullInt64 ( & input . RequestedByUserID ) ,
input . SourceErrorID ,
strings . TrimSpace ( input . Mode ) ,
opsNullInt64 ( input . PinnedAccountID ) ,
strings . TrimSpace ( input . Status ) ,
input . StartedAt ,
) . Scan ( & id )
if err != nil {
return 0 , err
}
return id , nil
}
func ( r * opsRepository ) UpdateRetryAttempt ( ctx context . Context , input * service . OpsUpdateRetryAttemptInput ) error {
if r == nil || r . db == nil {
return fmt . Errorf ( "nil ops repository" )
}
if input == nil {
return fmt . Errorf ( "nil input" )
}
if input . ID <= 0 {
return fmt . Errorf ( "invalid id" )
}
q := `
UPDATE ops_retry_attempts
SET
status = $ 2 ,
finished_at = $ 3 ,
duration_ms = $ 4 ,
2026-01-14 09:03:01 +08:00
success = $ 5 ,
http_status_code = $ 6 ,
upstream_request_id = $ 7 ,
used_account_id = $ 8 ,
response_preview = $ 9 ,
response_truncated = $ 10 ,
result_request_id = $ 11 ,
result_error_id = $ 12 ,
error_message = $ 13
2026-01-09 20:52:57 +08:00
WHERE id = $ 1 `
_ , err := r . db . ExecContext (
ctx ,
q ,
input . ID ,
strings . TrimSpace ( input . Status ) ,
nullTime ( input . FinishedAt ) ,
input . DurationMs ,
2026-01-14 09:03:01 +08:00
nullBool ( input . Success ) ,
nullInt ( input . HTTPStatusCode ) ,
opsNullString ( input . UpstreamRequestID ) ,
nullInt64 ( input . UsedAccountID ) ,
opsNullString ( input . ResponsePreview ) ,
nullBool ( input . ResponseTruncated ) ,
2026-01-09 20:52:57 +08:00
opsNullString ( input . ResultRequestID ) ,
2026-01-14 09:03:01 +08:00
nullInt64 ( input . ResultErrorID ) ,
2026-01-09 20:52:57 +08:00
opsNullString ( input . ErrorMessage ) ,
)
return err
}
func ( r * opsRepository ) GetLatestRetryAttemptForError ( ctx context . Context , sourceErrorID int64 ) ( * service . OpsRetryAttempt , error ) {
if r == nil || r . db == nil {
return nil , fmt . Errorf ( "nil ops repository" )
}
if sourceErrorID <= 0 {
return nil , fmt . Errorf ( "invalid source_error_id" )
}
q := `
SELECT
id ,
created_at ,
COALESCE ( requested_by_user_id , 0 ) ,
source_error_id ,
COALESCE ( mode , ' ' ) ,
pinned_account_id ,
COALESCE ( status , ' ' ) ,
started_at ,
finished_at ,
duration_ms ,
2026-01-14 09:03:01 +08:00
success ,
http_status_code ,
upstream_request_id ,
used_account_id ,
response_preview ,
response_truncated ,
2026-01-09 20:52:57 +08:00
result_request_id ,
result_error_id ,
error_message
FROM ops_retry_attempts
WHERE source_error_id = $ 1
ORDER BY created_at DESC
LIMIT 1 `
var out service . OpsRetryAttempt
var pinnedAccountID sql . NullInt64
var requestedBy sql . NullInt64
var startedAt sql . NullTime
var finishedAt sql . NullTime
var durationMs sql . NullInt64
2026-01-14 09:03:01 +08:00
var success sql . NullBool
var httpStatusCode sql . NullInt64
var upstreamRequestID sql . NullString
var usedAccountID sql . NullInt64
var responsePreview sql . NullString
var responseTruncated sql . NullBool
2026-01-09 20:52:57 +08:00
var resultRequestID sql . NullString
var resultErrorID sql . NullInt64
var errorMessage sql . NullString
err := r . db . QueryRowContext ( ctx , q , sourceErrorID ) . Scan (
& out . ID ,
& out . CreatedAt ,
& requestedBy ,
& out . SourceErrorID ,
& out . Mode ,
& pinnedAccountID ,
& out . Status ,
& startedAt ,
& finishedAt ,
& durationMs ,
2026-01-14 09:03:01 +08:00
& success ,
& httpStatusCode ,
& upstreamRequestID ,
& usedAccountID ,
& responsePreview ,
& responseTruncated ,
2026-01-09 20:52:57 +08:00
& resultRequestID ,
& resultErrorID ,
& errorMessage ,
)
if err != nil {
return nil , err
}
out . RequestedByUserID = requestedBy . Int64
if pinnedAccountID . Valid {
v := pinnedAccountID . Int64
out . PinnedAccountID = & v
}
if startedAt . Valid {
t := startedAt . Time
out . StartedAt = & t
}
if finishedAt . Valid {
t := finishedAt . Time
out . FinishedAt = & t
}
if durationMs . Valid {
v := durationMs . Int64
out . DurationMs = & v
}
2026-01-14 09:03:01 +08:00
if success . Valid {
v := success . Bool
out . Success = & v
}
if httpStatusCode . Valid {
v := int ( httpStatusCode . Int64 )
out . HTTPStatusCode = & v
}
if upstreamRequestID . Valid {
s := upstreamRequestID . String
out . UpstreamRequestID = & s
}
if usedAccountID . Valid {
v := usedAccountID . Int64
out . UsedAccountID = & v
}
if responsePreview . Valid {
s := responsePreview . String
out . ResponsePreview = & s
}
if responseTruncated . Valid {
v := responseTruncated . Bool
out . ResponseTruncated = & v
}
2026-01-09 20:52:57 +08:00
if resultRequestID . Valid {
s := resultRequestID . String
out . ResultRequestID = & s
}
if resultErrorID . Valid {
v := resultErrorID . Int64
out . ResultErrorID = & v
}
if errorMessage . Valid {
s := errorMessage . String
out . ErrorMessage = & s
}
return & out , nil
}
func nullTime ( t time . Time ) sql . NullTime {
if t . IsZero ( ) {
return sql . NullTime { }
}
return sql . NullTime { Time : t , Valid : true }
}
2026-01-14 09:03:01 +08:00
func nullBool ( v * bool ) sql . NullBool {
if v == nil {
return sql . NullBool { }
}
return sql . NullBool { Bool : * v , Valid : true }
}
func ( r * opsRepository ) ListRetryAttemptsByErrorID ( ctx context . Context , sourceErrorID int64 , limit int ) ( [ ] * service . OpsRetryAttempt , error ) {
if r == nil || r . db == nil {
return nil , fmt . Errorf ( "nil ops repository" )
}
if sourceErrorID <= 0 {
return nil , fmt . Errorf ( "invalid source_error_id" )
}
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
q := `
SELECT
2026-01-14 12:40:34 +08:00
r . id ,
r . created_at ,
COALESCE ( r . requested_by_user_id , 0 ) ,
r . source_error_id ,
COALESCE ( r . mode , ' ' ) ,
r . pinned_account_id ,
COALESCE ( pa . name , ' ' ) ,
COALESCE ( r . status , ' ' ) ,
r . started_at ,
r . finished_at ,
r . duration_ms ,
r . success ,
r . http_status_code ,
r . upstream_request_id ,
r . used_account_id ,
COALESCE ( ua . name , ' ' ) ,
r . response_preview ,
r . response_truncated ,
r . result_request_id ,
r . result_error_id ,
r . error_message
FROM ops_retry_attempts r
LEFT JOIN accounts pa ON r . pinned_account_id = pa . id
LEFT JOIN accounts ua ON r . used_account_id = ua . id
WHERE r . source_error_id = $ 1
ORDER BY r . created_at DESC
2026-01-14 09:03:01 +08:00
LIMIT $ 2 `
rows , err := r . db . QueryContext ( ctx , q , sourceErrorID , limit )
if err != nil {
return nil , err
}
defer func ( ) { _ = rows . Close ( ) } ( )
out := make ( [ ] * service . OpsRetryAttempt , 0 , 16 )
for rows . Next ( ) {
var item service . OpsRetryAttempt
var pinnedAccountID sql . NullInt64
2026-01-14 12:40:34 +08:00
var pinnedAccountName string
2026-01-14 09:03:01 +08:00
var requestedBy sql . NullInt64
var startedAt sql . NullTime
var finishedAt sql . NullTime
var durationMs sql . NullInt64
var success sql . NullBool
var httpStatusCode sql . NullInt64
var upstreamRequestID sql . NullString
var usedAccountID sql . NullInt64
2026-01-14 12:40:34 +08:00
var usedAccountName string
2026-01-14 09:03:01 +08:00
var responsePreview sql . NullString
var responseTruncated sql . NullBool
var resultRequestID sql . NullString
var resultErrorID sql . NullInt64
var errorMessage sql . NullString
if err := rows . Scan (
& item . ID ,
& item . CreatedAt ,
& requestedBy ,
& item . SourceErrorID ,
& item . Mode ,
& pinnedAccountID ,
2026-01-14 12:40:34 +08:00
& pinnedAccountName ,
2026-01-14 09:03:01 +08:00
& item . Status ,
& startedAt ,
& finishedAt ,
& durationMs ,
& success ,
& httpStatusCode ,
& upstreamRequestID ,
& usedAccountID ,
2026-01-14 12:40:34 +08:00
& usedAccountName ,
2026-01-14 09:03:01 +08:00
& responsePreview ,
& responseTruncated ,
& resultRequestID ,
& resultErrorID ,
& errorMessage ,
) ; err != nil {
return nil , err
}
item . RequestedByUserID = requestedBy . Int64
if pinnedAccountID . Valid {
v := pinnedAccountID . Int64
item . PinnedAccountID = & v
}
2026-01-14 12:40:34 +08:00
item . PinnedAccountName = pinnedAccountName
2026-01-14 09:03:01 +08:00
if startedAt . Valid {
t := startedAt . Time
item . StartedAt = & t
}
if finishedAt . Valid {
t := finishedAt . Time
item . FinishedAt = & t
}
if durationMs . Valid {
v := durationMs . Int64
item . DurationMs = & v
}
if success . Valid {
v := success . Bool
item . Success = & v
}
if httpStatusCode . Valid {
v := int ( httpStatusCode . Int64 )
item . HTTPStatusCode = & v
}
if upstreamRequestID . Valid {
2026-01-14 12:40:34 +08:00
item . UpstreamRequestID = & upstreamRequestID . String
2026-01-14 09:03:01 +08:00
}
if usedAccountID . Valid {
v := usedAccountID . Int64
item . UsedAccountID = & v
}
2026-01-14 12:40:34 +08:00
item . UsedAccountName = usedAccountName
2026-01-14 09:03:01 +08:00
if responsePreview . Valid {
2026-01-14 12:40:34 +08:00
item . ResponsePreview = & responsePreview . String
2026-01-14 09:03:01 +08:00
}
if responseTruncated . Valid {
v := responseTruncated . Bool
item . ResponseTruncated = & v
}
if resultRequestID . Valid {
2026-01-14 12:40:34 +08:00
item . ResultRequestID = & resultRequestID . String
2026-01-14 09:03:01 +08:00
}
if resultErrorID . Valid {
v := resultErrorID . Int64
item . ResultErrorID = & v
}
if errorMessage . Valid {
2026-01-14 12:40:34 +08:00
item . ErrorMessage = & errorMessage . String
2026-01-14 09:03:01 +08:00
}
out = append ( out , & item )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return out , nil
}
func ( r * opsRepository ) UpdateErrorResolution ( ctx context . Context , errorID int64 , resolved bool , resolvedByUserID * int64 , resolvedRetryID * int64 , resolvedAt * time . Time ) error {
if r == nil || r . db == nil {
return fmt . Errorf ( "nil ops repository" )
}
if errorID <= 0 {
return fmt . Errorf ( "invalid error id" )
}
q := `
UPDATE ops_error_logs
SET
resolved = $ 2 ,
resolved_at = $ 3 ,
resolved_by_user_id = $ 4 ,
resolved_retry_id = $ 5
WHERE id = $ 1 `
at := sql . NullTime { }
if resolvedAt != nil && ! resolvedAt . IsZero ( ) {
at = sql . NullTime { Time : resolvedAt . UTC ( ) , Valid : true }
} else if resolved {
now := time . Now ( ) . UTC ( )
at = sql . NullTime { Time : now , Valid : true }
}
_ , err := r . db . ExecContext (
ctx ,
q ,
errorID ,
resolved ,
at ,
nullInt64 ( resolvedByUserID ) ,
nullInt64 ( resolvedRetryID ) ,
)
return err
}
2026-02-12 16:27:29 +08:00
func ( r * opsRepository ) BatchInsertSystemLogs ( ctx context . Context , inputs [ ] * service . OpsInsertSystemLogInput ) ( int64 , error ) {
if r == nil || r . db == nil {
return 0 , fmt . Errorf ( "nil ops repository" )
}
if len ( inputs ) == 0 {
return 0 , nil
}
tx , err := r . db . BeginTx ( ctx , nil )
if err != nil {
return 0 , err
}
stmt , err := tx . PrepareContext ( ctx , pq . CopyIn (
"ops_system_logs" ,
"created_at" ,
"level" ,
"component" ,
"message" ,
"request_id" ,
"client_request_id" ,
"user_id" ,
"account_id" ,
"platform" ,
"model" ,
"extra" ,
) )
if err != nil {
_ = tx . Rollback ( )
return 0 , err
}
var inserted int64
for _ , input := range inputs {
if input == nil {
continue
}
createdAt := input . CreatedAt
if createdAt . IsZero ( ) {
createdAt = time . Now ( ) . UTC ( )
}
component := strings . TrimSpace ( input . Component )
level := strings . ToLower ( strings . TrimSpace ( input . Level ) )
message := strings . TrimSpace ( input . Message )
if level == "" || message == "" {
continue
}
if component == "" {
component = "app"
}
extra := strings . TrimSpace ( input . ExtraJSON )
if extra == "" {
extra = "{}"
}
if _ , err := stmt . ExecContext (
ctx ,
createdAt . UTC ( ) ,
level ,
component ,
message ,
opsNullString ( input . RequestID ) ,
opsNullString ( input . ClientRequestID ) ,
opsNullInt64 ( input . UserID ) ,
opsNullInt64 ( input . AccountID ) ,
opsNullString ( input . Platform ) ,
opsNullString ( input . Model ) ,
extra ,
) ; err != nil {
_ = stmt . Close ( )
_ = tx . Rollback ( )
return inserted , err
}
inserted ++
}
if _ , err := stmt . ExecContext ( ctx ) ; err != nil {
_ = stmt . Close ( )
_ = tx . Rollback ( )
return inserted , err
}
if err := stmt . Close ( ) ; err != nil {
_ = tx . Rollback ( )
return inserted , err
}
if err := tx . Commit ( ) ; err != nil {
return inserted , err
}
return inserted , nil
}
func ( r * opsRepository ) ListSystemLogs ( ctx context . Context , filter * service . OpsSystemLogFilter ) ( * service . OpsSystemLogList , error ) {
if r == nil || r . db == nil {
return nil , fmt . Errorf ( "nil ops repository" )
}
if filter == nil {
filter = & service . OpsSystemLogFilter { }
}
page := filter . Page
if page <= 0 {
page = 1
}
pageSize := filter . PageSize
if pageSize <= 0 {
pageSize = 50
}
if pageSize > 200 {
pageSize = 200
}
where , args , _ := buildOpsSystemLogsWhere ( filter )
countSQL := "SELECT COUNT(*) FROM ops_system_logs l " + where
var total int
if err := r . db . QueryRowContext ( ctx , countSQL , args ... ) . Scan ( & total ) ; err != nil {
return nil , err
}
offset := ( page - 1 ) * pageSize
argsWithLimit := append ( args , pageSize , offset )
query := `
SELECT
l . id ,
l . created_at ,
l . level ,
COALESCE ( l . component , ' ' ) ,
COALESCE ( l . message , ' ' ) ,
COALESCE ( l . request_id , ' ' ) ,
COALESCE ( l . client_request_id , ' ' ) ,
l . user_id ,
l . account_id ,
COALESCE ( l . platform , ' ' ) ,
COALESCE ( l . model , ' ' ) ,
COALESCE ( l . extra : : text , ' { } ' )
FROM ops_system_logs l
` + where + `
ORDER BY l . created_at DESC , l . id DESC
LIMIT $ ` + itoa(len(args)+1) + ` OFFSET $ ` + itoa ( len ( args ) + 2 )
rows , err := r . db . QueryContext ( ctx , query , argsWithLimit ... )
if err != nil {
return nil , err
}
defer func ( ) { _ = rows . Close ( ) } ( )
logs := make ( [ ] * service . OpsSystemLog , 0 , pageSize )
for rows . Next ( ) {
item := & service . OpsSystemLog { }
var userID sql . NullInt64
var accountID sql . NullInt64
var extraRaw string
if err := rows . Scan (
& item . ID ,
& item . CreatedAt ,
& item . Level ,
& item . Component ,
& item . Message ,
& item . RequestID ,
& item . ClientRequestID ,
& userID ,
& accountID ,
& item . Platform ,
& item . Model ,
& extraRaw ,
) ; err != nil {
return nil , err
}
if userID . Valid {
v := userID . Int64
item . UserID = & v
}
if accountID . Valid {
v := accountID . Int64
item . AccountID = & v
}
extraRaw = strings . TrimSpace ( extraRaw )
if extraRaw != "" && extraRaw != "null" && extraRaw != "{}" {
extra := make ( map [ string ] any )
if err := json . Unmarshal ( [ ] byte ( extraRaw ) , & extra ) ; err == nil {
item . Extra = extra
}
}
logs = append ( logs , item )
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return & service . OpsSystemLogList {
Logs : logs ,
Total : total ,
Page : page ,
PageSize : pageSize ,
} , nil
}
func ( r * opsRepository ) DeleteSystemLogs ( ctx context . Context , filter * service . OpsSystemLogCleanupFilter ) ( int64 , error ) {
if r == nil || r . db == nil {
return 0 , fmt . Errorf ( "nil ops repository" )
}
if filter == nil {
filter = & service . OpsSystemLogCleanupFilter { }
}
where , args , hasConstraint := buildOpsSystemLogsCleanupWhere ( filter )
if ! hasConstraint {
return 0 , fmt . Errorf ( "cleanup requires at least one filter condition" )
}
query := "DELETE FROM ops_system_logs l " + where
res , err := r . db . ExecContext ( ctx , query , args ... )
if err != nil {
return 0 , err
}
return res . RowsAffected ( )
}
func ( r * opsRepository ) InsertSystemLogCleanupAudit ( ctx context . Context , input * service . OpsSystemLogCleanupAudit ) error {
if r == nil || r . db == nil {
return fmt . Errorf ( "nil ops repository" )
}
if input == nil {
return fmt . Errorf ( "nil input" )
}
createdAt := input . CreatedAt
if createdAt . IsZero ( ) {
createdAt = time . Now ( ) . UTC ( )
}
_ , err := r . db . ExecContext ( ctx , `
INSERT INTO ops_system_log_cleanup_audits (
created_at ,
operator_id ,
conditions ,
deleted_rows
) VALUES ( $ 1 , $ 2 , $ 3 , $ 4 )
` , createdAt . UTC ( ) , input . OperatorID , input . Conditions , input . DeletedRows )
return err
}
2026-01-09 20:52:57 +08:00
func buildOpsErrorLogsWhere ( filter * service . OpsErrorLogFilter ) ( string , [ ] any ) {
2026-01-14 09:03:01 +08:00
clauses := make ( [ ] string , 0 , 12 )
args := make ( [ ] any , 0 , 12 )
2026-01-09 20:52:57 +08:00
clauses = append ( clauses , "1=1" )
2026-01-11 23:00:31 +08:00
phaseFilter := ""
if filter != nil {
phaseFilter = strings . TrimSpace ( strings . ToLower ( filter . Phase ) )
}
2026-01-14 09:03:01 +08:00
// ops_error_logs stores client-visible error requests (status>=400),
2026-01-11 23:00:31 +08:00
// but we also persist "recovered" upstream errors (status<400) for upstream health visibility.
2026-01-14 16:26:33 +08:00
// If Resolved is not specified, do not filter by resolved state (backward-compatible).
2026-01-14 09:39:18 +08:00
resolvedFilter := ( * bool ) ( nil )
if filter != nil {
resolvedFilter = filter . Resolved
}
2026-01-14 09:03:01 +08:00
// Keep list endpoints scoped to client errors unless explicitly filtering upstream phase.
2026-01-11 23:00:31 +08:00
if phaseFilter != "upstream" {
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.status_code, 0) >= 400" )
2026-01-11 23:00:31 +08:00
}
2026-01-09 20:52:57 +08:00
if filter . StartTime != nil && ! filter . StartTime . IsZero ( ) {
args = append ( args , filter . StartTime . UTC ( ) )
2026-01-14 12:40:34 +08:00
clauses = append ( clauses , "e.created_at >= $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
if filter . EndTime != nil && ! filter . EndTime . IsZero ( ) {
args = append ( args , filter . EndTime . UTC ( ) )
// Keep time-window semantics consistent with other ops queries: [start, end)
2026-01-14 12:40:34 +08:00
clauses = append ( clauses , "e.created_at < $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
if p := strings . TrimSpace ( filter . Platform ) ; p != "" {
args = append ( args , p )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "e.platform = $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
if filter . GroupID != nil && * filter . GroupID > 0 {
args = append ( args , * filter . GroupID )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "e.group_id = $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
if filter . AccountID != nil && * filter . AccountID > 0 {
args = append ( args , * filter . AccountID )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "e.account_id = $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
2026-01-11 23:00:31 +08:00
if phase := phaseFilter ; phase != "" {
2026-01-09 20:52:57 +08:00
args = append ( args , phase )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "e.error_phase = $" + itoa ( len ( args ) ) )
2026-01-09 20:52:57 +08:00
}
2026-01-14 23:49:27 +08:00
if filter != nil {
if owner := strings . TrimSpace ( strings . ToLower ( filter . Owner ) ) ; owner != "" {
args = append ( args , owner )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "LOWER(COALESCE(e.error_owner,'')) = $" + itoa ( len ( args ) ) )
2026-01-14 23:49:27 +08:00
}
if source := strings . TrimSpace ( strings . ToLower ( filter . Source ) ) ; source != "" {
args = append ( args , source )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "LOWER(COALESCE(e.error_source,'')) = $" + itoa ( len ( args ) ) )
2026-01-14 23:49:27 +08:00
}
2026-01-14 09:03:01 +08:00
}
2026-01-14 09:39:18 +08:00
if resolvedFilter != nil {
args = append ( args , * resolvedFilter )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.resolved,false) = $" + itoa ( len ( args ) ) )
2026-01-14 09:03:01 +08:00
}
2026-01-14 14:29:39 +08:00
// View filter: errors vs excluded vs all.
2026-01-17 17:54:33 +08:00
// Excluded = business-limited errors (quota/concurrency/billing).
// Upstream 429/529 are included in errors view to match SLA calculation.
2026-01-14 14:29:39 +08:00
view := ""
if filter != nil {
view = strings . ToLower ( strings . TrimSpace ( filter . View ) )
}
switch view {
case "" , "errors" :
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.is_business_limited,false) = false" )
2026-01-14 14:29:39 +08:00
case "excluded" :
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.is_business_limited,false) = true" )
2026-01-14 14:29:39 +08:00
case "all" :
// no-op
default :
// treat unknown as default 'errors'
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.is_business_limited,false) = false" )
2026-01-14 14:29:39 +08:00
}
2026-01-09 20:52:57 +08:00
if len ( filter . StatusCodes ) > 0 {
args = append ( args , pq . Array ( filter . StatusCodes ) )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.upstream_status_code, e.status_code, 0) = ANY($" + itoa ( len ( args ) ) + ")" )
2026-01-14 16:26:33 +08:00
} else if filter . StatusCodesOther {
// "Other" means: status codes not in the common list.
known := [ ] int { 400 , 401 , 403 , 404 , 409 , 422 , 429 , 500 , 502 , 503 , 504 , 529 }
args = append ( args , pq . Array ( known ) )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "NOT (COALESCE(e.upstream_status_code, e.status_code, 0) = ANY($" + itoa ( len ( args ) ) + "))" )
2026-01-09 20:52:57 +08:00
}
2026-01-14 23:16:01 +08:00
// Exact correlation keys (preferred for request↔upstream linkage).
if rid := strings . TrimSpace ( filter . RequestID ) ; rid != "" {
args = append ( args , rid )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.request_id,'') = $" + itoa ( len ( args ) ) )
2026-01-14 23:16:01 +08:00
}
if crid := strings . TrimSpace ( filter . ClientRequestID ) ; crid != "" {
args = append ( args , crid )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "COALESCE(e.client_request_id,'') = $" + itoa ( len ( args ) ) )
2026-01-14 23:16:01 +08:00
}
2026-01-09 20:52:57 +08:00
if q := strings . TrimSpace ( filter . Query ) ; q != "" {
like := "%" + q + "%"
args = append ( args , like )
n := itoa ( len ( args ) )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "(e.request_id ILIKE $" + n + " OR e.client_request_id ILIKE $" + n + " OR e.error_message ILIKE $" + n + ")" )
2026-01-09 20:52:57 +08:00
}
2026-01-14 23:56:45 +08:00
if userQuery := strings . TrimSpace ( filter . UserQuery ) ; userQuery != "" {
like := "%" + userQuery + "%"
args = append ( args , like )
n := itoa ( len ( args ) )
2026-02-14 11:21:30 +08:00
clauses = append ( clauses , "EXISTS (SELECT 1 FROM users u WHERE u.id = e.user_id AND u.email ILIKE $" + n + ")" )
2026-01-14 23:56:45 +08:00
}
2026-01-09 20:52:57 +08:00
return "WHERE " + strings . Join ( clauses , " AND " ) , args
}
2026-02-12 16:27:29 +08:00
func buildOpsSystemLogsWhere ( filter * service . OpsSystemLogFilter ) ( string , [ ] any , bool ) {
clauses := make ( [ ] string , 0 , 10 )
args := make ( [ ] any , 0 , 10 )
clauses = append ( clauses , "1=1" )
hasConstraint := false
if filter != nil && filter . StartTime != nil && ! filter . StartTime . IsZero ( ) {
args = append ( args , filter . StartTime . UTC ( ) )
clauses = append ( clauses , "l.created_at >= $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if filter != nil && filter . EndTime != nil && ! filter . EndTime . IsZero ( ) {
args = append ( args , filter . EndTime . UTC ( ) )
clauses = append ( clauses , "l.created_at < $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if filter != nil {
if v := strings . ToLower ( strings . TrimSpace ( filter . Level ) ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "LOWER(COALESCE(l.level,'')) = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . Component ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "COALESCE(l.component,'') = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . RequestID ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "COALESCE(l.request_id,'') = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . ClientRequestID ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "COALESCE(l.client_request_id,'') = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if filter . UserID != nil && * filter . UserID > 0 {
args = append ( args , * filter . UserID )
clauses = append ( clauses , "l.user_id = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if filter . AccountID != nil && * filter . AccountID > 0 {
args = append ( args , * filter . AccountID )
clauses = append ( clauses , "l.account_id = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . Platform ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "COALESCE(l.platform,'') = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . Model ) ; v != "" {
args = append ( args , v )
clauses = append ( clauses , "COALESCE(l.model,'') = $" + itoa ( len ( args ) ) )
hasConstraint = true
}
if v := strings . TrimSpace ( filter . Query ) ; v != "" {
like := "%" + v + "%"
args = append ( args , like )
n := itoa ( len ( args ) )
clauses = append ( clauses , "(l.message ILIKE $" + n + " OR COALESCE(l.request_id,'') ILIKE $" + n + " OR COALESCE(l.client_request_id,'') ILIKE $" + n + " OR COALESCE(l.extra::text,'') ILIKE $" + n + ")" )
hasConstraint = true
}
}
return "WHERE " + strings . Join ( clauses , " AND " ) , args , hasConstraint
}
func buildOpsSystemLogsCleanupWhere ( filter * service . OpsSystemLogCleanupFilter ) ( string , [ ] any , bool ) {
if filter == nil {
filter = & service . OpsSystemLogCleanupFilter { }
}
listFilter := & service . OpsSystemLogFilter {
StartTime : filter . StartTime ,
EndTime : filter . EndTime ,
Level : filter . Level ,
Component : filter . Component ,
RequestID : filter . RequestID ,
ClientRequestID : filter . ClientRequestID ,
UserID : filter . UserID ,
AccountID : filter . AccountID ,
Platform : filter . Platform ,
Model : filter . Model ,
Query : filter . Query ,
}
return buildOpsSystemLogsWhere ( listFilter )
}
2026-01-09 20:52:57 +08:00
// Helpers for nullable args
func opsNullString ( v any ) any {
switch s := v . ( type ) {
case nil :
return sql . NullString { }
case * string :
if s == nil || strings . TrimSpace ( * s ) == "" {
return sql . NullString { }
}
return sql . NullString { String : strings . TrimSpace ( * s ) , Valid : true }
case string :
if strings . TrimSpace ( s ) == "" {
return sql . NullString { }
}
return sql . NullString { String : strings . TrimSpace ( s ) , Valid : true }
default :
return sql . NullString { }
}
}
func opsNullInt64 ( v * int64 ) any {
if v == nil || * v == 0 {
return sql . NullInt64 { }
}
return sql . NullInt64 { Int64 : * v , Valid : true }
}
func opsNullInt ( v any ) any {
switch n := v . ( type ) {
case nil :
return sql . NullInt64 { }
case * int :
if n == nil || * n == 0 {
return sql . NullInt64 { }
}
return sql . NullInt64 { Int64 : int64 ( * n ) , Valid : true }
case * int64 :
if n == nil || * n == 0 {
return sql . NullInt64 { }
}
return sql . NullInt64 { Int64 : * n , Valid : true }
case int :
if n == 0 {
return sql . NullInt64 { }
}
return sql . NullInt64 { Int64 : int64 ( n ) , Valid : true }
default :
return sql . NullInt64 { }
}
}