227 lines
7.4 KiB
Go
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
|
|
}
|