available_channel_handler.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. package handler
  2. import (
  3. "sort"
  4. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  5. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. "github.com/gin-gonic/gin"
  8. )
  9. // AvailableChannelHandler 处理用户侧「可用渠道」查询。
  10. //
  11. // 用户侧接口委托 ChannelService.ListAvailable,并在返回前做四层过滤:
  12. // 1. 行过滤:只保留状态为 Active 且与当前用户可访问分组有交集的渠道;
  13. // 2. 分组过滤:渠道的 Groups 只保留用户可访问的那些;
  14. // 3. 平台过滤:普通分组只保留自身平台模型;Composite 分组按渠道已配置的具体模型平台
  15. // 展开。这样既防止普通分组跨平台泄漏,也让 Composite 正确展示其多平台能力;
  16. // 4. 字段白名单:仅返回用户需要的字段(省略 BillingModelSource / RestrictModels
  17. // / 内部 ID / Status 等管理字段)。
  18. type AvailableChannelHandler struct {
  19. channelService *service.ChannelService
  20. apiKeyService *service.APIKeyService
  21. settingService *service.SettingService
  22. }
  23. // NewAvailableChannelHandler 创建用户侧可用渠道 handler。
  24. func NewAvailableChannelHandler(
  25. channelService *service.ChannelService,
  26. apiKeyService *service.APIKeyService,
  27. settingService *service.SettingService,
  28. ) *AvailableChannelHandler {
  29. return &AvailableChannelHandler{
  30. channelService: channelService,
  31. apiKeyService: apiKeyService,
  32. settingService: settingService,
  33. }
  34. }
  35. // featureEnabled 返回 available-channels 开关是否启用。默认关闭(opt-in)。
  36. func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool {
  37. if h.settingService == nil {
  38. return false
  39. }
  40. return h.settingService.GetAvailableChannelsRuntime(c.Request.Context()).Enabled
  41. }
  42. // userAvailableGroup 用户可见的分组概要(白名单字段)。
  43. //
  44. // 前端据此区分专属 vs 公开分组(IsExclusive)、订阅 vs 标准分组(SubscriptionType,
  45. // 订阅视觉加深),并展示默认倍率与高峰倍率规则;用户专属倍率前端走
  46. // /groups/rates,和 API 密钥页面保持一致。
  47. type userAvailableGroup struct {
  48. ID int64 `json:"id"`
  49. Name string `json:"name"`
  50. Platform string `json:"platform"`
  51. SubscriptionType string `json:"subscription_type"`
  52. RateMultiplier float64 `json:"rate_multiplier"`
  53. PeakRateEnabled bool `json:"peak_rate_enabled"`
  54. PeakStart string `json:"peak_start"`
  55. PeakEnd string `json:"peak_end"`
  56. PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
  57. IsExclusive bool `json:"is_exclusive"`
  58. }
  59. // userSupportedModelPricing 用户可见的定价字段白名单。
  60. type userSupportedModelPricing struct {
  61. BillingMode string `json:"billing_mode"`
  62. InputPrice *float64 `json:"input_price"`
  63. OutputPrice *float64 `json:"output_price"`
  64. CacheWritePrice *float64 `json:"cache_write_price"`
  65. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  66. CacheReadPrice *float64 `json:"cache_read_price"`
  67. MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier,omitempty"`
  68. ImageInputPrice *float64 `json:"image_input_price"`
  69. ImageOutputPrice *float64 `json:"image_output_price"`
  70. PerRequestPrice *float64 `json:"per_request_price"`
  71. Intervals []userPricingIntervalDTO `json:"intervals"`
  72. }
  73. // userPricingIntervalDTO 定价区间白名单(去掉内部 ID、SortOrder 等前端不渲染的字段)。
  74. type userPricingIntervalDTO struct {
  75. MinTokens int `json:"min_tokens"`
  76. MaxTokens *int `json:"max_tokens"`
  77. TierLabel string `json:"tier_label,omitempty"`
  78. InputPrice *float64 `json:"input_price"`
  79. OutputPrice *float64 `json:"output_price"`
  80. CacheWritePrice *float64 `json:"cache_write_price"`
  81. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  82. CacheReadPrice *float64 `json:"cache_read_price"`
  83. InputMultiplier *float64 `json:"input_multiplier"`
  84. OutputMultiplier *float64 `json:"output_multiplier"`
  85. CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
  86. CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
  87. PerRequestPrice *float64 `json:"per_request_price"`
  88. }
  89. // userSupportedModel 用户可见的支持模型条目。
  90. type userSupportedModel struct {
  91. Name string `json:"name"`
  92. Platform string `json:"platform"`
  93. Pricing *userSupportedModelPricing `json:"pricing"`
  94. }
  95. // userChannelPlatformSection 单渠道内某个平台的子视图:用户可见的分组 + 该平台
  96. // 支持的模型。按 platform 聚合后让前端可以把渠道名作为 row-group 一次渲染,
  97. // 后面的平台行按 sections 顺序铺开。
  98. type userChannelPlatformSection struct {
  99. Platform string `json:"platform"`
  100. Groups []userAvailableGroup `json:"groups"`
  101. SupportedModels []userSupportedModel `json:"supported_models"`
  102. }
  103. // userAvailableChannel 用户可见的渠道条目(白名单字段)。
  104. //
  105. // 每个渠道聚合为一条记录,内嵌 platforms 子数组:每个 section 对应一个平台,
  106. // 包含该平台的 groups 和 supported_models。
  107. type userAvailableChannel struct {
  108. Name string `json:"name"`
  109. Description string `json:"description"`
  110. Platforms []userChannelPlatformSection `json:"platforms"`
  111. }
  112. // List 列出当前用户可见的「可用渠道」。
  113. // GET /api/v1/channels/available
  114. func (h *AvailableChannelHandler) List(c *gin.Context) {
  115. subject, ok := middleware.GetAuthSubjectFromContext(c)
  116. if !ok {
  117. response.Unauthorized(c, "User not authenticated")
  118. return
  119. }
  120. // Feature 未启用时返回空数组(不暴露渠道信息)。检查放在认证之后,
  121. // 保持与未开关前的 401 行为一致:未登录先 401,登录后再按开关决定。
  122. if !h.featureEnabled(c) {
  123. response.Success(c, []userAvailableChannel{})
  124. return
  125. }
  126. userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
  127. if err != nil {
  128. response.ErrorFrom(c, err)
  129. return
  130. }
  131. allowedGroupIDs := make(map[int64]struct{}, len(userGroups))
  132. for i := range userGroups {
  133. allowedGroupIDs[userGroups[i].ID] = struct{}{}
  134. }
  135. channels, err := h.channelService.ListAvailable(c.Request.Context())
  136. if err != nil {
  137. response.ErrorFrom(c, err)
  138. return
  139. }
  140. out := make([]userAvailableChannel, 0, len(channels))
  141. for _, ch := range channels {
  142. if ch.Status != service.StatusActive {
  143. continue
  144. }
  145. visibleGroups := filterUserVisibleGroups(ch.Groups, allowedGroupIDs)
  146. if len(visibleGroups) == 0 {
  147. continue
  148. }
  149. sections := buildPlatformSections(ch, visibleGroups)
  150. if len(sections) == 0 {
  151. continue
  152. }
  153. out = append(out, userAvailableChannel{
  154. Name: ch.Name,
  155. Description: ch.Description,
  156. Platforms: sections,
  157. })
  158. }
  159. response.Success(c, out)
  160. }
  161. // buildPlatformSections 把一个渠道按 visibleGroups 的平台集合拆成有序的 section 列表:
  162. // 每个 section 对应一个具体平台,只包含该平台的 groups 和 supported_models。
  163. //
  164. // Composite 分组可访问渠道中所有已配置的具体平台,因此会被展开到每个有支持模型的
  165. // 平台 section。普通分组仍严格留在自身平台,避免跨平台模型信息泄漏。Composite 渠道
  166. // 尚未配置任何模型时保留 composite section,以便前端继续展示该分组和“未配置模型”状态。
  167. // 输出按 platform 字母序稳定排序,便于前端等效比较与回归测试。
  168. func buildPlatformSections(
  169. ch service.AvailableChannel,
  170. visibleGroups []userAvailableGroup,
  171. ) []userChannelPlatformSection {
  172. groupsByPlatform := make(map[string][]userAvailableGroup, 4)
  173. compositeGroups := make([]userAvailableGroup, 0, 1)
  174. for _, g := range visibleGroups {
  175. if g.Platform == "" {
  176. continue
  177. }
  178. if g.Platform == service.PlatformComposite {
  179. compositeGroups = append(compositeGroups, g)
  180. continue
  181. }
  182. groupsByPlatform[g.Platform] = append(groupsByPlatform[g.Platform], g)
  183. }
  184. if len(compositeGroups) > 0 {
  185. modelPlatforms := make(map[string]struct{}, len(ch.SupportedModels))
  186. for i := range ch.SupportedModels {
  187. if platform := ch.SupportedModels[i].Platform; platform != "" {
  188. modelPlatforms[platform] = struct{}{}
  189. }
  190. }
  191. if len(modelPlatforms) == 0 {
  192. groupsByPlatform[service.PlatformComposite] = append(
  193. groupsByPlatform[service.PlatformComposite],
  194. compositeGroups...,
  195. )
  196. } else {
  197. for platform := range modelPlatforms {
  198. groupsByPlatform[platform] = append(groupsByPlatform[platform], compositeGroups...)
  199. }
  200. }
  201. }
  202. if len(groupsByPlatform) == 0 {
  203. return nil
  204. }
  205. platforms := make([]string, 0, len(groupsByPlatform))
  206. for p := range groupsByPlatform {
  207. platforms = append(platforms, p)
  208. }
  209. sort.Strings(platforms)
  210. sections := make([]userChannelPlatformSection, 0, len(platforms))
  211. for _, platform := range platforms {
  212. platformSet := map[string]struct{}{platform: {}}
  213. sections = append(sections, userChannelPlatformSection{
  214. Platform: platform,
  215. Groups: groupsByPlatform[platform],
  216. SupportedModels: toUserSupportedModels(ch.SupportedModels, platformSet),
  217. })
  218. }
  219. return sections
  220. }
  221. // filterUserVisibleGroups 仅保留用户可访问的分组。
  222. func filterUserVisibleGroups(
  223. groups []service.AvailableGroupRef,
  224. allowed map[int64]struct{},
  225. ) []userAvailableGroup {
  226. visible := make([]userAvailableGroup, 0, len(groups))
  227. for _, g := range groups {
  228. if _, ok := allowed[g.ID]; !ok {
  229. continue
  230. }
  231. visible = append(visible, userAvailableGroup{
  232. ID: g.ID,
  233. Name: g.Name,
  234. Platform: g.Platform,
  235. SubscriptionType: g.SubscriptionType,
  236. RateMultiplier: g.RateMultiplier,
  237. PeakRateEnabled: g.PeakRateEnabled,
  238. PeakStart: g.PeakStart,
  239. PeakEnd: g.PeakEnd,
  240. PeakRateMultiplier: g.PeakRateMultiplier,
  241. IsExclusive: g.IsExclusive,
  242. })
  243. }
  244. return visible
  245. }
  246. // toUserSupportedModels 将 service 层支持模型转换为用户 DTO(字段白名单)。
  247. // 仅保留平台在 allowedPlatforms 中的条目,防止跨平台模型信息泄漏。
  248. // allowedPlatforms 为 nil 时不做平台过滤(保留全部,供测试或明确无过滤场景使用)。
  249. func toUserSupportedModels(
  250. src []service.SupportedModel,
  251. allowedPlatforms map[string]struct{},
  252. ) []userSupportedModel {
  253. out := make([]userSupportedModel, 0, len(src))
  254. for i := range src {
  255. m := src[i]
  256. if allowedPlatforms != nil {
  257. if _, ok := allowedPlatforms[m.Platform]; !ok {
  258. continue
  259. }
  260. }
  261. out = append(out, userSupportedModel{
  262. Name: m.Name,
  263. Platform: m.Platform,
  264. Pricing: toUserPricing(m.Pricing),
  265. })
  266. }
  267. return out
  268. }
  269. // toUserPricingIntervals 将定价区间转换为用户 DTO 白名单形态;nil 入参返回 nil(JSON omitempty 可省略)。
  270. func toUserPricingIntervals(src []service.PricingInterval) []userPricingIntervalDTO {
  271. if src == nil {
  272. return nil
  273. }
  274. intervals := make([]userPricingIntervalDTO, 0, len(src))
  275. for _, iv := range src {
  276. intervals = append(intervals, userPricingIntervalDTO{
  277. MinTokens: iv.MinTokens,
  278. MaxTokens: iv.MaxTokens,
  279. TierLabel: iv.TierLabel,
  280. InputPrice: iv.InputPrice,
  281. OutputPrice: iv.OutputPrice,
  282. CacheWritePrice: iv.CacheWritePrice,
  283. CacheWrite1hPrice: iv.CacheWrite1hPrice,
  284. CacheReadPrice: iv.CacheReadPrice,
  285. InputMultiplier: iv.InputMultiplier,
  286. OutputMultiplier: iv.OutputMultiplier,
  287. CacheWriteMultiplier: iv.CacheWriteMultiplier,
  288. CacheReadMultiplier: iv.CacheReadMultiplier,
  289. PerRequestPrice: iv.PerRequestPrice,
  290. })
  291. }
  292. return intervals
  293. }
  294. // toUserPricing 将 service 层定价转换为用户 DTO;入参为 nil 时返回 nil。
  295. func toUserPricing(p *service.ChannelModelPricing) *userSupportedModelPricing {
  296. if p == nil {
  297. return nil
  298. }
  299. intervals := toUserPricingIntervals(p.Intervals)
  300. if intervals == nil {
  301. // 用户侧定价的 intervals 固定输出数组(空配置为 []),保持既有契约。
  302. intervals = []userPricingIntervalDTO{}
  303. }
  304. billingMode := string(p.BillingMode)
  305. if billingMode == "" {
  306. billingMode = string(service.BillingModeToken)
  307. }
  308. return &userSupportedModelPricing{
  309. BillingMode: billingMode,
  310. InputPrice: p.InputPrice,
  311. OutputPrice: p.OutputPrice,
  312. CacheWritePrice: p.CacheWritePrice,
  313. CacheWrite1hPrice: p.CacheWrite1hPrice,
  314. CacheReadPrice: p.CacheReadPrice,
  315. MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
  316. ImageInputPrice: p.ImageInputPrice,
  317. ImageOutputPrice: p.ImageOutputPrice,
  318. PerRequestPrice: p.PerRequestPrice,
  319. Intervals: intervals,
  320. }
  321. }