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 }