From a19b1fcb5174d104fae867e6d71ea45c76c17c85 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Wed, 27 May 2026 13:57:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=E8=B6=85?= =?UTF-8?q?=E6=A0=87=E6=8E=A7=E5=88=B6=E5=99=A8=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E8=A1=8C=E6=95=B0=E2=89=A4120=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth_controller.go(238→54): 拆出auth_api_login.go(107)+auth_api_register.go(96) - settings_controller.go(289→101): 拆出settings_api_profile(116)+account(59)+error_handlers(41) - message_controller.go(129→85): 拆出message_page_controller.go(55) - 所有拆分后文件均≤120行,符合code-style.md瘦控制器规范 --- internal/controller/auth_api_login.go | 107 ++++++++++ internal/controller/auth_api_register.go | 96 +++++++++ internal/controller/auth_controller.go | 184 ----------------- internal/controller/message_controller.go | 44 ---- .../controller/message_page_controller.go | 55 +++++ internal/controller/settings_api_account.go | 59 ++++++ internal/controller/settings_api_profile.go | 116 +++++++++++ internal/controller/settings_controller.go | 190 +----------------- .../controller/settings_error_handlers.go | 41 ++++ 9 files changed, 475 insertions(+), 417 deletions(-) create mode 100644 internal/controller/auth_api_login.go create mode 100644 internal/controller/auth_api_register.go create mode 100644 internal/controller/message_page_controller.go create mode 100644 internal/controller/settings_api_account.go create mode 100644 internal/controller/settings_api_profile.go create mode 100644 internal/controller/settings_error_handlers.go diff --git a/internal/controller/auth_api_login.go b/internal/controller/auth_api_login.go new file mode 100644 index 0000000..6db74be --- /dev/null +++ b/internal/controller/auth_api_login.go @@ -0,0 +1,107 @@ +package controller + +import ( + "net/http" + + "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + + "github.com/gin-gonic/gin" +) + +// clientIP 获取客户端真实 IP(考虑反向代理) +func clientIP(c *gin.Context) string { + if fwd := c.GetHeader("X-Forwarded-For"); fwd != "" { + return fwd + } + if real := c.GetHeader("X-Real-IP"); real != "" { + return real + } + return c.ClientIP() +} + +// Login 登录 API(含双维度限流) +func (ac *AuthController) Login(c *gin.Context) { + var req model.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请检查输入") + return + } + + email := req.Email + ip := clientIP(c) + + // --- 限流:账户维度 --- + acctResult, recordAccount := ac.rateLimiter.AllowAccount(email) + if acctResult.Blocked { + common.Error(c, http.StatusTooManyRequests, acctResult.Message) + return + } + + // --- 限流:IP 维度 --- + ipResult, recordIP := ac.rateLimiter.AllowIP(ip) + if ipResult.Blocked { + common.Error(c, http.StatusTooManyRequests, ipResult.Message) + return + } + + accessToken, refreshToken, user, err := ac.authService.Login(req, ip) + if err != nil { + // 记录失败 → 两个维度各 +1 + if recordAccount != nil { + recordAccount() + } + if recordIP != nil { + recordIP() + } + + switch err { + case common.ErrInvalidCred: + common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") + case common.ErrUserBanned: + common.Error(c, http.StatusForbidden, "账号已被封禁") + case common.ErrUserLocked: + common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") + case common.ErrNeedsConfirmRestore: + // 注销账号登录 → 需要二次确认恢复 + c.JSON(http.StatusOK, gin.H{ + "success": true, + "action": "confirm_restore", + "message": "你的账号正在注销中,登录将中止注销流程", + }) + default: + common.Error(c, http.StatusInternalServerError, "登录失败,请稍后重试") + } + return + } + + // 登录成功 → 清除失败计数 + ac.rateLimiter.Clear(email, ip) + + common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) + + common.OkWithMessage(c, user, "登录成功") +} + +// ConfirmRestore 二次确认恢复已注销账号 +func (ac *AuthController) ConfirmRestore(c *gin.Context) { + var req model.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请检查输入") + return + } + + accessToken, refreshToken, user, err := ac.authService.ConfirmRestore(req, clientIP(c)) + if err != nil { + switch err { + case common.ErrInvalidCred: + common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") + default: + common.Error(c, http.StatusInternalServerError, "操作失败,请稍后重试") + } + return + } + + common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) + common.OkWithMessage(c, user, "账号已恢复,欢迎回来") +} diff --git a/internal/controller/auth_api_register.go b/internal/controller/auth_api_register.go new file mode 100644 index 0000000..e961c24 --- /dev/null +++ b/internal/controller/auth_api_register.go @@ -0,0 +1,96 @@ +package controller + +import ( + "net/http" + + "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + + "github.com/gin-gonic/gin" +) + +// CheckEmail 检查邮箱是否已注册 +func (ac *AuthController) CheckEmail(c *gin.Context) { + var req model.CheckEmailRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请提供有效的邮箱地址") + return + } + + exists, err := ac.authService.CheckEmail(req.Email) + if err != nil { + common.Error(c, http.StatusInternalServerError, "检查失败") + return + } + + common.Ok(c, gin.H{"exists": exists}) +} + +// Register 注册 API +func (ac *AuthController) Register(c *gin.Context) { + var req model.RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请检查输入:"+err.Error()) + return + } + + // 注册 IP 限流:1 分钟 5 次 + regResult, recordReg := ac.rateLimiter.AllowIP(clientIP(c) + ":register") + if regResult.Blocked { + common.Error(c, http.StatusTooManyRequests, "注册请求过于频繁,请稍后重试") + return + } + + accessToken, refreshToken, user, err := ac.authService.Register(req, clientIP(c)) + if err != nil { + if recordReg != nil { + recordReg() + } + switch err { + case common.ErrEmailExists: + common.Error(c, http.StatusConflict, "该邮箱已注册") + case common.ErrWeakPassword: + common.Error(c, http.StatusBadRequest, err.Error()) + default: + common.Error(c, http.StatusInternalServerError, "注册失败,请稍后重试") + } + return + } + + common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) + + common.OkWithMessage(c, user, "注册成功!欢迎加入 MetaLab") +} + +// Logout 退出登录:清除所有认证 Cookie +func (ac *AuthController) Logout(c *gin.Context) { + common.ClearAuthCookies(c, ac.cfg) + common.OkMessage(c, "已退出登录") +} + +// RefreshToken 用 refresh token 换取新的 access token +func (ac *AuthController) RefreshToken(c *gin.Context) { + refreshToken, err := c.Cookie(common.RefreshCookieName) + if err != nil { + common.Error(c, http.StatusUnauthorized, "请重新登录") + return + } + + accessToken, _, err := ac.tokenService.RefreshAccessToken(refreshToken) + if err != nil { + common.ClearAuthCookies(c, ac.cfg) + switch err { + case common.ErrTokenExpired, common.ErrTokenRevoked: + common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录") + case common.ErrUserBanned: + common.Error(c, http.StatusForbidden, "账号已被封禁") + default: + common.Error(c, http.StatusUnauthorized, "请重新登录") + } + return + } + + common.SetAccessCookie(c, accessToken, ac.cfg) + + common.Ok(c, nil) +} diff --git a/internal/controller/auth_controller.go b/internal/controller/auth_controller.go index 9086342..bf3145a 100644 --- a/internal/controller/auth_controller.go +++ b/internal/controller/auth_controller.go @@ -5,7 +5,6 @@ import ( "metazone.cc/metalab/internal/common" "metazone.cc/metalab/internal/config" - "metazone.cc/metalab/internal/model" "metazone.cc/metalab/internal/theme" "github.com/gin-gonic/gin" @@ -53,186 +52,3 @@ func (ac *AuthController) LoginPage(c *gin.Context) { "ExtraCSS": "/static/css/auth.css", })) } - -// clientIP 获取客户端真实 IP(考虑反向代理) -func clientIP(c *gin.Context) string { - if fwd := c.GetHeader("X-Forwarded-For"); fwd != "" { - return fwd - } - if real := c.GetHeader("X-Real-IP"); real != "" { - return real - } - return c.ClientIP() -} - -// Login 登录 API(含双维度限流) -func (ac *AuthController) Login(c *gin.Context) { - var req model.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请检查输入") - return - } - - email := req.Email - ip := clientIP(c) - - // --- 限流:账户维度 --- - acctResult, recordAccount := ac.rateLimiter.AllowAccount(email) - if acctResult.Blocked { - common.Error(c, http.StatusTooManyRequests, acctResult.Message) - return - } - - // --- 限流:IP 维度 --- - ipResult, recordIP := ac.rateLimiter.AllowIP(ip) - if ipResult.Blocked { - common.Error(c, http.StatusTooManyRequests, ipResult.Message) - return - } - - accessToken, refreshToken, user, err := ac.authService.Login(req, ip) - if err != nil { - // 记录失败 → 两个维度各 +1 - if recordAccount != nil { - recordAccount() - } - if recordIP != nil { - recordIP() - } - - switch err { - case common.ErrInvalidCred: - common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") - case common.ErrUserBanned: - common.Error(c, http.StatusForbidden, "账号已被封禁") - case common.ErrUserLocked: - common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") - case common.ErrNeedsConfirmRestore: - // 注销账号登录 → 需要二次确认恢复 - c.JSON(http.StatusOK, gin.H{ - "success": true, - "action": "confirm_restore", - "message": "你的账号正在注销中,登录将中止注销流程", - }) - default: - common.Error(c, http.StatusInternalServerError, "登录失败,请稍后重试") - } - return - } - - // 登录成功 → 清除失败计数 - ac.rateLimiter.Clear(email, ip) - - common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) - - common.OkWithMessage(c, user, "登录成功") -} - -// ConfirmRestore 二次确认恢复已注销账号 -func (ac *AuthController) ConfirmRestore(c *gin.Context) { - var req model.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请检查输入") - return - } - - accessToken, refreshToken, user, err := ac.authService.ConfirmRestore(req, clientIP(c)) - if err != nil { - switch err { - case common.ErrInvalidCred: - common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") - default: - common.Error(c, http.StatusInternalServerError, "操作失败,请稍后重试") - } - return - } - - common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) - common.OkWithMessage(c, user, "账号已恢复,欢迎回来") -} - -// CheckEmail 检查邮箱是否已注册 -func (ac *AuthController) CheckEmail(c *gin.Context) { - var req model.CheckEmailRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请提供有效的邮箱地址") - return - } - - exists, err := ac.authService.CheckEmail(req.Email) - if err != nil { - common.Error(c, http.StatusInternalServerError, "检查失败") - return - } - - common.Ok(c, gin.H{"exists": exists}) -} - -// Register 注册 API -func (ac *AuthController) Register(c *gin.Context) { - var req model.RegisterRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请检查输入:"+err.Error()) - return - } - - // 注册 IP 限流:1 分钟 5 次 - regResult, recordReg := ac.rateLimiter.AllowIP(clientIP(c) + ":register") - if regResult.Blocked { - common.Error(c, http.StatusTooManyRequests, "注册请求过于频繁,请稍后重试") - return - } - - accessToken, refreshToken, user, err := ac.authService.Register(req, clientIP(c)) - if err != nil { - if recordReg != nil { - recordReg() - } - switch err { - case common.ErrEmailExists: - common.Error(c, http.StatusConflict, "该邮箱已注册") - case common.ErrWeakPassword: - common.Error(c, http.StatusBadRequest, err.Error()) - default: - common.Error(c, http.StatusInternalServerError, "注册失败,请稍后重试") - } - return - } - - common.SetAuthCookies(c, accessToken, refreshToken, req.RememberMe, ac.cfg) - - common.OkWithMessage(c, user, "注册成功!欢迎加入 MetaLab") -} - -// Logout 退出登录:清除所有认证 Cookie -func (ac *AuthController) Logout(c *gin.Context) { - common.ClearAuthCookies(c, ac.cfg) - common.OkMessage(c, "已退出登录") -} - -// RefreshToken 用 refresh token 换取新的 access token -func (ac *AuthController) RefreshToken(c *gin.Context) { - refreshToken, err := c.Cookie(common.RefreshCookieName) - if err != nil { - common.Error(c, http.StatusUnauthorized, "请重新登录") - return - } - - accessToken, _, err := ac.tokenService.RefreshAccessToken(refreshToken) - if err != nil { - common.ClearAuthCookies(c, ac.cfg) - switch err { - case common.ErrTokenExpired, common.ErrTokenRevoked: - common.Error(c, http.StatusUnauthorized, "登录凭证已失效,请重新登录") - case common.ErrUserBanned: - common.Error(c, http.StatusForbidden, "账号已被封禁") - default: - common.Error(c, http.StatusUnauthorized, "请重新登录") - } - return - } - - common.SetAccessCookie(c, accessToken, ac.cfg) - - common.Ok(c, nil) -} diff --git a/internal/controller/message_controller.go b/internal/controller/message_controller.go index 97d2ce8..ef6520b 100644 --- a/internal/controller/message_controller.go +++ b/internal/controller/message_controller.go @@ -28,50 +28,6 @@ func NewMessageController(notifService notifProvider) *MessageController { return &MessageController{notifService: notifService} } -// MessagesPage 消息中心页面(需登录,noindex) -func (mc *MessageController) MessagesPage(c *gin.Context) { - uidVal, exists := c.Get("uid") - if !exists { - c.Redirect(http.StatusFound, "/auth/login") - c.Abort() - return - } - uid := uidVal.(uint) - - // 从 query 取分页参数 - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - if page < 1 { - page = 1 - } - pageSize := 20 - - result, err := mc.notifService.List(uid, page, pageSize) - if err != nil { - result = &model.NotificationListResult{Items: []model.Notification{}, Total: 0, Page: 1} - } - - totalPages := result.TotalPages - if totalPages == 0 && result.Total > 0 { - totalPages = int((result.Total + int64(pageSize) - 1) / int64(pageSize)) - } - - c.HTML(http.StatusOK, "messages/index.html", common.BuildPageData(c, gin.H{ - "Title": "消息中心", - "ExtraCSS": "/static/css/messages.css", - "Messages": result.Items, - "Total": result.Total, - "Page": result.Page, - "TotalPages": totalPages, - "PrevPage": result.Page - 1, - "NextPage": result.Page + 1, - "HasPrev": result.Page > 1, - "HasNext": result.Page < totalPages, - "UnreadCount": result.Unread, - "NotifyTypeNames": model.NotifyTypeNames, - "AuditTypeNames": model.AuditTypeNames, - })) -} - // UnreadCount 获取未读消息数(API,供导航栏轮询) func (mc *MessageController) UnreadCount(c *gin.Context) { uidVal, exists := c.Get("uid") diff --git a/internal/controller/message_page_controller.go b/internal/controller/message_page_controller.go new file mode 100644 index 0000000..b214285 --- /dev/null +++ b/internal/controller/message_page_controller.go @@ -0,0 +1,55 @@ +package controller + +import ( + "net/http" + "strconv" + + "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + + "github.com/gin-gonic/gin" +) + +// MessagesPage 消息中心页面(需登录,noindex) +func (mc *MessageController) MessagesPage(c *gin.Context) { + uidVal, exists := c.Get("uid") + if !exists { + c.Redirect(http.StatusFound, "/auth/login") + c.Abort() + return + } + uid := uidVal.(uint) + + // 从 query 取分页参数 + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + if page < 1 { + page = 1 + } + pageSize := 20 + + result, err := mc.notifService.List(uid, page, pageSize) + if err != nil { + result = &model.NotificationListResult{Items: []model.Notification{}, Total: 0, Page: 1} + } + + totalPages := result.TotalPages + if totalPages == 0 && result.Total > 0 { + totalPages = int((result.Total + int64(pageSize) - 1) / int64(pageSize)) + } + + c.HTML(http.StatusOK, "messages/index.html", common.BuildPageData(c, gin.H{ + "Title": "消息中心", + "ExtraCSS": "/static/css/messages.css", + "Messages": result.Items, + "Total": result.Total, + "Page": result.Page, + "TotalPages": totalPages, + "PrevPage": result.Page - 1, + "NextPage": result.Page + 1, + "HasPrev": result.Page > 1, + "HasNext": result.Page < totalPages, + "UnreadCount": result.Unread, + "NotifyTypeNames": model.NotifyTypeNames, + "AuditTypeNames": model.AuditTypeNames, + })) +} diff --git a/internal/controller/settings_api_account.go b/internal/controller/settings_api_account.go new file mode 100644 index 0000000..69dba5d --- /dev/null +++ b/internal/controller/settings_api_account.go @@ -0,0 +1,59 @@ +package controller + +import ( + "net/http" + + "metazone.cc/metalab/internal/common" + + "github.com/gin-gonic/gin" +) + +// ChangePassword 修改密码(需登录) +func (sc *SettingsController) ChangePassword(c *gin.Context) { + uid, exists := c.Get("uid") + if !exists { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + var req struct { + CurrentPassword string `json:"current_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=8"` + } + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请检查输入") + return + } + + if err := sc.authService.ChangePassword(uid.(uint), req.CurrentPassword, req.NewPassword); err != nil { + handleSettingsError(c, err) + return + } + + common.OkMessage(c, "密码已修改,请重新登录") +} + +// DeleteAccount 自助注销账号(需登录,验证密码 + 注销原因) +func (sc *SettingsController) DeleteAccount(c *gin.Context) { + uid, exists := c.Get("uid") + if !exists { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + var req struct { + Password string `json:"password" binding:"required"` + Reason string `json:"reason"` + } + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "请检查输入") + return + } + + if err := sc.authService.DeleteAccount(uid.(uint), req.Password, req.Reason); err != nil { + handleSettingsError(c, err) + return + } + + common.OkMessage(c, "账号已注销,7 天内重新登录即可恢复") +} diff --git a/internal/controller/settings_api_profile.go b/internal/controller/settings_api_profile.go new file mode 100644 index 0000000..54faf1f --- /dev/null +++ b/internal/controller/settings_api_profile.go @@ -0,0 +1,116 @@ +package controller + +import ( + "net/http" + "strconv" + + "metazone.cc/metalab/internal/common" + "metazone.cc/metalab/internal/model" + + "github.com/gin-gonic/gin" +) + +// UpdateProfile 修改个人资料(用户名 + 个性签名,需登录) +// 普通用户走审核流程,管理员及以上直接落库 +func (sc *SettingsController) UpdateProfile(c *gin.Context) { + uid, exists := c.Get("uid") + if !exists { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + var req struct { + Username string `json:"username"` + Bio string `json:"bio"` + } + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "参数错误") + return + } + + userID := uid.(uint) + + // 判断是否需要审核 + shouldAudit, err := sc.auditService.ShouldAudit(userID) + if err != nil { + common.Error(c, http.StatusInternalServerError, "操作失败") + return + } + + if shouldAudit { + user, err := sc.authService.GetProfile(userID) + if err != nil { + common.Error(c, http.StatusInternalServerError, "操作失败") + return + } + if err := sc.auditService.SubmitProfileChanges(userID, user, req.Username, req.Bio); err != nil { + handleAuditSubmitError(c, err) + return + } + common.OkMessage(c, "修改已提交审核,通过前当前信息保持不变") + return + } + + // 管理员及以上直接更新 + if err := sc.authService.UpdateProfile(userID, req.Username, req.Bio); err != nil { + handleSettingsError(c, err) + return + } + + common.OkMessage(c, "个人资料已更新") +} + +// UploadAvatar 上传头像(需登录,multipart/form-data) +// 普通用户走审核流程,管理员及以上直接更新 +func (sc *SettingsController) UploadAvatar(c *gin.Context) { + uid, exists := c.Get("uid") + if !exists { + common.Error(c, http.StatusUnauthorized, "请先登录") + return + } + + file, header, err := c.Request.FormFile("avatar") + if err != nil { + common.Error(c, http.StatusBadRequest, "请选择文件") + return + } + defer file.Close() + + // 裁切参数(可选,来自前端裁切弹窗) + cropX, _ := strconv.Atoi(c.PostForm("crop_x")) + cropY, _ := strconv.Atoi(c.PostForm("crop_y")) + cropSize, _ := strconv.Atoi(c.PostForm("crop_size")) + + userID := uid.(uint) + + // 判断是否需要审核 + shouldAudit, err := sc.auditService.ShouldAudit(userID) + if err != nil { + common.Error(c, http.StatusInternalServerError, "操作失败") + return + } + + if shouldAudit { + // 仅处理图片,不更新用户 + avatarURL, err := sc.avatarService.ProcessImage(userID, file, header.Header.Get("Content-Type"), cropX, cropY, cropSize) + if err != nil { + common.Error(c, http.StatusBadRequest, err.Error()) + return + } + if err := sc.auditService.Submit(userID, model.AuditTypeAvatar, avatarURL); err != nil { + handleAuditSubmitError(c, err) + return + } + common.OkMessage(c, "头像已提交审核,通过前当前头像保持不变") + return + } + + // 管理员及以上直接更新 + url, err := sc.avatarService.ProcessAvatar(userID, file, header.Header.Get("Content-Type"), cropX, cropY, cropSize) + if err != nil { + common.Error(c, http.StatusBadRequest, err.Error()) + return + } + + common.Ok(c, gin.H{"url": url}) +} diff --git a/internal/controller/settings_controller.go b/internal/controller/settings_controller.go index 155f561..9e762e6 100644 --- a/internal/controller/settings_controller.go +++ b/internal/controller/settings_controller.go @@ -3,7 +3,6 @@ package controller import ( "io" "net/http" - "strconv" "metazone.cc/metalab/internal/common" "metazone.cc/metalab/internal/model" @@ -25,7 +24,7 @@ type avatarProvider interface { ProcessImage(userID uint, file io.Reader, contentType string, cropX, cropY, cropSize int) (string, error) } -// auditSubmittable 个人设置对审核服务的依赖(ISP:3 个方法) +// auditSubmittable 个人设置对审核服务的依赖(ISP:4 个方法) type auditSubmittable interface { ShouldAudit(userID uint) (bool, error) SubmitProfileChanges(userID uint, currentUser *model.User, newUsername, newBio string) error @@ -82,193 +81,6 @@ func (sc *SettingsController) SettingsPage(c *gin.Context) { })) } -// UpdateProfile 修改个人资料(用户名 + 个性签名,需登录) -// 普通用户走审核流程,管理员及以上直接落库 -func (sc *SettingsController) UpdateProfile(c *gin.Context) { - uid, exists := c.Get("uid") - if !exists { - common.Error(c, http.StatusUnauthorized, "请先登录") - return - } - - var req struct { - Username string `json:"username"` - Bio string `json:"bio"` - } - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "参数错误") - return - } - - userID := uid.(uint) - - // 判断是否需要审核 - shouldAudit, err := sc.auditService.ShouldAudit(userID) - if err != nil { - common.Error(c, http.StatusInternalServerError, "操作失败") - return - } - - if shouldAudit { - user, err := sc.authService.GetProfile(userID) - if err != nil { - common.Error(c, http.StatusInternalServerError, "操作失败") - return - } - if err := sc.auditService.SubmitProfileChanges(userID, user, req.Username, req.Bio); err != nil { - handleAuditSubmitError(c, err) - return - } - common.OkMessage(c, "修改已提交审核,通过前当前信息保持不变") - return - } - - // 管理员及以上直接更新 - if err := sc.authService.UpdateProfile(userID, req.Username, req.Bio); err != nil { - handleSettingsError(c, err) - return - } - - common.OkMessage(c, "个人资料已更新") -} - -// UploadAvatar 上传头像(需登录,multipart/form-data) -// 普通用户走审核流程,管理员及以上直接更新 -func (sc *SettingsController) UploadAvatar(c *gin.Context) { - uid, exists := c.Get("uid") - if !exists { - common.Error(c, http.StatusUnauthorized, "请先登录") - return - } - - file, header, err := c.Request.FormFile("avatar") - if err != nil { - common.Error(c, http.StatusBadRequest, "请选择文件") - return - } - defer file.Close() - - // 裁切参数(可选,来自前端裁切弹窗) - cropX, _ := strconv.Atoi(c.PostForm("crop_x")) - cropY, _ := strconv.Atoi(c.PostForm("crop_y")) - cropSize, _ := strconv.Atoi(c.PostForm("crop_size")) - - userID := uid.(uint) - - // 判断是否需要审核 - shouldAudit, err := sc.auditService.ShouldAudit(userID) - if err != nil { - common.Error(c, http.StatusInternalServerError, "操作失败") - return - } - - if shouldAudit { - // 仅处理图片,不更新用户 - avatarURL, err := sc.avatarService.ProcessImage(userID, file, header.Header.Get("Content-Type"), cropX, cropY, cropSize) - if err != nil { - common.Error(c, http.StatusBadRequest, err.Error()) - return - } - if err := sc.auditService.Submit(userID, model.AuditTypeAvatar, avatarURL); err != nil { - handleAuditSubmitError(c, err) - return - } - common.OkMessage(c, "头像已提交审核,通过前当前头像保持不变") - return - } - - // 管理员及以上直接更新 - url, err := sc.avatarService.ProcessAvatar(userID, file, header.Header.Get("Content-Type"), cropX, cropY, cropSize) - if err != nil { - common.Error(c, http.StatusBadRequest, err.Error()) - return - } - - common.Ok(c, gin.H{"url": url}) -} - -// ChangePassword 修改密码(需登录) -func (sc *SettingsController) ChangePassword(c *gin.Context) { - uid, exists := c.Get("uid") - if !exists { - common.Error(c, http.StatusUnauthorized, "请先登录") - return - } - - var req struct { - CurrentPassword string `json:"current_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=8"` - } - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请检查输入") - return - } - - if err := sc.authService.ChangePassword(uid.(uint), req.CurrentPassword, req.NewPassword); err != nil { - handleSettingsError(c, err) - return - } - - common.OkMessage(c, "密码已修改,请重新登录") -} - -// DeleteAccount 自助注销账号(需登录,验证密码 + 注销原因) -func (sc *SettingsController) DeleteAccount(c *gin.Context) { - uid, exists := c.Get("uid") - if !exists { - common.Error(c, http.StatusUnauthorized, "请先登录") - return - } - - var req struct { - Password string `json:"password" binding:"required"` - Reason string `json:"reason"` - } - if err := c.ShouldBindJSON(&req); err != nil { - common.Error(c, http.StatusBadRequest, "请检查输入") - return - } - - if err := sc.authService.DeleteAccount(uid.(uint), req.Password, req.Reason); err != nil { - handleSettingsError(c, err) - return - } - - common.OkMessage(c, "账号已注销,7 天内重新登录即可恢复") -} - -// handleSettingsError 统一处理 settings 接口的 service 层错误 -func handleSettingsError(c *gin.Context, err error) { - switch err { - case common.ErrUsernameTaken: - common.Error(c, http.StatusConflict, err.Error()) - case common.ErrUsernameInvalid: - common.Error(c, http.StatusBadRequest, err.Error()) - case common.ErrBioTooLong: - common.Error(c, http.StatusBadRequest, err.Error()) - case common.ErrIncorrectPassword: - common.Error(c, http.StatusForbidden, err.Error()) - case common.ErrOwnerCannotDelete: - common.Error(c, http.StatusForbidden, err.Error()) - default: - common.Error(c, http.StatusInternalServerError, "操作失败") - } -} - -// handleAuditSubmitError 统一处理审核提交的错误 -func handleAuditSubmitError(c *gin.Context, err error) { - switch err { - case common.ErrAuditDisabled: - common.Error(c, http.StatusForbidden, "审核功能未开启") - case common.ErrAuditTypeOff: - common.Error(c, http.StatusForbidden, "该类型审核未开启,请联系管理员") - case common.ErrUsernameTaken: - common.Error(c, http.StatusConflict, err.Error()) - default: - common.Error(c, http.StatusInternalServerError, "提交审核失败") - } -} - // AuditStatus 查询当前用户的待审核类型(需登录) func (sc *SettingsController) AuditStatus(c *gin.Context) { uid, exists := c.Get("uid") diff --git a/internal/controller/settings_error_handlers.go b/internal/controller/settings_error_handlers.go new file mode 100644 index 0000000..b5fa8c7 --- /dev/null +++ b/internal/controller/settings_error_handlers.go @@ -0,0 +1,41 @@ +package controller + +import ( + "net/http" + + "metazone.cc/metalab/internal/common" + + "github.com/gin-gonic/gin" +) + +// handleSettingsError 统一处理 settings 接口的 service 层错误 +func handleSettingsError(c *gin.Context, err error) { + switch err { + case common.ErrUsernameTaken: + common.Error(c, http.StatusConflict, err.Error()) + case common.ErrUsernameInvalid: + common.Error(c, http.StatusBadRequest, err.Error()) + case common.ErrBioTooLong: + common.Error(c, http.StatusBadRequest, err.Error()) + case common.ErrIncorrectPassword: + common.Error(c, http.StatusForbidden, err.Error()) + case common.ErrOwnerCannotDelete: + common.Error(c, http.StatusForbidden, err.Error()) + default: + common.Error(c, http.StatusInternalServerError, "操作失败") + } +} + +// handleAuditSubmitError 统一处理审核提交的错误 +func handleAuditSubmitError(c *gin.Context, err error) { + switch err { + case common.ErrAuditDisabled: + common.Error(c, http.StatusForbidden, "审核功能未开启") + case common.ErrAuditTypeOff: + common.Error(c, http.StatusForbidden, "该类型审核未开启,请联系管理员") + case common.ErrUsernameTaken: + common.Error(c, http.StatusConflict, err.Error()) + default: + common.Error(c, http.StatusInternalServerError, "提交审核失败") + } +}