- 新增收藏夹系统 (Folder/FolderItem 模型、CRUD API、前端管理页) - 新增文章详情页收藏按钮 (书签图标、ToggleFavorite 一键收藏) - 新增 Studio 趋势图 (赋能值/评论/点赞/收藏 4 线图, 7d/30d/90d) - 新增通知偏好设置页 (评论/回复/点赞/关注 4 类开关) - 后端通知列表支持按偏好过滤 (排除已关闭的通知类型) - 下载 Chart.js 到本地静态资源 (static/js/chart.umd.min.js) - 补充收藏夹卡片、开关滑块、趋势图网格等 CSS 样式
164 lines
5.2 KiB
Go
164 lines
5.2 KiB
Go
package controller
|
||
|
||
import (
|
||
"io"
|
||
"net/http"
|
||
"time"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/model"
|
||
"metazone.cc/metalab/internal/session"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// profileProvider SettingsController 对 Service 层的最小依赖(ISP:4 个方法)
|
||
type profileProvider interface {
|
||
GetProfile(userID uint) (*model.User, error)
|
||
UpdateProfile(userID uint, username, bio string) error
|
||
ChangePassword(userID uint, currentPassword, newPassword string) error
|
||
DeleteAccount(userID uint, password, reason string) error
|
||
}
|
||
|
||
// avatarProvider 头像上传对 Service 层的最小依赖(ISP:2 个方法)
|
||
type avatarProvider interface {
|
||
ProcessAvatar(userID uint, file io.Reader, contentType string, cropX, cropY, cropSize int) (string, error)
|
||
ProcessImage(userID uint, file io.Reader, contentType string, cropX, cropY, cropSize int) (string, error)
|
||
}
|
||
|
||
// auditSubmittable 个人设置对审核服务的依赖(ISP:4 个方法)
|
||
type auditSubmittable interface {
|
||
ShouldAudit(userID uint) (bool, error)
|
||
SubmitProfileChanges(userID uint, currentUser *model.User, newUsername, newBio string) error
|
||
Submit(userID uint, auditType, newValue string) error
|
||
GetPendingTypes(userID uint) ([]string, error)
|
||
}
|
||
|
||
// sessionConfig 登录管理对配置的最小依赖(ISP:2 个方法)
|
||
type sessionConfig interface {
|
||
GetIdleTimeout() int // 分钟
|
||
GetRememberTimeout() int // 分钟
|
||
}
|
||
|
||
// taskCompleter 设置页对等级服务的依赖(用于首次任务奖励 + 检查任务)
|
||
type taskCompleter interface {
|
||
CompleteTask(userID uint, taskType string) (newExp int, levelUp bool, err error)
|
||
HasCompletedTask(userID uint, taskType string) (bool, error)
|
||
}
|
||
|
||
// SettingsController 个人设置控制器
|
||
type SettingsController struct {
|
||
authService profileProvider
|
||
avatarService avatarProvider
|
||
auditService auditSubmittable
|
||
sessionMgr sessionManager
|
||
sessionCfg sessionConfig
|
||
levelSvc taskCompleter
|
||
energySvc energyRenameHandler
|
||
notifyPrefReader notifyPrefReader
|
||
}
|
||
|
||
// NewSettingsController 构造函数
|
||
func NewSettingsController(authService profileProvider, avatarService avatarProvider, auditService auditSubmittable, sessionMgr sessionManager, sessionCfg sessionConfig, levelSvc taskCompleter) *SettingsController {
|
||
return &SettingsController{authService: authService, avatarService: avatarService, auditService: auditService, sessionMgr: sessionMgr, sessionCfg: sessionCfg, levelSvc: levelSvc}
|
||
}
|
||
|
||
// WithEnergyService 链式注入域能服务
|
||
func (sc *SettingsController) WithEnergyService(svc energyRenameHandler) *SettingsController {
|
||
sc.energySvc = svc
|
||
return sc
|
||
}
|
||
|
||
// SettingsPage 个人设置页面(需登录)
|
||
func (sc *SettingsController) SettingsPage(c *gin.Context) {
|
||
uidVal, exists := c.Get("uid")
|
||
if !exists {
|
||
common.RedirectToLogin(c)
|
||
return
|
||
}
|
||
uid := uidVal.(uint)
|
||
|
||
tab := c.Param("tab")
|
||
if tab != "profile" && tab != "account" && tab != "sessions" && tab != "energy" && tab != "favorites" && tab != "notify" {
|
||
c.String(http.StatusNotFound, "页面不存在")
|
||
return
|
||
}
|
||
|
||
user, err := sc.authService.GetProfile(uid)
|
||
if err != nil || user == nil {
|
||
c.String(http.StatusNotFound, "用户不存在")
|
||
return
|
||
}
|
||
|
||
// 查询待审核类型(用于前端显示审核提示)
|
||
pendingTypes, _ := sc.auditService.GetPendingTypes(uid)
|
||
|
||
data := gin.H{
|
||
"Title": "个人设置",
|
||
"ExtraCSS": "/static/css/settings.css",
|
||
"ActiveTab": tab,
|
||
"User": user,
|
||
"RoleName": model.RoleDisplayNames[user.Role],
|
||
"StatusName": model.StatusDisplayNames[user.Status],
|
||
"PendingTypes": pendingTypes,
|
||
"AuditTypes": model.AuditTypeNames,
|
||
}
|
||
|
||
// 登录管理 tab 需要额外数据
|
||
if tab == "sessions" {
|
||
sessions, _ := sc.sessionMgr.ListByUID(uid)
|
||
if sessions == nil {
|
||
sessions = []*session.Session{}
|
||
}
|
||
currentSID, _ := c.Cookie(common.SessionCookieName)
|
||
// 解析每个 session 的设备信息
|
||
type sessionView struct {
|
||
*session.Session
|
||
DeviceInfo session.DeviceInfo
|
||
IsCurrent bool
|
||
TTLMinutes int // 剩余有效分钟数(仅非当前设备显示)
|
||
}
|
||
var views []sessionView
|
||
for _, s := range sessions {
|
||
timeout := sc.sessionCfg.GetIdleTimeout() // 临时会话超时(分钟)
|
||
if s.RememberMe {
|
||
timeout = sc.sessionCfg.GetRememberTimeout() // 记住我超时(分钟)
|
||
}
|
||
elapsed := int(time.Since(s.LastAccess).Minutes())
|
||
ttl := timeout - elapsed
|
||
if ttl < 0 {
|
||
ttl = 0
|
||
}
|
||
views = append(views, sessionView{
|
||
Session: s,
|
||
DeviceInfo: session.ParseUA(s.UserAgent),
|
||
IsCurrent: s.ID == currentSID,
|
||
TTLMinutes: ttl,
|
||
})
|
||
}
|
||
data["Sessions"] = views
|
||
data["CurrentSID"] = currentSID
|
||
}
|
||
|
||
c.HTML(http.StatusOK, "settings/index.html", common.BuildPageData(c, data))
|
||
}
|
||
|
||
// AuditStatus 查询当前用户的待审核类型(需登录)
|
||
func (sc *SettingsController) AuditStatus(c *gin.Context) {
|
||
uid, exists := c.Get("uid")
|
||
if !exists {
|
||
common.Error(c, http.StatusUnauthorized, "请先登录")
|
||
return
|
||
}
|
||
|
||
types, err := sc.auditService.GetPendingTypes(uid.(uint))
|
||
if err != nil {
|
||
common.Ok(c, gin.H{"pending_types": []string{}})
|
||
return
|
||
}
|
||
if types == nil {
|
||
types = []string{}
|
||
}
|
||
common.Ok(c, gin.H{"pending_types": types})
|
||
}
|