feat(1.0): 新增上传图片

This commit is contained in:
summer 2025-10-16 15:06:15 +08:00
parent ae0aa4f617
commit 4f582eb802
9 changed files with 258 additions and 5 deletions

View File

@ -260,6 +260,44 @@ const docTemplate = `{
} }
} }
} }
},
"/admin/upload/image": {
"post": {
"description": "上传图片",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"通用"
],
"summary": "上传图片",
"parameters": [
{
"type": "file",
"description": "选择文件",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/upload.uploadImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/code.Failure"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -446,6 +484,19 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"upload.uploadImageResponse": {
"type": "object",
"properties": {
"preview_image_url": {
"description": "可预览图片地址",
"type": "string"
},
"real_image_url": {
"description": "真实图片地址",
"type": "string"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -252,6 +252,44 @@
} }
} }
} }
},
"/admin/upload/image": {
"post": {
"description": "上传图片",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"通用"
],
"summary": "上传图片",
"parameters": [
{
"type": "file",
"description": "选择文件",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/upload.uploadImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/code.Failure"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -438,6 +476,19 @@
"type": "string" "type": "string"
} }
} }
},
"upload.uploadImageResponse": {
"type": "object",
"properties": {
"preview_image_url": {
"description": "可预览图片地址",
"type": "string"
},
"real_image_url": {
"description": "真实图片地址",
"type": "string"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -129,6 +129,15 @@ definitions:
description: 描述信息 description: 描述信息
type: string type: string
type: object type: object
upload.uploadImageResponse:
properties:
preview_image_url:
description: 可预览图片地址
type: string
real_image_url:
description: 真实图片地址
type: string
type: object
info: info:
contact: {} contact: {}
title: mini-chat 接口文档 title: mini-chat 接口文档
@ -291,6 +300,31 @@ paths:
summary: 管理员登录 summary: 管理员登录
tags: tags:
- 管理端.登录 - 管理端.登录
/admin/upload/image:
post:
consumes:
- multipart/form-data
description: 上传图片
parameters:
- description: 选择文件
in: formData
name: file
required: true
type: file
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/upload.uploadImageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/code.Failure'
summary: 上传图片
tags:
- 通用
securityDefinitions: securityDefinitions:
LoginVerifyToken: LoginVerifyToken:
in: header in: header

View File

@ -0,0 +1,21 @@
package upload
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
}
}

View File

@ -0,0 +1,90 @@
package upload
import (
"fmt"
"net/http"
"strings"
"mini-chat/configs"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/idgen"
)
type uploadImageResponse struct {
RealImageUrl string `json:"real_image_url"` // 真实图片地址
PreviewImageUrl string `json:"preview_image_url"` // 可预览图片地址
}
// UploadImage 上传图片
// @Summary 上传图片
// @Description 上传图片
// @Tags 通用
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "选择文件"
// @Success 200 {object} uploadImageResponse
// @Failure 400 {object} code.Failure
// @Router /admin/upload/image [post]
func (h *handler) UploadImage() core.HandlerFunc {
return func(ctx core.Context) {
file, err := ctx.FormFile("file")
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), err.Error()),
))
return
}
if file == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), "缺少 file 文件"),
))
return
}
// 校验文件后缀
extension := ""
if dot := strings.LastIndexByte(file.Filename, '.'); dot != -1 {
extension = file.Filename[dot+1:]
}
allowedExtensions := map[string]bool{
"jpg": true,
"jpeg": true,
"png": true,
"gif": true,
}
if !allowedExtensions[strings.ToLower(extension)] {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), "文件后缀应为 .jpg、.jpeg、.png、.gif"),
))
return
}
// 保存文件
imagePath := fmt.Sprintf("image/%s.%s", idgen.GenerateUniqueID(), strings.ToLower(extension))
filePath := fmt.Sprintf("%s/%s", configs.GetResourcesFilePath(), imagePath)
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), err.Error()),
))
return
}
res := new(uploadImageResponse)
res.RealImageUrl = filePath
res.PreviewImageUrl = fmt.Sprintf("resources/%s", imagePath)
ctx.Payload(res)
}
}

View File

@ -19,6 +19,7 @@ const (
ServerError = 10101 ServerError = 10101
ParamBindError = 10102 ParamBindError = 10102
JWTAuthVerifyError = 10103 JWTAuthVerifyError = 10103
UploadError = 10104
AdminLoginError = 20101 AdminLoginError = 20101
CreateAppError = 20201 CreateAppError = 20201

View File

@ -4,6 +4,7 @@ var zhCNText = map[int]string{
ServerError: "内部服务器错误", ServerError: "内部服务器错误",
ParamBindError: "参数错误", ParamBindError: "参数错误",
JWTAuthVerifyError: "JWT 授权验证错误", JWTAuthVerifyError: "JWT 授权验证错误",
UploadError: "上传失败",
AdminLoginError: "登录失败", AdminLoginError: "登录失败",

View File

@ -3,6 +3,7 @@ package core
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mini-chat/internal/pkg/cors"
"net/http" "net/http"
"net/url" "net/url"
"runtime/debug" "runtime/debug"
@ -11,7 +12,6 @@ import (
"mini-chat/configs" "mini-chat/configs"
_ "mini-chat/docs" _ "mini-chat/docs"
"mini-chat/internal/code" "mini-chat/internal/code"
"mini-chat/internal/pkg/cors"
"mini-chat/internal/pkg/env" "mini-chat/internal/pkg/env"
"mini-chat/internal/pkg/errors" "mini-chat/internal/pkg/errors"
"mini-chat/internal/pkg/logger" "mini-chat/internal/pkg/logger"
@ -231,6 +231,7 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) {
// 启动信息 // 启动信息
startup.PrintInfo() startup.PrintInfo()
mux.engine.Use(cors.New())
mux.engine.StaticFS("resources", gin.Dir(configs.GetResourcesFilePath(), true)) mux.engine.StaticFS("resources", gin.Dir(configs.GetResourcesFilePath(), true))
// withoutTracePaths 这些请求,默认不记录日志 // withoutTracePaths 这些请求,默认不记录日志
@ -275,9 +276,9 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) {
mux.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) // register prometheus mux.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) // register prometheus
} }
if opt.enableCors { //if opt.enableCors {
mux.engine.Use(cors.New()) // mux.engine.Use(cors.New())
} //}
// recover 两次,防止 recover 过程中时发生 panic // recover 两次,防止 recover 过程中时发生 panic
mux.engine.Use(func(ctx *gin.Context) { mux.engine.Use(func(ctx *gin.Context) {

View File

@ -4,6 +4,7 @@ import (
"mini-chat/internal/alert" "mini-chat/internal/alert"
"mini-chat/internal/api/admin" "mini-chat/internal/api/admin"
"mini-chat/internal/api/app" "mini-chat/internal/api/app"
"mini-chat/internal/api/upload"
"mini-chat/internal/cron" "mini-chat/internal/cron"
"mini-chat/internal/dblogger" "mini-chat/internal/dblogger"
"mini-chat/internal/pkg/core" "mini-chat/internal/pkg/core"
@ -41,6 +42,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co
adminHandler := admin.New(logger, db) adminHandler := admin.New(logger, db)
appHandler := app.New(logger, db) appHandler := app.New(logger, db)
uploadHandler := upload.New(logger, db)
// 管理端非认证接口路由组 // 管理端非认证接口路由组
adminNonAuthApiRouter := mux.Group("/admin") adminNonAuthApiRouter := mux.Group("/admin")
@ -49,7 +51,8 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co
ctx.Payload(startup.Info()) ctx.Payload(startup.Info())
}) })
adminNonAuthApiRouter.POST("/login", adminHandler.Login()) // 登录 adminNonAuthApiRouter.POST("/login", adminHandler.Login()) // 登录
adminNonAuthApiRouter.POST("/upload/image", uploadHandler.UploadImage()) // 上传图片
} }
// 管理端认证接口路由组 // 管理端认证接口路由组