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:
hero920103
2026-09-03 05:55:23 +08:00
commit 57d9866dab
82 changed files with 12668 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
package handler
import (
"photowall/internal/database"
"photowall/internal/middleware"
"photowall/internal/service"
"photowall/pkg/response"
"photowall/pkg/sensitive"
"strconv"
"github.com/gin-gonic/gin"
)
type AdminHandler struct {
svc *service.AdminService
}
func NewAdminHandler(svc *service.AdminService) *AdminHandler {
return &AdminHandler{svc: svc}
}
// Stats GET /api/admin/stats
func (h *AdminHandler) Stats(c *gin.Context) {
stats, err := h.svc.Stats()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, stats)
}
// DetailedStats GET /api/admin/stats/detailed 详细数据统计(仪表盘用)
func (h *AdminHandler) DetailedStats(c *gin.Context) {
stats, err := h.svc.DetailedStats()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, stats)
}
// AuditQueue GET /api/admin/classes
func (h *AdminHandler) AuditQueue(c *gin.Context) {
var req service.AuditQueueReq
if err := c.ShouldBindQuery(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
result, err := h.svc.ListAuditQueue(&req)
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, result)
}
// Approve POST /api/admin/classes/:id/approve
func (h *AdminHandler) Approve(c *gin.Context) {
auditorID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Reason string `json:"reason"`
}
c.ShouldBindJSON(&body)
if err := h.svc.Approve(uint(id), auditorID, body.Reason); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已通过审核"})
}
// Reject POST /api/admin/classes/:id/reject
func (h *AdminHandler) Reject(c *gin.Context) {
auditorID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Reason string `json:"reason" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "请填写打回原因")
return
}
if err := h.svc.Reject(uint(id), auditorID, body.Reason); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已打回,等待用户补齐"})
}
// Delete DELETE /api/admin/classes/:id
func (h *AdminHandler) Delete(c *gin.Context) {
auditorID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Reason string `json:"reason"`
}
c.ShouldBindJSON(&body)
if err := h.svc.ManualDelete(uint(id), auditorID, body.Reason); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已删除"})
}
// AuditLogs GET /api/admin/classes/:id/logs
func (h *AdminHandler) AuditLogs(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
logs, err := h.svc.AuditLogs(uint(id))
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, logs)
}
// TriggerCleanup POST /api/admin/cleanup 手动触发过期清理(也可由定时任务自动执行)
func (h *AdminHandler) TriggerCleanup(c *gin.Context) {
count, err := h.svc.CleanupExpired()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, gin.H{"deleted": count, "message": "清理完成"})
}
// ============ 账号封禁管理 ============
// BanUser POST /api/admin/users/:id/ban 封禁用户
func (h *AdminHandler) BanUser(c *gin.Context) {
adminID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Reason string `json:"reason" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "请填写封禁原因")
return
}
req := &service.BanUserReq{UserID: uint(id), Reason: body.Reason}
if err := h.svc.BanUser(req, adminID); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已封禁"})
}
// UnbanUser POST /api/admin/users/:id/unban 解封用户
func (h *AdminHandler) UnbanUser(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.UnbanUser(uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已解封"})
}
// BannedUsers GET /api/admin/users/banned 封禁用户列表
func (h *AdminHandler) BannedUsers(c *gin.Context) {
list, err := h.svc.ListBannedUsers()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// ============ 敏感词管理 ============
// SensitiveWords GET /api/admin/sensitive-words 敏感词列表
func (h *AdminHandler) SensitiveWords(c *gin.Context) {
list, err := h.svc.ListSensitiveWords()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// AddSensitiveWord POST /api/admin/sensitive-words 添加敏感词
func (h *AdminHandler) AddSensitiveWord(c *gin.Context) {
var body struct {
Word string `json:"word" binding:"required"`
Category string `json:"category"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "请填写敏感词")
return
}
if body.Category == "" {
body.Category = "other"
}
sw, err := h.svc.AddSensitiveWord(body.Word, body.Category)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 重新加载敏感词过滤器
sensitive.Reload(database.DB)
response.Created(c, sw)
}
// DeleteSensitiveWord DELETE /api/admin/sensitive-words/:id 删除敏感词
func (h *AdminHandler) DeleteSensitiveWord(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.DeleteSensitiveWord(uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
sensitive.Reload(database.DB)
response.OK(c, gin.H{"message": "已删除"})
}
// ToggleSensitiveWord PUT /api/admin/sensitive-words/:id/toggle 启用/禁用
func (h *AdminHandler) ToggleSensitiveWord(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Enabled bool `json:"enabled"`
}
c.ShouldBindJSON(&body)
if err := h.svc.ToggleSensitiveWord(uint(id), body.Enabled); err != nil {
response.BadRequest(c, err.Error())
return
}
sensitive.Reload(database.DB)
response.OK(c, gin.H{"message": "已更新"})
}
+146
View File
@@ -0,0 +1,146 @@
package handler
import (
"photowall/internal/middleware"
"photowall/internal/service"
"photowall/pkg/captcha"
"photowall/pkg/response"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
auth *service.AuthService
}
func NewAuthHandler(auth *service.AuthService) *AuthHandler {
return &AuthHandler{auth: auth}
}
// Captcha GET /api/captcha 获取图形验证码
func (h *AuthHandler) Captcha(c *gin.Context) {
id, img := captcha.Generate()
response.OK(c, gin.H{
"captcha_id": id,
"image": img,
})
}
// Register POST /api/auth/register
func (h *AuthHandler) Register(c *gin.Context) {
var req service.RegisterReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
user, err := h.auth.Register(&req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, user)
}
// Login POST /api/auth/login
func (h *AuthHandler) Login(c *gin.Context) {
var req service.LoginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
resp, err := h.auth.Login(&req)
if err != nil {
response.Unauthorized(c, err.Error())
return
}
response.OK(c, resp)
}
// Me GET /api/auth/me
func (h *AuthHandler) Me(c *gin.Context) {
userID := middleware.CurrentUserID(c)
username := middleware.CurrentUsername(c)
role := middleware.CurrentRole(c)
response.OK(c, gin.H{
"user_id": userID,
"username": username,
"role": role,
})
}
// ForgotPassword POST /api/auth/forgot-password 忘记密码(生成重置码)
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
var req service.ForgotPasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
code, err := h.auth.ForgotPassword(&req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 开发阶段返回重置码(生产环境应通过邮件发送,不返回)
response.OK(c, gin.H{
"message": "重置码已生成,有效期30分钟",
"reset_code": code,
"note": "开发阶段直接返回,生产环境将通过邮箱发送",
})
}
// ResetPassword POST /api/auth/reset-password 重置密码
func (h *AuthHandler) ResetPassword(c *gin.Context) {
var req service.ResetPasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
if err := h.auth.ResetPassword(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "密码重置成功,请使用新密码登录"})
}
// SendSms POST /api/auth/send-sms 发送短信验证码
func (h *AuthHandler) SendSms(c *gin.Context) {
var req service.SendSmsReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
code, err := h.auth.SendSmsCode(&req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{
"message": "验证码已发送,有效期5分钟",
"code": code, // 开发阶段返回,生产环境不返回
"note": "短信参数未配置时为开发模式,直接返回验证码",
})
}
// WechatLogin POST /api/auth/wechat-login 微信扫码登录
func (h *AuthHandler) WechatLogin(c *gin.Context) {
var req service.WechatLoginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
resp, err := h.auth.WechatLogin(&req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, resp)
}
// WechatQrcode GET /api/auth/wechat-qrcode 获取微信扫码登录二维码(预留)
func (h *AuthHandler) WechatQrcode(c *gin.Context) {
// TODO: 接入微信开放平台后生成真实扫码二维码
response.OK(c, gin.H{
"message": "微信扫码登录功能开发中,请配置微信开放平台参数后使用",
"qrcode": "",
})
}
+224
View File
@@ -0,0 +1,224 @@
package handler
import (
"photowall/internal/middleware"
"photowall/internal/service"
"photowall/pkg/response"
"strconv"
"github.com/gin-gonic/gin"
)
type ClassHandler struct {
svc *service.ClassService
}
func NewClassHandler(svc *service.ClassService) *ClassHandler {
return &ClassHandler{svc: svc}
}
// Create POST /api/classes
func (h *ClassHandler) Create(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var req service.CreateClassReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
class, err := h.svc.Create(userID, &req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, class)
}
// Detail GET /api/classes/:id
func (h *ClassHandler) Detail(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
userID := middleware.CurrentUserID(c)
isSuperAdmin := middleware.CurrentRole(c) == "admin"
detail, err := h.svc.GetDetail(uint(id), userID, isSuperAdmin)
if err != nil {
response.NotFound(c, err.Error())
return
}
response.OK(c, detail)
}
// Search GET /api/classes/search
func (h *ClassHandler) Search(c *gin.Context) {
var req service.SearchClassReq
if err := c.ShouldBindQuery(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
result, err := h.svc.Search(&req)
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, result)
}
// JoinBySearch POST /api/classes/join/search
func (h *ClassHandler) JoinBySearch(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var req service.JoinBySearchReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
if err := h.svc.JoinBySearch(userID, &req); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "加入成功"})
}
// JoinByInvitation POST /api/classes/join/invite
func (h *ClassHandler) JoinByInvitation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var req service.JoinByInvitationReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "邀请码不能为空")
return
}
class, err := h.svc.JoinByInvitation(userID, &req)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "加入成功", "class_id": class.ID, "class_name": class.Name})
}
// Mine GET /api/classes/mine
func (h *ClassHandler) Mine(c *gin.Context) {
userID := middleware.CurrentUserID(c)
list, err := h.svc.ListMyClasses(userID)
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// CreateInvitation POST /api/classes/:id/invitation
func (h *ClassHandler) CreateInvitation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
MaxUses int `json:"max_uses"`
ExpireDays int `json:"expire_days"`
}
c.ShouldBindJSON(&body)
inv, err := h.svc.CreateInvitation(uint(id), userID, body.MaxUses, body.ExpireDays)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, inv)
}
// ListInvitations GET /api/classes/:id/invitations
func (h *ClassHandler) ListInvitations(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
list, err := h.svc.ListInvitations(uint(id), userID)
if err != nil {
response.Forbidden(c, err.Error())
return
}
response.OK(c, list)
}
// DisableInvitation DELETE /api/classes/:id/invitations/:inv_id
func (h *ClassHandler) DisableInvitation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
invID, _ := strconv.ParseUint(c.Param("inv_id"), 10, 64)
if err := h.svc.DisableInvitation(uint(id), uint(invID), userID); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已禁用"})
}
// TransferAdmin POST /api/classes/:id/transfer
func (h *ClassHandler) TransferAdmin(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
ToUserID uint `json:"to_user_id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "请指定目标用户")
return
}
if err := h.svc.TransferAdmin(uint(id), userID, body.ToUserID); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "管理员已转移"})
}
// RemoveMember DELETE /api/classes/:id/members/:user_id
func (h *ClassHandler) RemoveMember(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
targetID, _ := strconv.ParseUint(c.Param("user_id"), 10, 64)
if err := h.svc.RemoveMember(uint(id), userID, uint(targetID)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已移除"})
}
// UploadPhoto POST /api/classes/:id/photos
func (h *ClassHandler) UploadPhoto(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
PhotoURL string `json:"photo_url" binding:"required"`
Caption string `json:"caption"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "请先上传图片获取URL")
return
}
photo, err := h.svc.UploadPhoto(uint(id), userID, body.PhotoURL, body.Caption)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, photo)
}
// DeletePhoto DELETE /api/classes/:id/photos/:photo_id
func (h *ClassHandler) DeletePhoto(c *gin.Context) {
userID := middleware.CurrentUserID(c)
isSuperAdmin := middleware.CurrentRole(c) == "admin"
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
photoID, _ := strconv.ParseUint(c.Param("photo_id"), 10, 64)
if err := h.svc.DeletePhoto(uint(id), uint(photoID), userID, isSuperAdmin); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已删除"})
}
// Resubmit POST /api/classes/:id/resubmit
func (h *ClassHandler) Resubmit(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req service.ResubmitReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.svc.Resubmit(uint(id), userID, &req); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "已重新提交审核"})
}
+58
View File
@@ -0,0 +1,58 @@
package handler
import (
"photowall/internal/service"
"photowall/pkg/response"
"github.com/gin-gonic/gin"
)
type ConfigHandler struct {
cfg *service.ConfigService
}
func NewConfigHandler(cfg *service.ConfigService) *ConfigHandler {
return &ConfigHandler{cfg: cfg}
}
// GetAll GET /api/admin/config 获取所有系统配置
func (h *ConfigHandler) GetAll(c *gin.Context) {
list, err := h.cfg.GetAll()
if err != nil {
response.Internal(c, "获取配置失败")
return
}
// 按分类分组返回
result := gin.H{
"basic": []gin.H{},
"geetest": []gin.H{},
"sms": []gin.H{},
"wechat": []gin.H{},
"storage": []gin.H{},
}
for _, item := range list {
result[item.Category] = append(result[item.Category].([]gin.H), gin.H{
"key": item.Key,
"value": item.Value,
"label": item.Label,
"description": item.Description,
"placeholder": item.Placeholder,
"doc_link": item.DocLink,
})
}
response.OK(c, result)
}
// Update PUT /api/admin/config 批量更新配置
func (h *ConfigHandler) Update(c *gin.Context) {
var req service.UpdateConfigReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
if err := h.cfg.Update(&req); err != nil {
response.Internal(c, "更新配置失败")
return
}
response.OK(c, gin.H{"message": "配置更新成功,部分配置需重启服务生效"})
}
+80
View File
@@ -0,0 +1,80 @@
package handler
import (
"photowall/internal/middleware"
"photowall/internal/model"
"photowall/internal/service"
"photowall/pkg/response"
"strconv"
"github.com/gin-gonic/gin"
)
type MenuHandler struct {
menu *service.MenuService
}
func NewMenuHandler(menu *service.MenuService) *MenuHandler {
return &MenuHandler{menu: menu}
}
// GetUserMenus GET /api/menus 获取当前用户的菜单树(动态路由用)
func (h *MenuHandler) GetUserMenus(c *gin.Context) {
isAdmin := middleware.CurrentRole(c) == "admin"
tree, err := h.menu.GetMenuTree(isAdmin)
if err != nil {
response.Internal(c, "获取菜单失败")
return
}
response.OK(c, tree)
}
// GetAll GET /api/admin/menus 获取所有菜单(管理用)
func (h *MenuHandler) GetAll(c *gin.Context) {
list, err := h.menu.GetAll()
if err != nil {
response.Internal(c, "获取菜单失败")
return
}
response.OK(c, list)
}
// Create POST /api/admin/menus 创建菜单
func (h *MenuHandler) Create(c *gin.Context) {
var m model.Menu
if err := c.ShouldBindJSON(&m); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
if err := h.menu.Create(&m); err != nil {
response.Internal(c, "创建菜单失败")
return
}
response.Created(c, m)
}
// Update PUT /api/admin/menus/:id 更新菜单
func (h *MenuHandler) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var m model.Menu
if err := c.ShouldBindJSON(&m); err != nil {
response.BadRequest(c, "参数错误:"+err.Error())
return
}
m.ID = uint(id)
if err := h.menu.Update(&m); err != nil {
response.Internal(c, "更新菜单失败")
return
}
response.OK(c, m)
}
// Delete DELETE /api/admin/menus/:id 删除菜单
func (h *MenuHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.menu.Delete(uint(id)); err != nil {
response.Internal(c, "删除菜单失败")
return
}
response.OK(c, gin.H{"message": "删除成功"})
}
+64
View File
@@ -0,0 +1,64 @@
package handler
import (
"photowall/internal/service"
"photowall/pkg/response"
"strconv"
"github.com/gin-gonic/gin"
)
type RegionHandler struct {
svc *service.RegionService
}
func NewRegionHandler(svc *service.RegionService) *RegionHandler {
return &RegionHandler{svc: svc}
}
// List GET /api/regions?parent_id=0
func (h *RegionHandler) List(c *gin.Context) {
parentID, _ := strconv.ParseUint(c.DefaultQuery("parent_id", "0"), 10, 64)
list, err := h.svc.ListByParent(uint(parentID))
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// Countries GET /api/regions/countries
func (h *RegionHandler) Countries(c *gin.Context) {
list, err := h.svc.ListCountries()
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// Path GET /api/regions/:id/path
func (h *RegionHandler) Path(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
path, err := h.svc.GetPath(uint(id))
if err != nil {
response.NotFound(c, "地区不存在")
return
}
response.OK(c, path)
}
// Search GET /api/regions/search?keyword=xxx
func (h *RegionHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
if keyword == "" {
response.BadRequest(c, "请输入搜索关键词")
return
}
list, err := h.svc.Search(keyword, 20)
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
+157
View File
@@ -0,0 +1,157 @@
package handler
import (
"photowall/internal/model"
"photowall/internal/service"
"photowall/pkg/response"
"strconv"
"github.com/gin-gonic/gin"
)
type SchoolHandler struct {
svc *service.SchoolService
}
func NewSchoolHandler(svc *service.SchoolService) *SchoolHandler {
return &SchoolHandler{svc: svc}
}
// List GET /api/schools
func (h *SchoolHandler) List(c *gin.Context) {
var q service.SchoolQuery
if err := c.ShouldBindQuery(&q); err != nil {
response.BadRequest(c, "参数错误")
return
}
result, err := h.svc.List(&q)
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, result)
}
// Create POST /api/schools
func (h *SchoolHandler) Create(c *gin.Context) {
var school model.School
if err := c.ShouldBindJSON(&school); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.svc.Create(&school); err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, school)
}
// Get GET /api/schools/:id
func (h *SchoolHandler) Get(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
school, err := h.svc.GetByID(uint(id))
if err != nil {
response.NotFound(c, err.Error())
return
}
// 附带学院列表
colleges, _ := h.svc.ListColleges(uint(id))
response.OK(c, gin.H{"school": school, "colleges": colleges})
}
// Update PUT /api/schools/:id
func (h *SchoolHandler) Update(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body map[string]interface{}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "参数错误")
return
}
// 只允许更新部分字段
updates := map[string]interface{}{}
if v, ok := body["name"]; ok {
updates["name"] = v
}
if v, ok := body["name_en"]; ok {
updates["name_en"] = v
}
if v, ok := body["school_type"]; ok {
updates["school_type"] = v
}
if v, ok := body["address"]; ok {
updates["address"] = v
}
if err := h.svc.Update(uint(id), updates); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "更新成功"})
}
// Delete DELETE /api/schools/:id
func (h *SchoolHandler) Delete(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.Delete(uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "删除成功"})
}
// ---- 学院 ----
// ListColleges GET /api/schools/:id/colleges
func (h *SchoolHandler) ListColleges(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
list, err := h.svc.ListColleges(uint(id))
if err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, list)
}
// CreateCollege POST /api/schools/:id/colleges
func (h *SchoolHandler) CreateCollege(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Name string `json:"name" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "学院名称不能为空")
return
}
college, err := h.svc.CreateCollege(uint(id), body.Name)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, college)
}
// UpdateCollege PUT /api/colleges/:id
func (h *SchoolHandler) UpdateCollege(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Name string `json:"name" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "学院名称不能为空")
return
}
if err := h.svc.UpdateCollege(uint(id), body.Name); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "更新成功"})
}
// DeleteCollege DELETE /api/colleges/:id
func (h *SchoolHandler) DeleteCollege(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.DeleteCollege(uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "删除成功"})
}
+129
View File
@@ -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,
})
}
+159
View File
@@ -0,0 +1,159 @@
package handler
import (
"photowall/internal/middleware"
"photowall/internal/model"
"photowall/internal/service"
"photowall/pkg/response"
"strconv"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
// Profile GET /api/user/profile
func (h *UserHandler) Profile(c *gin.Context) {
userID := middleware.CurrentUserID(c)
profile, err := h.svc.GetProfile(userID)
if err != nil {
response.NotFound(c, err.Error())
return
}
response.OK(c, profile)
}
// UpdateProfile PUT /api/user/profile
func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var req service.UpdateProfileReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.svc.UpdateProfile(userID, &req); err != nil {
response.Internal(c, err.Error())
return
}
response.OK(c, gin.H{"message": "更新成功"})
}
// AddContact POST /api/user/contacts
func (h *UserHandler) AddContact(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var body struct {
Type model.ContactType `json:"type" binding:"required"`
Value string `json:"value" binding:"required"`
IsPrimary bool `json:"is_primary"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "参数错误")
return
}
contact, err := h.svc.AddContact(userID, body.Type, body.Value, body.IsPrimary)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, contact)
}
// UpdateContact PUT /api/user/contacts/:id
func (h *UserHandler) UpdateContact(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var body struct {
Value string `json:"value"`
IsPrimary *bool `json:"is_primary"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.svc.UpdateContact(userID, uint(id), body.Value, body.IsPrimary); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "更新成功"})
}
// DeleteContact DELETE /api/user/contacts/:id
func (h *UserHandler) DeleteContact(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.DeleteContact(userID, uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "删除成功"})
}
// AddEducation POST /api/user/education
func (h *UserHandler) AddEducation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
var e model.EducationHistory
if err := c.ShouldBindJSON(&e); err != nil {
response.BadRequest(c, "参数错误")
return
}
if e.SchoolName == "" {
response.BadRequest(c, "学校名称不能为空")
return
}
if err := h.svc.AddEducation(userID, &e); err != nil {
response.Internal(c, err.Error())
return
}
response.Created(c, e)
}
// UpdateEducation PUT /api/user/education/:id
func (h *UserHandler) UpdateEducation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var e model.EducationHistory
if err := c.ShouldBindJSON(&e); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.svc.UpdateEducation(userID, uint(id), &e); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "更新成功"})
}
// DeleteEducation DELETE /api/user/education/:id
func (h *UserHandler) DeleteEducation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.svc.DeleteEducation(userID, uint(id)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"message": "删除成功"})
}
// HasEducation GET /api/user/has-education (创建班级前置检查)
func (h *UserHandler) HasEducation(c *gin.Context) {
userID := middleware.CurrentUserID(c)
response.OK(c, gin.H{"has_education": h.svc.HasEducation(userID)})
}
// PublicProfile GET /api/user/:id 公开信息(班级成员查看)
func (h *UserHandler) PublicProfile(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
user, err := h.svc.GetUserByID(uint(id))
if err != nil {
response.NotFound(c, err.Error())
return
}
contacts := h.svc.GetUserContacts(uint(id))
response.OK(c, gin.H{"user": user, "contacts": contacts})
}