Files
photowall/go/internal/handler/upload_handler.go
T
hero920103 57d9866dab feat: PhotoWall 毕业照存储系统初始版本
后端: 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
2026-09-03 05:55:23 +08:00

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,
})
}