- 优化错误日志中间件,即使请求成功也记录上游重试/故障转移事件 - 新增OpsScheduledReportService支持定时报告功能 - 使用Redis分布式锁确保定时任务单实例执行 - 完善依赖注入配置 - 优化前端错误趋势图表展示
287 lines
7.1 KiB
Go
287 lines
7.1 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
func (r *opsRepository) ListRequestDetails(ctx context.Context, filter *service.OpsRequestDetailFilter) ([]*service.OpsRequestDetail, int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, 0, fmt.Errorf("nil ops repository")
|
|
}
|
|
|
|
page, pageSize, startTime, endTime := filter.Normalize()
|
|
offset := (page - 1) * pageSize
|
|
|
|
conditions := make([]string, 0, 16)
|
|
args := make([]any, 0, 24)
|
|
|
|
// Placeholders $1/$2 reserved for time window inside the CTE.
|
|
args = append(args, startTime.UTC(), endTime.UTC())
|
|
|
|
addCondition := func(condition string, values ...any) {
|
|
conditions = append(conditions, condition)
|
|
args = append(args, values...)
|
|
}
|
|
|
|
if filter != nil {
|
|
if kind := strings.TrimSpace(strings.ToLower(filter.Kind)); kind != "" && kind != "all" {
|
|
if kind != string(service.OpsRequestKindSuccess) && kind != string(service.OpsRequestKindError) {
|
|
return nil, 0, fmt.Errorf("invalid kind")
|
|
}
|
|
addCondition(fmt.Sprintf("kind = $%d", len(args)+1), kind)
|
|
}
|
|
|
|
if platform := strings.TrimSpace(strings.ToLower(filter.Platform)); platform != "" {
|
|
addCondition(fmt.Sprintf("platform = $%d", len(args)+1), platform)
|
|
}
|
|
if filter.GroupID != nil && *filter.GroupID > 0 {
|
|
addCondition(fmt.Sprintf("group_id = $%d", len(args)+1), *filter.GroupID)
|
|
}
|
|
|
|
if filter.UserID != nil && *filter.UserID > 0 {
|
|
addCondition(fmt.Sprintf("user_id = $%d", len(args)+1), *filter.UserID)
|
|
}
|
|
if filter.APIKeyID != nil && *filter.APIKeyID > 0 {
|
|
addCondition(fmt.Sprintf("api_key_id = $%d", len(args)+1), *filter.APIKeyID)
|
|
}
|
|
if filter.AccountID != nil && *filter.AccountID > 0 {
|
|
addCondition(fmt.Sprintf("account_id = $%d", len(args)+1), *filter.AccountID)
|
|
}
|
|
|
|
if model := strings.TrimSpace(filter.Model); model != "" {
|
|
addCondition(fmt.Sprintf("model = $%d", len(args)+1), model)
|
|
}
|
|
if requestID := strings.TrimSpace(filter.RequestID); requestID != "" {
|
|
addCondition(fmt.Sprintf("request_id = $%d", len(args)+1), requestID)
|
|
}
|
|
if q := strings.TrimSpace(filter.Query); q != "" {
|
|
like := "%" + strings.ToLower(q) + "%"
|
|
startIdx := len(args) + 1
|
|
addCondition(
|
|
fmt.Sprintf("(LOWER(COALESCE(request_id,'')) LIKE $%d OR LOWER(COALESCE(model,'')) LIKE $%d OR LOWER(COALESCE(message,'')) LIKE $%d)",
|
|
startIdx, startIdx+1, startIdx+2,
|
|
),
|
|
like, like, like,
|
|
)
|
|
}
|
|
|
|
if filter.MinDurationMs != nil {
|
|
addCondition(fmt.Sprintf("duration_ms >= $%d", len(args)+1), *filter.MinDurationMs)
|
|
}
|
|
if filter.MaxDurationMs != nil {
|
|
addCondition(fmt.Sprintf("duration_ms <= $%d", len(args)+1), *filter.MaxDurationMs)
|
|
}
|
|
}
|
|
|
|
where := ""
|
|
if len(conditions) > 0 {
|
|
where = "WHERE " + strings.Join(conditions, " AND ")
|
|
}
|
|
|
|
cte := `
|
|
WITH combined AS (
|
|
SELECT
|
|
'success'::TEXT AS kind,
|
|
ul.created_at AS created_at,
|
|
ul.request_id AS request_id,
|
|
COALESCE(NULLIF(g.platform, ''), NULLIF(a.platform, ''), '') AS platform,
|
|
ul.model AS model,
|
|
ul.duration_ms AS duration_ms,
|
|
NULL::INT AS status_code,
|
|
NULL::BIGINT AS error_id,
|
|
NULL::TEXT AS phase,
|
|
NULL::TEXT AS severity,
|
|
NULL::TEXT AS message,
|
|
ul.user_id AS user_id,
|
|
ul.api_key_id AS api_key_id,
|
|
ul.account_id AS account_id,
|
|
ul.group_id AS group_id,
|
|
ul.stream AS stream
|
|
FROM usage_logs ul
|
|
LEFT JOIN groups g ON g.id = ul.group_id
|
|
LEFT JOIN accounts a ON a.id = ul.account_id
|
|
WHERE ul.created_at >= $1 AND ul.created_at < $2
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
'error'::TEXT AS kind,
|
|
o.created_at AS created_at,
|
|
COALESCE(NULLIF(o.request_id,''), NULLIF(o.client_request_id,''), '') AS request_id,
|
|
COALESCE(NULLIF(o.platform, ''), NULLIF(g.platform, ''), NULLIF(a.platform, ''), '') AS platform,
|
|
o.model AS model,
|
|
o.duration_ms AS duration_ms,
|
|
o.status_code AS status_code,
|
|
o.id AS error_id,
|
|
o.error_phase AS phase,
|
|
o.severity AS severity,
|
|
o.error_message AS message,
|
|
o.user_id AS user_id,
|
|
o.api_key_id AS api_key_id,
|
|
o.account_id AS account_id,
|
|
o.group_id AS group_id,
|
|
o.stream AS stream
|
|
FROM ops_error_logs o
|
|
LEFT JOIN groups g ON g.id = o.group_id
|
|
LEFT JOIN accounts a ON a.id = o.account_id
|
|
WHERE o.created_at >= $1 AND o.created_at < $2
|
|
AND COALESCE(o.status_code, 0) >= 400
|
|
)
|
|
`
|
|
|
|
countQuery := fmt.Sprintf(`%s SELECT COUNT(1) FROM combined %s`, cte, where)
|
|
var total int64
|
|
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
total = 0
|
|
} else {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
sort := "ORDER BY created_at DESC"
|
|
if filter != nil {
|
|
switch strings.TrimSpace(strings.ToLower(filter.Sort)) {
|
|
case "", "created_at_desc":
|
|
// default
|
|
case "duration_desc":
|
|
sort = "ORDER BY duration_ms DESC NULLS LAST, created_at DESC"
|
|
default:
|
|
return nil, 0, fmt.Errorf("invalid sort")
|
|
}
|
|
}
|
|
|
|
listQuery := fmt.Sprintf(`
|
|
%s
|
|
SELECT
|
|
kind,
|
|
created_at,
|
|
request_id,
|
|
platform,
|
|
model,
|
|
duration_ms,
|
|
status_code,
|
|
error_id,
|
|
phase,
|
|
severity,
|
|
message,
|
|
user_id,
|
|
api_key_id,
|
|
account_id,
|
|
group_id,
|
|
stream
|
|
FROM combined
|
|
%s
|
|
%s
|
|
LIMIT $%d OFFSET $%d
|
|
`, cte, where, sort, len(args)+1, len(args)+2)
|
|
|
|
listArgs := append(append([]any{}, args...), pageSize, offset)
|
|
rows, err := r.db.QueryContext(ctx, listQuery, listArgs...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
toIntPtr := func(v sql.NullInt64) *int {
|
|
if !v.Valid {
|
|
return nil
|
|
}
|
|
i := int(v.Int64)
|
|
return &i
|
|
}
|
|
toInt64Ptr := func(v sql.NullInt64) *int64 {
|
|
if !v.Valid {
|
|
return nil
|
|
}
|
|
i := v.Int64
|
|
return &i
|
|
}
|
|
|
|
out := make([]*service.OpsRequestDetail, 0, pageSize)
|
|
for rows.Next() {
|
|
var (
|
|
kind string
|
|
createdAt time.Time
|
|
requestID sql.NullString
|
|
platform sql.NullString
|
|
model sql.NullString
|
|
|
|
durationMs sql.NullInt64
|
|
statusCode sql.NullInt64
|
|
errorID sql.NullInt64
|
|
|
|
phase sql.NullString
|
|
severity sql.NullString
|
|
message sql.NullString
|
|
|
|
userID sql.NullInt64
|
|
apiKeyID sql.NullInt64
|
|
accountID sql.NullInt64
|
|
groupID sql.NullInt64
|
|
|
|
stream bool
|
|
)
|
|
|
|
if err := rows.Scan(
|
|
&kind,
|
|
&createdAt,
|
|
&requestID,
|
|
&platform,
|
|
&model,
|
|
&durationMs,
|
|
&statusCode,
|
|
&errorID,
|
|
&phase,
|
|
&severity,
|
|
&message,
|
|
&userID,
|
|
&apiKeyID,
|
|
&accountID,
|
|
&groupID,
|
|
&stream,
|
|
); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
item := &service.OpsRequestDetail{
|
|
Kind: service.OpsRequestKind(kind),
|
|
CreatedAt: createdAt,
|
|
RequestID: strings.TrimSpace(requestID.String),
|
|
Platform: strings.TrimSpace(platform.String),
|
|
Model: strings.TrimSpace(model.String),
|
|
|
|
DurationMs: toIntPtr(durationMs),
|
|
StatusCode: toIntPtr(statusCode),
|
|
ErrorID: toInt64Ptr(errorID),
|
|
Phase: phase.String,
|
|
Severity: severity.String,
|
|
Message: message.String,
|
|
|
|
UserID: toInt64Ptr(userID),
|
|
APIKeyID: toInt64Ptr(apiKeyID),
|
|
AccountID: toInt64Ptr(accountID),
|
|
GroupID: toInt64Ptr(groupID),
|
|
|
|
Stream: stream,
|
|
}
|
|
|
|
if item.Platform == "" {
|
|
item.Platform = "unknown"
|
|
}
|
|
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return out, total, nil
|
|
}
|