channel_repo_pricing.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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. // --- 模型定价 ---
  13. func (r *channelRepository) ListModelPricing(ctx context.Context, channelID int64) ([]service.ChannelModelPricing, error) {
  14. rows, err := r.db.QueryContext(ctx,
  15. `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at
  16. FROM channel_model_pricing WHERE channel_id = $1 ORDER BY id`, channelID,
  17. )
  18. if err != nil {
  19. return nil, fmt.Errorf("list model pricing: %w", err)
  20. }
  21. defer func() { _ = rows.Close() }()
  22. result, pricingIDs, err := scanModelPricingRows(rows)
  23. if err != nil {
  24. return nil, err
  25. }
  26. if len(pricingIDs) > 0 {
  27. intervalMap, err := r.batchLoadIntervals(ctx, pricingIDs)
  28. if err != nil {
  29. return nil, err
  30. }
  31. for i := range result {
  32. result[i].Intervals = intervalMap[result[i].ID]
  33. }
  34. }
  35. return result, nil
  36. }
  37. func (r *channelRepository) CreateModelPricing(ctx context.Context, pricing *service.ChannelModelPricing) error {
  38. return createModelPricingExec(ctx, r.db, pricing)
  39. }
  40. func (r *channelRepository) UpdateModelPricing(ctx context.Context, pricing *service.ChannelModelPricing) error {
  41. modelsJSON, err := json.Marshal(pricing.Models)
  42. if err != nil {
  43. return fmt.Errorf("marshal models: %w", err)
  44. }
  45. timePricingJSON, err := marshalChannelTimePricing(pricing.TimePricing)
  46. if err != nil {
  47. return err
  48. }
  49. billingMode := pricing.BillingMode
  50. if billingMode == "" {
  51. billingMode = service.BillingModeToken
  52. }
  53. result, err := r.db.ExecContext(ctx,
  54. `UPDATE channel_model_pricing
  55. SET models = $1, billing_mode = $2, input_price = $3, output_price = $4, cache_write_price = $5, cache_write_1h_price = $6, cache_read_price = $7, fast_multiplier = $8, flex_multiplier = $9, max_reasoning_effort_multiplier = $10, image_input_price = $11, image_output_price = $12, per_request_price = $13, time_pricing = $14, platform = $15, updated_at = NOW()
  56. WHERE id = $16`,
  57. modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice,
  58. pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice,
  59. timePricingJSON, pricing.Platform, pricing.ID,
  60. )
  61. if err != nil {
  62. return fmt.Errorf("update model pricing: %w", err)
  63. }
  64. rows, _ := result.RowsAffected()
  65. if rows == 0 {
  66. return fmt.Errorf("pricing entry not found: %d", pricing.ID)
  67. }
  68. return nil
  69. }
  70. func (r *channelRepository) DeleteModelPricing(ctx context.Context, id int64) error {
  71. _, err := r.db.ExecContext(ctx, `DELETE FROM channel_model_pricing WHERE id = $1`, id)
  72. if err != nil {
  73. return fmt.Errorf("delete model pricing: %w", err)
  74. }
  75. return nil
  76. }
  77. func (r *channelRepository) ReplaceModelPricing(ctx context.Context, channelID int64, pricingList []service.ChannelModelPricing) error {
  78. return r.runInTx(ctx, func(tx *sql.Tx) error {
  79. return replaceModelPricingTx(ctx, tx, channelID, pricingList)
  80. })
  81. }
  82. // --- 批量加载辅助方法 ---
  83. // batchLoadModelPricing 批量加载多个渠道的模型定价(含区间)
  84. func (r *channelRepository) batchLoadModelPricing(ctx context.Context, channelIDs []int64) (map[int64][]service.ChannelModelPricing, error) {
  85. rows, err := r.db.QueryContext(ctx,
  86. `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at
  87. FROM channel_model_pricing WHERE channel_id = ANY($1) ORDER BY channel_id, id`,
  88. pq.Array(channelIDs),
  89. )
  90. if err != nil {
  91. return nil, fmt.Errorf("batch load model pricing: %w", err)
  92. }
  93. defer func() { _ = rows.Close() }()
  94. allPricing, allPricingIDs, err := scanModelPricingRows(rows)
  95. if err != nil {
  96. return nil, err
  97. }
  98. // 按 channelID 分组
  99. pricingMap := make(map[int64][]service.ChannelModelPricing, len(channelIDs))
  100. for _, p := range allPricing {
  101. pricingMap[p.ChannelID] = append(pricingMap[p.ChannelID], p)
  102. }
  103. // 批量加载所有区间
  104. if len(allPricingIDs) > 0 {
  105. intervalMap, err := r.batchLoadIntervals(ctx, allPricingIDs)
  106. if err != nil {
  107. return nil, err
  108. }
  109. for chID := range pricingMap {
  110. for i := range pricingMap[chID] {
  111. pricingMap[chID][i].Intervals = intervalMap[pricingMap[chID][i].ID]
  112. }
  113. }
  114. }
  115. return pricingMap, nil
  116. }
  117. // batchLoadIntervals 批量加载多个定价条目的区间
  118. func (r *channelRepository) batchLoadIntervals(ctx context.Context, pricingIDs []int64) (map[int64][]service.PricingInterval, error) {
  119. rows, err := r.db.QueryContext(ctx,
  120. `SELECT id, pricing_id, min_tokens, max_tokens, tier_label,
  121. input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price,
  122. input_multiplier, output_multiplier, cache_write_multiplier, cache_read_multiplier,
  123. per_request_price, sort_order, created_at, updated_at
  124. FROM channel_pricing_intervals
  125. WHERE pricing_id = ANY($1) ORDER BY pricing_id, sort_order, id`,
  126. pq.Array(pricingIDs),
  127. )
  128. if err != nil {
  129. return nil, fmt.Errorf("batch load intervals: %w", err)
  130. }
  131. defer func() { _ = rows.Close() }()
  132. intervalMap := make(map[int64][]service.PricingInterval, len(pricingIDs))
  133. for rows.Next() {
  134. var iv service.PricingInterval
  135. if err := rows.Scan(
  136. &iv.ID, &iv.PricingID, &iv.MinTokens, &iv.MaxTokens, &iv.TierLabel,
  137. &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheWrite1hPrice, &iv.CacheReadPrice,
  138. &iv.InputMultiplier, &iv.OutputMultiplier, &iv.CacheWriteMultiplier, &iv.CacheReadMultiplier,
  139. &iv.PerRequestPrice, &iv.SortOrder, &iv.CreatedAt, &iv.UpdatedAt,
  140. ); err != nil {
  141. return nil, fmt.Errorf("scan interval: %w", err)
  142. }
  143. intervalMap[iv.PricingID] = append(intervalMap[iv.PricingID], iv)
  144. }
  145. if err := rows.Err(); err != nil {
  146. return nil, fmt.Errorf("iterate intervals: %w", err)
  147. }
  148. return intervalMap, nil
  149. }
  150. // --- 共享 scan 辅助 ---
  151. // scanModelPricingRows 扫描 model pricing 行,返回结果列表和 ID 列表
  152. func scanModelPricingRows(rows *sql.Rows) ([]service.ChannelModelPricing, []int64, error) {
  153. var result []service.ChannelModelPricing
  154. var pricingIDs []int64
  155. for rows.Next() {
  156. var p service.ChannelModelPricing
  157. var modelsJSON []byte
  158. var timePricingJSON []byte
  159. if err := rows.Scan(
  160. &p.ID, &p.ChannelID, &p.Platform, &modelsJSON, &p.BillingMode,
  161. &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice,
  162. &p.FastMultiplier, &p.FlexMultiplier, &p.MaxReasoningEffortMultiplier,
  163. &p.ImageInputPrice, &p.ImageOutputPrice, &p.PerRequestPrice, &timePricingJSON, &p.CreatedAt, &p.UpdatedAt,
  164. ); err != nil {
  165. return nil, nil, fmt.Errorf("scan model pricing: %w", err)
  166. }
  167. if err := json.Unmarshal(modelsJSON, &p.Models); err != nil {
  168. p.Models = []string{}
  169. }
  170. timePricing, err := unmarshalChannelTimePricing(timePricingJSON)
  171. if err != nil {
  172. return nil, nil, err
  173. }
  174. p.TimePricing = timePricing
  175. pricingIDs = append(pricingIDs, p.ID)
  176. result = append(result, p)
  177. }
  178. if err := rows.Err(); err != nil {
  179. return nil, nil, fmt.Errorf("iterate model pricing: %w", err)
  180. }
  181. return result, pricingIDs, nil
  182. }
  183. // --- 事务内辅助方法 ---
  184. // dbExec 是 *sql.DB 和 *sql.Tx 共享的最小 SQL 执行接口
  185. type dbExec interface {
  186. ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
  187. QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
  188. QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
  189. }
  190. func setGroupIDsTx(ctx context.Context, exec dbExec, channelID int64, groupIDs []int64) error {
  191. if _, err := exec.ExecContext(ctx, `DELETE FROM channel_groups WHERE channel_id = $1`, channelID); err != nil {
  192. return fmt.Errorf("delete old group associations: %w", err)
  193. }
  194. if len(groupIDs) == 0 {
  195. return nil
  196. }
  197. _, err := exec.ExecContext(ctx,
  198. `INSERT INTO channel_groups (channel_id, group_id)
  199. SELECT $1, unnest($2::bigint[])`,
  200. channelID, pq.Array(groupIDs),
  201. )
  202. if err != nil {
  203. return fmt.Errorf("insert group associations: %w", err)
  204. }
  205. return nil
  206. }
  207. func createModelPricingExec(ctx context.Context, exec dbExec, pricing *service.ChannelModelPricing) error {
  208. modelsJSON, err := json.Marshal(pricing.Models)
  209. if err != nil {
  210. return fmt.Errorf("marshal models: %w", err)
  211. }
  212. timePricingJSON, err := marshalChannelTimePricing(pricing.TimePricing)
  213. if err != nil {
  214. return err
  215. }
  216. billingMode := pricing.BillingMode
  217. if billingMode == "" {
  218. billingMode = service.BillingModeToken
  219. }
  220. platform := pricing.Platform
  221. if platform == "" {
  222. platform = "anthropic"
  223. }
  224. err = exec.QueryRowContext(ctx,
  225. `INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)
  226. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id, created_at, updated_at`,
  227. pricing.ChannelID, platform, modelsJSON, billingMode,
  228. pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice,
  229. pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice,
  230. pricing.PerRequestPrice, timePricingJSON,
  231. ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt)
  232. if err != nil {
  233. return fmt.Errorf("insert model pricing: %w", err)
  234. }
  235. for i := range pricing.Intervals {
  236. pricing.Intervals[i].PricingID = pricing.ID
  237. if err := createIntervalExec(ctx, exec, &pricing.Intervals[i]); err != nil {
  238. return err
  239. }
  240. }
  241. return nil
  242. }
  243. func marshalChannelTimePricing(config *service.ChannelTimePricing) (any, error) {
  244. if config == nil || len(config.Periods) == 0 {
  245. return nil, nil
  246. }
  247. data, err := json.Marshal(config)
  248. if err != nil {
  249. return nil, fmt.Errorf("marshal time pricing: %w", err)
  250. }
  251. return string(data), nil
  252. }
  253. func unmarshalChannelTimePricing(data []byte) (*service.ChannelTimePricing, error) {
  254. if len(data) == 0 {
  255. return nil, nil
  256. }
  257. var config service.ChannelTimePricing
  258. if err := json.Unmarshal(data, &config); err != nil {
  259. return nil, fmt.Errorf("unmarshal time pricing: %w", err)
  260. }
  261. return &config, nil
  262. }
  263. func createIntervalExec(ctx context.Context, exec dbExec, iv *service.PricingInterval) error {
  264. return exec.QueryRowContext(ctx,
  265. `INSERT INTO channel_pricing_intervals
  266. (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, input_multiplier, output_multiplier, cache_write_multiplier, cache_read_multiplier, per_request_price, sort_order)
  267. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at`,
  268. iv.PricingID, iv.MinTokens, iv.MaxTokens, iv.TierLabel,
  269. iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheWrite1hPrice, iv.CacheReadPrice,
  270. iv.InputMultiplier, iv.OutputMultiplier, iv.CacheWriteMultiplier, iv.CacheReadMultiplier,
  271. iv.PerRequestPrice, iv.SortOrder,
  272. ).Scan(&iv.ID, &iv.CreatedAt, &iv.UpdatedAt)
  273. }
  274. func replaceModelPricingTx(ctx context.Context, exec dbExec, channelID int64, pricingList []service.ChannelModelPricing) error {
  275. if _, err := exec.ExecContext(ctx, `DELETE FROM channel_model_pricing WHERE channel_id = $1`, channelID); err != nil {
  276. return fmt.Errorf("delete old model pricing: %w", err)
  277. }
  278. for i := range pricingList {
  279. pricingList[i].ChannelID = channelID
  280. if err := createModelPricingExec(ctx, exec, &pricingList[i]); err != nil {
  281. return fmt.Errorf("insert model pricing: %w", err)
  282. }
  283. }
  284. return nil
  285. }
  286. // isUniqueViolation 检查 pq 唯一约束违反错误
  287. func isUniqueViolation(err error) bool {
  288. var pqErr *pq.Error
  289. if errors.As(err, &pqErr) && pqErr != nil {
  290. return pqErr.Code == "23505"
  291. }
  292. return false
  293. }
  294. // escapeLike 转义 LIKE/ILIKE 模式中的特殊字符
  295. func escapeLike(s string) string {
  296. s = strings.ReplaceAll(s, `\`, `\\`)
  297. s = strings.ReplaceAll(s, `%`, `\%`)
  298. s = strings.ReplaceAll(s, `_`, `\_`)
  299. return s
  300. }