Files
photowall/go/internal/service/admin_service.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

389 lines
12 KiB
Go

package service
import (
"errors"
"fmt"
"photowall/internal/model"
"time"
"gorm.io/gorm"
)
type AdminService struct {
db *gorm.DB
}
func NewAdminService(db *gorm.DB) *AdminService {
return &AdminService{db: db}
}
// AuditQueueReq 审核队列查询
type AuditQueueReq struct {
Status model.ClassStatus `form:"status"`
Keyword string `form:"keyword"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
// ListAuditQueue 平台管理员查看审核队列
func (s *AdminService) ListAuditQueue(req *AuditQueueReq) (*SchoolListResp, error) {
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
db := s.db.Model(&model.Class{}).Where("status != ?", model.ClassDeleted)
if req.Status != "" {
db = db.Where("status = ?", req.Status)
}
if req.Keyword != "" {
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}
var total int64
db.Count(&total)
var list []model.Class
err := db.Order("audit_deadline asc, id desc").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&list).Error
if err != nil {
return nil, err
}
return &SchoolListResp{List: list, Total: total, Page: req.Page}, nil
}
// Approve 审核通过
func (s *AdminService) Approve(classID, auditorID uint, reason string) error {
var class model.Class
if err := s.db.First(&class, classID).Error; err != nil {
return errors.New("班级不存在")
}
if class.Status == model.ClassDeleted {
return errors.New("班级已删除")
}
class.Status = model.ClassActive
class.RejectReason = ""
if err := s.db.Save(&class).Error; err != nil {
return err
}
s.log(classID, auditorID, model.AuditApprove, reason)
return nil
}
// Reject 打回补齐
func (s *AdminService) Reject(classID, auditorID uint, reason string) error {
if reason == "" {
return errors.New("请填写打回原因")
}
var class model.Class
if err := s.db.First(&class, classID).Error; err != nil {
return errors.New("班级不存在")
}
if class.Status == model.ClassDeleted {
return errors.New("班级已删除")
}
class.Status = model.ClassRejected
class.RejectReason = reason
if err := s.db.Save(&class).Error; err != nil {
return err
}
s.log(classID, auditorID, model.AuditReject, reason)
return nil
}
// ManualDelete 平台管理员手动删除班级
func (s *AdminService) ManualDelete(classID, auditorID uint, reason string) error {
var class model.Class
if err := s.db.First(&class, classID).Error; err != nil {
return errors.New("班级不存在")
}
return s.deleteClass(class, auditorID, model.AuditManualDelete, reason)
}
// CleanupExpired 清理超过审核截止时间仍未通过的班级(30天未补齐自动删除)
// 返回被删除的班级数量
func (s *AdminService) CleanupExpired() (int, error) {
var expired []model.Class
// 状态为 pending 或 rejected,且已过 audit_deadline
err := s.db.Where("status IN ? AND audit_deadline < ?",
[]model.ClassStatus{model.ClassPending, model.ClassRejected},
time.Now()).Find(&expired).Error
if err != nil {
return 0, err
}
count := 0
for _, class := range expired {
if err := s.deleteClass(class, 0, model.AuditAutoDelete,
fmt.Sprintf("超过 %d 天补齐期限未完成审核,系统自动删除", 30)); err == nil {
count++
}
}
return count, nil
}
// deleteClass 事务删除班级及其关联数据(成员、照片、邀请码)
func (s *AdminService) deleteClass(class model.Class, auditorID uint, action model.AuditAction, reason string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 标记删除
if err := tx.Model(&model.Class{}).Where("id = ?", class.ID).
Updates(map[string]interface{}{
"status": model.ClassDeleted,
"reject_reason": reason,
}).Error; err != nil {
return err
}
// 删除成员关系
tx.Where("class_id = ?", class.ID).Delete(&model.ClassMember{})
// 删除照片记录(文件保留在磁盘,避免误删;如需清理可加定时任务)
tx.Where("class_id = ?", class.ID).Delete(&model.ClassPhoto{})
// 禁用邀请码
tx.Model(&model.Invitation{}).Where("class_id = ?", class.ID).Update("status", "disabled")
// 审核日志
log := &model.AuditLog{
ClassID: class.ID,
AuditorID: auditorID,
Action: action,
Reason: reason,
}
return tx.Create(log).Error
})
}
func (s *AdminService) log(classID, auditorID uint, action model.AuditAction, reason string) {
log := &model.AuditLog{
ClassID: classID,
AuditorID: auditorID,
Action: action,
Reason: reason,
}
s.db.Create(log)
}
// AuditLogs 查看班级审核日志
func (s *AdminService) AuditLogs(classID uint) ([]model.AuditLog, error) {
var list []model.AuditLog
err := s.db.Where("class_id = ?", classID).Order("id desc").Find(&list).Error
return list, err
}
// Stats 平台数据概览
type AdminStats struct {
TotalUsers int64 `json:"total_users"`
TotalClasses int64 `json:"total_classes"`
ActiveClasses int64 `json:"active_classes"`
PendingClasses int64 `json:"pending_classes"`
RejectedClasses int64 `json:"rejected_classes"`
TotalSchools int64 `json:"total_schools"`
TotalPhotos int64 `json:"total_photos"`
}
func (s *AdminService) Stats() (*AdminStats, error) {
stats := &AdminStats{}
s.db.Model(&model.User{}).Where("role = ?", "user").Count(&stats.TotalUsers)
s.db.Model(&model.Class{}).Where("status != ?", model.ClassDeleted).Count(&stats.TotalClasses)
s.db.Model(&model.Class{}).Where("status = ?", model.ClassActive).Count(&stats.ActiveClasses)
s.db.Model(&model.Class{}).Where("status = ?", model.ClassPending).Count(&stats.PendingClasses)
s.db.Model(&model.Class{}).Where("status = ?", model.ClassRejected).Count(&stats.RejectedClasses)
s.db.Model(&model.School{}).Count(&stats.TotalSchools)
s.db.Model(&model.ClassPhoto{}).Count(&stats.TotalPhotos)
return stats, nil
}
// ============ 详细数据统计 ============
type TrendItem struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
type NameCountItem struct {
Name string `json:"name"`
Count int64 `json:"count"`
}
type DetailedStats struct {
Core *AdminStats `json:"core"`
UserTrend7d []TrendItem `json:"user_trend_7d"`
ClassTrend7d []TrendItem `json:"class_trend_7d"`
ClassStatus []NameCountItem `json:"class_status"`
SchoolTypes []NameCountItem `json:"school_types"`
TopRegions []NameCountItem `json:"top_regions"`
GraduateYears []NameCountItem `json:"graduate_years"`
PhotoStats map[string]int64 `json:"photo_stats"`
}
func (s *AdminService) DetailedStats() (*DetailedStats, error) {
core, _ := s.Stats()
ds := &DetailedStats{Core: core}
// 近7天用户增长
ds.UserTrend7d = s.dailyTrend("yz_pw_users", 7)
// 近7天班级增长
ds.ClassTrend7d = s.dailyTrend("yz_pw_classes", 7)
// 班级状态分布
ds.ClassStatus = []NameCountItem{
{Name: "待审核", Count: core.PendingClasses},
{Name: "已通过", Count: core.ActiveClasses},
{Name: "被打回", Count: core.RejectedClasses},
}
// 学校类型分布
type schoolTypeRow struct {
SchoolType string `gorm:"column:school_type"`
Cnt int64 `gorm:"column:cnt"`
}
var stRows []schoolTypeRow
s.db.Model(&model.School{}).Select("school_type, COUNT(*) as cnt").Group("school_type").Scan(&stRows)
typeMap := map[string]string{"university": "大学", "vocational": "职业学校", "senior_high": "高中", "junior_high": "初中", "primary": "小学", "kindergarten": "幼儿园", "other": "其他"}
for _, r := range stRows {
name := typeMap[r.SchoolType]
if name == "" {
name = r.SchoolType
}
ds.SchoolTypes = append(ds.SchoolTypes, NameCountItem{Name: name, Count: r.Cnt})
}
// 地区TOP10(按省级行政区统计班级数)
type regionRow struct {
Name string `gorm:"column:name"`
Cnt int64 `gorm:"column:cnt"`
}
var rRows []regionRow
s.db.Raw(`
SELECT r.name, COUNT(c.id) as cnt
FROM yz_pw_classes c
JOIN yz_pw_schools s ON c.school_id = s.id
JOIN yz_pw_regions r ON s.region_id = r.id
WHERE c.status != 'deleted'
GROUP BY r.name
ORDER BY cnt DESC
LIMIT 10
`).Scan(&rRows)
for _, r := range rRows {
ds.TopRegions = append(ds.TopRegions, NameCountItem{Name: r.Name, Count: r.Cnt})
}
// 毕业年份分布
var gyRows []struct {
GraduateYear int `gorm:"column:graduate_year"`
Cnt int64 `gorm:"column:cnt"`
}
s.db.Model(&model.Class{}).Select("graduate_year, COUNT(*) as cnt").
Where("status != ?", "deleted").Group("graduate_year").Order("graduate_year DESC").Limit(10).Scan(&gyRows)
for _, r := range gyRows {
ds.GraduateYears = append(ds.GraduateYears, NameCountItem{Name: fmt.Sprintf("%d届", r.GraduateYear), Count: r.Cnt})
}
// 照片统计
var gradPhotos int64
s.db.Model(&model.Class{}).Where("graduation_photo != ? AND status != ?", "", "deleted").Count(&gradPhotos)
ds.PhotoStats = map[string]int64{
"graduation_photos": gradPhotos,
"personal_photos": core.TotalPhotos,
"total_photos": gradPhotos + core.TotalPhotos,
}
return ds, nil
}
// dailyTrend 查询近N天每日新增数量
func (s *AdminService) dailyTrend(table string, days int) []TrendItem {
var items []TrendItem
s.db.Raw(fmt.Sprintf(`
SELECT DATE(created_at) as date, COUNT(*) as count
FROM %s
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL %d DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
`, table, days)).Scan(&items)
// 补全缺失的日期
result := make([]TrendItem, 0, days)
now := time.Now()
for i := days - 1; i >= 0; i-- {
date := now.AddDate(0, 0, -i).Format("2006-01-02")
count := int64(0)
for _, item := range items {
if item.Date == date {
count = item.Count
break
}
}
result = append(result, TrendItem{Date: date, Count: count})
}
return result
}
// ============ 账号封禁管理 ============
type BanUserReq struct {
UserID uint `json:"user_id" binding:"required"`
Reason string `json:"reason" binding:"required"`
}
// BanUser 封禁用户
func (s *AdminService) BanUser(req *BanUserReq, adminID uint) error {
var user model.User
if err := s.db.First(&user, req.UserID).Error; err != nil {
return errors.New("用户不存在")
}
if user.Role == "admin" {
return errors.New("不能封禁管理员账号")
}
now := time.Now()
user.Status = "banned"
user.BanReason = req.Reason
user.BannedAt = &now
return s.db.Save(&user).Error
}
// UnbanUser 解封用户
func (s *AdminService) UnbanUser(userID uint) error {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
user.Status = "active"
user.BanReason = ""
user.BannedAt = nil
return s.db.Save(&user).Error
}
// ListBannedUsers 获取封禁用户列表
func (s *AdminService) ListBannedUsers() ([]model.User, error) {
var list []model.User
err := s.db.Where("status = ?", "banned").Order("banned_at desc").Find(&list).Error
return list, err
}
// ============ 敏感词管理 ============
// ListSensitiveWords 获取敏感词列表
func (s *AdminService) ListSensitiveWords() ([]model.SensitiveWord, error) {
var list []model.SensitiveWord
err := s.db.Order("category, id").Find(&list).Error
return list, err
}
// AddSensitiveWord 添加敏感词
func (s *AdminService) AddSensitiveWord(word, category string) (*model.SensitiveWord, error) {
var count int64
s.db.Model(&model.SensitiveWord{}).Where("word = ?", word).Count(&count)
if count > 0 {
return nil, errors.New("该敏感词已存在")
}
sw := &model.SensitiveWord{Word: word, Category: category, Enabled: true}
if err := s.db.Create(sw).Error; err != nil {
return nil, err
}
return sw, nil
}
// DeleteSensitiveWord 删除敏感词
func (s *AdminService) DeleteSensitiveWord(id uint) error {
return s.db.Delete(&model.SensitiveWord{}, id).Error
}
// ToggleSensitiveWord 启用/禁用敏感词
func (s *AdminService) ToggleSensitiveWord(id uint, enabled bool) error {
return s.db.Model(&model.SensitiveWord{}).Where("id = ?", id).Update("enabled", enabled).Error
}