channel_monitor_template_handler.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package admin
  2. import (
  3. "strconv"
  4. "strings"
  5. "time"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/gin-gonic/gin"
  10. )
  11. // ChannelMonitorRequestTemplateHandler 请求模板管理后台 handler。
  12. type ChannelMonitorRequestTemplateHandler struct {
  13. templateService *service.ChannelMonitorRequestTemplateService
  14. }
  15. // NewChannelMonitorRequestTemplateHandler 创建 handler。
  16. func NewChannelMonitorRequestTemplateHandler(templateService *service.ChannelMonitorRequestTemplateService) *ChannelMonitorRequestTemplateHandler {
  17. return &ChannelMonitorRequestTemplateHandler{templateService: templateService}
  18. }
  19. // --- DTO ---
  20. type channelMonitorTemplateCreateRequest struct {
  21. Name string `json:"name" binding:"required,max=100"`
  22. Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok"`
  23. APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
  24. Description string `json:"description" binding:"max=500"`
  25. ExtraHeaders map[string]string `json:"extra_headers"`
  26. BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
  27. BodyOverride map[string]any `json:"body_override"`
  28. }
  29. type channelMonitorTemplateUpdateRequest struct {
  30. Name *string `json:"name" binding:"omitempty,max=100"`
  31. APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
  32. Description *string `json:"description" binding:"omitempty,max=500"`
  33. ExtraHeaders *map[string]string `json:"extra_headers"`
  34. BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
  35. BodyOverride *map[string]any `json:"body_override"`
  36. }
  37. type channelMonitorTemplateResponse struct {
  38. ID int64 `json:"id"`
  39. Name string `json:"name"`
  40. Provider string `json:"provider"`
  41. APIMode string `json:"api_mode"`
  42. Description string `json:"description"`
  43. ExtraHeaders map[string]string `json:"extra_headers"`
  44. BodyOverrideMode string `json:"body_override_mode"`
  45. BodyOverride map[string]any `json:"body_override"`
  46. CreatedAt string `json:"created_at"`
  47. UpdatedAt string `json:"updated_at"`
  48. AssociatedMonitors int64 `json:"associated_monitors"`
  49. }
  50. func (h *ChannelMonitorRequestTemplateHandler) toResponse(c *gin.Context, t *service.ChannelMonitorRequestTemplate) *channelMonitorTemplateResponse {
  51. if t == nil {
  52. return nil
  53. }
  54. headers := t.ExtraHeaders
  55. if headers == nil {
  56. headers = map[string]string{}
  57. }
  58. count, _ := h.templateService.CountAssociatedMonitors(c.Request.Context(), t.ID)
  59. return &channelMonitorTemplateResponse{
  60. ID: t.ID,
  61. Name: t.Name,
  62. Provider: t.Provider,
  63. APIMode: t.APIMode,
  64. Description: t.Description,
  65. ExtraHeaders: headers,
  66. BodyOverrideMode: t.BodyOverrideMode,
  67. BodyOverride: t.BodyOverride,
  68. CreatedAt: t.CreatedAt.UTC().Format(time.RFC3339),
  69. UpdatedAt: t.UpdatedAt.UTC().Format(time.RFC3339),
  70. AssociatedMonitors: count,
  71. }
  72. }
  73. // parseTemplateID 提取并校验 :id。
  74. func parseTemplateID(c *gin.Context) (int64, bool) {
  75. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  76. if err != nil || id <= 0 {
  77. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_TEMPLATE_ID", "invalid template id"))
  78. return 0, false
  79. }
  80. return id, true
  81. }
  82. // --- Handlers ---
  83. // List GET /api/v1/admin/channel-monitor-templates?provider=anthropic
  84. func (h *ChannelMonitorRequestTemplateHandler) List(c *gin.Context) {
  85. items, err := h.templateService.List(c.Request.Context(), service.ChannelMonitorRequestTemplateListParams{
  86. Provider: strings.TrimSpace(c.Query("provider")),
  87. APIMode: strings.TrimSpace(c.Query("api_mode")),
  88. })
  89. if err != nil {
  90. response.ErrorFrom(c, err)
  91. return
  92. }
  93. out := make([]*channelMonitorTemplateResponse, 0, len(items))
  94. for _, t := range items {
  95. out = append(out, h.toResponse(c, t))
  96. }
  97. response.Success(c, gin.H{"items": out})
  98. }
  99. // Get GET /api/v1/admin/channel-monitor-templates/:id
  100. func (h *ChannelMonitorRequestTemplateHandler) Get(c *gin.Context) {
  101. id, ok := parseTemplateID(c)
  102. if !ok {
  103. return
  104. }
  105. t, err := h.templateService.Get(c.Request.Context(), id)
  106. if err != nil {
  107. response.ErrorFrom(c, err)
  108. return
  109. }
  110. response.Success(c, h.toResponse(c, t))
  111. }
  112. // Create POST /api/v1/admin/channel-monitor-templates
  113. func (h *ChannelMonitorRequestTemplateHandler) Create(c *gin.Context) {
  114. var req channelMonitorTemplateCreateRequest
  115. if err := c.ShouldBindJSON(&req); err != nil {
  116. response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
  117. return
  118. }
  119. t, err := h.templateService.Create(c.Request.Context(), service.ChannelMonitorRequestTemplateCreateParams{
  120. Name: req.Name,
  121. Provider: req.Provider,
  122. APIMode: req.APIMode,
  123. Description: req.Description,
  124. ExtraHeaders: req.ExtraHeaders,
  125. BodyOverrideMode: req.BodyOverrideMode,
  126. BodyOverride: req.BodyOverride,
  127. })
  128. if err != nil {
  129. response.ErrorFrom(c, err)
  130. return
  131. }
  132. response.Created(c, h.toResponse(c, t))
  133. }
  134. // Update PUT /api/v1/admin/channel-monitor-templates/:id
  135. func (h *ChannelMonitorRequestTemplateHandler) Update(c *gin.Context) {
  136. id, ok := parseTemplateID(c)
  137. if !ok {
  138. return
  139. }
  140. var req channelMonitorTemplateUpdateRequest
  141. if err := c.ShouldBindJSON(&req); err != nil {
  142. response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
  143. return
  144. }
  145. t, err := h.templateService.Update(c.Request.Context(), id, service.ChannelMonitorRequestTemplateUpdateParams{
  146. Name: req.Name,
  147. APIMode: req.APIMode,
  148. Description: req.Description,
  149. ExtraHeaders: req.ExtraHeaders,
  150. BodyOverrideMode: req.BodyOverrideMode,
  151. BodyOverride: req.BodyOverride,
  152. })
  153. if err != nil {
  154. response.ErrorFrom(c, err)
  155. return
  156. }
  157. response.Success(c, h.toResponse(c, t))
  158. }
  159. // Delete DELETE /api/v1/admin/channel-monitor-templates/:id
  160. func (h *ChannelMonitorRequestTemplateHandler) Delete(c *gin.Context) {
  161. id, ok := parseTemplateID(c)
  162. if !ok {
  163. return
  164. }
  165. if err := h.templateService.Delete(c.Request.Context(), id); err != nil {
  166. response.ErrorFrom(c, err)
  167. return
  168. }
  169. response.Success(c, nil)
  170. }
  171. type channelMonitorTemplateApplyRequest struct {
  172. // MonitorIDs 必填、非空:用户在 picker 里勾选的要被覆盖的监控 ID 列表。
  173. // 仅当对应监控当前 template_id == :id 时才会真的被覆盖。
  174. MonitorIDs []int64 `json:"monitor_ids" binding:"required,min=1"`
  175. }
  176. // Apply POST /api/v1/admin/channel-monitor-templates/:id/apply
  177. // 把模板当前配置覆盖到 monitor_ids 列表里的关联监控(picker 选中的子集)。
  178. func (h *ChannelMonitorRequestTemplateHandler) Apply(c *gin.Context) {
  179. id, ok := parseTemplateID(c)
  180. if !ok {
  181. return
  182. }
  183. var req channelMonitorTemplateApplyRequest
  184. if err := c.ShouldBindJSON(&req); err != nil {
  185. response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
  186. return
  187. }
  188. affected, err := h.templateService.ApplyToMonitors(c.Request.Context(), id, req.MonitorIDs)
  189. if err != nil {
  190. response.ErrorFrom(c, err)
  191. return
  192. }
  193. response.Success(c, gin.H{"affected": affected})
  194. }
  195. type associatedMonitorBriefResponse struct {
  196. ID int64 `json:"id"`
  197. Name string `json:"name"`
  198. Provider string `json:"provider"`
  199. APIMode string `json:"api_mode"`
  200. Enabled bool `json:"enabled"`
  201. }
  202. // AssociatedMonitors GET /api/v1/admin/channel-monitor-templates/:id/monitors
  203. // 列出关联监控(picker 弹窗用)。
  204. func (h *ChannelMonitorRequestTemplateHandler) AssociatedMonitors(c *gin.Context) {
  205. id, ok := parseTemplateID(c)
  206. if !ok {
  207. return
  208. }
  209. items, err := h.templateService.ListAssociatedMonitors(c.Request.Context(), id)
  210. if err != nil {
  211. response.ErrorFrom(c, err)
  212. return
  213. }
  214. out := make([]associatedMonitorBriefResponse, 0, len(items))
  215. for _, m := range items {
  216. out = append(out, associatedMonitorBriefResponse{
  217. ID: m.ID, Name: m.Name, Provider: m.Provider, APIMode: m.APIMode, Enabled: m.Enabled,
  218. })
  219. }
  220. response.Success(c, gin.H{"items": out})
  221. }