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
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user