chore: remove license code, fix compilation, update README bilingual

- Remove all license-related code (handler/service/repository/model)
  License authorization is managed by separate server:
  https://github.com/ShukeBta/MediaStationLicenseServer
- Fix compilation errors: model field alignment, method name fixes,
  route conflicts, struct literal corrections (7 files)
- Add Chinese README.md as primary, English README_EN.md
- Update .gitignore: exclude .workbuddy/, editor backups
- Add new repository files: assistant, play_profile, storage_config
This commit is contained in:
ShukeBta
2026-05-17 00:02:41 +08:00
parent 37dd83c56d
commit 8e06b7e149
30 changed files with 756 additions and 1625 deletions
-94
View File
@@ -265,12 +265,6 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
authed.GET("/download/tasks", downloadTasksAliasHandler(svc))
authed.POST("/download/add", addDownloadHandler(svc))
// ── License (anyone authenticated can activate / heartbeat) ──
authed.POST("/license/activate", licenseActivateHandler(svc))
authed.POST("/license/heartbeat", licenseHeartbeatHandler(svc))
authed.GET("/license/status", licenseStatusHandler(svc))
authed.GET("/license/heartbeat-status", licenseStatusHandler(svc))
// ── Assistant (multi-turn AI chat) ──
authed.GET("/admin/assistant/sessions", listAssistantSessionsHandler(svc))
authed.POST("/admin/assistant/sessions", createAssistantSessionHandler(svc))
@@ -312,13 +306,6 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
admin.POST("/download/clients/:id/test", testDownloadClientHandler(svc))
admin.GET("/download/aria2/stats", aria2StatsHandler(svc))
// License generation / revocation.
admin.POST("/license/generate", licenseGenerateHandler(svc))
admin.GET("/license/list", licenseListHandler(svc))
admin.GET("/license/:id/activations", licenseListActivationsHandler(svc))
admin.POST("/license/activation/:id/unbind", licenseUnbindHandler(svc))
admin.POST("/license/:id/revoke", licenseRevokeHandler(svc))
// System scheduler trigger alias.
admin.POST("/system/scheduler/:name/trigger", schedulerTriggerHandler(svc))
@@ -352,10 +339,6 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
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).
@@ -396,38 +379,11 @@ func versionInfo(c *gin.Context) {
// ─── 权限 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 {
@@ -467,73 +423,23 @@ func testApiConfigHandler(svc *service.Container) gin.HandlerFunc {
// ─── 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 {
-145
View File
@@ -1,145 +0,0 @@
// Package handler — license key endpoints.
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
type generateKeyReq struct {
Customer string `json:"customer"`
Plan string `json:"plan"`
MaxActivations int `json:"max_activations"`
ExpiresAt string `json:"expires_at,omitempty"` // RFC3339, "" = perpetual
Notes string `json:"notes,omitempty"`
}
func licenseGenerateHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req generateKeyReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var expires *time.Time
if req.ExpiresAt != "" {
t, err := time.Parse(time.RFC3339, req.ExpiresAt)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "expires_at must be RFC3339"})
return
}
expires = &t
}
k, err := svc.License.Generate(
c.Request.Context(),
req.Customer, req.Plan, req.Notes,
req.MaxActivations, expires,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, k)
}
}
func licenseListHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
rows, err := svc.License.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rows)
}
}
type activateReq struct {
Key string `json:"key" binding:"required"`
DeviceID string `json:"device_id" binding:"required"`
DeviceName string `json:"device_name"`
}
func licenseActivateHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req activateReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
a, err := svc.License.Activate(
c.Request.Context(), req.Key, req.DeviceID, req.DeviceName, c.ClientIP(),
)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, a)
}
}
func licenseListActivationsHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
rows, err := svc.License.ListActivations(c.Request.Context(), c.Param("id"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rows)
}
}
func licenseUnbindHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
if err := svc.License.Unbind(c.Request.Context(), c.Param("id")); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
}
func licenseRevokeHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
if err := svc.License.Revoke(c.Request.Context(), c.Param("id")); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
}
func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
actID := c.Query("activation_id")
if actID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "activation_id required"})
return
}
if err := svc.License.Heartbeat(c.Request.Context(), actID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}
func licenseStatusHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
keyID := c.Query("key_id")
if keyID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "key_id required"})
return
}
out, err := svc.License.Status(c.Request.Context(), keyID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, out)
}
}
+7 -8
View File
@@ -8,6 +8,7 @@ import (
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/middleware"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service"
)
@@ -38,7 +39,7 @@ func (h *PermissionHandler) GetUserPermissions(c *gin.Context) {
return
}
perms, err := h.svc.Permission.GetByUserID(c.Request.Context(), userID)
perms, err := h.svc.Permissions.Effective(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})
@@ -64,15 +65,13 @@ func (h *PermissionHandler) UpdateUserPermissions(c *gin.Context) {
return
}
var req struct {
Permissions map[string]bool `json:"permissions"`
}
var req model.UserPermission
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 {
if err := h.svc.Permissions.Save(c.Request.Context(), userID, &req); 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
@@ -97,7 +96,7 @@ func (h *PermissionHandler) ResetUserPermissions(c *gin.Context) {
return
}
if err := h.svc.Permission.ResetToDefault(c.Request.Context(), userID); err != nil {
if _, err := h.svc.Permissions.Reset(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
@@ -115,7 +114,7 @@ func (h *PermissionHandler) GetMyPermissions(c *gin.Context) {
return
}
perms, err := h.svc.Permission.GetPermissionMap(c.Request.Context(), currentUserID)
perms, err := h.svc.Permissions.Effective(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})
@@ -127,7 +126,7 @@ func (h *PermissionHandler) GetMyPermissions(c *gin.Context) {
tier := middleware.GetUserTier(c)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"code": 0,
"message": "ok",
"data": gin.H{
"permissions": perms,
+20 -26
View File
@@ -33,17 +33,17 @@ func (h *SiteHandler) ListSites(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": sites})
}
// GetSite 获取单个站点详情(解密敏感字段)。
// GetSite 获取单个站点详情。
func (h *SiteHandler) GetSite(c *gin.Context) {
site, err := h.svc.Site.GetByID(c.Request.Context(), c.Param("id"))
site, err := h.svc.Site.FindByID(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
}
if site == nil {
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": site})
}
@@ -54,41 +54,30 @@ func (h *SiteHandler) CreateSite(c *gin.Context) {
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 {
if err := h.svc.Site.Create(c.Request.Context(), &site); 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})
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "ok", "data": site})
}
// UpdateSite 更新站点。
func (h *SiteHandler) UpdateSite(c *gin.Context) {
var site model.Site
if err := c.ShouldBindJSON(&site); err != nil {
patch := make(map[string]any)
if err := c.ShouldBindJSON(&patch); 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
}
if err := h.svc.Site.Update(c.Request.Context(), c.Param("id"), patch); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": updated})
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
// 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
}
@@ -97,11 +86,16 @@ func (h *SiteHandler) DeleteSite(c *gin.Context) {
// 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()})
ok, msg, err := h.svc.Site.TestConnection(c.Request.Context(), c.Param("id"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": msg})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": msg})
}
// GetSiteTypes 返回支持的站点类型列表。
+24 -8
View File
@@ -2,6 +2,7 @@
package handler
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
@@ -66,21 +67,36 @@ func createSiteHandler(svc *service.Container) gin.HandlerFunc {
if req.Enabled != nil {
enabled = *req.Enabled
}
// Pack fields not in the core model into Extra JSON.
extraMap := map[string]any{}
if req.UserAgent != "" {
extraMap["user_agent"] = req.UserAgent
}
if req.RSSURL != "" {
extraMap["rss_url"] = req.RSSURL
}
if req.Timeout > 0 {
extraMap["timeout"] = req.Timeout
}
if req.Priority > 0 {
extraMap["priority"] = req.Priority
}
extraMap["use_proxy"] = req.UseProxy
if req.Downloader != "" {
extraMap["downloader"] = req.Downloader
}
extraJSON, _ := json.Marshal(extraMap)
site := &model.Site{
Name: req.Name,
BaseURL: req.BaseURL,
SiteType: req.SiteType,
URL: req.BaseURL,
Type: req.SiteType,
AuthType: req.AuthType,
Cookie: req.Cookie,
APIKey: req.APIKey,
AuthHeader: req.AuthHeader,
UserAgent: req.UserAgent,
RSSURL: req.RSSURL,
Timeout: req.Timeout,
Priority: req.Priority,
UseProxy: req.UseProxy,
Extra: string(extraJSON),
Enabled: enabled,
Downloader: req.Downloader,
}
if err := svc.Site.Create(c.Request.Context(), site); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+7 -1
View File
@@ -50,11 +50,17 @@ func siteUserdataHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "site not found"})
return
}
loginStatus := "unknown"
if s.LastError == "ok" {
loginStatus = "ok"
} else if s.LastError != "" {
loginStatus = "fail"
}
c.JSON(http.StatusOK, gin.H{
"site_id": s.ID,
"name": s.Name,
"cookie_set": len(s.Cookie) > 0,
"login_status": s.LoginStatus,
"login_status": loginStatus,
"note": "userdata parsing not implemented; stub",
})
}