后端: Go(Gin+GORM+MySQL+Redis) - 17张业务表(yz_pw_前缀), 自动迁移 - JWT认证(3小时) + 盐+MD5密码 + 图形验证码 - 班级CRUD/加入(8人姓名验证/邀请码)/审核/30天自动清理 - 系统配置(16项)/菜单管理(动态路由)/数据统计 - 文件MD5去重/数据隔离/账号封禁/敏感词DFA检测 前端: Vue3+Vite+Element Plus+Less+ECharts+FontAwesome - 动态路由(数据库菜单驱动) - 蓝白配色, H5响应式 - 登录/注册/忘记密码/个人中心 - 班级创建向导/详情/加入/列表 - 管理后台: 审核/配置/菜单/统计/用户/敏感词 数据库: MySQL 10.31.100.3:3306/photowall 管理员: hero920103 / 920103
130 lines
3.1 KiB
Go
130 lines
3.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"photowall/internal/config"
|
|
"photowall/internal/middleware"
|
|
"photowall/internal/model"
|
|
"photowall/pkg/response"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type UploadHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewUploadHandler(db *gorm.DB) *UploadHandler {
|
|
return &UploadHandler{db: db}
|
|
}
|
|
|
|
// Upload POST /api/upload
|
|
// 通用文件上传,MD5去重,返回可访问的 URL。type 参数:class(毕业照)/photo(个人照片)/avatar
|
|
func (h *UploadHandler) Upload(c *gin.Context) {
|
|
userID := middleware.CurrentUserID(c)
|
|
uploadType := c.DefaultPostForm("type", "photo")
|
|
|
|
allowedDirs := map[string]string{
|
|
"class": "classes",
|
|
"photo": "photos",
|
|
"avatar": "avatars",
|
|
}
|
|
subDir, ok := allowedDirs[uploadType]
|
|
if !ok {
|
|
subDir = "photos"
|
|
}
|
|
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
response.BadRequest(c, "请选择要上传的文件")
|
|
return
|
|
}
|
|
|
|
// 限制大小 10MB
|
|
if file.Size > 10*1024*1024 {
|
|
response.BadRequest(c, "文件大小不能超过 10MB")
|
|
return
|
|
}
|
|
|
|
// 校验图片格式
|
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
|
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
|
|
if !allowedExts[ext] {
|
|
response.BadRequest(c, "仅支持 JPG/PNG/GIF/WEBP 格式")
|
|
return
|
|
}
|
|
|
|
// 打开文件计算 MD5
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
response.Internal(c, "文件读取失败")
|
|
return
|
|
}
|
|
defer src.Close()
|
|
|
|
hash := md5.New()
|
|
if _, err := io.Copy(hash, src); err != nil {
|
|
response.Internal(c, "文件MD5计算失败")
|
|
return
|
|
}
|
|
fileMd5 := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
// MD5 去重:已存在则返回已有路径
|
|
var existing model.UploadFile
|
|
if err := h.db.Where("md5 = ?", fileMd5).First(&existing).Error; err == nil {
|
|
// 引用次数+1
|
|
h.db.Model(&existing).Update("ref_count", gorm.Expr("ref_count + 1"))
|
|
response.OK(c, gin.H{
|
|
"url": existing.FileUrl,
|
|
"filename": existing.FileName,
|
|
"size": existing.FileSize,
|
|
"md5": fileMd5,
|
|
"duplicate": true,
|
|
"message": "文件已存在,使用已有路径",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 新文件:保存到磁盘
|
|
src.Seek(0, io.SeekStart) // 重置文件指针
|
|
now := time.Now().Format("20060102")
|
|
filename := fmt.Sprintf("%s_%d_%s%s", now, userID, uuid.New().String()[:8], ext)
|
|
relPath := filepath.Join(subDir, filename)
|
|
fullPath := filepath.Join(config.C.UploadDir, relPath)
|
|
|
|
if err := c.SaveUploadedFile(file, fullPath); err != nil {
|
|
response.Internal(c, "文件保存失败:"+err.Error())
|
|
return
|
|
}
|
|
|
|
// 记录到文件表
|
|
url := "/uploads/" + strings.ReplaceAll(relPath, "\\", "/")
|
|
record := model.UploadFile{
|
|
Md5: fileMd5,
|
|
FilePath: relPath,
|
|
FileUrl: url,
|
|
FileName: filename,
|
|
FileSize: file.Size,
|
|
FileType: uploadType,
|
|
UploaderID: userID,
|
|
RefCount: 1,
|
|
}
|
|
h.db.Create(&record)
|
|
|
|
response.OK(c, gin.H{
|
|
"url": url,
|
|
"filename": filename,
|
|
"size": file.Size,
|
|
"md5": fileMd5,
|
|
"duplicate": false,
|
|
})
|
|
}
|