package service import ( "errors" "fmt" "photowall/internal/model" "photowall/pkg/sensitive" "strings" "time" "github.com/google/uuid" "gorm.io/gorm" ) type ClassService struct { db *gorm.DB configSvc *ConfigService } func NewClassService(db *gorm.DB, configSvc *ConfigService) *ClassService { return &ClassService{db: db, configSvc: configSvc} } // RosterItemReq 毕业照人员录入项 type RosterItemReq struct { RowNo int `json:"row_no"` ColNo int `json:"col_no"` Name string `json:"name"` Role string `json:"role"` // "student" | "teacher" } // CreateClassReq 创建班级请求 type CreateClassReq struct { SchoolID uint `json:"school_id" binding:"required"` CollegeID uint `json:"college_id"` Name string `json:"name"` Grade string `json:"grade" binding:"required"` // 如"2018级" ClassNo string `json:"class_no" binding:"required"` GraduateYear int `json:"graduate_year"` GraduationPhoto string `json:"graduation_photo"` RowCount int `json:"row_count"` Roster []RosterItemReq `json:"roster"` StudentNames []string `json:"student_names"` TeacherNames []string `json:"teacher_names"` } // cleanNames 清洗名字:去空格、去空、去重,保持顺序 func cleanNames(names []string) []string { seen := map[string]bool{} result := []string{} for _, n := range names { n = strings.TrimSpace(n) if n == "" { continue } if seen[n] { continue } seen[n] = true result = append(result, n) } return result } // Create 创建班级 func (s *ClassService) Create(userID uint, req *CreateClassReq) (*model.Class, error) { // 1. 校验用户有学习履历 var eduCount int64 s.db.Model(&model.EducationHistory{}).Where("user_id = ?", userID).Count(&eduCount) if eduCount == 0 { return nil, errors.New("请先完善个人学习履历,有学习履历的用户才能创建班级") } // 2. 校验学校 var school model.School if err := s.db.First(&school, req.SchoolID).Error; err != nil { return nil, errors.New("学校不存在") } // 3. 校验学院(如果传了) var collegeName string if req.CollegeID > 0 { if !school.HasCollegeType() { return nil, errors.New("该学校类型不支持学院层级") } var college model.College if err := s.db.Where("id = ? AND school_id = ?", req.CollegeID, req.SchoolID).First(&college).Error; err != nil { return nil, errors.New("学院不存在或不属于该学校") } collegeName = college.Name } else if school.RequiresCollege() { // 纯大学/职业学校必须选学院;同时涵盖中小学学段的学校可不选 return nil, errors.New("该学校需要选择所属学院") } // 规范化年级与班级,自动补齐班级名称与推导毕业年份 req.Grade = strings.TrimSpace(req.Grade) req.ClassNo = strings.TrimSpace(req.ClassNo) if req.Name == "" { req.Name = school.Name + collegeName + req.Grade + req.ClassNo } // 0. 敏感词检测 if found, word := sensitive.Check(req.Name); found { return nil, fmt.Errorf("班级名称包含敏感词:%s", word) } if req.GraduateYear <= 0 { startYear := 0 var numStr string for _, ch := range req.Grade { if ch >= '0' && ch <= '9' { numStr += string(ch) if len(numStr) == 4 { fmt.Sscanf(numStr, "%d", &startYear) break } } else { numStr = "" } } duration := 3 if strings.Contains(school.SchoolTypes, "primary") || school.SchoolType == model.SchoolPrimary { duration = 6 } else if strings.Contains(school.SchoolTypes, "university") || school.SchoolType == model.SchoolUniversity { duration = 4 } if startYear > 0 { req.GraduateYear = startYear + duration } else { req.GraduateYear = time.Now().Year() } } // 4. 唯一性校验 var existCount int64 q := s.db.Model(&model.Class{}).Where("school_id = ? AND grade = ? AND class_no = ? AND graduate_year = ? AND status != ?", req.SchoolID, req.Grade, req.ClassNo, req.GraduateYear, model.ClassDeleted) if req.CollegeID > 0 { q = q.Where("college_id = ?", req.CollegeID) } else { q = q.Where("college_id = 0 OR college_id IS NULL") } q.Count(&existCount) if existCount > 0 { return nil, errors.New("该班级已存在,请勿重复创建") } // 5. 清洗名单与构建排位花名册(预录入每个同学与老师,未注册账号预留) var rosters []model.ClassRoster var students []string var teachers []string now := time.Now() var creator model.User s.db.Select("id, username, nickname").First(&creator, userID) if len(req.Roster) > 0 { maxRow := 1 for _, item := range req.Roster { name := strings.TrimSpace(item.Name) if name == "" { continue } rowNo := item.RowNo if rowNo <= 0 { rowNo = 1 } if rowNo > maxRow { maxRow = rowNo } role := item.Role if role == "" { role = "student" } if role == "teacher" { teachers = append(teachers, name) } else { students = append(students, name) } r := model.ClassRoster{ RowNo: rowNo, ColNo: item.ColNo, Name: name, Role: role, } // 若当前录入的名字与创建者本人姓名/昵称一致,且尚未绑定任何一条,自动认领绑定 if creator.ID > 0 && (name == creator.Nickname || name == creator.Username) { r.UserID = creator.ID r.IsClaimed = true r.ClaimedAt = &now } rosters = append(rosters, r) } if req.RowCount < maxRow { req.RowCount = maxRow } students = cleanNames(students) teachers = cleanNames(teachers) } else { // 兼容旧格式调用 students = cleanNames(req.StudentNames) teachers = cleanNames(req.TeacherNames) col := 1 for _, name := range students { r := model.ClassRoster{ RowNo: 1, ColNo: col, Name: name, Role: "student", } if creator.ID > 0 && (name == creator.Nickname || name == creator.Username) { r.UserID = creator.ID r.IsClaimed = true r.ClaimedAt = &now } rosters = append(rosters, r) col++ } for _, name := range teachers { rosters = append(rosters, model.ClassRoster{ RowNo: 1, ColNo: col, Name: name, Role: "teacher", }) col++ } if req.RowCount <= 0 { req.RowCount = 1 } } // 6. 构建班级 graceDays := 30 if s.configSvc != nil { graceDays = s.configSvc.GetInt("audit_grace_days", 30) } class := &model.Class{ SchoolID: req.SchoolID, CollegeID: req.CollegeID, Name: req.Name, Grade: req.Grade, ClassNo: req.ClassNo, GraduateYear: req.GraduateYear, AdminUserID: userID, GraduationPhoto: req.GraduationPhoto, RowCount: req.RowCount, AuditDeadline: time.Now().AddDate(0, 0, graceDays), } class.SetStudentNames(students) class.SetTeacherNames(teachers) // 7. 总人数不足8个直接打回 totalPeople := len(students) + len(teachers) if totalPeople < model.MinStudentCount { class.Status = model.ClassRejected class.RejectReason = fmt.Sprintf("班级录入人员仅 %d 人,需至少录入 %d 人(毕业照上通常有全部人员姓名,请补齐后重新提交)", totalPeople, model.MinStudentCount) } else { class.Status = model.ClassPending } // 8. 事务:创建班级 + 保存分开的花名册预录入记录 + 创建者成为管理员成员 + 审核日志 err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(class).Error; err != nil { return err } // 写入花名册:每个同学/老师作为独立记录存储(未绑定具体账号) if len(rosters) > 0 { for i := range rosters { rosters[i].ClassID = class.ID } if err := tx.Create(&rosters).Error; err != nil { return err } } member := &model.ClassMember{ ClassID: class.ID, UserID: userID, Role: model.MemberRoleAdmin, JoinMethod: model.JoinByInvite, // 创建者视同邀请 JoinedAt: time.Now(), } if err := tx.Create(member).Error; err != nil { return err } // 检查创建者是否在花名册中,若是则自动认领绑定对应站位 var creator model.User if tx.Select("id, nickname, username, real_name").First(&creator, userID).Error == nil { for i := range rosters { name := rosters[i].Name if (creator.RealName != "" && name == creator.RealName) || (creator.Nickname != "" && name == creator.Nickname) || name == creator.Username { tx.Model(&model.ClassRoster{}).Where("class_id = ? AND row_no = ? AND col_no = ?", class.ID, rosters[i].RowNo, rosters[i].ColNo).Updates(map[string]interface{}{ "user_id": userID, "is_claimed": true, "claimed_at": time.Now(), }) break } } } log := &model.AuditLog{ ClassID: class.ID, AuditorID: userID, Action: model.AuditSubmit, Reason: fmt.Sprintf("创建班级,按 %d 排录入 %d 位人员(学生 %d 人,老师 %d 人)", class.RowCount, len(rosters), len(students), len(teachers)), } return tx.Create(log).Error }) if err != nil { return nil, err } return class, nil } // ClassDetail 班级详情 type ClassDetail struct { Class model.Class `json:"class"` RowCount int `json:"row_count"` Roster []model.ClassRoster `json:"roster"` StudentNames []string `json:"student_names"` TeacherNames []string `json:"teacher_names"` School *model.School `json:"school,omitempty"` College *model.College `json:"college,omitempty"` Admin *model.User `json:"admin,omitempty"` Members []ClassMemberInfo `json:"members"` Photos []model.ClassPhoto `json:"photos"` MyRole string `json:"my_role"` // 当前用户角色,未加入为空 IsMember bool `json:"is_member"` CanViewSensitive bool `json:"can_view_sensitive"` // 是否可查看照片和联系方式(成员/管理员) } type ClassMemberInfo struct { Member model.ClassMember `json:"member"` User model.User `json:"user"` Contacts []model.UserContact `json:"contacts,omitempty"` } func (s *ClassService) GetDetail(classID, currentUserID uint, isSuperAdmin bool) (*ClassDetail, error) { var class model.Class if err := s.db.First(&class, classID).Error; err != nil { return nil, errors.New("班级不存在") } if class.Status == model.ClassDeleted { return nil, errors.New("班级已删除") } // 数据隔离:只有班级成员或超级管理员可查看完整信息(照片+联系方式) canViewSensitive := isSuperAdmin || (currentUserID > 0 && s.isMember(classID, currentUserID)) var rosters []model.ClassRoster s.db.Where("class_id = ?", classID).Order("row_no asc, col_no asc, id asc").Find(&rosters) detail := &ClassDetail{ Class: class, RowCount: class.RowCount, Roster: rosters, StudentNames: class.StudentNames(), TeacherNames: class.TeacherNames(), CanViewSensitive: canViewSensitive, } // 兼容老数据:如果花名册表中暂无记录,则按 StudentNames/TeacherNames 构造第1排返回 if len(rosters) == 0 && (len(detail.StudentNames) > 0 || len(detail.TeacherNames) > 0) { col := 1 for _, name := range detail.StudentNames { detail.Roster = append(detail.Roster, model.ClassRoster{ ClassID: classID, RowNo: 1, ColNo: col, Name: name, Role: "student", }) col++ } for _, name := range detail.TeacherNames { detail.Roster = append(detail.Roster, model.ClassRoster{ ClassID: classID, RowNo: 1, ColNo: col, Name: name, Role: "teacher", }) col++ } detail.RowCount = 1 } if detail.RowCount <= 0 && len(detail.Roster) > 0 { for _, r := range detail.Roster { if r.RowNo > detail.RowCount { detail.RowCount = r.RowNo } } } // 学校 var school model.School if s.db.First(&school, class.SchoolID).Error == nil { detail.School = &school } // 学院 if class.CollegeID > 0 { var college model.College if s.db.First(&college, class.CollegeID).Error == nil { detail.College = &college } } // 管理员 var admin model.User if s.db.First(&admin, class.AdminUserID).Error == nil { detail.Admin = &admin } // 成员列表 var members []model.ClassMember s.db.Where("class_id = ?", classID).Order("role desc, joined_at").Find(&members) for _, m := range members { var u model.User s.db.First(&u, m.UserID) // 敏感信息脱敏:非本人且非平台管理员时脱敏身份证号 if !isSuperAdmin && m.UserID != currentUserID { if len(u.IdCard) == 18 { u.IdCard = u.IdCard[:6] + "********" + u.IdCard[14:] } else if u.IdCard != "" { u.IdCard = "****************" } } // 数据隔离:同班成员或超级管理员可查看联系方式;非成员清空敏感联系信息 if canViewSensitive { info := ClassMemberInfo{Member: m, User: u} info.Contacts = s.getUserContacts(m.UserID) detail.Members = append(detail.Members, info) } else { u.Phone = "" u.Email = "" u.Address = "" info := ClassMemberInfo{Member: m, User: u} detail.Members = append(detail.Members, info) } } // 照片墙:数据隔离,只有成员或超级管理员可查看 if canViewSensitive { var photos []model.ClassPhoto s.db.Where("class_id = ?", classID).Order("user_id, id desc").Find(&photos) detail.Photos = photos } else { detail.Photos = []model.ClassPhoto{} } // 当前用户角色 if currentUserID > 0 { var myMember model.ClassMember if s.db.Where("class_id = ? AND user_id = ?", classID, currentUserID).First(&myMember).Error == nil { detail.MyRole = string(myMember.Role) detail.IsMember = true } } return detail, nil } func (s *ClassService) isMember(classID, userID uint) bool { var count int64 s.db.Model(&model.ClassMember{}).Where("class_id = ? AND user_id = ?", classID, userID).Count(&count) return count > 0 } func (s *ClassService) getUserContacts(userID uint) []model.UserContact { var list []model.UserContact s.db.Where("user_id = ?", userID).Order("type, is_primary desc, id").Find(&list) var u model.User if s.db.Select("id, phone, email, address").First(&u, userID).Error == nil { hasPhone := false hasEmail := false hasAddress := false for _, c := range list { if c.Type == model.ContactPhone { hasPhone = true } if c.Type == model.ContactEmail { hasEmail = true } if c.Type == model.ContactAddress { hasAddress = true } } if !hasPhone && u.Phone != "" { list = append(list, model.UserContact{UserID: userID, Type: model.ContactPhone, Value: u.Phone, IsPrimary: true}) } if !hasEmail && u.Email != "" { list = append(list, model.UserContact{UserID: userID, Type: model.ContactEmail, Value: u.Email, IsPrimary: true}) } if !hasAddress && u.Address != "" { list = append(list, model.UserContact{UserID: userID, Type: model.ContactAddress, Value: u.Address, IsPrimary: true}) } } return list } // SearchClassReq 搜索班级 type SearchClassReq struct { Keyword string `form:"keyword"` SchoolID uint `form:"school_id"` Grade string `form:"grade"` GraduateYear int `form:"graduate_year"` Page int `form:"page"` PageSize int `form:"page_size"` } // Search 搜索可加入的班级(仅 active 状态) func (s *ClassService) Search(req *SearchClassReq) (*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.ClassActive) if req.SchoolID > 0 { db = db.Where("school_id = ?", req.SchoolID) } if req.Grade != "" { db = db.Where("grade = ?", req.Grade) } if req.GraduateYear > 0 { db = db.Where("graduate_year = ?", req.GraduateYear) } if req.Keyword != "" { db = db.Where("name LIKE ?", "%"+req.Keyword+"%") } var total int64 db.Count(&total) var list []model.Class err := db.Order("graduate_year desc, 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 } // JoinBySearchReq 搜索加入验证 type JoinBySearchReq struct { ClassID uint `json:"class_id" binding:"required"` StudentNames []string `json:"student_names" binding:"required,min=8"` } // JoinBySearch 通过搜索+8人验证加入班级 func (s *ClassService) JoinBySearch(userID uint, req *JoinBySearchReq) error { // 0. 校验用户有学习履历 var eduCount int64 s.db.Model(&model.EducationHistory{}).Where("user_id = ?", userID).Count(&eduCount) if eduCount == 0 { return errors.New("请先完善个人学习履历,有学习履历的用户才能加入班级") } var class model.Class if err := s.db.First(&class, req.ClassID).Error; err != nil { return errors.New("班级不存在") } if class.Status != model.ClassActive { return errors.New("该班级尚未通过审核,暂不能加入") } // 已加入? if s.isMember(class.ID, userID) { return errors.New("你已经是该班级成员") } // 验证8个名字 classStudents := class.StudentNames() classSet := map[string]bool{} for _, n := range classStudents { classSet[strings.TrimSpace(n)] = true } input := cleanNames(req.StudentNames) if len(input) < model.MinStudentCount { return fmt.Errorf("请输入至少 %d 位同学的名字", model.MinStudentCount) } for _, n := range input { if !classSet[n] { return fmt.Errorf("同学「%s」不在该班级名单中,验证失败,请确认名字与毕业照一致", n) } } // 全部匹配,加入 member := &model.ClassMember{ ClassID: class.ID, UserID: userID, Role: model.MemberRoleMember, JoinMethod: model.JoinBySearch, JoinedAt: time.Now(), } if err := s.db.Create(member).Error; err != nil { return err } // 联查班级花名册:若当前用户姓名与花名册中未绑定人员匹配,自动绑定该坑位 var user model.User if s.db.Select("id, nickname, username, real_name").First(&user, userID).Error == nil { var targetRoster model.ClassRoster q := s.db.Where("class_id = ? AND is_claimed = ? AND user_id = 0", class.ID, false) if user.RealName != "" { q = q.Where("name = ? OR name = ? OR name = ?", user.RealName, user.Nickname, user.Username) } else { q = q.Where("name = ? OR name = ?", user.Nickname, user.Username) } if err := q.First(&targetRoster).Error; err == nil { s.db.Model(&targetRoster).Updates(map[string]interface{}{ "user_id": userID, "is_claimed": true, "claimed_at": time.Now(), }) } } return nil } // ---- 邀请码 ---- // CreateInvitation 管理员生成邀请码 func (s *ClassService) CreateInvitation(classID, userID uint, maxUses int, expireDays int) (*model.Invitation, error) { if !s.isAdmin(classID, userID) { return nil, errors.New("只有班级管理员可以生成邀请码") } var class model.Class if err := s.db.First(&class, classID).Error; err != nil { return nil, errors.New("班级不存在") } if class.Status == model.ClassDeleted { return nil, errors.New("班级已删除") } if maxUses <= 0 { maxUses = 50 } if expireDays <= 0 { expireDays = 30 } inv := &model.Invitation{ ClassID: classID, InviterID: userID, Code: strings.ToUpper(strings.ReplaceAll(uuid.New().String()[:8], "-", "")), MaxUses: maxUses, ExpiresAt: time.Now().AddDate(0, 0, expireDays), Status: "active", } if err := s.db.Create(inv).Error; err != nil { return nil, err } return inv, nil } // ListInvitations 管理员查看邀请码列表 func (s *ClassService) ListInvitations(classID, userID uint) ([]model.Invitation, error) { if !s.isAdmin(classID, userID) { return nil, errors.New("只有班级管理员可以查看邀请码") } var list []model.Invitation err := s.db.Where("class_id = ?", classID).Order("id desc").Find(&list).Error return list, err } // JoinByInvitationReq 邀请码加入 type JoinByInvitationReq struct { Code string `json:"code" binding:"required"` } // JoinByInvitation 通过邀请码加入 func (s *ClassService) JoinByInvitation(userID uint, req *JoinByInvitationReq) (*model.Class, error) { // 0. 校验用户有学习履历 var eduCount int64 s.db.Model(&model.EducationHistory{}).Where("user_id = ?", userID).Count(&eduCount) if eduCount == 0 { return nil, errors.New("请先完善个人学习履历,有学习履历的用户才能加入班级") } code := strings.ToUpper(strings.TrimSpace(req.Code)) var inv model.Invitation if err := s.db.Where("code = ?", code).First(&inv).Error; err != nil { return nil, errors.New("邀请码无效") } if inv.Status != "active" { return nil, errors.New("邀请码已失效") } if inv.UsedCount >= inv.MaxUses { return nil, errors.New("邀请码使用次数已达上限") } if time.Now().After(inv.ExpiresAt) { return nil, errors.New("邀请码已过期") } var class model.Class if err := s.db.First(&class, inv.ClassID).Error; err != nil { return nil, errors.New("班级不存在") } if class.Status == model.ClassDeleted { return nil, errors.New("班级已删除") } if s.isMember(class.ID, userID) { return nil, errors.New("你已经是该班级成员") } // 事务:加成员 + 邀请码使用数+1 err := s.db.Transaction(func(tx *gorm.DB) error { member := &model.ClassMember{ ClassID: class.ID, UserID: userID, Role: model.MemberRoleMember, JoinMethod: model.JoinByInvite, JoinedAt: time.Now(), } if err := tx.Create(member).Error; err != nil { return err } // 联查花名册自动绑定坑位 var u model.User if tx.Select("id, nickname, username, real_name").First(&u, userID).Error == nil { var targetRoster model.ClassRoster q := tx.Where("class_id = ? AND is_claimed = ? AND user_id = 0", class.ID, false) if u.RealName != "" { q = q.Where("name = ? OR name = ? OR name = ?", u.RealName, u.Nickname, u.Username) } else { q = q.Where("name = ? OR name = ?", u.Nickname, u.Username) } if err := q.First(&targetRoster).Error; err == nil { tx.Model(&targetRoster).Updates(map[string]interface{}{ "user_id": userID, "is_claimed": true, "claimed_at": time.Now(), }) } } return tx.Model(&model.Invitation{}).Where("id = ?", inv.ID). Update("used_count", gorm.Expr("used_count + 1")).Error }) if err != nil { return nil, err } return &class, nil } // DisableInvitation 管理员禁用邀请码 func (s *ClassService) DisableInvitation(classID, invID, userID uint) error { if !s.isAdmin(classID, userID) { return errors.New("只有班级管理员可以操作") } res := s.db.Model(&model.Invitation{}).Where("id = ? AND class_id = ?", invID, classID).Update("status", "disabled") if res.RowsAffected == 0 { return errors.New("邀请码不存在") } return nil } // isAdmin 检查用户是否为班级管理员 func (s *ClassService) isAdmin(classID, userID uint) bool { var count int64 s.db.Model(&model.ClassMember{}).Where("class_id = ? AND user_id = ? AND role = ?", classID, userID, model.MemberRoleAdmin).Count(&count) return count > 0 } // TransferAdmin 管理员转移 func (s *ClassService) TransferAdmin(classID, fromUserID, toUserID uint) error { if !s.isAdmin(classID, fromUserID) { return errors.New("只有班级管理员可以转移管理员身份") } if fromUserID == toUserID { return errors.New("不能转移给自己") } // 目标必须是同班成员 var target model.ClassMember if err := s.db.Where("class_id = ? AND user_id = ?", classID, toUserID).First(&target).Error; err != nil { return errors.New("目标用户不是该班级成员") } err := s.db.Transaction(func(tx *gorm.DB) error { // 原管理员降为成员 if err := tx.Model(&model.ClassMember{}). Where("class_id = ? AND user_id = ?", classID, fromUserID). Update("role", model.MemberRoleMember).Error; err != nil { return err } // 目标升为管理员 if err := tx.Model(&model.ClassMember{}). Where("class_id = ? AND user_id = ?", classID, toUserID). Update("role", model.MemberRoleAdmin).Error; err != nil { return err } // 更新班级 admin_user_id return tx.Model(&model.Class{}).Where("id = ?", classID).Update("admin_user_id", toUserID).Error }) return err } // RemoveMember 管理员移除成员(不能移除自己) func (s *ClassService) RemoveMember(classID, adminID, targetUserID uint) error { if !s.isAdmin(classID, adminID) { return errors.New("只有班级管理员可以移除成员") } if adminID == targetUserID { return errors.New("不能移除自己,请先转移管理员") } res := s.db.Where("class_id = ? AND user_id = ?", classID, targetUserID).Delete(&model.ClassMember{}) if res.RowsAffected == 0 { return errors.New("成员不存在") } return nil } // ---- 个人照片墙 ---- // UploadPhoto 上传个人当期照片(每人每班级最多10张) func (s *ClassService) UploadPhoto(classID, userID uint, photoURL, caption string) (*model.ClassPhoto, error) { if !s.isMember(classID, userID) { return nil, errors.New("只有班级成员可以上传照片") } var class model.Class if err := s.db.First(&class, classID).Error; err != nil { return nil, errors.New("班级不存在") } if class.Status == model.ClassDeleted { return nil, errors.New("班级已删除") } var count int64 s.db.Model(&model.ClassPhoto{}).Where("class_id = ? AND user_id = ?", classID, userID).Count(&count) if count >= int64(model.MaxPhotosPerUserPerClass) { return nil, fmt.Errorf("每人在每个班级最多上传 %d 张照片", model.MaxPhotosPerUserPerClass) } photo := &model.ClassPhoto{ ClassID: classID, UserID: userID, PhotoURL: photoURL, Caption: caption, } if err := s.db.Create(photo).Error; err != nil { return nil, err } return photo, nil } // DeletePhoto 删除照片(本人或班级管理员或超级管理员) func (s *ClassService) DeletePhoto(classID, photoID, userID uint, isSuperAdmin bool) error { var photo model.ClassPhoto if err := s.db.Where("id = ? AND class_id = ?", photoID, classID).First(&photo).Error; err != nil { return errors.New("照片不存在") } // 权限校验:本人、班级管理员、超级管理员 canDelete := isSuperAdmin || photo.UserID == userID || s.isAdmin(classID, userID) if !canDelete { return errors.New("无权删除此照片") } s.db.Delete(&photo) return nil } // ListMyClasses 我的班级列表 func (s *ClassService) ListMyClasses(userID uint) ([]ClassDetail, error) { var members []model.ClassMember s.db.Where("user_id = ?", userID).Order("id desc").Find(&members) result := []ClassDetail{} for _, m := range members { var class model.Class if err := s.db.First(&class, m.ClassID).Error; err != nil { continue } if class.Status == model.ClassDeleted { continue } d := ClassDetail{ Class: class, MyRole: string(m.Role), IsMember: true, } var school model.School if s.db.First(&school, class.SchoolID).Error == nil { d.School = &school } result = append(result, d) } return result, nil } // ResubmitReq 重新提交审核(补齐名单/照片) type ResubmitReq struct { GraduationPhoto string `json:"graduation_photo"` StudentNames []string `json:"student_names"` TeacherNames []string `json:"teacher_names"` } // Resubmit 被打回后补齐重新提交 func (s *ClassService) Resubmit(classID, userID uint, req *ResubmitReq) error { var class model.Class if err := s.db.First(&class, classID).Error; err != nil { return errors.New("班级不存在") } if class.AdminUserID != userID { return errors.New("只有班级管理员可以重新提交审核") } if class.Status != model.ClassRejected && class.Status != model.ClassPending { return errors.New("当前状态不支持重新提交") } students := cleanNames(req.StudentNames) teachers := cleanNames(req.TeacherNames) if len(students) < model.MinStudentCount { return fmt.Errorf("同学名单至少需要 %d 人,当前仅 %d 人", model.MinStudentCount, len(students)) } if req.GraduationPhoto != "" { class.GraduationPhoto = req.GraduationPhoto } class.SetStudentNames(students) class.SetTeacherNames(teachers) class.Status = model.ClassPending class.RejectReason = "" // 重新提交后审核截止时间顺延?保持原截止时间,30天从创建算起 if err := s.db.Save(&class).Error; err != nil { return err } // 审核日志 log := &model.AuditLog{ ClassID: class.ID, AuditorID: userID, Action: model.AuditSubmit, Reason: fmt.Sprintf("重新提交,补齐同学 %d 人、老师 %d 人", len(students), len(teachers)), } s.db.Create(log) return nil }