model_plaza_handler.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package handler
  2. import (
  3. "log/slog"
  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. // ModelPlazaHandler 处理「模型广场」查询。
  10. //
  11. // 广场路由挂 OptionalJWT 中间件:匿名可访问(除非 require_auth 开启),带 token 则
  12. // 识别用户。可见性规则(橱窗语义,与「可用渠道」的可绑定语义不同):
  13. // - 匿名:仅非专属分组(订阅型照常展示);
  14. // - 登录:非专属分组 + user_allowed_groups 授权或持有有效订阅的专属分组;
  15. // 若该用户开启了公开分组限制,则公开分组同样需要落在授权集合内。
  16. type ModelPlazaHandler struct {
  17. plazaService *service.ModelPlazaService
  18. apiKeyService *service.APIKeyService
  19. settingService *service.SettingService
  20. }
  21. // NewModelPlazaHandler 创建模型广场 handler。
  22. func NewModelPlazaHandler(
  23. plazaService *service.ModelPlazaService,
  24. apiKeyService *service.APIKeyService,
  25. settingService *service.SettingService,
  26. ) *ModelPlazaHandler {
  27. return &ModelPlazaHandler{
  28. plazaService: plazaService,
  29. apiKeyService: apiKeyService,
  30. settingService: settingService,
  31. }
  32. }
  33. // modelPlazaOfficialPricing 官方参考价(USD per token,与计费目录同源)。
  34. type modelPlazaOfficialPricing struct {
  35. InputPrice *float64 `json:"input_price"`
  36. OutputPrice *float64 `json:"output_price"`
  37. CacheWritePrice *float64 `json:"cache_write_price"`
  38. CacheWrite1hPrice *float64 `json:"cache_write_1h_price,omitempty"`
  39. CacheReadPrice *float64 `json:"cache_read_price"`
  40. // Intervals 官方长上下文阶梯,仅多档模型给出。
  41. Intervals []userPricingIntervalDTO `json:"intervals,omitempty"`
  42. }
  43. // modelPlazaTimePricingPeriod 分时倍率时段(配置时区当天 [start, end))。
  44. type modelPlazaTimePricingPeriod struct {
  45. StartTime string `json:"start_time"`
  46. EndTime string `json:"end_time"`
  47. Multiplier float64 `json:"multiplier"`
  48. }
  49. // modelPlazaTimePricing 计费会生效的分时倍率(仅倍率 ≠ 1 的时段)。
  50. // WeekdaysOnly 为 true 时时段仅周一至周五生效,周末整天按标准价计费。
  51. type modelPlazaTimePricing struct {
  52. Timezone string `json:"timezone"`
  53. WeekdaysOnly bool `json:"weekdays_only,omitempty"`
  54. Periods []modelPlazaTimePricingPeriod `json:"periods"`
  55. }
  56. // modelPlazaModel 广场模型条目:实收口径展示定价(白名单形态)+ 官方参考价。
  57. type modelPlazaModel struct {
  58. Name string `json:"name"`
  59. Platform string `json:"platform"`
  60. Pricing *userSupportedModelPricing `json:"pricing"`
  61. OfficialPricing *modelPlazaOfficialPricing `json:"official_pricing"`
  62. // LongContextBasis 多档时的计价基准:"whole_request"(整单按档)| "marginal"(仅超出部分)。
  63. LongContextBasis string `json:"long_context_basis,omitempty"`
  64. // TimePricing 分时倍率时段,落在时段内的请求整单乘倍率;无分时省略。
  65. TimePricing *modelPlazaTimePricing `json:"time_pricing,omitempty"`
  66. }
  67. // modelPlazaGroup 广场分组条目(白名单字段)。
  68. type modelPlazaGroup struct {
  69. ID int64 `json:"id"`
  70. Name string `json:"name"`
  71. Description string `json:"description"`
  72. Platform string `json:"platform"`
  73. SubscriptionType string `json:"subscription_type"`
  74. RateMultiplier float64 `json:"rate_multiplier"`
  75. UserRateMultiplier *float64 `json:"user_rate_multiplier,omitempty"`
  76. PeakRateEnabled bool `json:"peak_rate_enabled"`
  77. PeakStart string `json:"peak_start"`
  78. PeakEnd string `json:"peak_end"`
  79. PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
  80. IsExclusive bool `json:"is_exclusive"`
  81. // 生图独立倍率:为 true 时图片计费模型的实付倍率取 ImageRateMultiplier,
  82. // 不取分组/用户专属倍率。
  83. ImageRateIndependent bool `json:"image_rate_independent"`
  84. ImageRateMultiplier float64 `json:"image_rate_multiplier"`
  85. // 分组是否启用长上下文阶梯计费;关闭时模型实付列只展示最低档/基础价。
  86. LongContextPricingEnabled bool `json:"long_context_pricing_enabled"`
  87. Models []modelPlazaModel `json:"models"`
  88. }
  89. // modelPlazaResponse 广场页响应。
  90. type modelPlazaResponse struct {
  91. Description string `json:"description"`
  92. Groups []modelPlazaGroup `json:"groups"`
  93. }
  94. // Get 返回模型广场数据。
  95. // GET /api/v1/model-plaza
  96. func (h *ModelPlazaHandler) Get(c *gin.Context) {
  97. if h.settingService == nil {
  98. response.NotFound(c, "Model plaza is not enabled")
  99. return
  100. }
  101. rt := h.settingService.GetModelPlazaRuntime(c.Request.Context())
  102. if !rt.Enabled {
  103. response.NotFound(c, "Model plaza is not enabled")
  104. return
  105. }
  106. subject, authed := middleware.GetAuthSubjectFromContext(c)
  107. if rt.RequireAuth && !authed {
  108. response.Unauthorized(c, "Authentication required")
  109. return
  110. }
  111. groups, err := h.plazaService.ListGroups(c.Request.Context())
  112. if err != nil {
  113. response.ErrorFrom(c, err)
  114. return
  115. }
  116. // allowedGroups == nil 表示匿名;登录用户恒为非 nil(可能为空集合)。
  117. var allowedGroups map[int64]struct{}
  118. var restrictPublicGroups bool
  119. var userRates map[int64]float64
  120. if authed {
  121. allowedGroups, restrictPublicGroups, err = h.apiKeyService.GetUserGroupVisibility(c.Request.Context(), subject.UserID)
  122. if err != nil {
  123. // 可见性数据拿不到时不能静默降级成匿名视图(会错漏专属分组),直接报错。
  124. response.ErrorFrom(c, err)
  125. return
  126. }
  127. userRates, err = h.apiKeyService.GetUserGroupRates(c.Request.Context(), subject.UserID)
  128. if err != nil {
  129. // 专属倍率仅是展示增强,失败降级为分组默认倍率。
  130. slog.Warn("model_plaza_user_rates_failed", "error", err, "user_id", subject.UserID)
  131. userRates = nil
  132. }
  133. }
  134. visible := filterPlazaVisibleGroups(groups, allowedGroups, restrictPublicGroups)
  135. out := make([]modelPlazaGroup, 0, len(visible))
  136. for i := range visible {
  137. out = append(out, toModelPlazaGroupDTO(&visible[i], userRates))
  138. }
  139. response.Success(c, modelPlazaResponse{
  140. Description: rt.Description,
  141. Groups: out,
  142. })
  143. }
  144. // filterPlazaVisibleGroups 按登录态裁剪分组可见性。
  145. // allowedGroups == nil 表示匿名(仅非专属);非 nil 包含普通授权及有效订阅分组。
  146. // restrictPublicGroups 为 true 时,公开分组也必须落在 allowedGroups 内,否则用户会
  147. // 在广场看到自己实际绑定不了的分组。
  148. func filterPlazaVisibleGroups(
  149. groups []service.PlazaGroup,
  150. allowedGroups map[int64]struct{},
  151. restrictPublicGroups bool,
  152. ) []service.PlazaGroup {
  153. visible := make([]service.PlazaGroup, 0, len(groups))
  154. for _, g := range groups {
  155. if g.IsExclusive || (restrictPublicGroups && allowedGroups != nil) {
  156. if allowedGroups == nil {
  157. continue
  158. }
  159. if _, ok := allowedGroups[g.ID]; !ok {
  160. continue
  161. }
  162. }
  163. visible = append(visible, g)
  164. }
  165. return visible
  166. }
  167. // toModelPlazaGroupDTO 将 service 层广场分组映射为白名单 DTO,并合并用户专属倍率。
  168. func toModelPlazaGroupDTO(g *service.PlazaGroup, userRates map[int64]float64) modelPlazaGroup {
  169. models := make([]modelPlazaModel, 0, len(g.Models))
  170. for i := range g.Models {
  171. m := &g.Models[i]
  172. models = append(models, modelPlazaModel{
  173. Name: m.Name,
  174. Platform: m.Platform,
  175. Pricing: toUserPricing(m.Pricing),
  176. OfficialPricing: toModelPlazaOfficialPricing(m.OfficialPricing),
  177. LongContextBasis: string(m.LongContextBasis),
  178. TimePricing: toModelPlazaTimePricing(m.TimePricing),
  179. })
  180. }
  181. dto := modelPlazaGroup{
  182. ID: g.ID,
  183. Name: g.Name,
  184. Description: g.Description,
  185. Platform: g.Platform,
  186. SubscriptionType: g.SubscriptionType,
  187. RateMultiplier: g.RateMultiplier,
  188. PeakRateEnabled: g.PeakRateEnabled,
  189. PeakStart: g.PeakStart,
  190. PeakEnd: g.PeakEnd,
  191. PeakRateMultiplier: g.PeakRateMultiplier,
  192. IsExclusive: g.IsExclusive,
  193. ImageRateIndependent: g.ImageRateIndependent,
  194. ImageRateMultiplier: g.ImageRateMultiplier,
  195. LongContextPricingEnabled: g.LongContextPricingEnabled,
  196. Models: models,
  197. }
  198. if rate, ok := userRates[g.ID]; ok {
  199. dto.UserRateMultiplier = &rate
  200. }
  201. return dto
  202. }
  203. // toModelPlazaTimePricing 转换分时倍率;nil 透传(JSON 省略)。
  204. func toModelPlazaTimePricing(p *service.TimePricingSchedule) *modelPlazaTimePricing {
  205. if p == nil || len(p.Periods) == 0 {
  206. return nil
  207. }
  208. periods := make([]modelPlazaTimePricingPeriod, 0, len(p.Periods))
  209. for _, period := range p.Periods {
  210. periods = append(periods, modelPlazaTimePricingPeriod{
  211. StartTime: period.StartTime,
  212. EndTime: period.EndTime,
  213. Multiplier: period.Multiplier,
  214. })
  215. }
  216. return &modelPlazaTimePricing{Timezone: p.Timezone, WeekdaysOnly: p.WeekdaysOnly, Periods: periods}
  217. }
  218. // toModelPlazaOfficialPricing 转换官方参考价;nil 透传(前端显示 "-")。
  219. func toModelPlazaOfficialPricing(p *service.PlazaOfficialPricing) *modelPlazaOfficialPricing {
  220. if p == nil {
  221. return nil
  222. }
  223. return &modelPlazaOfficialPricing{
  224. InputPrice: p.InputPrice,
  225. OutputPrice: p.OutputPrice,
  226. CacheWritePrice: p.CacheWritePrice,
  227. CacheWrite1hPrice: p.CacheWrite1hPrice,
  228. CacheReadPrice: p.CacheReadPrice,
  229. Intervals: toUserPricingIntervals(p.Intervals),
  230. }
  231. }