channel_repo_account_stats_pricing.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "fmt"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/lib/pq"
  9. )
  10. // --- 账号统计定价规则 ---
  11. // batchLoadAccountStatsPricingRules 批量加载多个渠道的账号统计定价规则(含模型定价)
  12. func (r *channelRepository) batchLoadAccountStatsPricingRules(ctx context.Context, channelIDs []int64) (map[int64][]service.AccountStatsPricingRule, error) {
  13. // 1. 查询规则
  14. rows, err := r.db.QueryContext(ctx,
  15. `SELECT id, channel_id, name, group_ids, account_ids, sort_order, created_at, updated_at
  16. FROM channel_account_stats_pricing_rules WHERE channel_id = ANY($1) ORDER BY channel_id, sort_order, id`,
  17. pq.Array(channelIDs),
  18. )
  19. if err != nil {
  20. return nil, fmt.Errorf("batch load account stats pricing rules: %w", err)
  21. }
  22. defer func() { _ = rows.Close() }()
  23. var allRules []service.AccountStatsPricingRule
  24. var ruleIDs []int64
  25. for rows.Next() {
  26. var rule service.AccountStatsPricingRule
  27. if err := rows.Scan(
  28. &rule.ID, &rule.ChannelID, &rule.Name,
  29. pq.Array(&rule.GroupIDs), pq.Array(&rule.AccountIDs),
  30. &rule.SortOrder, &rule.CreatedAt, &rule.UpdatedAt,
  31. ); err != nil {
  32. return nil, fmt.Errorf("scan account stats pricing rule: %w", err)
  33. }
  34. ruleIDs = append(ruleIDs, rule.ID)
  35. allRules = append(allRules, rule)
  36. }
  37. if err := rows.Err(); err != nil {
  38. return nil, fmt.Errorf("iterate account stats pricing rules: %w", err)
  39. }
  40. // 2. 批量加载规则的模型定价
  41. pricingMap, err := r.batchLoadAccountStatsModelPricing(ctx, ruleIDs)
  42. if err != nil {
  43. return nil, err
  44. }
  45. // 3. 按 channelID 分组并关联定价
  46. result := make(map[int64][]service.AccountStatsPricingRule, len(channelIDs))
  47. for i := range allRules {
  48. allRules[i].Pricing = pricingMap[allRules[i].ID]
  49. result[allRules[i].ChannelID] = append(result[allRules[i].ChannelID], allRules[i])
  50. }
  51. return result, nil
  52. }
  53. // batchLoadAccountStatsModelPricing 批量加载规则的模型定价
  54. func (r *channelRepository) batchLoadAccountStatsModelPricing(ctx context.Context, ruleIDs []int64) (map[int64][]service.ChannelModelPricing, error) {
  55. if len(ruleIDs) == 0 {
  56. return make(map[int64][]service.ChannelModelPricing), nil
  57. }
  58. rows, err := r.db.QueryContext(ctx,
  59. `SELECT id, rule_id, platform, models, billing_mode, input_price, output_price,
  60. cache_write_price, cache_write_1h_price, cache_read_price, image_output_price, per_request_price, created_at, updated_at
  61. FROM channel_account_stats_model_pricing WHERE rule_id = ANY($1) ORDER BY rule_id, id`,
  62. pq.Array(ruleIDs),
  63. )
  64. if err != nil {
  65. return nil, fmt.Errorf("batch load account stats model pricing: %w", err)
  66. }
  67. defer func() { _ = rows.Close() }()
  68. pricingMap := make(map[int64][]service.ChannelModelPricing, len(ruleIDs))
  69. for rows.Next() {
  70. var p service.ChannelModelPricing
  71. var ruleID int64
  72. var modelsJSON []byte
  73. if err := rows.Scan(
  74. &p.ID, &ruleID, &p.Platform, &modelsJSON, &p.BillingMode,
  75. &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice,
  76. &p.ImageOutputPrice, &p.PerRequestPrice, &p.CreatedAt, &p.UpdatedAt,
  77. ); err != nil {
  78. return nil, fmt.Errorf("scan account stats model pricing: %w", err)
  79. }
  80. if err := json.Unmarshal(modelsJSON, &p.Models); err != nil {
  81. p.Models = []string{}
  82. }
  83. pricingMap[ruleID] = append(pricingMap[ruleID], p)
  84. }
  85. if err := rows.Err(); err != nil {
  86. return nil, fmt.Errorf("iterate account stats model pricing: %w", err)
  87. }
  88. // Load intervals for all pricing entries.
  89. var allPricingIDs []int64
  90. for _, pricings := range pricingMap {
  91. for _, p := range pricings {
  92. allPricingIDs = append(allPricingIDs, p.ID)
  93. }
  94. }
  95. if len(allPricingIDs) > 0 {
  96. intervalsMap, err := r.batchLoadAccountStatsIntervals(ctx, allPricingIDs)
  97. if err != nil {
  98. return nil, err
  99. }
  100. for ruleID, pricings := range pricingMap {
  101. for i := range pricings {
  102. pricings[i].Intervals = intervalsMap[pricings[i].ID]
  103. }
  104. pricingMap[ruleID] = pricings
  105. }
  106. }
  107. return pricingMap, nil
  108. }
  109. // loadAccountStatsPricingRules 加载单个渠道的账号统计定价规则(供 GetByID 使用)
  110. func (r *channelRepository) loadAccountStatsPricingRules(ctx context.Context, channelID int64) ([]service.AccountStatsPricingRule, error) {
  111. result, err := r.batchLoadAccountStatsPricingRules(ctx, []int64{channelID})
  112. if err != nil {
  113. return nil, err
  114. }
  115. return result[channelID], nil
  116. }
  117. // replaceAccountStatsPricingRulesTx 在事务中替换渠道的账号统计定价规则(删除旧的 + 插入新的)
  118. func replaceAccountStatsPricingRulesTx(ctx context.Context, tx *sql.Tx, channelID int64, rules []service.AccountStatsPricingRule) error {
  119. // CASCADE 会自动删除关联的 model_pricing
  120. if _, err := tx.ExecContext(ctx,
  121. `DELETE FROM channel_account_stats_pricing_rules WHERE channel_id = $1`, channelID,
  122. ); err != nil {
  123. return fmt.Errorf("delete old account stats pricing rules: %w", err)
  124. }
  125. for i := range rules {
  126. rules[i].ChannelID = channelID
  127. if err := createAccountStatsPricingRuleTx(ctx, tx, &rules[i]); err != nil {
  128. return fmt.Errorf("insert account stats pricing rule: %w", err)
  129. }
  130. }
  131. return nil
  132. }
  133. // createAccountStatsPricingRuleTx 在事务中创建单条账号统计定价规则及其模型定价
  134. func createAccountStatsPricingRuleTx(ctx context.Context, tx *sql.Tx, rule *service.AccountStatsPricingRule) error {
  135. err := tx.QueryRowContext(ctx,
  136. `INSERT INTO channel_account_stats_pricing_rules (channel_id, name, group_ids, account_ids, sort_order)
  137. VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at, updated_at`,
  138. rule.ChannelID, rule.Name, pq.Array(rule.GroupIDs), pq.Array(rule.AccountIDs), rule.SortOrder,
  139. ).Scan(&rule.ID, &rule.CreatedAt, &rule.UpdatedAt)
  140. if err != nil {
  141. return fmt.Errorf("insert account stats pricing rule: %w", err)
  142. }
  143. for j := range rule.Pricing {
  144. if err := createAccountStatsModelPricingTx(ctx, tx, rule.ID, &rule.Pricing[j]); err != nil {
  145. return err
  146. }
  147. }
  148. return nil
  149. }
  150. // createAccountStatsModelPricingTx 在事务中创建单条账号统计模型定价
  151. func createAccountStatsModelPricingTx(ctx context.Context, tx *sql.Tx, ruleID int64, pricing *service.ChannelModelPricing) error {
  152. modelsJSON, err := json.Marshal(pricing.Models)
  153. if err != nil {
  154. return fmt.Errorf("marshal models: %w", err)
  155. }
  156. billingMode := pricing.BillingMode
  157. if billingMode == "" {
  158. billingMode = service.BillingModeToken
  159. }
  160. platform := pricing.Platform
  161. err = tx.QueryRowContext(ctx,
  162. `INSERT INTO channel_account_stats_model_pricing (rule_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, image_output_price, per_request_price)
  163. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at`,
  164. ruleID, platform, modelsJSON, billingMode,
  165. pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice,
  166. pricing.ImageOutputPrice, pricing.PerRequestPrice,
  167. ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt)
  168. if err != nil {
  169. return fmt.Errorf("insert account stats model pricing: %w", err)
  170. }
  171. // Persist intervals (mirrors channel_pricing_intervals logic).
  172. for i := range pricing.Intervals {
  173. iv := &pricing.Intervals[i]
  174. iv.PricingID = pricing.ID
  175. if err := createAccountStatsIntervalTx(ctx, tx, iv); err != nil {
  176. return err
  177. }
  178. }
  179. return nil
  180. }
  181. // createAccountStatsIntervalTx inserts a single interval for an account stats pricing entry.
  182. func createAccountStatsIntervalTx(ctx context.Context, tx *sql.Tx, iv *service.PricingInterval) error {
  183. return tx.QueryRowContext(ctx,
  184. `INSERT INTO channel_account_stats_pricing_intervals
  185. (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, per_request_price, sort_order)
  186. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at`,
  187. iv.PricingID, iv.MinTokens, iv.MaxTokens, iv.TierLabel,
  188. iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheWrite1hPrice, iv.CacheReadPrice,
  189. iv.PerRequestPrice, iv.SortOrder,
  190. ).Scan(&iv.ID, &iv.CreatedAt, &iv.UpdatedAt)
  191. }
  192. // batchLoadAccountStatsIntervals loads intervals for account stats pricing entries.
  193. func (r *channelRepository) batchLoadAccountStatsIntervals(ctx context.Context, pricingIDs []int64) (map[int64][]service.PricingInterval, error) {
  194. if len(pricingIDs) == 0 {
  195. return nil, nil
  196. }
  197. rows, err := r.db.QueryContext(ctx,
  198. `SELECT id, pricing_id, min_tokens, max_tokens, tier_label,
  199. input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price,
  200. per_request_price, sort_order, created_at, updated_at
  201. FROM channel_account_stats_pricing_intervals
  202. WHERE pricing_id = ANY($1) ORDER BY pricing_id, sort_order, id`,
  203. pq.Array(pricingIDs),
  204. )
  205. if err != nil {
  206. return nil, fmt.Errorf("batch load account stats pricing intervals: %w", err)
  207. }
  208. defer func() { _ = rows.Close() }()
  209. result := make(map[int64][]service.PricingInterval)
  210. for rows.Next() {
  211. var iv service.PricingInterval
  212. if err := rows.Scan(
  213. &iv.ID, &iv.PricingID, &iv.MinTokens, &iv.MaxTokens, &iv.TierLabel,
  214. &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheWrite1hPrice, &iv.CacheReadPrice,
  215. &iv.PerRequestPrice, &iv.SortOrder, &iv.CreatedAt, &iv.UpdatedAt,
  216. ); err != nil {
  217. return nil, fmt.Errorf("scan account stats pricing interval: %w", err)
  218. }
  219. result[iv.PricingID] = append(result[iv.PricingID], iv)
  220. }
  221. return result, rows.Err()
  222. }