**新增功能:**
用户名编辑:输入框替换静态文本,白名单验证(中文/英文/数字/下划线/连字符),前端计数器(n/16),utf8对齐PG VARCHAR,XSS防控。
个性签名编辑:Textarea,128字符上限,实时计数器。
头像上传管线:校验→解码→裁切→CatmullRom缩放→WebP二分编码≤100KB→原子写入(.tmp→os.Rename)。限制5MB,128-3840px,JPEG/PNG/WebP。输出512x512 WebP。文件名 {uid}_{timestamp}.webp。清理旧头像。
自定义裁切弹窗:浅色主题,固定裁切框+图片平移/缩放(1×-3×滚轮),box-shadow遮罩,三等分网格。坐标映射pan/zoom→原图像素→subImage。
CSP修复:img-src允许data:URI(FileReader预览)。
**文件变更:**
修改: README, docs/{development,structure}.md, go.{mod,sum}, cmd/server/main.go, controller/settings, middleware/security, router, templates/settings/{index.html,css}
新增: internal/service/avatar_service.go, docs/settings.md
135 lines
3.7 KiB
Go
135 lines
3.7 KiB
Go
package controller
|
||
|
||
import (
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// profileProvider SettingsController 对 Service 层的最小依赖(ISP:2 个方法)
|
||
type profileProvider interface {
|
||
GetProfile(userID uint) (*model.User, error)
|
||
UpdateProfile(userID uint, username, bio string) error
|
||
}
|
||
|
||
// avatarProvider 头像上传对 Service 层的最小依赖(ISP:1 个方法)
|
||
type avatarProvider interface {
|
||
ProcessAvatar(userID uint, file io.Reader, contentType string, cropX, cropY, cropSize int) (string, error)
|
||
}
|
||
|
||
// SettingsController 个人设置控制器
|
||
type SettingsController struct {
|
||
authService profileProvider
|
||
avatarService avatarProvider
|
||
}
|
||
|
||
// NewSettingsController 构造函数
|
||
func NewSettingsController(authService profileProvider, avatarService avatarProvider) *SettingsController {
|
||
return &SettingsController{authService: authService, avatarService: avatarService}
|
||
}
|
||
|
||
// SettingsPage 个人设置页面(需登录)
|
||
func (sc *SettingsController) SettingsPage(c *gin.Context) {
|
||
uidVal, exists := c.Get("uid")
|
||
if !exists {
|
||
c.Redirect(http.StatusFound, "/auth/login")
|
||
c.Abort()
|
||
return
|
||
}
|
||
uid := uidVal.(uint)
|
||
|
||
tab := c.Param("tab")
|
||
if tab != "profile" && tab != "account" {
|
||
c.String(http.StatusNotFound, "页面不存在")
|
||
return
|
||
}
|
||
|
||
user, err := sc.authService.GetProfile(uid)
|
||
if err != nil || user == nil {
|
||
c.String(http.StatusNotFound, "用户不存在")
|
||
return
|
||
}
|
||
|
||
c.HTML(http.StatusOK, "settings/index.html", common.BuildPageData(c, gin.H{
|
||
"Title": "个人设置",
|
||
"ExtraCSS": "/static/css/settings.css",
|
||
"ActiveTab": tab,
|
||
"User": user,
|
||
"RoleName": model.RoleDisplayNames[user.Role],
|
||
"StatusName": model.StatusDisplayNames[user.Status],
|
||
}))
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
if err := sc.authService.UpdateProfile(uid.(uint), 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"))
|
||
|
||
url, err := sc.avatarService.ProcessAvatar(uid.(uint), 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})
|
||
}
|
||
|
||
// 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())
|
||
default:
|
||
common.Error(c, http.StatusInternalServerError, "操作失败")
|
||
}
|
||
}
|