intelligent_test_repo.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "strings"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/lib/pq"
  11. )
  12. type intelligentTestRepository struct{ db *sql.DB }
  13. func NewIntelligentTestRepository(db *sql.DB) service.IntelligentTestRepository {
  14. return &intelligentTestRepository{db: db}
  15. }
  16. const intelligentProtectionSQL = `COALESCE((a.extra->'anti_degradation')='true'::jsonb,(a.extra#>'{anti_degrade,enabled}')='true'::jsonb,false)`
  17. const intelligentRecordColumns = `t.id,t.account_id,t.test_type,t.status,t.score,t.result,t.result_image,t.input,t.raw_response,t.raw_truncated,t.error_message,t.duration_ms,t.model,t.anti_degradation,t.config_snapshot,t.evaluation,t.started_at,t.finished_at,t.created_at,COALESCE(t.lease_token,''),t.queue_reason,t.available_at`
  18. const intelligentSummaryColumns = `t.id,t.account_id,t.test_type,t.status,t.score,LEFT(t.result,1200),t.result_image,'','',t.raw_truncated,t.error_message,t.duration_ms,t.model,t.anti_degradation,'{}'::jsonb,t.evaluation,t.started_at,t.finished_at,t.created_at,'',t.queue_reason,t.available_at`
  19. type intelligentScanner interface{ Scan(...any) error }
  20. func scanIntelligentRecord(row intelligentScanner) (*service.IntelligentTestRecord, error) {
  21. r := &service.IntelligentTestRecord{}
  22. var cfg, eval []byte
  23. err := row.Scan(&r.ID, &r.AccountID, &r.TestType, &r.Status, &r.Score, &r.Result, &r.ResultImage, &r.Input, &r.RawResponse, &r.RawTruncated, &r.ErrorMessage, &r.DurationMS, &r.Model, &r.AntiDegradation, &cfg, &eval, &r.StartedAt, &r.FinishedAt, &r.CreatedAt, &r.LeaseToken, &r.QueueReason, &r.AvailableAt)
  24. if errors.Is(err, sql.ErrNoRows) {
  25. return nil, service.ErrIntelligentTestNotFound
  26. }
  27. if err != nil {
  28. return nil, err
  29. }
  30. r.ConfigSnapshot = &service.IntelligentTestConfig{}
  31. if err := json.Unmarshal(cfg, r.ConfigSnapshot); err != nil {
  32. return nil, err
  33. }
  34. if err := json.Unmarshal(eval, &r.Evaluation); err != nil {
  35. return nil, err
  36. }
  37. return r, nil
  38. }
  39. func readIntelligentRecords(rows *sql.Rows) ([]*service.IntelligentTestRecord, error) {
  40. defer rows.Close()
  41. out := []*service.IntelligentTestRecord{}
  42. for rows.Next() {
  43. r, err := scanIntelligentRecord(rows)
  44. if err != nil {
  45. return nil, err
  46. }
  47. out = append(out, r)
  48. }
  49. return out, rows.Err()
  50. }
  51. func compactIntelligentRecord(r *service.IntelligentTestRecord) {
  52. r.Input = ""
  53. r.RawResponse = ""
  54. r.ConfigSnapshot = nil
  55. if len([]rune(r.Result)) > 600 {
  56. r.Result = string([]rune(r.Result)[:600]) + "…"
  57. }
  58. }
  59. func (r *intelligentTestRepository) IsAdmin(ctx context.Context, id int64) (bool, error) {
  60. var ok bool
  61. err := r.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE id=$1 AND role IN ('admin','super_admin') AND status='active' AND deleted_at IS NULL)`, id).Scan(&ok)
  62. return ok, err
  63. }
  64. func (r *intelligentTestRepository) Settings(ctx context.Context) ([]service.IntelligentTestSetting, error) {
  65. rows, err := r.db.QueryContext(ctx, `SELECT test_type,enabled,user_visible,config,updated_at FROM test_settings ORDER BY test_type`)
  66. if err != nil {
  67. return nil, err
  68. }
  69. defer rows.Close()
  70. out := []service.IntelligentTestSetting{}
  71. for rows.Next() {
  72. var s service.IntelligentTestSetting
  73. var cfg []byte
  74. if err := rows.Scan(&s.TestType, &s.Enabled, &s.UserVisible, &cfg, &s.UpdatedAt); err != nil {
  75. return nil, err
  76. }
  77. if err := json.Unmarshal(cfg, &s.Config); err != nil {
  78. return nil, err
  79. }
  80. s.Name = s.TestType
  81. switch s.TestType {
  82. case "pelican":
  83. s.Name = "鹈鹕测试"
  84. case "candy":
  85. s.Name = "糖果测试"
  86. }
  87. out = append(out, s)
  88. }
  89. return out, rows.Err()
  90. }
  91. func insertIntelligentAudit(ctx context.Context, tx *sql.Tx, actor int64, action string, extra any) error {
  92. encoded, err := json.Marshal(extra)
  93. if err != nil {
  94. return err
  95. }
  96. _, err = tx.ExecContext(ctx, `INSERT INTO audit_logs(actor_user_id,actor_email,actor_role,action,method,path,status_code,extra) SELECT id,email,role,$2,'POST','/admin/intelligent-tests',200,$3::jsonb FROM users WHERE id=$1`, actor, action, string(encoded))
  97. return err
  98. }
  99. func (r *intelligentTestRepository) UpdateSetting(ctx context.Context, actor int64, s *service.IntelligentTestSetting) error {
  100. tx, err := r.db.BeginTx(ctx, nil)
  101. if err != nil {
  102. return err
  103. }
  104. defer tx.Rollback()
  105. cfg, err := json.Marshal(s.Config)
  106. if err != nil {
  107. return err
  108. }
  109. var previous json.RawMessage
  110. err = tx.QueryRowContext(ctx, `SELECT jsonb_build_object('enabled',enabled,'user_visible',user_visible,'config',config) FROM test_settings WHERE test_type=$1 FOR UPDATE`, s.TestType).Scan(&previous)
  111. if errors.Is(err, sql.ErrNoRows) {
  112. return service.ErrIntelligentTestNotFound
  113. }
  114. if err != nil {
  115. return err
  116. }
  117. _, err = tx.ExecContext(ctx, `UPDATE test_settings SET enabled=$2,user_visible=$3,config=$4,updated_at=NOW() WHERE test_type=$1`, s.TestType, s.Enabled, s.UserVisible, string(cfg))
  118. if err != nil {
  119. return err
  120. }
  121. if err = insertIntelligentAudit(ctx, tx, actor, "admin.intelligent_tests.settings", map[string]any{"test_type": s.TestType, "before": previous, "after": s}); err != nil {
  122. return err
  123. }
  124. return tx.Commit()
  125. }
  126. func (r *intelligentTestRepository) Get(ctx context.Context, id int64) (*service.IntelligentTestRecord, error) {
  127. return scanIntelligentRecord(r.db.QueryRowContext(ctx, `SELECT `+intelligentRecordColumns+` FROM account_tests t WHERE t.id=$1`, id))
  128. }
  129. type intelligentWhere struct {
  130. parts []string
  131. args []any
  132. }
  133. func (w *intelligentWhere) add(condition string, value any) {
  134. w.args = append(w.args, value)
  135. w.parts = append(w.parts, fmt.Sprintf(condition, len(w.args)))
  136. }
  137. func (w *intelligentWhere) sql() string { return strings.Join(w.parts, " AND ") }
  138. func intelligentRecordWhere(f service.IntelligentTestFilter) *intelligentWhere {
  139. w := &intelligentWhere{parts: []string{"true"}}
  140. if f.AccountID > 0 {
  141. w.add("t.account_id=$%d", f.AccountID)
  142. }
  143. if f.TestType != "" {
  144. w.add("t.test_type=$%d", f.TestType)
  145. }
  146. if f.Status != "" {
  147. w.add("t.status=$%d", f.Status)
  148. }
  149. if f.From != nil {
  150. w.add("t.created_at >= $%d", *f.From)
  151. }
  152. if f.To != nil {
  153. w.add("t.created_at <= $%d", *f.To)
  154. }
  155. if f.OnlyAbnormal {
  156. w.parts = append(w.parts, `(t.status IN ('failed','rate_limited','account_error','model_error','request_error','network_error','suspected_degradation') OR t.evaluation->>'answer_verdict' IN ('incorrect','undetermined'))`)
  157. }
  158. return w
  159. }
  160. func (r *intelligentTestRepository) Records(ctx context.Context, f service.IntelligentTestFilter) (*service.IntelligentTestRecords, error) {
  161. out := &service.IntelligentTestRecords{Items: []*service.IntelligentTestRecord{}, Page: f.Page, PageSize: f.PageSize}
  162. w := intelligentRecordWhere(f)
  163. if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM account_tests t WHERE `+w.sql(), w.args...).Scan(&out.Total); err != nil {
  164. return nil, err
  165. }
  166. args := append(append([]any{}, w.args...), f.PageSize, (f.Page-1)*f.PageSize)
  167. rows, err := r.db.QueryContext(ctx, `SELECT `+intelligentSummaryColumns+` FROM account_tests t WHERE `+w.sql()+fmt.Sprintf(` ORDER BY t.id DESC LIMIT $%d OFFSET $%d`, len(args)-1, len(args)), args...)
  168. if err != nil {
  169. return nil, err
  170. }
  171. out.Items, err = readIntelligentRecords(rows)
  172. for _, record := range out.Items {
  173. compactIntelligentRecord(record)
  174. }
  175. return out, err
  176. }
  177. func intelligentAccountWhere(f service.IntelligentTestFilter) *intelligentWhere {
  178. w := &intelligentWhere{parts: []string{"a.deleted_at IS NULL"}}
  179. if f.AccountID > 0 {
  180. w.add("a.id=$%d", f.AccountID)
  181. }
  182. if f.Search != "" {
  183. w.add("(a.name ILIKE $%[1]d OR COALESCE(a.notes,'') ILIKE $%[1]d OR a.id::text ILIKE $%[1]d)", "%"+f.Search+"%")
  184. }
  185. if f.Platform != "" {
  186. w.add("a.platform=$%d", f.Platform)
  187. }
  188. if f.AccountType != "" {
  189. w.add("a.type=$%d", f.AccountType)
  190. }
  191. if f.AccountStatus != "" {
  192. w.add("a.status=$%d", f.AccountStatus)
  193. }
  194. if f.GroupID > 0 {
  195. w.add("EXISTS(SELECT 1 FROM account_groups ag WHERE ag.account_id=a.id AND ag.group_id=$%d)", f.GroupID)
  196. }
  197. if f.AntiDegradation != nil {
  198. w.add(intelligentProtectionSQL+"=$%d", *f.AntiDegradation)
  199. }
  200. sub := `SELECT DISTINCT ON(t.test_type) t.test_type,t.status,t.evaluation FROM account_tests t WHERE t.account_id=a.id`
  201. if f.TestType != "" && (f.Status != "" || f.OnlyAbnormal) {
  202. w.args = append(w.args, f.TestType)
  203. sub += fmt.Sprintf(` AND t.test_type=$%d`, len(w.args))
  204. }
  205. completedSub := sub + ` AND t.status NOT IN ('queued','running','cancelled') ORDER BY t.test_type,t.id DESC`
  206. sub += ` ORDER BY t.test_type,t.id DESC`
  207. if f.Status == "waiting" {
  208. w.parts = append(w.parts, `NOT EXISTS(`+sub+`)`)
  209. } else if f.Status != "" {
  210. w.args = append(w.args, f.Status)
  211. w.parts = append(w.parts, `EXISTS(SELECT 1 FROM (`+sub+fmt.Sprintf(`) latest WHERE latest.status=$%d)`, len(w.args)))
  212. }
  213. if f.OnlyAbnormal {
  214. w.parts = append(w.parts, `EXISTS(SELECT 1 FROM (`+completedSub+`) latest WHERE latest.status IN ('failed','rate_limited','account_error','model_error','request_error','network_error','suspected_degradation') OR latest.evaluation->>'answer_verdict' IN ('incorrect','undetermined'))`)
  215. }
  216. return w
  217. }
  218. func (r *intelligentTestRepository) Accounts(ctx context.Context, f service.IntelligentTestFilter) (*service.IntelligentTestAccounts, error) {
  219. out := &service.IntelligentTestAccounts{Items: []service.IntelligentTestAccountCard{}, Page: f.Page, PageSize: f.PageSize}
  220. w := intelligentAccountWhere(f)
  221. if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM accounts a WHERE `+w.sql(), w.args...).Scan(&out.Total); err != nil {
  222. return nil, err
  223. }
  224. out.Overview.TotalAccounts = out.Total
  225. overviewArgs := append([]any{}, w.args...)
  226. typeCondition := "true"
  227. if f.TestType != "" {
  228. overviewArgs = append(overviewArgs, f.TestType)
  229. typeCondition = fmt.Sprintf("t.test_type=$%d", len(overviewArgs))
  230. }
  231. overviewSQL := `WITH selected AS(SELECT a.id FROM accounts a WHERE ` + w.sql() + `), latest AS(
  232. SELECT DISTINCT ON(t.account_id,t.test_type) t.account_id,t.test_type,t.status,t.evaluation FROM account_tests t JOIN selected a ON a.id=t.account_id WHERE ` + typeCondition + ` AND t.status NOT IN ('queued','running','cancelled') ORDER BY t.account_id,t.test_type,t.id DESC), per_account AS(
  233. SELECT account_id,bool_or(COALESCE(evaluation->>'answer_verdict'='correct',false)) AS success,
  234. bool_or(status IN ('failed','rate_limited','account_error','model_error','request_error','network_error')) AS abnormal,
  235. bool_or(status='suspected_degradation' OR COALESCE(evaluation->>'answer_verdict' IN ('incorrect','undetermined'),false)) AS review
  236. FROM latest GROUP BY account_id)
  237. SELECT (SELECT COUNT(DISTINCT t.account_id) FROM account_tests t JOIN selected a ON a.id=t.account_id WHERE t.finished_at>=date_trunc('day',NOW()) AND t.status<>'cancelled' AND ` + typeCondition + `),COUNT(*) FILTER(WHERE success AND NOT abnormal AND NOT review),COUNT(*) FILTER(WHERE abnormal),COUNT(*) FILTER(WHERE review) FROM per_account`
  238. if err := r.db.QueryRowContext(ctx, overviewSQL, overviewArgs...).Scan(&out.Overview.TestedToday, &out.Overview.SuccessAccounts, &out.Overview.AbnormalAccounts, &out.Overview.ReviewAccounts); err != nil {
  239. return nil, err
  240. }
  241. args := append(append([]any{}, w.args...), f.PageSize, (f.Page-1)*f.PageSize)
  242. rows, err := r.db.QueryContext(ctx, `SELECT a.id,a.name,COALESCE(a.notes,''),a.platform,a.type,a.status,`+intelligentProtectionSQL+`,ARRAY(SELECT group_id FROM account_groups WHERE account_id=a.id ORDER BY group_id) FROM accounts a WHERE `+w.sql()+fmt.Sprintf(` ORDER BY a.id DESC LIMIT $%d OFFSET $%d`, len(args)-1, len(args)), args...)
  243. if err != nil {
  244. return nil, err
  245. }
  246. ids := []int64{}
  247. for rows.Next() {
  248. var card service.IntelligentTestAccountCard
  249. if err := rows.Scan(&card.AccountID, &card.Name, &card.Notes, &card.Platform, &card.AccountType, &card.AccountStatus, &card.AntiDegradation, pq.Array(&card.GroupIDs)); err != nil {
  250. rows.Close()
  251. return nil, err
  252. }
  253. ids = append(ids, card.AccountID)
  254. if card.GroupIDs == nil {
  255. card.GroupIDs = []int64{}
  256. }
  257. card.Tests = []service.IntelligentTestCardTest{}
  258. out.Items = append(out.Items, card)
  259. }
  260. err = rows.Err()
  261. rows.Close()
  262. if err != nil || len(ids) == 0 {
  263. return out, err
  264. }
  265. settings, err := r.Settings(ctx)
  266. if err != nil {
  267. return nil, err
  268. }
  269. latestRows, err := r.db.QueryContext(ctx, `SELECT `+intelligentSummaryColumns+` FROM account_tests t JOIN (SELECT DISTINCT ON(account_id,test_type) id FROM account_tests WHERE account_id=ANY($1) ORDER BY account_id,test_type,id DESC) latest ON latest.id=t.id`, pq.Array(ids))
  270. if err != nil {
  271. return nil, err
  272. }
  273. latest, err := readIntelligentRecords(latestRows)
  274. if err != nil {
  275. return nil, err
  276. }
  277. byKey := map[string]*service.IntelligentTestRecord{}
  278. key := func(id int64, t string) string { return fmt.Sprintf("%d:%s", id, t) }
  279. for _, record := range latest {
  280. compactIntelligentRecord(record)
  281. byKey[key(record.AccountID, record.TestType)] = record
  282. }
  283. completedRows, err := r.db.QueryContext(ctx, `SELECT `+intelligentSummaryColumns+` FROM account_tests t JOIN (SELECT DISTINCT ON(account_id,test_type) id FROM account_tests WHERE account_id=ANY($1) AND status NOT IN ('queued','running','cancelled') ORDER BY account_id,test_type,id DESC) latest ON latest.id=t.id`, pq.Array(ids))
  284. if err != nil {
  285. return nil, err
  286. }
  287. completed, err := readIntelligentRecords(completedRows)
  288. if err != nil {
  289. return nil, err
  290. }
  291. completedByKey := map[string]*service.IntelligentTestRecord{}
  292. for _, record := range completed {
  293. compactIntelligentRecord(record)
  294. completedByKey[key(record.AccountID, record.TestType)] = record
  295. }
  296. counts := map[string]int64{}
  297. countRows, err := r.db.QueryContext(ctx, `SELECT account_id,test_type,COUNT(*) FROM account_tests WHERE account_id=ANY($1) GROUP BY account_id,test_type`, pq.Array(ids))
  298. if err != nil {
  299. return nil, err
  300. }
  301. for countRows.Next() {
  302. var id, count int64
  303. var kind string
  304. if err := countRows.Scan(&id, &kind, &count); err != nil {
  305. countRows.Close()
  306. return nil, err
  307. }
  308. counts[key(id, kind)] = count
  309. }
  310. err = countRows.Err()
  311. countRows.Close()
  312. if err != nil {
  313. return nil, err
  314. }
  315. for i := range out.Items {
  316. card := &out.Items[i]
  317. for _, setting := range settings {
  318. if f.TestType != "" && setting.TestType != f.TestType {
  319. continue
  320. }
  321. k := key(card.AccountID, setting.TestType)
  322. item := service.IntelligentTestCardTest{TestType: setting.TestType, Latest: byKey[k], LatestCompleted: completedByKey[k], HistoryCount: counts[k]}
  323. card.Tests = append(card.Tests, item)
  324. }
  325. }
  326. return out, nil
  327. }