router.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package handler
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "time"
  7. "github.com/gin-gonic/gin"
  8. "github.com/redis/go-redis/v9"
  9. "spider/internal/config"
  10. "spider/internal/notification"
  11. "spider/internal/store"
  12. "spider/internal/task"
  13. "spider/internal/telegram"
  14. )
  15. // rateLimitMiddleware limits requests per IP using Redis.
  16. // maxRequests per window duration.
  17. func rateLimitMiddleware(rdb *redis.Client, prefix string, maxRequests int64, window time.Duration) gin.HandlerFunc {
  18. return func(c *gin.Context) {
  19. key := fmt.Sprintf("spider:ratelimit:%s:%s", prefix, c.ClientIP())
  20. ctx := context.Background()
  21. count, _ := rdb.Incr(ctx, key).Result()
  22. if count == 1 {
  23. rdb.Expire(ctx, key, window)
  24. }
  25. if count > maxRequests {
  26. c.AbortWithStatusJSON(http.StatusTooManyRequests, Response{
  27. Code: 429,
  28. Message: "请求过于频繁,请稍后再试",
  29. })
  30. return
  31. }
  32. c.Next()
  33. }
  34. }
  35. // scheduleHandler is a package-level reference so main.go can set the scheduler after creation.
  36. var scheduleHandler *ScheduleHandler
  37. // GetScheduleHandler returns the package-level ScheduleHandler so main.go
  38. // can call SetScheduler after the scheduler is created.
  39. func GetScheduleHandler() *ScheduleHandler {
  40. return scheduleHandler
  41. }
  42. // SetupRouter builds and returns the Gin engine with all routes registered.
  43. func SetupRouter(s *store.Store, taskMgr *task.Manager, rdb *redis.Client, tgMgr *telegram.AccountManager, tgAccountHandler *TgAccountHandler) *gin.Engine {
  44. r := gin.Default()
  45. // CORS middleware
  46. cfg := config.Get()
  47. var corsOrigins []string
  48. if cfg != nil {
  49. corsOrigins = cfg.Server.CORSOrigins
  50. }
  51. r.Use(corsMiddleware(corsOrigins))
  52. // Upload size limit
  53. maxUpload := 10
  54. if cfg != nil && cfg.Server.MaxUploadMB > 0 {
  55. maxUpload = cfg.Server.MaxUploadMB
  56. }
  57. r.MaxMultipartMemory = int64(maxUpload) << 20
  58. // Health check (public, no auth)
  59. r.GET("/ping", func(c *gin.Context) {
  60. health := gin.H{"status": "ok"}
  61. httpStatus := http.StatusOK
  62. sqlDB, err := s.DB.DB()
  63. if err != nil {
  64. health["mysql"] = "error: " + err.Error()
  65. health["status"] = "degraded"
  66. httpStatus = http.StatusServiceUnavailable
  67. } else {
  68. ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
  69. defer cancel()
  70. if err := sqlDB.PingContext(ctx); err != nil {
  71. health["mysql"] = "error: " + err.Error()
  72. health["status"] = "degraded"
  73. httpStatus = http.StatusServiceUnavailable
  74. } else {
  75. health["mysql"] = "ok"
  76. }
  77. }
  78. ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
  79. defer cancel()
  80. if err := rdb.Ping(ctx).Err(); err != nil {
  81. health["redis"] = "error: " + err.Error()
  82. health["status"] = "degraded"
  83. httpStatus = http.StatusServiceUnavailable
  84. } else {
  85. health["redis"] = "ok"
  86. }
  87. c.JSON(httpStatus, health)
  88. })
  89. api := r.Group("/api/v1")
  90. // Set Redis for token blacklist and DB for permissions
  91. SetAuthRedis(rdb)
  92. setPermissionDB(s.DB)
  93. // ── Public routes (no auth) ──
  94. auth := &AuthHandler{store: s}
  95. api.POST("/auth/login", rateLimitMiddleware(rdb, "login", 10, time.Minute), auth.Login)
  96. // App info (public, needed for branding)
  97. api.GET("/app/info", func(c *gin.Context) {
  98. appCfg := config.Get()
  99. name := "商户采集系统"
  100. if appCfg != nil && appCfg.App.Name != "" {
  101. name = appCfg.App.Name
  102. }
  103. OK(c, gin.H{"name": name})
  104. })
  105. // Level map (needed by login page for display)
  106. sh := &SettingHandler{store: s}
  107. api.GET("/settings/level-map", sh.GetLevelMap)
  108. // ── Protected routes (require login) ──
  109. protected := api.Group("")
  110. protected.Use(JWTAuth())
  111. // Dashboard & System Health
  112. dh := &DashboardHandler{store: s, rdb: rdb}
  113. protected.GET("/dashboard", dh.Get)
  114. protected.GET("/system/health", dh.Health)
  115. // Auth - self
  116. protected.GET("/auth/profile", auth.GetProfile)
  117. protected.PUT("/auth/profile", auth.UpdateProfile)
  118. protected.PUT("/auth/password", auth.ChangePassword)
  119. protected.POST("/auth/logout", auth.Logout)
  120. // User's own permissions
  121. ph := &PermissionHandler{store: s}
  122. protected.GET("/auth/permissions", ph.GetMyPermissions)
  123. // Keywords
  124. kw := &KeywordHandler{store: s}
  125. protected.GET("/keywords", kw.List)
  126. protected.POST("/keywords", RequireRole("admin", "operator"), RequireAction("keyword_manage"), kw.Create)
  127. protected.POST("/keywords/import", RequireRole("admin", "operator"), RequireAction("keyword_manage"), kw.ImportCSV)
  128. protected.PUT("/keywords/:id", RequireRole("admin", "operator"), RequireAction("keyword_manage"), kw.Update)
  129. protected.DELETE("/keywords/:id", RequireRole("admin", "operator"), RequireAction("keyword_manage"), kw.Delete)
  130. // Merchants
  131. mc := &MerchantHandler{store: s}
  132. protected.GET("/merchants/stats", mc.Stats)
  133. protected.GET("/merchants/raw", mc.ListRaw)
  134. protected.GET("/merchants/clean", mc.ListClean)
  135. protected.GET("/merchants/clean/export", mc.ExportCSV)
  136. protected.GET("/merchants/clean/users", mc.ListUsers)
  137. protected.GET("/merchants/clean/industry-tags", mc.ListIndustryTags)
  138. protected.POST("/merchants/clean/import", RequireRole("admin", "operator"), RequireAction("merchant_import"), mc.ImportCSV)
  139. protected.GET("/merchants/:id", mc.GetByID)
  140. protected.PUT("/merchants/clean/:id", RequireRole("admin", "operator"), RequireAction("merchant_edit"), mc.UpdateClean)
  141. protected.PUT("/merchants/clean/:id/follow-status", RequireRole("admin", "operator"), RequireAction("merchant_edit"), mc.UpdateFollowStatus)
  142. protected.PUT("/merchants/clean/:id/assign", RequireRole("admin", "operator"), RequireAction("merchant_assign"), mc.AssignMerchant)
  143. protected.POST("/merchants/clean/:id/recheck", RequireRole("admin", "operator"), RequireAction("merchant_edit"), mc.RecheckMerchant)
  144. protected.GET("/merchants/clean/:id/notes", mc.ListNotes)
  145. protected.POST("/merchants/clean/:id/notes", RequireRole("admin", "operator"), mc.AddNote)
  146. protected.DELETE("/merchants/raw/batch", RequireRole("admin", "operator"), RequireAction("merchant_delete"), mc.BatchDeleteRaw)
  147. protected.DELETE("/merchants/clean/batch", RequireRole("admin", "operator"), RequireAction("merchant_delete"), mc.BatchDeleteClean)
  148. protected.PUT("/merchants/clean/batch-assign", RequireRole("admin", "operator"), mc.BatchAssign)
  149. protected.PUT("/merchants/clean/batch-follow-status", RequireRole("admin", "operator"), mc.BatchFollowStatus)
  150. protected.PUT("/merchants/clean/batch-level", RequireRole("admin", "operator"), mc.BatchLevel)
  151. protected.POST("/merchants/clean/merge", RequireRole("admin", "operator"), mc.MergeMerchants)
  152. protected.POST("/merchants/clean/batch-recheck", RequireRole("admin", "operator"), mc.BatchRecheck)
  153. protected.POST("/merchants/archive", RequireRole("admin"), mc.ArchiveMerchants)
  154. protected.GET("/merchants/archived", mc.ListArchived)
  155. protected.POST("/merchants/archived/:id/restore", RequireRole("admin"), mc.RestoreArchived)
  156. // Tasks
  157. th := &TaskHandler{store: s, taskMgr: taskMgr}
  158. protected.GET("/tasks", th.List)
  159. protected.POST("/tasks/start", RequireRole("admin", "operator"), RequireAction("task_start"), th.Start)
  160. protected.GET("/tasks/:id", th.Get)
  161. protected.POST("/tasks/:id/stop", RequireRole("admin", "operator"), RequireAction("task_stop"), th.Stop)
  162. protected.POST("/tasks/:id/retry", RequireRole("admin", "operator"), RequireAction("task_start"), th.Retry)
  163. protected.GET("/tasks/:id/logs", th.Logs)
  164. protected.GET("/tasks/:id/details", th.Details)
  165. protected.GET("/plugins", th.Plugins)
  166. // Analytics
  167. an := &AnalyticsHandler{store: s}
  168. protected.GET("/analytics/funnel", an.Funnel)
  169. protected.GET("/analytics/source-efficiency", an.SourceEfficiency)
  170. protected.GET("/analytics/trends", an.Trends)
  171. protected.GET("/analytics/trends/export", an.ExportTrends)
  172. // Proxies
  173. px := &ProxyHandler{store: s, taskMgr: taskMgr}
  174. protected.GET("/proxies", px.List)
  175. protected.GET("/proxies/enabled", px.ListEnabled)
  176. protected.GET("/proxies/pool-status", px.PoolStatus)
  177. protected.POST("/proxies", RequireRole("admin"), px.Create)
  178. protected.POST("/proxies/test-all", RequireRole("admin"), px.TestAll)
  179. protected.PUT("/proxies/:id", RequireRole("admin"), px.Update)
  180. protected.DELETE("/proxies/:id", RequireRole("admin"), px.Delete)
  181. protected.POST("/proxies/:id/test", RequireRole("admin"), px.Test)
  182. // Channels
  183. ch := &ChannelHandler{store: s}
  184. protected.GET("/channels", ch.List)
  185. protected.GET("/channels/stats", ch.Stats)
  186. protected.PUT("/channels/:id/status", RequireRole("admin", "operator"), ch.UpdateStatus)
  187. protected.DELETE("/channels/:id", RequireRole("admin"), ch.Delete)
  188. // Groups
  189. gh := &GroupHandler{store: s, tgManager: tgMgr}
  190. protected.GET("/groups", gh.ListGroups)
  191. protected.GET("/groups/:username/members", gh.ListMembers)
  192. protected.GET("/groups/:username/members/export", gh.ExportMembers)
  193. protected.GET("/member-groups/:username", gh.ListMemberGroups)
  194. protected.GET("/members/search", gh.SearchMembers)
  195. protected.POST("/groups/:username/clone-members", RequireRole("admin", "operator"), gh.CloneMembers)
  196. // Settings (admin only)
  197. protected.GET("/settings/grading", sh.GetGrading)
  198. protected.PUT("/settings/grading", RequireRole("admin"), sh.UpdateGrading)
  199. protected.POST("/settings/grading/reset", RequireRole("admin"), sh.ResetGrading)
  200. // Audit logs (admin only)
  201. ah := &AuditHandler{store: s}
  202. // User management (admin only)
  203. uh := &UserHandler{store: s}
  204. adminOnly := protected.Group("", RequireRole("admin"))
  205. adminOnly.GET("/audit-logs", ah.List)
  206. // Permissions management (admin only)
  207. adminOnly.GET("/permissions", ph.ListAll)
  208. adminOnly.PUT("/permissions/:role", ph.Update)
  209. adminOnly.POST("/permissions/reset", ph.Reset)
  210. // Notification configs (admin only)
  211. notifMgr := notification.NewManager(s.DB)
  212. nh := &NotificationHandler{store: s, manager: notifMgr}
  213. adminOnly.GET("/notification-configs", nh.List)
  214. adminOnly.POST("/notification-configs", nh.Create)
  215. adminOnly.PUT("/notification-configs/:id", nh.Update)
  216. adminOnly.DELETE("/notification-configs/:id", nh.Delete)
  217. adminOnly.POST("/notification-configs/:id/test", nh.Test)
  218. adminOnly.GET("/users", uh.List)
  219. adminOnly.POST("/users", uh.Create)
  220. adminOnly.PUT("/users/:id", uh.Update)
  221. adminOnly.POST("/users/:id/reset-password", uh.ResetPassword)
  222. adminOnly.POST("/users/:id/force-logout", uh.ForceLogout)
  223. // Backup (admin only)
  224. bh := &BackupHandler{store: s}
  225. adminOnly.GET("/backup/export", bh.ExportJSON)
  226. adminOnly.GET("/backup/stats", bh.Stats)
  227. adminOnly.DELETE("/users/:id", uh.Delete)
  228. // Schedules (admin only)
  229. scheduleHandler = &ScheduleHandler{store: s, taskMgr: taskMgr}
  230. adminOnly.GET("/schedules", scheduleHandler.List)
  231. adminOnly.POST("/schedules", scheduleHandler.Create)
  232. adminOnly.PUT("/schedules/:id", scheduleHandler.Update)
  233. adminOnly.DELETE("/schedules/:id", scheduleHandler.Delete)
  234. adminOnly.POST("/schedules/:id/run", scheduleHandler.RunNow)
  235. // TG account management
  236. ta := tgAccountHandler
  237. protected.GET("/tg-accounts", ta.List) // all logged-in users can view
  238. adminOnly.POST("/tg-accounts", ta.Create)
  239. adminOnly.POST("/tg-accounts/import", ta.Import)
  240. adminOnly.PUT("/tg-accounts/:id", ta.Update)
  241. adminOnly.POST("/tg-accounts/:id/test", ta.Test)
  242. adminOnly.POST("/tg-accounts/:id/reveal-2fa", ta.RevealTwoFA)
  243. adminOnly.DELETE("/tg-accounts/:id", ta.Delete)
  244. return r
  245. }
  246. // corsMiddleware handles Cross-Origin Resource Sharing.
  247. func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
  248. return func(c *gin.Context) {
  249. origin := c.GetHeader("Origin")
  250. if len(allowedOrigins) == 0 {
  251. c.Header("Access-Control-Allow-Origin", "*")
  252. } else {
  253. for _, o := range allowedOrigins {
  254. if o == origin || o == "*" {
  255. c.Header("Access-Control-Allow-Origin", origin)
  256. break
  257. }
  258. }
  259. }
  260. c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
  261. c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization")
  262. c.Header("Access-Control-Max-Age", "86400")
  263. if c.Request.Method == "OPTIONS" {
  264. c.AbortWithStatus(http.StatusNoContent)
  265. return
  266. }
  267. c.Next()
  268. }
  269. }
  270. // ServerAddr returns the listen address string for the given port.
  271. func ServerAddr(port int) string {
  272. return fmt.Sprintf(":%d", port)
  273. }