feat: merge conflict resolution, site management, UI fixes

This commit is contained in:
ShukeBta
2026-05-16 17:57:34 +08:00
parent 0be8c7952e
commit cbb4b806be
84 changed files with 13826 additions and 202 deletions
+186
View File
@@ -0,0 +1,186 @@
// Package handler — API 配置 HTTP Handler。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// ApiConfigHandler API 配置 HTTP 处理。
type ApiConfigHandler struct {
svc *service.Container
log *zap.Logger
}
// NewApiConfigHandler 创建 API 配置处理器。
func NewApiConfigHandler(svc *service.Container, log *zap.Logger) *ApiConfigHandler {
return &ApiConfigHandler{svc: svc, log: log}
}
// ListApiConfigs 获取所有 API 配置。
// GET /api/api-config
func (h *ApiConfigHandler) ListApiConfigs(c *gin.Context) {
configs, err := h.svc.ApiConfig.List(c.Request.Context())
if err != nil {
h.log.Error("list api configs failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
// 遮蔽 API Key
for i := range configs {
if configs[i].APIKey != "" {
configs[i].APIKey = h.svc.ApiConfig.MaskAPIKey(configs[i].APIKey)
}
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": configs})
}
// ListProviders 获取预定义的提供者列表。
// GET /api/api-config/providers/list
func (h *ApiConfigHandler) ListProviders(c *gin.Context) {
providers := h.svc.ApiConfig.GetProviders()
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": providers})
}
// GetApiConfig 获取指定提供者的配置。
// GET /api/api-config/:provider
func (h *ApiConfigHandler) GetApiConfig(c *gin.Context) {
provider := c.Param("provider")
if provider == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
return
}
cfg, err := h.svc.ApiConfig.GetByProvider(c.Request.Context(), provider)
if err != nil {
if err == service.ErrApiConfigNotFound {
c.JSON(http.StatusNotFound, gin.H{"code": 40401, "message": "api config not found", "data": nil})
return
}
h.log.Error("get api config failed", zap.Error(err), zap.String("provider", provider))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
// 遮蔽 API Key
if cfg.APIKey != "" {
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
}
// GetEffectiveConfig 获取生效的配置(数据库配置优先于配置文件)。
// GET /api/api-config/:provider/effective
func (h *ApiConfigHandler) GetEffectiveConfig(c *gin.Context) {
provider := c.Param("provider")
if provider == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
return
}
cfg, err := h.svc.ApiConfig.GetEffectiveConfig(c.Request.Context(), provider)
if err != nil {
if err == service.ErrApiConfigNotFound {
c.JSON(http.StatusNotFound, gin.H{"code": 40401, "message": "api config not found", "data": nil})
return
}
h.log.Error("get effective config failed", zap.Error(err), zap.String("provider", provider))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
// 遮蔽 API Key
if cfg.APIKey != "" {
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
}
// UpsertApiConfig 创建或更新 API 配置。
// POST /api/api-config/:provider
func (h *ApiConfigHandler) UpsertApiConfig(c *gin.Context) {
provider := c.Param("provider")
if provider == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
return
}
var req struct {
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
Extra string `json:"extra"`
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid request", "data": nil})
return
}
cfg, err := h.svc.ApiConfig.Upsert(c.Request.Context(), provider, req.APIKey, req.BaseURL, req.Extra, req.Enabled)
if err != nil {
if err == service.ErrInvalidProvider {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid provider", "data": nil})
return
}
h.log.Error("upsert api config failed", zap.Error(err), zap.String("provider", provider))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
// 返回遮蔽后的配置
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
}
// DeleteApiConfig 删除 API 配置。
// DELETE /api/api-config/:provider
func (h *ApiConfigHandler) DeleteApiConfig(c *gin.Context) {
provider := c.Param("provider")
if provider == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
return
}
if err := h.svc.ApiConfig.Delete(c.Request.Context(), provider); err != nil {
h.log.Error("delete api config failed", zap.Error(err), zap.String("provider", provider))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
}
// TestApiConfig 测试 API 连接。
// POST /api/api-config/:provider/test
func (h *ApiConfigHandler) TestApiConfig(c *gin.Context) {
provider := c.Param("provider")
if provider == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
return
}
result, err := h.svc.ApiConfig.TestConnection(c.Request.Context(), provider)
if err != nil {
h.log.Debug("test api config failed", zap.Error(err), zap.String("provider", provider))
// 不返回错误,只返回测试结果
}
// 更新测试结果
_ = h.svc.ApiConfig.UpdateTestResult(c.Request.Context(), provider, result)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"data": gin.H{
"result": result,
},
})
}
+13 -6
View File
@@ -28,20 +28,24 @@ func loginHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, token, err := svc.Auth.Login(c.Request.Context(), req.Username, req.Password)
resp, err := svc.Auth.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
if errors.Is(err, service.ErrInvalidCredentials) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
if errors.Is(err, service.ErrUserInactive) {
c.JSON(http.StatusForbidden, gin.H{"error": "user account is inactive"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": u,
"user": resp.User,
"tokens": resp.Tokens,
})
svc.Audit.Record(c.Request.Context(), u.ID, "auth.login", u.Username, c.ClientIP(), "")
svc.Audit.Record(c.Request.Context(), resp.User.ID, "auth.login", resp.User.Username, c.ClientIP(), "")
}
}
@@ -52,7 +56,7 @@ func registerHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := svc.Auth.Register(c.Request.Context(), req.Username, req.Password)
u, tokens, err := svc.Auth.Register(c.Request.Context(), req.Username, req.Password)
if err != nil {
if errors.Is(err, service.ErrUsernameTaken) {
c.JSON(http.StatusConflict, gin.H{"error": "username taken"})
@@ -61,7 +65,10 @@ func registerHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
c.JSON(http.StatusOK, gin.H{
"user": u,
"tokens": tokens,
})
}
}
+235
View File
@@ -0,0 +1,235 @@
// Package handler — 下载客户端管理 HTTP 端点。
package handler
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// DownloadClientHandler 处理下载客户端的 CRUD 操作。
type DownloadClientHandler struct {
svc *service.Container
log *zap.Logger
}
// NewDownloadClientHandler 创建下载客户端处理器。
func NewDownloadClientHandler(svc *service.Container, log *zap.Logger) *DownloadClientHandler {
return &DownloadClientHandler{svc: svc, log: log}
}
// downloadClientCreateRequest 创建下载客户端请求体。
type downloadClientCreateRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required,oneof=qbittorrent transmission aria2"`
Host string `json:"host" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
IsDefault bool `json:"is_default"`
Extra map[string]string `json:"extra,omitempty"`
}
// downloadClientUpdateRequest 更新下载客户端请求体。
type downloadClientUpdateRequest struct {
Name string `json:"name"`
Type string `json:"type" binding:"omitempty,oneof=qbittorrent transmission aria2"`
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
IsDefault *bool `json:"is_default"`
Enabled *bool `json:"enabled"`
Extra map[string]string `json:"extra,omitempty"`
}
// Create 创建新的下载客户端。
func (h *DownloadClientHandler) Create(c *gin.Context) {
var req downloadClientCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
return
}
ctx := c.Request.Context()
// 加密密码
password := req.Password
if password != "" && h.svc.Crypto != nil {
password = h.svc.Crypto.Encrypt(password)
}
// 加密 Extra 配置
extraStr := ""
if len(req.Extra) > 0 {
extraJSON, _ := json.Marshal(req.Extra)
extraStr = string(extraJSON)
if h.svc.Crypto != nil {
extraStr = h.svc.Crypto.Encrypt(extraStr)
}
}
// 如果设为默认,先清除其他默认
if req.IsDefault {
_ = h.svc.Repo.DownloadClient.ClearDefault(ctx)
}
client := &model.DownloadClient{
Name: req.Name,
Type: req.Type,
Host: req.Host,
Username: req.Username,
Password: password,
IsDefault: req.IsDefault,
Enabled: true,
Extra: extraStr,
}
if err := h.svc.Repo.DownloadClient.Create(ctx, client); err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "创建失败: "+err.Error())
return
}
// 热插拔:加载新客户端
go func() {
if initErr := h.svc.DownloadMgr.AddClient(ctx, client); initErr != nil {
h.log.Warn("failed to hot-add download client", zap.Error(initErr))
}
}()
Success(c, client)
}
// List 返回所有下载客户端。
func (h *DownloadClientHandler) List(c *gin.Context) {
ctx := c.Request.Context()
clients, err := h.svc.Repo.DownloadClient.List(ctx)
if err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "查询失败")
return
}
Success(c, clients)
}
// Get 返回指定下载客户端详情。
func (h *DownloadClientHandler) Get(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
if err != nil || client == nil {
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
return
}
Success(c, client)
}
// Update 更新下载客户端。
func (h *DownloadClientHandler) Update(c *gin.Context) {
id := c.Param("id")
var req downloadClientUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
return
}
ctx := c.Request.Context()
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
if err != nil || client == nil {
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
return
}
if req.Name != "" {
client.Name = req.Name
}
if req.Type != "" {
client.Type = req.Type
}
if req.Host != "" {
client.Host = req.Host
}
if req.Username != "" {
client.Username = req.Username
}
if req.Password != "" {
if h.svc.Crypto != nil {
client.Password = h.svc.Crypto.Encrypt(req.Password)
} else {
client.Password = req.Password
}
}
if req.IsDefault != nil && *req.IsDefault {
_ = h.svc.Repo.DownloadClient.ClearDefault(ctx)
client.IsDefault = *req.IsDefault
}
if req.Enabled != nil {
client.Enabled = *req.Enabled
}
if len(req.Extra) > 0 {
extraJSON, _ := json.Marshal(req.Extra)
extraStr := string(extraJSON)
if h.svc.Crypto != nil {
client.Extra = h.svc.Crypto.Encrypt(extraStr)
} else {
client.Extra = extraStr
}
}
if err := h.svc.Repo.DownloadClient.Update(ctx, client); err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "更新失败")
return
}
// 热更新适配器
go func() {
if updateErr := h.svc.DownloadMgr.UpdateClient(ctx, client); updateErr != nil {
h.log.Warn("failed to hot-update download client", zap.Error(updateErr))
}
}()
Success(c, client)
}
// Delete 删除下载客户端。
func (h *DownloadClientHandler) Delete(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
_, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
if err != nil {
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
return
}
if delErr := h.svc.Repo.DownloadClient.Delete(ctx, id); delErr != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "删除失败")
return
}
// 热移除
h.svc.DownloadMgr.RemoveClient(id)
SuccessWithMessage(c, "已删除", nil)
}
// Test 测试下载客户端连接。
func (h *DownloadClientHandler) Test(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
if err != nil || client == nil {
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
return
}
if err := h.svc.DownloadMgr.TestConnection(ctx, client); err != nil {
Error(c, http.StatusBadRequest, ErrExternal, "连接测试失败: "+err.Error())
return
}
SuccessWithMessage(c, "连接成功", nil)
}
+254
View File
@@ -25,6 +25,7 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
{
auth.POST("/login", loginHandler(svc))
auth.POST("/register", registerHandler(svc))
auth.POST("/refresh", refreshHandler(svc))
}
// Authenticated endpoints.
@@ -34,6 +35,10 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
authed.GET("/me", meHandler(svc))
authed.PATCH("/me", updateProfileHandler(svc))
authed.POST("/me/password", changePasswordHandler(svc))
authed.POST("/me/logout", logoutHandler(svc))
// Permissions.
authed.GET("/auth/permissions", getMyPermissionsHandler(svc))
// Libraries.
authed.GET("/libraries", listLibrariesHandler(svc))
@@ -129,6 +134,42 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
authed.GET("/recycle", middleware.AdminRequired(), listRecycleHandler(svc))
authed.GET("/ws", wsHandler(svc))
// SSE event stream.
authed.GET("/events", sseHandler(svc))
// Download clients.
authed.GET("/download-clients", listDownloadClientsHandler(svc))
authed.POST("/download-clients", middleware.AdminRequired(), createDownloadClientHandler(svc))
authed.GET("/download-clients/:id", getDownloadClientHandler(svc))
authed.PUT("/download-clients/:id", middleware.AdminRequired(), updateDownloadClientHandler(svc))
authed.DELETE("/download-clients/:id", middleware.AdminRequired(), deleteDownloadClientHandler(svc))
authed.POST("/download-clients/:id/test", middleware.AdminRequired(), testDownloadClientHandler(svc))
// Notify channels.
authed.GET("/notify-channels", listNotifyChannelsHandler(svc))
authed.GET("/notify-channels/types", getNotifyChannelTypesHandler(svc))
authed.POST("/notify-channels", middleware.AdminRequired(), createNotifyChannelHandler(svc))
authed.GET("/notify-channels/:id", getNotifyChannelHandler(svc))
authed.PUT("/notify-channels/:id", middleware.AdminRequired(), updateNotifyChannelHandler(svc))
authed.DELETE("/notify-channels/:id", middleware.AdminRequired(), deleteNotifyChannelHandler(svc))
authed.POST("/notify-channels/:id/test", middleware.AdminRequired(), testNotifyChannelHandler(svc))
// Scheduler.
authed.GET("/scheduler/tasks", schedulerListTasksHandler(svc))
authed.POST("/scheduler/tasks/:id/run", middleware.AdminRequired(), schedulerRunTaskHandler(svc))
authed.GET("/scheduler/status", schedulerGetStatusHandler(svc))
// Sites (PT 站点管理).
siteHandler := NewSiteHandler(svc)
authed.GET("/sites", siteHandler.ListSites)
authed.GET("/sites/types", siteHandler.GetSiteTypes)
authed.GET("/sites/auth-types", siteHandler.GetAuthTypes)
authed.POST("/sites", middleware.AdminRequired(), siteHandler.CreateSite)
authed.GET("/sites/:id", siteHandler.GetSite)
authed.PUT("/sites/:id", middleware.AdminRequired(), siteHandler.UpdateSite)
authed.DELETE("/sites/:id", middleware.AdminRequired(), siteHandler.DeleteSite)
authed.POST("/sites/:id/test", middleware.AdminRequired(), siteHandler.TestSite)
}
// Admin-only endpoints.
@@ -164,6 +205,24 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
// Scheduled jobs.
admin.GET("/scheduler", schedulerStatusHandler(svc))
admin.POST("/scheduler/:name/run", schedulerRunHandler(svc))
// User permissions management.
admin.GET("/users/:id/permissions", getUserPermissionsHandler(svc))
admin.PUT("/users/:id/permissions", updateUserPermissionsHandler(svc))
admin.POST("/users/:id/permissions/reset", resetUserPermissionsHandler(svc))
}
// API Config management (admin only).
apiConfig := api.Group("/api-config")
apiConfig.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret), middleware.AdminRequired())
{
apiConfig.GET("", listApiConfigsHandler(svc))
apiConfig.GET("/providers/list", listProvidersHandler(svc))
apiConfig.GET("/:provider", getApiConfigHandler(svc))
apiConfig.GET("/:provider/effective", getEffectiveConfigHandler(svc))
apiConfig.POST("/:provider", upsertApiConfigHandler(svc))
apiConfig.DELETE("/:provider", deleteApiConfigHandler(svc))
apiConfig.POST("/:provider/test", testApiConfigHandler(svc))
}
// Emby/Jellyfin compatibility shim (read-only).
@@ -188,3 +247,198 @@ func healthCheck(c *gin.Context) {
func versionInfo(c *gin.Context) {
c.JSON(200, gin.H{"name": "MediaStationGo", "version": "0.1.0"})
}
// ─── 权限 Handler 包装 ────────────────────────────────────────────────────────
func getUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
h := NewPermissionHandler(svc, svc.Log)
return h.GetUserPermissions
}
func updateUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
h := NewPermissionHandler(svc, svc.Log)
return h.UpdateUserPermissions
}
func resetUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
h := NewPermissionHandler(svc, svc.Log)
return h.ResetUserPermissions
}
func getMyPermissionsHandler(svc *service.Container) gin.HandlerFunc {
h := NewPermissionHandler(svc, svc.Log)
return h.GetMyPermissions
}
// ─── 刷新 Handler 包装 ────────────────────────────────────────────────────────
func refreshHandler(svc *service.Container) gin.HandlerFunc {
h := NewRefreshHandler(svc, svc.Log)
return h.RefreshToken
}
func logoutHandler(svc *service.Container) gin.HandlerFunc {
h := NewRefreshHandler(svc, svc.Log)
return h.Logout
}
// ─── API Config Handler 包装 ───────────────────────────────────────────────────
func listApiConfigsHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.ListApiConfigs
}
func listProvidersHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.ListProviders
}
func getApiConfigHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.GetApiConfig
}
func getEffectiveConfigHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.GetEffectiveConfig
}
func upsertApiConfigHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.UpsertApiConfig
}
func deleteApiConfigHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.DeleteApiConfig
}
func testApiConfigHandler(svc *service.Container) gin.HandlerFunc {
h := NewApiConfigHandler(svc, svc.Log)
return h.TestApiConfig
}
// ─── Download Client Handler 包装 ─────────────────────────────────────────────
func listDownloadClientsHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.List
}
func createDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.Create
}
func getDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.Get
}
func updateDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.Update
}
func deleteDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.Delete
}
func testDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
h := NewDownloadClientHandler(svc, svc.Log)
return h.Test
}
// ─── Notify Channel Handler 包装 ──────────────────────────────────────────────
func listNotifyChannelsHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.List
}
func getNotifyChannelTypesHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.GetTypes
}
func createNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.Create
}
func getNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.Get
}
func updateNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.Update
}
func deleteNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.Delete
}
func testNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
h := NewNotifyHandler(svc, svc.Log)
return h.Test
}
// ─── Scheduler Handler 包装 ──────────────────────────────────────────────────
func schedulerListTasksHandler(svc *service.Container) gin.HandlerFunc {
h := NewSchedulerHandler(svc, svc.Log)
return h.ListTasks
}
func schedulerRunTaskHandler(svc *service.Container) gin.HandlerFunc {
h := NewSchedulerHandler(svc, svc.Log)
return h.RunTask
}
func schedulerGetStatusHandler(svc *service.Container) gin.HandlerFunc {
h := NewSchedulerHandler(svc, svc.Log)
return h.GetStatus
}
// ─── SSE Handler ──────────────────────────────────────────────────────────────
func sseHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
// 获取 SSE Hub
hub := svc.SSEHub
// 设置 SSE 响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
// 订阅事件流
client := hub.Subscribe()
defer hub.Unsubscribe(client)
// 发送初始连接成功事件
c.SSEvent("connected", gin.H{"status": "ok"})
c.Writer.Flush()
// 持续发送事件直到客户端断开连接
clientGone := c.Request.Context().Done()
for {
select {
case <-clientGone:
return
case event, ok := <-client.Ch:
if !ok {
return
}
c.SSEvent(event.Type, event.Payload)
c.Writer.Flush()
}
}
}
}
+196
View File
@@ -0,0 +1,196 @@
// Package handler — 通知渠道管理 HTTP 端点。
package handler
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// NotifyHandler 处理通知渠道的 CRUD 操作。
type NotifyHandler struct {
svc *service.Container
log *zap.Logger
}
// NewNotifyHandler 创建通知渠道处理器。
func NewNotifyHandler(svc *service.Container, log *zap.Logger) *NotifyHandler {
return &NotifyHandler{svc: svc, log: log}
}
// notifyCreateRequest 创建通知渠道请求体。
type notifyCreateRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required,oneof=telegram wechat bark webhook email"`
Enabled bool `json:"enabled"`
Config map[string]string `json:"config" binding:"required"`
Events []string `json:"events"`
}
// notifyUpdateRequest 更新通知渠道请求体。
type notifyUpdateRequest struct {
Name string `json:"name"`
Enabled *bool `json:"enabled"`
Config map[string]string `json:"config"`
Events []string `json:"events"`
}
// Create 创建新的通知渠道。
func (h *NotifyHandler) Create(c *gin.Context) {
var req notifyCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
return
}
ctx := c.Request.Context()
// 验证配置
if err := h.svc.Notify.ValidateChannelConfig(req.Type, req.Config); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, "配置验证失败: "+err.Error())
return
}
// 加密配置
configJSON, _ := json.Marshal(req.Config)
configStr := string(configJSON)
if h.svc.Crypto != nil {
configStr = h.svc.Crypto.Encrypt(configStr)
}
// 序列化事件列表
eventsJSON, _ := json.Marshal(req.Events)
eventsStr := string(eventsJSON)
channel := &model.NotifyChannel{
Name: req.Name,
Type: req.Type,
Enabled: req.Enabled,
Config: configStr,
Events: eventsStr,
}
if err := h.svc.Repo.NotifyChannel.Create(ctx, channel); err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "创建失败: "+err.Error())
return
}
Success(c, channel)
}
// List 返回所有通知渠道。
func (h *NotifyHandler) List(c *gin.Context) {
ctx := c.Request.Context()
channels, err := h.svc.Repo.NotifyChannel.List(ctx)
if err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "查询失败")
return
}
Success(c, channels)
}
// Get 返回指定通知渠道详情。
func (h *NotifyHandler) Get(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
channel, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
if err != nil || channel == nil {
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
return
}
Success(c, channel)
}
// Update 更新通知渠道。
func (h *NotifyHandler) Update(c *gin.Context) {
id := c.Param("id")
var req notifyUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
return
}
ctx := c.Request.Context()
channel, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
if err != nil || channel == nil {
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
return
}
if req.Name != "" {
channel.Name = req.Name
}
if req.Enabled != nil {
channel.Enabled = *req.Enabled
}
// 更新配置
if len(req.Config) > 0 {
if err := h.svc.Notify.ValidateChannelConfig(channel.Type, req.Config); err != nil {
Error(c, http.StatusBadRequest, ErrInvalidParams, "配置验证失败: "+err.Error())
return
}
configJSON, _ := json.Marshal(req.Config)
configStr := string(configJSON)
if h.svc.Crypto != nil {
configStr = h.svc.Crypto.Encrypt(configStr)
}
channel.Config = configStr
}
// 更新事件列表
if req.Events != nil {
eventsJSON, _ := json.Marshal(req.Events)
channel.Events = string(eventsJSON)
}
if err := h.svc.Repo.NotifyChannel.Update(ctx, channel); err != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "更新失败")
return
}
Success(c, channel)
}
// Delete 删除通知渠道。
func (h *NotifyHandler) Delete(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
_, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
if err != nil {
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
return
}
if delErr := h.svc.Repo.NotifyChannel.Delete(ctx, id); delErr != nil {
Error(c, http.StatusInternalServerError, ErrInternal, "删除失败")
return
}
SuccessWithMessage(c, "已删除", nil)
}
// Test 发送测试通知。
func (h *NotifyHandler) Test(c *gin.Context) {
id := c.Param("id")
ctx := c.Request.Context()
if err := h.svc.Notify.SendTest(ctx, id); err != nil {
Error(c, http.StatusBadRequest, ErrExternal, "测试通知发送失败: "+err.Error())
return
}
SuccessWithMessage(c, "测试通知已发送", nil)
}
// GetTypes 返回支持的通知渠道类型列表。
func (h *NotifyHandler) GetTypes(c *gin.Context) {
types := h.svc.Notify.GetProviderTypes()
Success(c, types)
}
+139
View File
@@ -0,0 +1,139 @@
// Package handler — 权限相关 HTTP Handler。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/middleware"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// PermissionHandler 权限相关 HTTP 处理。
type PermissionHandler struct {
svc *service.Container
log *zap.Logger
}
// NewPermissionHandler 创建权限处理器。
func NewPermissionHandler(svc *service.Container, log *zap.Logger) *PermissionHandler {
return &PermissionHandler{svc: svc, log: log}
}
// GetUserPermissions 获取指定用户的权限(管理员)。
// GET /api/users/:id/permissions
func (h *PermissionHandler) GetUserPermissions(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
return
}
// 权限检查:需要管理员权限
role := middleware.GetUserRole(c)
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
return
}
perms, err := h.svc.Permission.GetByUserID(c.Request.Context(), userID)
if err != nil {
h.log.Error("get user permissions failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": perms, "message": "ok"})
}
// UpdateUserPermissions 更新指定用户的权限(管理员)。
// PUT /api/users/:id/permissions
func (h *PermissionHandler) UpdateUserPermissions(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
return
}
// 权限检查:需要管理员权限
role := middleware.GetUserRole(c)
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
return
}
var req struct {
Permissions map[string]bool `json:"permissions"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid request", "data": nil})
return
}
if err := h.svc.Permission.Update(c.Request.Context(), userID, req.Permissions); err != nil {
h.log.Error("update user permissions failed", zap.Error(err), zap.String("user_id", userID))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "permissions updated", "data": nil})
}
// ResetUserPermissions 重置指定用户的权限为默认值(管理员)。
// POST /api/users/:id/permissions/reset
func (h *PermissionHandler) ResetUserPermissions(c *gin.Context) {
userID := c.Param("id")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
return
}
// 权限检查:需要管理员权限
role := middleware.GetUserRole(c)
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
return
}
if err := h.svc.Permission.ResetToDefault(c.Request.Context(), userID); err != nil {
h.log.Error("reset user permissions failed", zap.Error(err), zap.String("user_id", userID))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "permissions reset to default", "data": nil})
}
// GetMyPermissions 获取当前用户的权限。
// GET /api/auth/permissions
func (h *PermissionHandler) GetMyPermissions(c *gin.Context) {
currentUserID := middleware.GetUserID(c)
if currentUserID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "authentication required", "data": nil})
return
}
perms, err := h.svc.Permission.GetPermissionMap(c.Request.Context(), currentUserID)
if err != nil {
h.log.Error("get my permissions failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
return
}
// 同时返回角色和等级信息
role := middleware.GetUserRole(c)
tier := middleware.GetUserTier(c)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"data": gin.H{
"permissions": perms,
"role": role,
"tier": tier,
"is_super": role == "admin" || tier == "plus",
},
})
}
+82
View File
@@ -0,0 +1,82 @@
// Package handler — 令牌刷新 HTTP Handler。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// RefreshHandler 令牌刷新 HTTP 处理。
type RefreshHandler struct {
svc *service.Container
log *zap.Logger
}
// NewRefreshHandler 创建刷新令牌处理器。
func NewRefreshHandler(svc *service.Container, log *zap.Logger) *RefreshHandler {
return &RefreshHandler{svc: svc, log: log}
}
// RefreshTokenRequest 刷新令牌请求结构。
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// RefreshToken 刷新访问令牌。
// POST /api/auth/refresh
func (h *RefreshHandler) RefreshToken(c *gin.Context) {
var req RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "refresh_token required", "data": nil})
return
}
tokens, err := h.svc.Auth.RefreshTokens(c.Request.Context(), req.RefreshToken)
if err != nil {
h.log.Debug("token refresh failed", zap.Error(err))
// 根据错误类型返回不同状态码
switch err {
case service.ErrInvalidRefreshToken:
c.JSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "invalid refresh token", "data": nil})
case service.ErrTokenExpired:
c.JSON(http.StatusUnauthorized, gin.H{"code": 40102, "message": "refresh token expired", "data": nil})
case service.ErrTokenRevoked:
c.JSON(http.StatusUnauthorized, gin.H{"code": 40103, "message": "refresh token revoked", "data": nil})
default:
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
}
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"data": gin.H{
"token": tokens.AccessToken,
"refresh_token": tokens.RefreshToken,
"expires_in": tokens.ExpiresIn,
"token_type": tokens.TokenType,
},
})
}
// Logout 登出当前用户。
// POST /api/auth/logout
func (h *RefreshHandler) Logout(c *gin.Context) {
userID := c.GetString("ctx_user_id")
if userID == "" {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
return
}
if err := h.svc.Auth.Logout(c.Request.Context(), userID); err != nil {
h.log.Warn("logout failed", zap.Error(err), zap.String("user_id", userID))
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
}
+69
View File
@@ -0,0 +1,69 @@
// Package handler — 统一响应格式和错误码。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
)
// ─── 错误码定义 ───────────────────────────────────────────────────────────────
// 统一错误码
const (
ErrOK = 0
ErrInvalidParams = 40001
ErrUnauthorized = 40101
ErrForbidden = 40301
ErrNotFound = 40401
ErrConflict = 40901
ErrInternal = 50001
ErrExternal = 50201
ErrEncryptFailed = 50801
)
// ─── Response Helpers ─────────────────────────────────────────────────────────
// APIResponse 统一 API 响应格式。
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// PaginatedResponse 分页响应格式。
type PaginatedResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// Success 返回成功响应。
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, APIResponse{Code: 0, Message: "ok", Data: data})
}
// SuccessWithMessage 返回带消息的成功响应。
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
c.JSON(http.StatusOK, APIResponse{Code: 0, Message: message, Data: data})
}
// Error 返回错误响应。
func Error(c *gin.Context, httpStatus int, code int, message string) {
c.JSON(httpStatus, APIResponse{Code: code, Message: message, Data: nil})
}
// Paginated 返回分页响应。
func Paginated(c *gin.Context, items interface{}, total int64, page, pageSize int) {
c.JSON(http.StatusOK, PaginatedResponse{
Code: 0,
Message: "ok",
Data: items,
Total: total,
Page: page,
PageSize: pageSize,
})
}
+47
View File
@@ -0,0 +1,47 @@
// Package handler — 定时任务管理 HTTP 端点。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// SchedulerHandler 处理定时任务的查询和管理操作。
type SchedulerHandler struct {
svc *service.Container
log *zap.Logger
}
// NewSchedulerHandler 创建定时任务处理器。
func NewSchedulerHandler(svc *service.Container, log *zap.Logger) *SchedulerHandler {
return &SchedulerHandler{svc: svc, log: log}
}
// ListTasks 返回所有定时任务列表。
func (h *SchedulerHandler) ListTasks(c *gin.Context) {
tasks := h.svc.Scheduler.Status()
Success(c, tasks)
}
// RunTask 手动触发指定任务。
func (h *SchedulerHandler) RunTask(c *gin.Context) {
name := c.Param("id")
ctx := c.Request.Context()
if err := h.svc.Scheduler.RunNow(ctx, name); err != nil {
Error(c, http.StatusBadRequest, ErrInternal, "任务执行失败: "+err.Error())
return
}
SuccessWithMessage(c, "任务已触发执行", nil)
}
// GetStatus 返回调度器运行状态。
func (h *SchedulerHandler) GetStatus(c *gin.Context) {
status := h.svc.Scheduler.Status()
Success(c, status)
}
+115
View File
@@ -0,0 +1,115 @@
// Package handler — PT 站点管理 HTTP 处理。
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
// SiteHandler 站点管理 CRUD。
type SiteHandler struct {
svc *service.Container
}
// NewSiteHandler 创建站点管理 Handler。
func NewSiteHandler(svc *service.Container) *SiteHandler {
return &SiteHandler{svc: svc}
}
// ListSites 列出所有站点。
func (h *SiteHandler) ListSites(c *gin.Context) {
sites, err := h.svc.Site.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
return
}
if sites == nil {
sites = []model.Site{}
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": sites})
}
// GetSite 获取单个站点详情(解密敏感字段)。
func (h *SiteHandler) GetSite(c *gin.Context) {
site, err := h.svc.Site.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
if err == service.ErrSiteNotFound {
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": site})
}
// CreateSite 创建站点。
func (h *SiteHandler) CreateSite(c *gin.Context) {
var site model.Site
if err := c.ShouldBindJSON(&site); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
created, err := h.svc.Site.Create(c.Request.Context(), &site)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "ok", "data": created})
}
// UpdateSite 更新站点。
func (h *SiteHandler) UpdateSite(c *gin.Context) {
var site model.Site
if err := c.ShouldBindJSON(&site); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
site.ID = c.Param("id")
updated, err := h.svc.Site.Update(c.Request.Context(), &site)
if err != nil {
if err == service.ErrSiteNotFound {
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": updated})
}
// DeleteSite 删除站点。
func (h *SiteHandler) DeleteSite(c *gin.Context) {
if err := h.svc.Site.Delete(c.Request.Context(), c.Param("id")); err != nil {
if err == service.ErrSiteNotFound {
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
// TestSite 测试站点连通性。
func (h *SiteHandler) TestSite(c *gin.Context) {
if err := h.svc.Site.Authenticate(c.Request.Context(), c.Param("id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
// GetSiteTypes 返回支持的站点类型列表。
func (h *SiteHandler) GetSiteTypes(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": model.SiteTypes()})
}
// GetAuthTypes 返回支持的认证方式列表。
func (h *SiteHandler) GetAuthTypes(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": model.AuthTypes()})
}