// =========================================== // 多班级版班级管理系统 - Go 后端 // // 开发者: Canglan // 联系方式: admin@sea-studio.top // 版权归属: Sea Network Technology Studio // 许可证: Apache License 2.0 // // 版权所有 © Sea Network Technology Studio // =========================================== package service import ( "context" "fmt" "regexp" "hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/model" "hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/repository" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/crypto" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/database" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger" ) // AdminService 管理员服务 type AdminService struct { userRepo *repository.UserRepo studentRepo *repository.StudentRepo adminRoleRepo *repository.AdminRoleRepo classRepo *repository.ClassRepo } // NewAdminService 创建管理员服务 func NewAdminService( userRepo *repository.UserRepo, studentRepo *repository.StudentRepo, adminRoleRepo *repository.AdminRoleRepo, classRepo *repository.ClassRepo, ) *AdminService { return &AdminService{ userRepo: userRepo, studentRepo: studentRepo, adminRoleRepo: adminRoleRepo, classRepo: classRepo, } } // dormitoryRegex 宿舍号格式校验:东南北西 + 1-2位楼号 - 3位房号 var dormitoryRegex = regexp.MustCompile(`^[东南北西]\d{1,2}-\d{3}$`) // validateDormitoryNumber 校验宿舍号格式(允许空值或合法格式) func validateDormitoryNumber(dn *string) bool { if dn == nil || *dn == "" { return true } return dormitoryRegex.MatchString(*dn) } // GetStudents 获取指定班级的学生列表 func (s *AdminService) GetStudents(classID int, page, pageSize int, search, dormitoryNumber string) (map[string]interface{}, error) { students, total, err := s.studentRepo.ListByClass(classID, page, pageSize, search, dormitoryNumber) if err != nil { return nil, err } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return map[string]interface{}{ "students": students, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages, }, nil } // GetDormitories 获取宿舍号列表 func (s *AdminService) GetDormitories(classID int) ([]string, error) { return s.studentRepo.GetDormitoryList(classID) } // getInitialPassword 从 class_settings 读取初始密码,若无则使用随机密码 func (s *AdminService) getInitialPassword(classID int) (string, error) { if s.classRepo != nil { setting, err := s.classRepo.GetSetting(classID, "initial_password") if err == nil && setting != nil && setting.SettingValue != "" { return setting.SettingValue, nil } } pwd, err := crypto.GenerateRandomPassword(8) if err != nil { logger.Sugared.Errorf("生成随机密码失败: %v", err) return "", fmt.Errorf("生成随机密码失败: %w", err) } return pwd, nil } // hashPassword 对密码进行 bcrypt 哈希,失败时 panic(不应发生) func hashPasswordOrPanic(password string) string { hash, err := crypto.HashPassword(password) if err != nil { logger.Sugared.Fatalf("密码哈希失败: %v", err) } return hash } // AddStudent 新增学生 func (s *AdminService) AddStudent(studentNo, name string, parentAccount *string, classID int, dormitoryNumber *string) (map[string]interface{}, error) { // 校验宿舍号格式 if !validateDormitoryNumber(dormitoryNumber) { return map[string]interface{}{"success": false, "message": "宿舍号格式不正确,应为如 东1-101 的格式"}, nil } // 检查学号是否已存在 existing, err := s.studentRepo.GetByStudentNo(studentNo, classID) if err == nil && existing != nil { return map[string]interface{}{"success": false, "message": "该班级中已存在此学号"}, nil } // 创建学生记录 student := &model.Student{ StudentNo: studentNo, ClassID: classID, Name: name, TotalPoints: 60, ParentAccount: parentAccount, DormitoryNumber: dormitoryNumber, Status: 1, } studentID, err := s.studentRepo.Create(student) if err != nil { return nil, err } // 创建学生登录账号(从 class_settings 读取初始密码或使用随机密码) defaultPassword, err := s.getInitialPassword(classID) if err != nil { return nil, err } passwordHash := hashPasswordOrPanic(defaultPassword) _, err = s.userRepo.CreateStudent(studentNo, passwordHash, name, studentID) if err != nil { logger.Sugared.Errorf("创建学生登录账号失败: student_no=%s, student_id=%d, err=%v", studentNo, studentID, err) // 回滚学生记录,避免存在无账号的孤儿学生 _ = s.studentRepo.SoftDelete(studentID) return nil, fmt.Errorf("创建学生登录账号失败") } // 创建家长账号(失败时不回滚学生记录,仅记录日志;管理员可手动补建家长账号) if parentAccount != nil && *parentAccount != "" { exists, _ := s.userRepo.CheckUsernameExists(*parentAccount) if !exists { parentHash := hashPasswordOrPanic(defaultPassword) parentRealName := fmt.Sprintf("%s家长", name) if _, err := s.userRepo.CreateParent(*parentAccount, parentHash, parentRealName, studentID); err != nil { logger.Sugared.Warnf("创建家长账号失败(学生记录已保留): parent_account=%s, student_id=%d, err=%v", *parentAccount, studentID, err) } } } return map[string]interface{}{ "success": true, "student_id": studentID, }, nil } // ImportStudents 批量导入学生 // 注意:当前实现为逐条创建,单条失败时回滚该条记录(SoftDelete),不影响其他记录。 // 这是设计上有意为之——允许部分成功,避免一条失败导致整个导入作废。 // 未使用数据库事务的原因:Repository 层未暴露事务接口,全量事务包裹需要较大重构; // 且批量导入场景下允许部分成功是合理的业务权衡(用户可修正失败记录后重新导入)。 func (s *AdminService) ImportStudents(students []map[string]interface{}, classID int) (map[string]interface{}, error) { successCount := 0 failedCount := 0 var details []map[string]interface{} // 预查重 existingNos, _ := s.studentRepo.GetStudentNosByClass(classID) existingSet := make(map[string]bool, len(existingNos)) for _, no := range existingNos { existingSet[no] = true } existingUsernames, _ := s.userRepo.GetActiveUsernames() usernameSet := make(map[string]bool, len(existingUsernames)) for _, u := range existingUsernames { usernameSet[u] = true } for _, stu := range students { studentNo, _ := stu["student_no"].(string) name, _ := stu["name"].(string) if studentNo == "" || name == "" { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": "学号或姓名不能为空", }) continue } if existingSet[studentNo] { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": "学号已存在", }) continue } var parentAccount *string if pa, ok := stu["parent_account"].(string); ok && pa != "" { parentAccount = &pa } var dormitoryNumber *string if dn, ok := stu["dormitory_number"].(string); ok && dn != "" { dormitoryNumber = &dn } // 校验宿舍号格式 if !validateDormitoryNumber(dormitoryNumber) { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": "宿舍号格式不正确,应为如 东1-101 的格式", }) continue } password, pwdErr := s.getInitialPassword(classID) if pwdErr != nil { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": "生成初始密码失败", }) continue } if pw, ok := stu["password"].(string); ok && pw != "" { if valid, msg := crypto.ValidatePasswordStrength(pw); !valid { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": msg, }) continue } password = pw } // 创建学生记录 student := &model.Student{ StudentNo: studentNo, ClassID: classID, Name: name, TotalPoints: 60, ParentAccount: parentAccount, DormitoryNumber: dormitoryNumber, Status: 1, } studentID, err := s.studentRepo.Create(student) if err != nil { failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": err.Error(), }) continue } existingSet[studentNo] = true // 创建学生登录账号 passwordHash := hashPasswordOrPanic(password) if _, err := s.userRepo.CreateStudent(studentNo, passwordHash, name, studentID); err != nil { logger.Sugared.Errorf("批量导入-创建学生登录账号失败: student_no=%s, student_id=%d, err=%v", studentNo, studentID, err) // 回滚学生记录 _ = s.studentRepo.SoftDelete(studentID) failedCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": false, "error": "创建登录账号失败", }) continue } usernameSet[studentNo] = true // 创建家长账号 if parentAccount != nil && *parentAccount != "" && !usernameSet[*parentAccount] { parentHash := hashPasswordOrPanic(password) parentRealName := fmt.Sprintf("%s家长", name) if _, err := s.userRepo.CreateParent(*parentAccount, parentHash, parentRealName, studentID); err != nil { logger.Sugared.Errorf("批量导入-创建家长账号失败: parent_account=%s, student_id=%d, err=%v", *parentAccount, studentID, err) } usernameSet[*parentAccount] = true } successCount++ details = append(details, map[string]interface{}{ "student_no": studentNo, "success": true, "student_id": studentID, }) } return map[string]interface{}{ "success": true, "total": len(students), "success_count": successCount, "failed_count": failedCount, "details": details, }, nil } // UpdateStudent 编辑学生信息 func (s *AdminService) UpdateStudent(studentID int, name, parentAccount, dormitoryNumber *string, classID int) error { // 校验学生是否属于当前班级 student, err := s.studentRepo.GetByID(studentID) if err != nil || student == nil { return fmt.Errorf("学生不存在") } if student.ClassID != classID { return fmt.Errorf("无权操作该学生") } // 校验宿舍号格式 if !validateDormitoryNumber(dormitoryNumber) { return fmt.Errorf("宿舍号格式不正确,应为如 东1-101 的格式") } updates := make(map[string]interface{}) if name != nil { updates["name"] = *name } if parentAccount != nil { updates["parent_account"] = *parentAccount } if dormitoryNumber != nil { updates["dormitory_number"] = *dormitoryNumber } return s.studentRepo.Update(studentID, updates) } // DeleteStudent 删除学生 func (s *AdminService) DeleteStudent(studentID int, classID int) error { // 校验学生是否属于当前班级 student, err := s.studentRepo.GetByID(studentID) if err != nil || student == nil { return fmt.Errorf("学生不存在") } if student.ClassID != classID { return fmt.Errorf("无权操作该学生") } return s.studentRepo.SoftDelete(studentID) } // ResetStudentPassword 重置学生密码 func (s *AdminService) ResetStudentPassword(studentID int, newPassword string) error { // 验证新密码强度(#11) if valid, msg := crypto.ValidatePasswordStrength(newPassword); !valid { return fmt.Errorf("%s", msg) } student, err := s.studentRepo.GetByID(studentID) if err != nil { return fmt.Errorf("学生不存在") } // 通过学号查找关联的用户账号 user, err := s.userRepo.GetByUsername(student.StudentNo) if err != nil { return fmt.Errorf("学生登录账号不存在") } passwordHash, err := crypto.HashPassword(newPassword) if err != nil { return fmt.Errorf("密码加密失败") } return s.userRepo.UpdatePassword(user.UserID, passwordHash) } // AddAdmin 添加管理员 func (s *AdminService) AddAdmin(username, realName, password, roleType string, classID int, subjectID *int) (map[string]interface{}, error) { exists, _ := s.userRepo.CheckUsernameExists(username) if exists { return map[string]interface{}{"success": false, "message": "用户名已存在"}, nil } if password == "" { pwd, err := crypto.GenerateRandomPassword(8) if err != nil { return nil, fmt.Errorf("生成随机密码失败: %w", err) } password = pwd } passwordHash, err := crypto.HashPassword(password) if err != nil { return nil, fmt.Errorf("密码加密失败: %w", err) } userID, err := s.userRepo.CreateAdmin(username, passwordHash, realName) if err != nil { return nil, err } role := &model.AdminRole{ UserID: userID, ClassID: classID, RoleType: roleType, SubjectID: subjectID, } _, err = s.adminRoleRepo.Create(role) if err != nil { // 角色创建失败,回滚用户记录,避免孤儿数据 _ = s.userRepo.DeleteUser(userID) return nil, fmt.Errorf("创建管理员角色失败,已回滚用户记录: %w", err) } return map[string]interface{}{ "success": true, "user_id": userID, "username": username, "role_type": roleType, }, nil } // GetAdmins 获取管理员列表 func (s *AdminService) GetAdmins(classID int) (map[string]interface{}, error) { admins, err := s.adminRoleRepo.GetAllByClass(classID) if err != nil { return nil, err } return map[string]interface{}{"admins": admins}, nil } // UpdateAdmin 更新管理员 func (s *AdminService) UpdateAdmin(userID int, realName, roleType string, classID int, subjectID *int) error { if err := s.userRepo.UpdateRealName(userID, realName); err != nil { return err } return s.adminRoleRepo.UpdateRole(userID, roleType, classID, subjectID) } // DeleteAdmin 硬删除管理员(同时删除 users 和 admin_roles 记录) func (s *AdminService) DeleteAdmin(userID int, classID int) error { // 先删除关联的 admin_roles 记录 if err := s.adminRoleRepo.Delete(userID, classID); err != nil { return err } // 硬删除 users 表记录 return s.userRepo.DeleteUser(userID) } // ResetAdminPassword 重置管理员密码 func (s *AdminService) ResetAdminPassword(userID int, newPassword string) error { if valid, msg := crypto.ValidatePasswordStrength(newPassword); !valid { return fmt.Errorf("%s", msg) } passwordHash, err := crypto.HashPassword(newPassword) if err != nil { return fmt.Errorf("密码加密失败") } return s.userRepo.UpdatePassword(userID, passwordHash) } // UnlockAccount 解锁账号(清除用户名级 + IP 级登录失败计数) func (s *AdminService) UnlockAccount(username, ip string) error { ctx := context.Background() keys := []string{fmt.Sprintf("login_attempts:%s", username)} if ip != "" { keys = append(keys, fmt.Sprintf("login_attempts:ip:%s", ip)) } return database.RDB.Del(ctx, keys...).Err() }