Files
2026-08-25 17:59:42 +08:00

227 lines
7.4 KiB
Go

// 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
}