// WEB 用户接口处理器,负责认证、账户资料、积分查询、头像上传和兑换请求。 package handler import ( "errors" "io" "net/http" "strings" mediakey "juhe-factory/api/internal/media" "juhe-factory/api/internal/model" "juhe-factory/api/internal/service" "juhe-factory/api/internal/storage" "github.com/gin-gonic/gin" "github.com/google/uuid" ) const webUserContextKey = "web_user" type Web struct { service *service.Web cos *storage.COS } func NewWeb(service *service.Web, cos *storage.COS) *Web { return &Web{service: service, cos: cos} } func (h *Web) Login(c *gin.Context) { var body struct { Account string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { fail(c, http.StatusBadRequest, "invalid_request", "请输入账号和密码") return } pair, err := h.service.Login(body.Account, body.Password) if err != nil { fail(c, http.StatusUnauthorized, "invalid_credentials", err.Error()) return } c.JSON(http.StatusOK, gin.H{"data": pair}) } func (h *Web) Refresh(c *gin.Context) { var body struct { RefreshToken string `json:"refresh_token" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { fail(c, http.StatusBadRequest, "invalid_request", "缺少刷新令牌") return } pair, err := h.service.Refresh(body.RefreshToken) if err != nil { fail(c, http.StatusUnauthorized, "invalid_refresh_token", err.Error()) return } c.JSON(http.StatusOK, gin.H{"data": pair}) } func (h *Web) Logout(c *gin.Context) { var body struct { RefreshToken string `json:"refresh_token"` } _ = c.ShouldBindJSON(&body) if err := h.service.Logout(body.RefreshToken); err != nil { fail(c, http.StatusInternalServerError, "logout_failed", "退出失败,请重试") return } c.Status(http.StatusNoContent) } func (h *Web) Middleware() gin.HandlerFunc { return func(c *gin.Context) { raw := strings.TrimSpace(strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")) user, err := h.service.Authenticate(raw) if err != nil { fail(c, http.StatusUnauthorized, "unauthorized", err.Error()) c.Abort() return } c.Set(webUserContextKey, user) c.Next() } } // Account 返回当前用户账户资料、积分额度和头像上传限制。 func (h *Web) Account(c *gin.Context) { data, err := h.service.Account(currentWebUser(c).ID) if err != nil { fail(c, http.StatusInternalServerError, "account_failed", "账户信息加载失败") return } if h.cos != nil { data["avatar_max_size_bytes"] = h.cos.MaxImageBytes() } c.JSON(http.StatusOK, gin.H{"data": data}) } func (h *Web) UpdateProfile(c *gin.Context) { var body struct { Username string `json:"username" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { fail(c, http.StatusBadRequest, "invalid_request", "请输入用户名") return } data, err := h.service.UpdateProfile(currentWebUser(c).ID, body.Username) if err != nil { if errors.Is(err, service.ErrInvalidUsername) || errors.Is(err, service.ErrUsernameExists) { fail(c, http.StatusBadRequest, "profile_invalid", err.Error()) return } fail(c, http.StatusInternalServerError, "profile_update_failed", "资料更新失败,请稍后重试") return } c.JSON(http.StatusOK, gin.H{"data": data}) } func (h *Web) ChangePassword(c *gin.Context) { var body struct { OriginalPassword string `json:"original_password" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { fail(c, http.StatusBadRequest, "invalid_request", "请完整填写密码") return } if err := h.service.ChangePassword(currentWebUser(c).ID, body.OriginalPassword, body.NewPassword); err != nil { if errors.Is(err, service.ErrOriginalPassword) || strings.Contains(err.Error(), "密码长度") { fail(c, http.StatusBadRequest, "password_invalid", err.Error()) return } fail(c, http.StatusInternalServerError, "password_update_failed", "密码修改失败,请稍后重试") return } c.Status(http.StatusNoContent) } func (h *Web) UploadAvatar(c *gin.Context) { if h.cos == nil { fail(c, http.StatusServiceUnavailable, "cos_not_configured", "对象存储未配置") return } file, header, err := c.Request.FormFile("file") if err != nil { fail(c, http.StatusBadRequest, "invalid_request", "请选择头像图片") return } defer file.Close() contentType := strings.TrimSpace(strings.Split(header.Header.Get("Content-Type"), ";")[0]) extensions := map[string]string{"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"} extension, allowed := extensions[contentType] if !allowed || header.Size <= 0 || header.Size > h.cos.MaxImageBytes() { fail(c, http.StatusBadRequest, "avatar_invalid", "头像仅支持 JPEG、PNG 或 WebP,且不得超过图片大小限制") return } headerBytes := make([]byte, 512) read, readErr := file.Read(headerBytes) if readErr != nil && !errors.Is(readErr, io.EOF) { fail(c, http.StatusBadRequest, "avatar_invalid", "头像图片内容无法解析") return } if detected := http.DetectContentType(headerBytes[:read]); detected != contentType { fail(c, http.StatusBadRequest, "avatar_invalid", "头像图片内容无法解析") return } if _, err := file.Seek(0, io.SeekStart); err != nil { fail(c, http.StatusInternalServerError, "avatar_upload_failed", "头像上传失败") return } userID := currentWebUser(c).ID oldKey, err := h.service.AvatarKey(userID) if err != nil { fail(c, http.StatusInternalServerError, "avatar_upload_failed", "头像上传失败") return } key := mediakey.UserAvatar(userID, uuid.New(), extension) url, err := h.cos.Put(c.Request.Context(), key, contentType, file, header.Size) if err != nil { fail(c, http.StatusInternalServerError, "avatar_upload_failed", "头像上传失败") return } if err := h.service.UpdateAvatar(userID, url, key); err != nil { _ = h.cos.Delete(c.Request.Context(), key) fail(c, http.StatusInternalServerError, "avatar_upload_failed", "头像上传失败") return } if oldKey != "" { _ = h.cos.Delete(c.Request.Context(), oldKey) } c.JSON(http.StatusOK, gin.H{"data": gin.H{"avatar_url": url}}) } func (h *Web) Usage30Days(c *gin.Context) { items, err := h.service.Usage30Days(currentWebUser(c).ID) if err != nil { fail(c, http.StatusInternalServerError, "usage_failed", "消耗数据加载失败") return } c.JSON(http.StatusOK, gin.H{"data": items}) } // ConsumptionRecords 按请求页码返回当前用户的积分变动流水。 func (h *Web) ConsumptionRecords(c *gin.Context) { page, pageSize := parsePage(c) result, err := h.service.ConsumptionRecords(currentWebUser(c).ID, page, pageSize) if err != nil { fail(c, http.StatusInternalServerError, "consumption_records_failed", "消耗记录加载失败") return } c.JSON(http.StatusOK, gin.H{"data": result}) } func (h *Web) Redeem(c *gin.Context) { var body struct { Code string `json:"code" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { fail(c, http.StatusBadRequest, "invalid_request", "请输入兑换码") return } result, err := h.service.Redeem(currentWebUser(c).ID, body.Code) if err != nil { fail(c, http.StatusBadRequest, "redeem_failed", err.Error()) return } c.JSON(http.StatusOK, gin.H{"data": result}) } func currentWebUser(c *gin.Context) *model.WebUser { value, _ := c.Get(webUserContextKey) user, _ := value.(*model.WebUser) return user }