channel_repo.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "fmt"
  7. "strings"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/lib/pq"
  11. )
  12. type channelRepository struct {
  13. db *sql.DB
  14. }
  15. // NewChannelRepository 创建渠道数据访问实例
  16. func NewChannelRepository(db *sql.DB) service.ChannelRepository {
  17. return &channelRepository{db: db}
  18. }
  19. // runInTx 在事务中执行 fn,成功 commit,失败 rollback。
  20. func (r *channelRepository) runInTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
  21. tx, err := r.db.BeginTx(ctx, nil)
  22. if err != nil {
  23. return fmt.Errorf("begin tx: %w", err)
  24. }
  25. defer func() { _ = tx.Rollback() }()
  26. if err := fn(tx); err != nil {
  27. return err
  28. }
  29. return tx.Commit()
  30. }
  31. func (r *channelRepository) Create(ctx context.Context, channel *service.Channel) error {
  32. return r.runInTx(ctx, func(tx *sql.Tx) error {
  33. modelMappingJSON, err := marshalModelMapping(channel.ModelMapping)
  34. if err != nil {
  35. return err
  36. }
  37. featuresConfigJSON, err := marshalFeaturesConfig(channel.FeaturesConfig)
  38. if err != nil {
  39. return err
  40. }
  41. err = tx.QueryRowContext(ctx,
  42. `INSERT INTO channels (name, description, status, model_mapping, billing_model_source, restrict_models, features, features_config, apply_pricing_to_account_stats) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
  43. RETURNING id, created_at, updated_at`,
  44. channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.Features, featuresConfigJSON, channel.ApplyPricingToAccountStats,
  45. ).Scan(&channel.ID, &channel.CreatedAt, &channel.UpdatedAt)
  46. if err != nil {
  47. if isUniqueViolation(err) {
  48. return service.ErrChannelExists
  49. }
  50. return fmt.Errorf("insert channel: %w", err)
  51. }
  52. // 设置分组关联
  53. if len(channel.GroupIDs) > 0 {
  54. if err := setGroupIDsTx(ctx, tx, channel.ID, channel.GroupIDs); err != nil {
  55. return err
  56. }
  57. }
  58. // 设置模型定价
  59. if len(channel.ModelPricing) > 0 {
  60. if err := replaceModelPricingTx(ctx, tx, channel.ID, channel.ModelPricing); err != nil {
  61. return err
  62. }
  63. }
  64. // 设置账号统计定价规则
  65. if len(channel.AccountStatsPricingRules) > 0 {
  66. if err := replaceAccountStatsPricingRulesTx(ctx, tx, channel.ID, channel.AccountStatsPricingRules); err != nil {
  67. return err
  68. }
  69. }
  70. return nil
  71. })
  72. }
  73. func (r *channelRepository) GetByID(ctx context.Context, id int64) (*service.Channel, error) {
  74. ch := &service.Channel{}
  75. var modelMappingJSON, featuresConfigJSON []byte
  76. err := r.db.QueryRowContext(ctx,
  77. `SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, features, features_config, apply_pricing_to_account_stats, created_at, updated_at
  78. FROM channels WHERE id = $1`, id,
  79. ).Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &featuresConfigJSON, &ch.ApplyPricingToAccountStats, &ch.CreatedAt, &ch.UpdatedAt)
  80. if err == sql.ErrNoRows {
  81. return nil, service.ErrChannelNotFound
  82. }
  83. if err != nil {
  84. return nil, fmt.Errorf("get channel: %w", err)
  85. }
  86. ch.ModelMapping = unmarshalModelMapping(modelMappingJSON)
  87. ch.FeaturesConfig = unmarshalFeaturesConfig(featuresConfigJSON)
  88. groupIDs, err := r.GetGroupIDs(ctx, id)
  89. if err != nil {
  90. return nil, err
  91. }
  92. ch.GroupIDs = groupIDs
  93. pricing, err := r.ListModelPricing(ctx, id)
  94. if err != nil {
  95. return nil, err
  96. }
  97. ch.ModelPricing = pricing
  98. statsPricingRules, err := r.loadAccountStatsPricingRules(ctx, id)
  99. if err != nil {
  100. return nil, err
  101. }
  102. ch.AccountStatsPricingRules = statsPricingRules
  103. return ch, nil
  104. }
  105. func (r *channelRepository) Update(ctx context.Context, channel *service.Channel) error {
  106. return r.runInTx(ctx, func(tx *sql.Tx) error {
  107. modelMappingJSON, err := marshalModelMapping(channel.ModelMapping)
  108. if err != nil {
  109. return err
  110. }
  111. featuresConfigJSON, err := marshalFeaturesConfig(channel.FeaturesConfig)
  112. if err != nil {
  113. return err
  114. }
  115. result, err := tx.ExecContext(ctx,
  116. `UPDATE channels SET name = $1, description = $2, status = $3, model_mapping = $4, billing_model_source = $5, restrict_models = $6, features = $7, features_config = $8, apply_pricing_to_account_stats = $9, updated_at = NOW()
  117. WHERE id = $10`,
  118. channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.Features, featuresConfigJSON, channel.ApplyPricingToAccountStats, channel.ID,
  119. )
  120. if err != nil {
  121. if isUniqueViolation(err) {
  122. return service.ErrChannelExists
  123. }
  124. return fmt.Errorf("update channel: %w", err)
  125. }
  126. rows, _ := result.RowsAffected()
  127. if rows == 0 {
  128. return service.ErrChannelNotFound
  129. }
  130. // 更新分组关联
  131. if channel.GroupIDs != nil {
  132. if err := setGroupIDsTx(ctx, tx, channel.ID, channel.GroupIDs); err != nil {
  133. return err
  134. }
  135. }
  136. // 更新模型定价
  137. if channel.ModelPricing != nil {
  138. if err := replaceModelPricingTx(ctx, tx, channel.ID, channel.ModelPricing); err != nil {
  139. return err
  140. }
  141. }
  142. // 更新账号统计定价规则
  143. if channel.AccountStatsPricingRules != nil {
  144. if err := replaceAccountStatsPricingRulesTx(ctx, tx, channel.ID, channel.AccountStatsPricingRules); err != nil {
  145. return err
  146. }
  147. }
  148. return nil
  149. })
  150. }
  151. func (r *channelRepository) Delete(ctx context.Context, id int64) error {
  152. result, err := r.db.ExecContext(ctx, `DELETE FROM channels WHERE id = $1`, id)
  153. if err != nil {
  154. return fmt.Errorf("delete channel: %w", err)
  155. }
  156. rows, _ := result.RowsAffected()
  157. if rows == 0 {
  158. return service.ErrChannelNotFound
  159. }
  160. return nil
  161. }
  162. func (r *channelRepository) List(ctx context.Context, params pagination.PaginationParams, status, search string) ([]service.Channel, *pagination.PaginationResult, error) {
  163. where := []string{"1=1"}
  164. args := []any{}
  165. argIdx := 1
  166. if status != "" {
  167. where = append(where, fmt.Sprintf("c.status = $%d", argIdx))
  168. args = append(args, status)
  169. argIdx++
  170. }
  171. if search != "" {
  172. where = append(where, fmt.Sprintf("(c.name ILIKE $%d OR c.description ILIKE $%d)", argIdx, argIdx))
  173. args = append(args, "%"+escapeLike(search)+"%")
  174. argIdx++
  175. }
  176. whereClause := strings.Join(where, " AND ")
  177. // 计数
  178. var total int64
  179. countQuery := fmt.Sprintf("SELECT COUNT(*) FROM channels c WHERE %s", whereClause)
  180. if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
  181. return nil, nil, fmt.Errorf("count channels: %w", err)
  182. }
  183. pageSize := params.Limit() // 约束在 [1, 100]
  184. page := params.Page
  185. if page < 1 {
  186. page = 1
  187. }
  188. offset := (page - 1) * pageSize
  189. // 查询 channel 列表
  190. dataQuery := fmt.Sprintf(
  191. `SELECT c.id, c.name, c.description, c.status, c.model_mapping, c.billing_model_source, c.restrict_models, c.features, c.features_config, c.apply_pricing_to_account_stats, c.created_at, c.updated_at
  192. FROM channels c WHERE %s ORDER BY %s LIMIT $%d OFFSET $%d`,
  193. whereClause, channelListOrderBy(params), argIdx, argIdx+1,
  194. )
  195. args = append(args, pageSize, offset)
  196. rows, err := r.db.QueryContext(ctx, dataQuery, args...)
  197. if err != nil {
  198. return nil, nil, fmt.Errorf("query channels: %w", err)
  199. }
  200. defer func() { _ = rows.Close() }()
  201. var channels []service.Channel
  202. var channelIDs []int64
  203. for rows.Next() {
  204. var ch service.Channel
  205. var modelMappingJSON, featuresConfigJSON []byte
  206. if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &featuresConfigJSON, &ch.ApplyPricingToAccountStats, &ch.CreatedAt, &ch.UpdatedAt); err != nil {
  207. return nil, nil, fmt.Errorf("scan channel: %w", err)
  208. }
  209. ch.ModelMapping = unmarshalModelMapping(modelMappingJSON)
  210. ch.FeaturesConfig = unmarshalFeaturesConfig(featuresConfigJSON)
  211. channels = append(channels, ch)
  212. channelIDs = append(channelIDs, ch.ID)
  213. }
  214. if err := rows.Err(); err != nil {
  215. return nil, nil, fmt.Errorf("iterate channels: %w", err)
  216. }
  217. // 批量加载分组 ID 和模型定价(避免 N+1)
  218. if len(channelIDs) > 0 {
  219. groupMap, err := r.batchLoadGroupIDs(ctx, channelIDs)
  220. if err != nil {
  221. return nil, nil, err
  222. }
  223. pricingMap, err := r.batchLoadModelPricing(ctx, channelIDs)
  224. if err != nil {
  225. return nil, nil, err
  226. }
  227. statsRulesMap, err := r.batchLoadAccountStatsPricingRules(ctx, channelIDs)
  228. if err != nil {
  229. return nil, nil, err
  230. }
  231. for i := range channels {
  232. channels[i].GroupIDs = groupMap[channels[i].ID]
  233. channels[i].ModelPricing = pricingMap[channels[i].ID]
  234. channels[i].AccountStatsPricingRules = statsRulesMap[channels[i].ID]
  235. }
  236. }
  237. pages := 0
  238. if total > 0 {
  239. pages = int((total + int64(pageSize) - 1) / int64(pageSize))
  240. }
  241. paginationResult := &pagination.PaginationResult{
  242. Total: total,
  243. Page: page,
  244. PageSize: pageSize,
  245. Pages: pages,
  246. }
  247. return channels, paginationResult, nil
  248. }
  249. func channelListOrderBy(params pagination.PaginationParams) string {
  250. sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
  251. sortOrder := strings.ToUpper(params.NormalizedSortOrder(pagination.SortOrderAsc))
  252. var column string
  253. switch sortBy {
  254. case "":
  255. column = "c.id"
  256. sortOrder = "ASC"
  257. case "id":
  258. column = "c.id"
  259. case "name":
  260. column = "c.name"
  261. case "status":
  262. column = "c.status"
  263. case "created_at":
  264. column = "c.created_at"
  265. default:
  266. column = "c.id"
  267. sortOrder = "ASC"
  268. }
  269. return fmt.Sprintf("%s %s, c.id %s", column, sortOrder, sortOrder)
  270. }
  271. func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, error) {
  272. rows, err := r.db.QueryContext(ctx,
  273. `SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, features, features_config, apply_pricing_to_account_stats, created_at, updated_at FROM channels ORDER BY id`,
  274. )
  275. if err != nil {
  276. return nil, fmt.Errorf("query all channels: %w", err)
  277. }
  278. defer func() { _ = rows.Close() }()
  279. var channels []service.Channel
  280. var channelIDs []int64
  281. for rows.Next() {
  282. var ch service.Channel
  283. var modelMappingJSON, featuresConfigJSON []byte
  284. if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &featuresConfigJSON, &ch.ApplyPricingToAccountStats, &ch.CreatedAt, &ch.UpdatedAt); err != nil {
  285. return nil, fmt.Errorf("scan channel: %w", err)
  286. }
  287. ch.ModelMapping = unmarshalModelMapping(modelMappingJSON)
  288. ch.FeaturesConfig = unmarshalFeaturesConfig(featuresConfigJSON)
  289. channels = append(channels, ch)
  290. channelIDs = append(channelIDs, ch.ID)
  291. }
  292. if err := rows.Err(); err != nil {
  293. return nil, fmt.Errorf("iterate channels: %w", err)
  294. }
  295. if len(channelIDs) == 0 {
  296. return channels, nil
  297. }
  298. // 批量加载分组 ID
  299. groupMap, err := r.batchLoadGroupIDs(ctx, channelIDs)
  300. if err != nil {
  301. return nil, err
  302. }
  303. // 批量加载模型定价
  304. pricingMap, err := r.batchLoadModelPricing(ctx, channelIDs)
  305. if err != nil {
  306. return nil, err
  307. }
  308. // 批量加载账号统计定价规则
  309. statsRulesMap, err := r.batchLoadAccountStatsPricingRules(ctx, channelIDs)
  310. if err != nil {
  311. return nil, err
  312. }
  313. for i := range channels {
  314. channels[i].GroupIDs = groupMap[channels[i].ID]
  315. channels[i].ModelPricing = pricingMap[channels[i].ID]
  316. channels[i].AccountStatsPricingRules = statsRulesMap[channels[i].ID]
  317. }
  318. return channels, nil
  319. }
  320. // --- 批量加载辅助方法 ---
  321. // batchLoadGroupIDs 批量加载多个渠道的分组 ID
  322. func (r *channelRepository) batchLoadGroupIDs(ctx context.Context, channelIDs []int64) (map[int64][]int64, error) {
  323. rows, err := r.db.QueryContext(ctx,
  324. `SELECT channel_id, group_id FROM channel_groups
  325. WHERE channel_id = ANY($1) ORDER BY channel_id, group_id`,
  326. pq.Array(channelIDs),
  327. )
  328. if err != nil {
  329. return nil, fmt.Errorf("batch load group ids: %w", err)
  330. }
  331. defer func() { _ = rows.Close() }()
  332. groupMap := make(map[int64][]int64, len(channelIDs))
  333. for rows.Next() {
  334. var channelID, groupID int64
  335. if err := rows.Scan(&channelID, &groupID); err != nil {
  336. return nil, fmt.Errorf("scan group id: %w", err)
  337. }
  338. groupMap[channelID] = append(groupMap[channelID], groupID)
  339. }
  340. if err := rows.Err(); err != nil {
  341. return nil, fmt.Errorf("iterate group ids: %w", err)
  342. }
  343. return groupMap, nil
  344. }
  345. func (r *channelRepository) ExistsByName(ctx context.Context, name string) (bool, error) {
  346. var exists bool
  347. err := r.db.QueryRowContext(ctx,
  348. `SELECT EXISTS(SELECT 1 FROM channels WHERE name = $1)`, name,
  349. ).Scan(&exists)
  350. return exists, err
  351. }
  352. func (r *channelRepository) ExistsByNameExcluding(ctx context.Context, name string, excludeID int64) (bool, error) {
  353. var exists bool
  354. err := r.db.QueryRowContext(ctx,
  355. `SELECT EXISTS(SELECT 1 FROM channels WHERE name = $1 AND id != $2)`, name, excludeID,
  356. ).Scan(&exists)
  357. return exists, err
  358. }
  359. // --- 分组关联 ---
  360. func (r *channelRepository) GetGroupIDs(ctx context.Context, channelID int64) ([]int64, error) {
  361. rows, err := r.db.QueryContext(ctx,
  362. `SELECT group_id FROM channel_groups WHERE channel_id = $1 ORDER BY group_id`, channelID,
  363. )
  364. if err != nil {
  365. return nil, fmt.Errorf("get group ids: %w", err)
  366. }
  367. defer func() { _ = rows.Close() }()
  368. var ids []int64
  369. for rows.Next() {
  370. var id int64
  371. if err := rows.Scan(&id); err != nil {
  372. return nil, fmt.Errorf("scan group id: %w", err)
  373. }
  374. ids = append(ids, id)
  375. }
  376. if err := rows.Err(); err != nil {
  377. return nil, fmt.Errorf("iterate group ids: %w", err)
  378. }
  379. return ids, nil
  380. }
  381. func (r *channelRepository) SetGroupIDs(ctx context.Context, channelID int64, groupIDs []int64) error {
  382. return setGroupIDsTx(ctx, r.db, channelID, groupIDs)
  383. }
  384. func (r *channelRepository) GetChannelIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
  385. var channelID int64
  386. err := r.db.QueryRowContext(ctx,
  387. `SELECT channel_id FROM channel_groups WHERE group_id = $1`, groupID,
  388. ).Scan(&channelID)
  389. if err == sql.ErrNoRows {
  390. return 0, nil
  391. }
  392. return channelID, err
  393. }
  394. func (r *channelRepository) GetGroupsInOtherChannels(ctx context.Context, channelID int64, groupIDs []int64) ([]int64, error) {
  395. if len(groupIDs) == 0 {
  396. return nil, nil
  397. }
  398. rows, err := r.db.QueryContext(ctx,
  399. `SELECT group_id FROM channel_groups WHERE group_id = ANY($1) AND channel_id != $2`,
  400. pq.Array(groupIDs), channelID,
  401. )
  402. if err != nil {
  403. return nil, fmt.Errorf("get groups in other channels: %w", err)
  404. }
  405. defer func() { _ = rows.Close() }()
  406. var conflicting []int64
  407. for rows.Next() {
  408. var id int64
  409. if err := rows.Scan(&id); err != nil {
  410. return nil, fmt.Errorf("scan conflicting group id: %w", err)
  411. }
  412. conflicting = append(conflicting, id)
  413. }
  414. if err := rows.Err(); err != nil {
  415. return nil, fmt.Errorf("iterate conflicting group ids: %w", err)
  416. }
  417. return conflicting, nil
  418. }
  419. // marshalModelMapping 将 model mapping 序列化为嵌套 JSON 字节
  420. // 格式:{"platform": {"src": "dst"}, ...}
  421. func marshalModelMapping(m map[string]map[string]string) ([]byte, error) {
  422. if len(m) == 0 {
  423. return []byte("{}"), nil
  424. }
  425. data, err := json.Marshal(m)
  426. if err != nil {
  427. return nil, fmt.Errorf("marshal model_mapping: %w", err)
  428. }
  429. return data, nil
  430. }
  431. // unmarshalModelMapping 将 JSON 字节反序列化为嵌套 model mapping
  432. func unmarshalModelMapping(data []byte) map[string]map[string]string {
  433. if len(data) == 0 {
  434. return nil
  435. }
  436. var m map[string]map[string]string
  437. if err := json.Unmarshal(data, &m); err != nil {
  438. return nil
  439. }
  440. return m
  441. }
  442. func marshalFeaturesConfig(m map[string]any) ([]byte, error) {
  443. if len(m) == 0 {
  444. return []byte("{}"), nil
  445. }
  446. data, err := json.Marshal(m)
  447. if err != nil {
  448. return nil, fmt.Errorf("marshal features_config: %w", err)
  449. }
  450. return data, nil
  451. }
  452. func unmarshalFeaturesConfig(data []byte) map[string]any {
  453. if len(data) == 0 {
  454. return nil
  455. }
  456. var m map[string]any
  457. if err := json.Unmarshal(data, &m); err != nil {
  458. return nil
  459. }
  460. return m
  461. }
  462. // GetGroupPlatforms 批量查询分组 ID 对应的平台
  463. func (r *channelRepository) GetGroupPlatforms(ctx context.Context, groupIDs []int64) (map[int64]string, error) {
  464. if len(groupIDs) == 0 {
  465. return make(map[int64]string), nil
  466. }
  467. rows, err := r.db.QueryContext(ctx,
  468. `SELECT id, platform FROM groups WHERE id = ANY($1)`,
  469. pq.Array(groupIDs),
  470. )
  471. if err != nil {
  472. return nil, fmt.Errorf("get group platforms: %w", err)
  473. }
  474. defer rows.Close() //nolint:errcheck
  475. result := make(map[int64]string, len(groupIDs))
  476. for rows.Next() {
  477. var id int64
  478. var platform string
  479. if err := rows.Scan(&id, &platform); err != nil {
  480. return nil, fmt.Errorf("scan group platform: %w", err)
  481. }
  482. result[id] = platform
  483. }
  484. if err := rows.Err(); err != nil {
  485. return nil, fmt.Errorf("iterate group platforms: %w", err)
  486. }
  487. return result, nil
  488. }