From 8bfdda3cd50348702f4e1b3eabf00559eea29335 Mon Sep 17 00:00:00 2001 From: Mark Yang Date: Tue, 9 Jun 2026 23:19:35 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 67 + LICENSE | 9 + README.md | 119 ++ docs/DEPLOYMENT.md | 571 +++++++ docs/EXTENSION_TASKS.md | 231 +++ pom.xml | 103 ++ sample_users.csv | 4 + .../java/com/student/bean/Announcement.java | 82 + src/main/java/com/student/bean/ApiResult.java | 64 + .../java/com/student/bean/Attendance.java | 86 ++ .../com/student/bean/AttendanceStats.java | 45 + .../java/com/student/bean/ChangeRequest.java | 122 ++ src/main/java/com/student/bean/Clazz.java | 63 + src/main/java/com/student/bean/Course.java | 101 ++ .../java/com/student/bean/CourseStats.java | 64 + .../java/com/student/bean/DashboardStats.java | 83 + .../java/com/student/bean/Department.java | 36 + .../java/com/student/bean/Enrollment.java | 82 + .../java/com/student/bean/LeaveRequest.java | 131 ++ src/main/java/com/student/bean/LoginLog.java | 73 + src/main/java/com/student/bean/Major.java | 54 + src/main/java/com/student/bean/Message.java | 82 + .../java/com/student/bean/OperationLog.java | 73 + src/main/java/com/student/bean/PageBean.java | 78 + .../java/com/student/bean/RememberToken.java | 46 + src/main/java/com/student/bean/Score.java | 82 + src/main/java/com/student/bean/User.java | 250 +++ .../java/com/student/bean/UserImportTemp.java | 109 ++ src/main/java/com/student/bean/UserQuery.java | 90 ++ .../java/com/student/dao/AnnouncementDao.java | 20 + .../java/com/student/dao/AttendanceDao.java | 23 + .../com/student/dao/ChangeRequestDao.java | 20 + src/main/java/com/student/dao/CourseDao.java | 29 + .../java/com/student/dao/EnrollmentDao.java | 20 + .../java/com/student/dao/ImportTempDao.java | 16 + src/main/java/com/student/dao/LeaveDao.java | 20 + src/main/java/com/student/dao/LogDao.java | 16 + src/main/java/com/student/dao/MessageDao.java | 20 + src/main/java/com/student/dao/OrgDao.java | 44 + .../com/student/dao/RememberTokenDao.java | 14 + src/main/java/com/student/dao/ScoreDao.java | 19 + src/main/java/com/student/dao/StatDao.java | 16 + src/main/java/com/student/dao/UserDao.java | 52 + .../student/dao/impl/AnnouncementDaoImpl.java | 155 ++ .../student/dao/impl/AttendanceDaoImpl.java | 177 +++ .../dao/impl/ChangeRequestDaoImpl.java | 135 ++ .../com/student/dao/impl/CourseDaoImpl.java | 260 ++++ .../student/dao/impl/EnrollmentDaoImpl.java | 151 ++ .../student/dao/impl/ImportTempDaoImpl.java | 184 +++ .../com/student/dao/impl/LeaveDaoImpl.java | 146 ++ .../java/com/student/dao/impl/LogDaoImpl.java | 184 +++ .../com/student/dao/impl/MessageDaoImpl.java | 173 +++ .../java/com/student/dao/impl/OrgDaoImpl.java | 323 ++++ .../dao/impl/RememberTokenDaoImpl.java | 97 ++ .../com/student/dao/impl/ScoreDaoImpl.java | 161 ++ .../com/student/dao/impl/StatDaoImpl.java | 102 ++ .../com/student/dao/impl/UserDaoImpl.java | 475 ++++++ .../java/com/student/filter/AuthFilter.java | 81 + .../filter/CharacterEncodingFilter.java | 38 + .../student/filter/OperationLogFilter.java | 65 + .../com/student/filter/RememberMeFilter.java | 117 ++ .../com/student/filter/UserContextFilter.java | 57 + .../com/student/listener/SessionListener.java | 79 + .../student/service/AnnouncementService.java | 44 + .../student/service/AttendanceService.java | 49 + .../java/com/student/service/AuthService.java | 124 ++ .../student/service/ChangeRequestService.java | 83 + .../com/student/service/CourseService.java | 56 + .../student/service/EnrollmentService.java | 56 + .../com/student/service/ImportService.java | 47 + .../com/student/service/LeaveService.java | 50 + .../com/student/service/MessageService.java | 47 + .../java/com/student/service/OrgService.java | 90 ++ .../com/student/service/ScoreService.java | 41 + .../java/com/student/service/StatService.java | 36 + .../java/com/student/service/UserService.java | 133 ++ .../java/com/student/servlet/BaseServlet.java | 68 + .../com/student/servlet/CaptchaServlet.java | 34 + .../student/servlet/CheckUsernameServlet.java | 37 + .../servlet/ForgotPasswordServlet.java | 92 ++ .../com/student/servlet/LoginServlet.java | 84 + .../com/student/servlet/LogoutServlet.java | 37 + .../com/student/servlet/PublicOrgServlet.java | 33 + .../com/student/servlet/RegisterServlet.java | 81 + .../servlet/admin/AnnouncementServlet.java | 80 + .../admin/AttendanceManageServlet.java | 80 + .../servlet/admin/AttendanceStatsServlet.java | 40 + .../servlet/admin/BigScreenServlet.java | 26 + .../servlet/admin/ChangeAuditServlet.java | 58 + .../servlet/admin/CourseDeleteServlet.java | 33 + .../servlet/admin/CourseEditServlet.java | 71 + .../servlet/admin/CourseListServlet.java | 33 + .../servlet/admin/DashboardServlet.java | 29 + .../servlet/admin/ImportPreviewServlet.java | 66 + .../servlet/admin/LeaveApproveServlet.java | 54 + .../student/servlet/admin/LogViewServlet.java | 40 + .../servlet/admin/MessageSendServlet.java | 60 + .../servlet/admin/OnlineUserServlet.java | 39 + .../student/servlet/admin/OrgApiServlet.java | 36 + .../servlet/admin/OrgManageServlet.java | 73 + .../servlet/admin/RegAuditServlet.java | 66 + .../servlet/admin/ScoreManageServlet.java | 89 ++ .../servlet/admin/ScoreStatsServlet.java | 46 + .../student/servlet/admin/StatApiServlet.java | 35 + .../student/servlet/admin/UserAddServlet.java | 74 + .../servlet/admin/UserDeleteServlet.java | 45 + .../servlet/admin/UserDetailServlet.java | 50 + .../servlet/admin/UserDownloadServlet.java | 66 + .../servlet/admin/UserEditServlet.java | 89 ++ .../servlet/admin/UserExcelServlet.java | 83 + .../servlet/admin/UserListServlet.java | 46 + .../servlet/admin/UserStatusServlet.java | 43 + .../servlet/admin/UserUploadServlet.java | 131 ++ .../teacher/TeacherAttendanceServlet.java | 108 ++ .../servlet/teacher/TeacherHomeServlet.java | 48 + .../servlet/teacher/TeacherLeaveServlet.java | 54 + .../servlet/teacher/TeacherScoreServlet.java | 89 ++ .../servlet/user/AvatarUploadServlet.java | 86 ++ .../student/servlet/user/EnrollServlet.java | 59 + .../servlet/user/LeaveApplyServlet.java | 66 + .../servlet/user/MessageInboxServlet.java | 84 + .../student/servlet/user/MyCourseServlet.java | 41 + .../student/servlet/user/MyScoreServlet.java | 49 + .../servlet/user/UserProfileServlet.java | 107 ++ .../java/com/student/util/CaptchaUtil.java | 50 + .../java/com/student/util/CookieUtil.java | 41 + src/main/java/com/student/util/DBUtil.java | 102 ++ src/main/java/com/student/util/IpUtil.java | 39 + src/main/java/com/student/util/JsonUtil.java | 25 + src/main/java/com/student/util/ParamUtil.java | 41 + .../java/com/student/util/PasswordUtil.java | 89 ++ .../java/com/student/util/StudentNoUtil.java | 26 + src/main/resources/db.properties | 4 + src/main/resources/db_init.sql | 221 +++ .../webapp/WEB-INF/jspf/admin-sidebar.jspf | 83 + .../webapp/WEB-INF/jspf/admin-topbar.jspf | 20 + src/main/webapp/WEB-INF/jspf/alert.jspf | 9 + src/main/webapp/WEB-INF/jspf/empty-state.jspf | 5 + src/main/webapp/WEB-INF/jspf/head.jspf | 6 + src/main/webapp/WEB-INF/jspf/pagination.jspf | 38 + src/main/webapp/WEB-INF/jspf/stat-num.jspf | 5 + .../webapp/WEB-INF/jspf/teacher-sidebar.jspf | 29 + .../webapp/WEB-INF/jspf/teacher-topbar.jspf | 15 + src/main/webapp/WEB-INF/jspf/user-avatar.jspf | 16 + .../webapp/WEB-INF/jspf/user-sidebar.jspf | 38 + src/main/webapp/WEB-INF/jspf/user-topbar.jspf | 32 + src/main/webapp/WEB-INF/web.xml | 105 ++ src/main/webapp/admin/announcement.jsp | 91 ++ src/main/webapp/admin/attendance.jsp | 111 ++ src/main/webapp/admin/attendance_stats.jsp | 66 + src/main/webapp/admin/bigscreen.jsp | 140 ++ src/main/webapp/admin/change_audit.jsp | 69 + src/main/webapp/admin/course_edit.jsp | 74 + src/main/webapp/admin/course_list.jsp | 65 + src/main/webapp/admin/dashboard.jsp | 133 ++ src/main/webapp/admin/import_preview.jsp | 76 + src/main/webapp/admin/leave_approve.jsp | 78 + src/main/webapp/admin/logs.jsp | 83 + src/main/webapp/admin/message_send.jsp | 51 + src/main/webapp/admin/online.jsp | 68 + src/main/webapp/admin/org.jsp | 99 ++ src/main/webapp/admin/reg_audit.jsp | 68 + src/main/webapp/admin/score_manage.jsp | 80 + src/main/webapp/admin/score_stats.jsp | 101 ++ src/main/webapp/admin/user_add.jsp | 114 ++ src/main/webapp/admin/user_detail.jsp | 90 ++ src/main/webapp/admin/user_edit.jsp | 106 ++ src/main/webapp/admin/user_list.jsp | 148 ++ src/main/webapp/admin/user_upload.jsp | 47 + src/main/webapp/css/style.css | 1362 +++++++++++++++++ src/main/webapp/error.jsp | 40 + src/main/webapp/forgot_password.jsp | 67 + src/main/webapp/js/common.js | 134 ++ src/main/webapp/login.jsp | 78 + src/main/webapp/register.jsp | 134 ++ src/main/webapp/teacher/attendance.jsp | 103 ++ src/main/webapp/teacher/home.jsp | 65 + src/main/webapp/teacher/leave.jsp | 77 + src/main/webapp/teacher/score.jsp | 77 + src/main/webapp/user/enroll.jsp | 72 + src/main/webapp/user/leave_apply.jsp | 88 ++ src/main/webapp/user/messages.jsp | 67 + src/main/webapp/user/my_courses.jsp | 60 + src/main/webapp/user/my_scores.jsp | 71 + src/main/webapp/user/profile.jsp | 101 ++ .../service/AnnouncementServiceTest.java | 67 + .../service/AttendanceServiceTest.java | 76 + .../com/student/service/AuthServiceTest.java | 160 ++ .../service/ChangeRequestServiceTest.java | 119 ++ .../student/service/CourseServiceTest.java | 98 ++ .../service/EnrollmentServiceTest.java | 86 ++ .../student/service/ImportServiceTest.java | 75 + .../com/student/service/LeaveServiceTest.java | 88 ++ .../student/service/MessageServiceTest.java | 73 + .../com/student/service/OrgServiceTest.java | 95 ++ .../com/student/service/ScoreServiceTest.java | 63 + .../com/student/service/StatServiceTest.java | 57 + .../com/student/service/UserServiceTest.java | 127 ++ .../com/student/util/PasswordUtilTest.java | 89 ++ 199 files changed, 17149 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/EXTENSION_TASKS.md create mode 100644 pom.xml create mode 100644 sample_users.csv create mode 100644 src/main/java/com/student/bean/Announcement.java create mode 100644 src/main/java/com/student/bean/ApiResult.java create mode 100644 src/main/java/com/student/bean/Attendance.java create mode 100644 src/main/java/com/student/bean/AttendanceStats.java create mode 100644 src/main/java/com/student/bean/ChangeRequest.java create mode 100644 src/main/java/com/student/bean/Clazz.java create mode 100644 src/main/java/com/student/bean/Course.java create mode 100644 src/main/java/com/student/bean/CourseStats.java create mode 100644 src/main/java/com/student/bean/DashboardStats.java create mode 100644 src/main/java/com/student/bean/Department.java create mode 100644 src/main/java/com/student/bean/Enrollment.java create mode 100644 src/main/java/com/student/bean/LeaveRequest.java create mode 100644 src/main/java/com/student/bean/LoginLog.java create mode 100644 src/main/java/com/student/bean/Major.java create mode 100644 src/main/java/com/student/bean/Message.java create mode 100644 src/main/java/com/student/bean/OperationLog.java create mode 100644 src/main/java/com/student/bean/PageBean.java create mode 100644 src/main/java/com/student/bean/RememberToken.java create mode 100644 src/main/java/com/student/bean/Score.java create mode 100644 src/main/java/com/student/bean/User.java create mode 100644 src/main/java/com/student/bean/UserImportTemp.java create mode 100644 src/main/java/com/student/bean/UserQuery.java create mode 100644 src/main/java/com/student/dao/AnnouncementDao.java create mode 100644 src/main/java/com/student/dao/AttendanceDao.java create mode 100644 src/main/java/com/student/dao/ChangeRequestDao.java create mode 100644 src/main/java/com/student/dao/CourseDao.java create mode 100644 src/main/java/com/student/dao/EnrollmentDao.java create mode 100644 src/main/java/com/student/dao/ImportTempDao.java create mode 100644 src/main/java/com/student/dao/LeaveDao.java create mode 100644 src/main/java/com/student/dao/LogDao.java create mode 100644 src/main/java/com/student/dao/MessageDao.java create mode 100644 src/main/java/com/student/dao/OrgDao.java create mode 100644 src/main/java/com/student/dao/RememberTokenDao.java create mode 100644 src/main/java/com/student/dao/ScoreDao.java create mode 100644 src/main/java/com/student/dao/StatDao.java create mode 100644 src/main/java/com/student/dao/UserDao.java create mode 100644 src/main/java/com/student/dao/impl/AnnouncementDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/AttendanceDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/ChangeRequestDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/CourseDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/EnrollmentDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/ImportTempDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/LeaveDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/LogDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/MessageDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/OrgDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/RememberTokenDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/ScoreDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/StatDaoImpl.java create mode 100644 src/main/java/com/student/dao/impl/UserDaoImpl.java create mode 100644 src/main/java/com/student/filter/AuthFilter.java create mode 100644 src/main/java/com/student/filter/CharacterEncodingFilter.java create mode 100644 src/main/java/com/student/filter/OperationLogFilter.java create mode 100644 src/main/java/com/student/filter/RememberMeFilter.java create mode 100644 src/main/java/com/student/filter/UserContextFilter.java create mode 100644 src/main/java/com/student/listener/SessionListener.java create mode 100644 src/main/java/com/student/service/AnnouncementService.java create mode 100644 src/main/java/com/student/service/AttendanceService.java create mode 100644 src/main/java/com/student/service/AuthService.java create mode 100644 src/main/java/com/student/service/ChangeRequestService.java create mode 100644 src/main/java/com/student/service/CourseService.java create mode 100644 src/main/java/com/student/service/EnrollmentService.java create mode 100644 src/main/java/com/student/service/ImportService.java create mode 100644 src/main/java/com/student/service/LeaveService.java create mode 100644 src/main/java/com/student/service/MessageService.java create mode 100644 src/main/java/com/student/service/OrgService.java create mode 100644 src/main/java/com/student/service/ScoreService.java create mode 100644 src/main/java/com/student/service/StatService.java create mode 100644 src/main/java/com/student/service/UserService.java create mode 100644 src/main/java/com/student/servlet/BaseServlet.java create mode 100644 src/main/java/com/student/servlet/CaptchaServlet.java create mode 100644 src/main/java/com/student/servlet/CheckUsernameServlet.java create mode 100644 src/main/java/com/student/servlet/ForgotPasswordServlet.java create mode 100644 src/main/java/com/student/servlet/LoginServlet.java create mode 100644 src/main/java/com/student/servlet/LogoutServlet.java create mode 100644 src/main/java/com/student/servlet/PublicOrgServlet.java create mode 100644 src/main/java/com/student/servlet/RegisterServlet.java create mode 100644 src/main/java/com/student/servlet/admin/AnnouncementServlet.java create mode 100644 src/main/java/com/student/servlet/admin/AttendanceManageServlet.java create mode 100644 src/main/java/com/student/servlet/admin/AttendanceStatsServlet.java create mode 100644 src/main/java/com/student/servlet/admin/BigScreenServlet.java create mode 100644 src/main/java/com/student/servlet/admin/ChangeAuditServlet.java create mode 100644 src/main/java/com/student/servlet/admin/CourseDeleteServlet.java create mode 100644 src/main/java/com/student/servlet/admin/CourseEditServlet.java create mode 100644 src/main/java/com/student/servlet/admin/CourseListServlet.java create mode 100644 src/main/java/com/student/servlet/admin/DashboardServlet.java create mode 100644 src/main/java/com/student/servlet/admin/ImportPreviewServlet.java create mode 100644 src/main/java/com/student/servlet/admin/LeaveApproveServlet.java create mode 100644 src/main/java/com/student/servlet/admin/LogViewServlet.java create mode 100644 src/main/java/com/student/servlet/admin/MessageSendServlet.java create mode 100644 src/main/java/com/student/servlet/admin/OnlineUserServlet.java create mode 100644 src/main/java/com/student/servlet/admin/OrgApiServlet.java create mode 100644 src/main/java/com/student/servlet/admin/OrgManageServlet.java create mode 100644 src/main/java/com/student/servlet/admin/RegAuditServlet.java create mode 100644 src/main/java/com/student/servlet/admin/ScoreManageServlet.java create mode 100644 src/main/java/com/student/servlet/admin/ScoreStatsServlet.java create mode 100644 src/main/java/com/student/servlet/admin/StatApiServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserAddServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserDeleteServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserDetailServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserDownloadServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserEditServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserExcelServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserListServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserStatusServlet.java create mode 100644 src/main/java/com/student/servlet/admin/UserUploadServlet.java create mode 100644 src/main/java/com/student/servlet/teacher/TeacherAttendanceServlet.java create mode 100644 src/main/java/com/student/servlet/teacher/TeacherHomeServlet.java create mode 100644 src/main/java/com/student/servlet/teacher/TeacherLeaveServlet.java create mode 100644 src/main/java/com/student/servlet/teacher/TeacherScoreServlet.java create mode 100644 src/main/java/com/student/servlet/user/AvatarUploadServlet.java create mode 100644 src/main/java/com/student/servlet/user/EnrollServlet.java create mode 100644 src/main/java/com/student/servlet/user/LeaveApplyServlet.java create mode 100644 src/main/java/com/student/servlet/user/MessageInboxServlet.java create mode 100644 src/main/java/com/student/servlet/user/MyCourseServlet.java create mode 100644 src/main/java/com/student/servlet/user/MyScoreServlet.java create mode 100644 src/main/java/com/student/servlet/user/UserProfileServlet.java create mode 100644 src/main/java/com/student/util/CaptchaUtil.java create mode 100644 src/main/java/com/student/util/CookieUtil.java create mode 100644 src/main/java/com/student/util/DBUtil.java create mode 100644 src/main/java/com/student/util/IpUtil.java create mode 100644 src/main/java/com/student/util/JsonUtil.java create mode 100644 src/main/java/com/student/util/ParamUtil.java create mode 100644 src/main/java/com/student/util/PasswordUtil.java create mode 100644 src/main/java/com/student/util/StudentNoUtil.java create mode 100644 src/main/resources/db.properties create mode 100644 src/main/resources/db_init.sql create mode 100644 src/main/webapp/WEB-INF/jspf/admin-sidebar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/admin-topbar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/alert.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/empty-state.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/head.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/pagination.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/stat-num.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/teacher-sidebar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/teacher-topbar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/user-avatar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/user-sidebar.jspf create mode 100644 src/main/webapp/WEB-INF/jspf/user-topbar.jspf create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/admin/announcement.jsp create mode 100644 src/main/webapp/admin/attendance.jsp create mode 100644 src/main/webapp/admin/attendance_stats.jsp create mode 100644 src/main/webapp/admin/bigscreen.jsp create mode 100644 src/main/webapp/admin/change_audit.jsp create mode 100644 src/main/webapp/admin/course_edit.jsp create mode 100644 src/main/webapp/admin/course_list.jsp create mode 100644 src/main/webapp/admin/dashboard.jsp create mode 100644 src/main/webapp/admin/import_preview.jsp create mode 100644 src/main/webapp/admin/leave_approve.jsp create mode 100644 src/main/webapp/admin/logs.jsp create mode 100644 src/main/webapp/admin/message_send.jsp create mode 100644 src/main/webapp/admin/online.jsp create mode 100644 src/main/webapp/admin/org.jsp create mode 100644 src/main/webapp/admin/reg_audit.jsp create mode 100644 src/main/webapp/admin/score_manage.jsp create mode 100644 src/main/webapp/admin/score_stats.jsp create mode 100644 src/main/webapp/admin/user_add.jsp create mode 100644 src/main/webapp/admin/user_detail.jsp create mode 100644 src/main/webapp/admin/user_edit.jsp create mode 100644 src/main/webapp/admin/user_list.jsp create mode 100644 src/main/webapp/admin/user_upload.jsp create mode 100644 src/main/webapp/css/style.css create mode 100644 src/main/webapp/error.jsp create mode 100644 src/main/webapp/forgot_password.jsp create mode 100644 src/main/webapp/js/common.js create mode 100644 src/main/webapp/login.jsp create mode 100644 src/main/webapp/register.jsp create mode 100644 src/main/webapp/teacher/attendance.jsp create mode 100644 src/main/webapp/teacher/home.jsp create mode 100644 src/main/webapp/teacher/leave.jsp create mode 100644 src/main/webapp/teacher/score.jsp create mode 100644 src/main/webapp/user/enroll.jsp create mode 100644 src/main/webapp/user/leave_apply.jsp create mode 100644 src/main/webapp/user/messages.jsp create mode 100644 src/main/webapp/user/my_courses.jsp create mode 100644 src/main/webapp/user/my_scores.jsp create mode 100644 src/main/webapp/user/profile.jsp create mode 100644 src/test/java/com/student/service/AnnouncementServiceTest.java create mode 100644 src/test/java/com/student/service/AttendanceServiceTest.java create mode 100644 src/test/java/com/student/service/AuthServiceTest.java create mode 100644 src/test/java/com/student/service/ChangeRequestServiceTest.java create mode 100644 src/test/java/com/student/service/CourseServiceTest.java create mode 100644 src/test/java/com/student/service/EnrollmentServiceTest.java create mode 100644 src/test/java/com/student/service/ImportServiceTest.java create mode 100644 src/test/java/com/student/service/LeaveServiceTest.java create mode 100644 src/test/java/com/student/service/MessageServiceTest.java create mode 100644 src/test/java/com/student/service/OrgServiceTest.java create mode 100644 src/test/java/com/student/service/ScoreServiceTest.java create mode 100644 src/test/java/com/student/service/StatServiceTest.java create mode 100644 src/test/java/com/student/service/UserServiceTest.java create mode 100644 src/test/java/com/student/util/PasswordUtilTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0186d8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# ========================================================================= +# 1. Java 编译与运行环境产生的垃圾文件(必须忽略) +# ========================================================================= +*.class +*.log +*.ctxt +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# ========================================================================= +# 2. 构建工具及服务器临时目录 +# ========================================================================= +# 如果你使用的是 Maven 构建 +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.nextDevelopmentVersion +pom.xml.releaseProperties +pom.xml.used +release.properties +dependency-reduced-pom.xml + +# 如果是普通的 Web 项目,忽略 Tomcat 或 Jetty 的临时和部署目录 +bin/ +build/ +dist/ +out/ +.tomcat/ +WEB-INF/classes/ + +# ========================================================================= +# 3. IDE(集成开发环境)个性化配置文件(防止与同学冲突) +# ========================================================================= +# IntelliJ IDEA +.idea/ +*.iws +*.iml +*.ipr +out/ + +# Eclipse +.metadata/ +.classpath +.project +.settings/ +*.launch + +# VS Code (如果你用 VS Code 写前端/后端) +.vscode/ + +# ========================================================================= +# 4. 操作系统及前端打包临时文件 +# ========================================================================= +# 系统文件 +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store + +# 前端可能产生的 node 依赖(如果有使用 npm) +node_modules/ +/dist/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04f2efc --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +<<<<<<< HEAD +# 学生信息管理系统 v2.0 + +基于 **JSP + Servlet + JavaBean + JDBC + DAO + Service** 架构的全功能学生信息管理系统。 + +## 技术栈 + +| 层次 | 技术 | +|------|------| +| 表现层 | JSP、EL、JSTL、jQuery、Ajax、ECharts | +| 控制层 | Servlet(@WebServlet)、Filter、Listener | +| 业务层 | Service | +| 数据层 | DAO、JDBC、HikariCP 连接池 | +| 工具 | Gson、Apache POI、Commons FileUpload | +| 数据库 | MySQL 8.x | + +## 已实现扩展功能(37/37) + +### 模块 A — 安全与账户 +- [x] A-01 密码 SHA-256 加密存储 +- [x] A-02 登录失败 5 次锁定 30 分钟 +- [x] A-03 找回密码(演示模式验证码) +- [x] A-04 记住我 / 7 天自动登录 +- [x] A-05 在线用户管理与强制下线 +- [x] A-06 三级角色(学生/教师/管理员) + +### 模块 B — 学生档案 +- [x] B-01 院系 / 专业 / 班级 +- [x] B-02 学号自动生成 +- [x] B-03 学生状态管理 +- [x] B-04 头像上传 +- [x] B-05 学生详情页 +- [x] B-06 Excel 导出 +- [x] B-07 高级组合搜索 + +### 模块 C — 课程与成绩 +- [x] C-01 课程管理 CRUD +- [x] C-02 学生选课 +- [x] C-03 成绩录入 +- [x] C-04 成绩查询 + GPA +- [x] C-05 成绩统计报表 +- [x] C-06 个人课表 + +### 模块 D — 考勤与请假 +- [x] D-01 考勤记录 +- [x] D-02 请假申请 +- [x] D-03 请假审批 +- [x] D-04 考勤统计 + +### 模块 E — 通知与日志 +- [x] E-01 系统公告 +- [x] E-02 站内消息 +- [x] E-03 操作日志 +- [x] E-04 登录日志 + +### 模块 F — 数据统计 +- [x] F-01 管理员 Dashboard +- [x] F-02 ECharts 图表 +- [x] F-03 数据大屏 + +### 模块 G — 流程审批 +- [x] G-01 信息变更审批 +- [x] G-02 注册审核 +- [x] G-03 批量导入预览确认 + +### 模块 H — 工程化 +- [x] H-01 Service 业务层 +- [x] H-02 HikariCP 连接池 +- [x] H-03 统一 JSON API(/api/stats 等) +- [x] H-04 JSP 公共片段(jspf) + +## 测试账号 + +| 角色 | 用户名 | 密码 | 登录后跳转 | +|------|--------|------|-----------| +| 管理员 | admin | admin123 | /admin/dashboard | +| 教师 | teacher | teacher123 | /teacher/home | +| 学生 | zhangsan | user123 | /user/profile | + +## 快速部署 + +```bash +# 1. 重新初始化数据库(v2 表结构有变化,需重建) +mysql -u root -p < src/main/resources/db_init.sql + +# 2. 修改 db.properties + +# 3. 打包 +mvn clean package + +# 4. 部署 WAR 到 Tomcat,访问 +http://localhost:8080/student-management/ +``` + +## 主要功能入口 + +| 角色 | 功能 | 路径 | +|------|------|------| +| 管理员 | 仪表盘 | /admin/dashboard | +| 管理员 | 用户管理 | /admin/userList | +| 管理员 | 注册审核 | /admin/regAudit | +| 管理员 | 变更审批 | /admin/changeAudit | +| 管理员 | 组织管理 | /admin/org | +| 管理员 | 课程管理 | /admin/courseList | +| 管理员 | 数据大屏 | /admin/bigscreen | +| 教师 | 工作台 | /teacher/home | +| 学生 | 在线选课 | /user/enroll | +| 学生 | 我的成绩 | /user/myScores | +| 学生 | 请假申请 | /user/leaveApply | + +## 文档 + +- [部署与常见问题](docs/DEPLOYMENT.md) +- [扩展任务清单](docs/EXTENSION_TASKS.md) +======= +# student-management + +学生管理系统作业 +>>>>>>> 199973ecc1a80cb639a57f856f6f18735ccff863 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..8aba67c --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,571 @@ +# 部署与常见问题手册 + +本文档整理了项目部署过程中遇到的典型错误及解决方案,分别提供 **Windows** 和 **Mac M1(Apple Silicon)** 两套操作说明。 + +--- + +## 目录 + +- [环境要求](#环境要求) +- [通用部署流程](#通用部署流程) +- [Windows 部署指南](#windows-部署指南) +- [Mac M1 部署指南](#mac-m1-部署指南) +- [常见错误与解决方案](#常见错误与解决方案) +- [Maven 推荐配置](#maven-推荐配置) +- [更新部署流程](#更新部署流程) + +--- + +## 环境要求 + +| 组件 | 版本要求 | 说明 | +|------|----------|------| +| JDK | 1.8+(推荐 8 或 11) | Tomcat 9 支持 Java 8–21 | +| Maven | 3.6+ | 构建 WAR 包 | +| MySQL | 8.x | 数据存储 | +| Tomcat | 9.x | Web 容器 | + +**Mac M1 注意**:请安装 **ARM64(aarch64)** 版本的 JDK 和 Tomcat,或通过 Homebrew 安装(会自动选择正确架构)。 + +--- + +## 通用部署流程 + +无论 Windows 还是 Mac,基本步骤一致: + +``` +1. 初始化 MySQL 数据库 +2. 修改 db.properties 数据库连接配置 +3. mvn clean package 打包 +4. 复制 WAR 到 Tomcat/webapps +5. 启动 Tomcat +6. 浏览器访问 http://localhost:8080/student-management/ +``` + +### 1. 初始化数据库 + +```bash +mysql -u root -p < src/main/resources/db_init.sql +``` + +### 2. 修改数据库配置 + +编辑 `src/main/resources/db.properties`: + +```properties +jdbc.driver=com.mysql.cj.jdbc.Driver +jdbc.url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false +jdbc.username=root +jdbc.password=你的密码 +``` + +> 修改后需重新执行 `mvn clean package`。 + +### 3. 编译打包 + +```bash +mvn clean package +``` + +成功后 WAR 包路径: + +``` +target/student-management.war +``` + +### 4. 访问地址 + +``` +http://localhost:8080/student-management/ +``` + +### 5. 测试账号 + +| 角色 | 用户名 | 密码 | +|------|--------|------| +| 管理员 | admin | admin123 | +| 一般用户 | zhangsan | user123 | + +--- + +## Windows 部署指南 + +### 安装 JDK 并配置 JAVA_HOME + +1. 安装 JDK(如 `C:\Program Files\Java\jdk-21`) +2. `Win + R` → 输入 `sysdm.cpl` → **高级** → **环境变量** +3. 新建系统变量: + - 变量名:`JAVA_HOME` + - 变量值:`C:\Program Files\Java\jdk-21`(按实际路径填写) +4. 编辑 `Path`,新增:`%JAVA_HOME%\bin` +5. 新开 cmd 验证: + +```cmd +echo %JAVA_HOME% +java -version +``` + +### 安装 Tomcat + +1. 下载 Tomcat 9:https://tomcat.apache.org/download-90.cgi +2. 选择 **64-bit Windows zip**,解压到如 `D:\apache-tomcat-9.0.98` + +### 部署 WAR + +**方法一:资源管理器** + +1. 复制 `target\student-management.war` +2. 粘贴到 `D:\apache-tomcat-9.0.98\webapps\` + +**方法二:PowerShell** + +```powershell +Copy-Item "D:\codefiles\javaweb-homework\student-management\target\student-management.war" ` + "D:\apache-tomcat-9.0.98\webapps\" +``` + +### 启动 / 停止 Tomcat + +```cmd +:: 启动(需先设置 JAVA_HOME,见下方临时方案) +D:\apache-tomcat-9.0.98\bin\startup.bat + +:: 停止 +D:\apache-tomcat-9.0.98\bin\shutdown.bat +``` + +**临时设置 JAVA_HOME 后启动(未配环境变量时):** + +```cmd +set JAVA_HOME=C:\Program Files\Java\jdk-21 +set JRE_HOME=C:\Program Files\Java\jdk-21 +cd /d D:\apache-tomcat-9.0.98\bin +startup.bat +``` + +--- + +## Mac M1 部署指南 + +### 安装依赖(Homebrew) + +```bash +# 安装 Homebrew(如未安装) +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +# 安装 JDK(ARM64 原生版本) +brew install openjdk@17 + +# 配置 JAVA_HOME(Apple Silicon 路径) +echo 'export JAVA_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"' >> ~/.zshrc +echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc + +# 验证 +java -version +echo $JAVA_HOME + +# 安装 Maven +brew install maven + +# 安装 MySQL +brew install mysql +brew services start mysql + +# 安装 Tomcat 9 +brew install tomcat@9 +``` + +> **Intel Mac** 用户:Homebrew 前缀为 `/usr/local` 而非 `/opt/homebrew`,JDK 路径类似 `/usr/local/opt/openjdk@17/...`。 + +### 初始化数据库(Mac) + +```bash +# 首次安装 MySQL 后设置 root 密码(按提示操作) +mysql_secure_installation + +# 导入数据 +cd /path/to/student-management +mysql -u root -p < src/main/resources/db_init.sql +``` + +### 编译打包(Mac) + +```bash +cd /path/to/student-management +mvn clean package +``` + +### 部署 WAR 到 Tomcat(Mac) + +**Homebrew 安装的 Tomcat 9:** + +```bash +# Tomcat 目录(M1 默认) +TOMCAT_HOME="/opt/homebrew/opt/tomcat@9/libexec" + +# 复制 WAR +cp target/student-management.war "$TOMCAT_HOME/webapps/" + +# 启动 Tomcat +brew services start tomcat@9 + +# 停止 Tomcat +brew services stop tomcat@9 + +# 查看日志 +tail -f "$TOMCAT_HOME/logs/catalina.out" +``` + +**手动解压的 Tomcat:** + +```bash +TOMCAT_HOME="$HOME/apache-tomcat-9.0.98" + +cp target/student-management.war "$TOMCAT_HOME/webapps/" +"$TOMCAT_HOME/bin/startup.sh" +``` + +### Mac 访问地址 + +``` +http://localhost:8080/student-management/ +``` + +Homebrew 版 Tomcat 9 默认端口为 **8080**。若端口冲突,编辑: + +```bash +# M1 +/opt/homebrew/opt/tomcat@9/libexec/conf/server.xml + +# 修改 为其他端口 +``` + +### Mac 常用命令对照 + +| 操作 | Windows | Mac M1 | +|------|---------|--------| +| 环境变量文件 | 系统设置 / cmd `set` | `~/.zshrc` | +| 启动 Tomcat | `startup.bat` | `startup.sh` 或 `brew services start tomcat@9` | +| 停止 Tomcat | `shutdown.bat` | `shutdown.sh` 或 `brew services stop tomcat@9` | +| 复制 WAR | `Copy-Item` / 资源管理器 | `cp` | +| Maven 配置 | `C:\Users\用户名\.m2\settings.xml` | `~/.m2/settings.xml` | + +--- + +## 常见错误与解决方案 + +### 错误 1:Maven 代理连接失败 + +**报错信息:** + +``` +Connect to 127.0.0.1:7890 failed: Connection refused: no further information +Could not transfer artifact ... from/to central (https://repo.maven.apache.org/maven2) +``` + +**原因:** + +`~/.m2/settings.xml`(Windows:`C:\Users\用户名\.m2\settings.xml`)中配置了 HTTP 代理 `127.0.0.1:7890`(Clash、V2Ray 等),但代理软件未启动。 + +**解决方案(二选一):** + +| 方案 | 操作 | +|------|------| +| A. 不使用代理 | 将 settings.xml 中 proxy 的 `true` 改为 `false` | +| B. 使用代理 | 先启动 Clash 等代理软件,保持 `true` | + +--- + +### 错误 2:依赖下载失败被缓存,无法重试 + +**报错信息:** + +``` +failed to transfer from https://repo.maven.apache.org/maven2 during a previous attempt. +This failure was cached in the local repository and resolution is not reattempted +until the update interval of central has elapsed or updates are forced. + +未解析的依赖项: 'javax.servlet:javax.servlet-api:jar:4.0.1' +未解析的依赖项: 'mysql:mysql-connector-java:jar:8.0.33' +... +``` + +**原因:** + +错误 1 导致的下载失败被 Maven 写入本地仓库缓存(`.lastUpdated` 文件),后续不会自动重试。 + +**解决方案:** + +**步骤 1 — 清理失败缓存** + +Windows PowerShell: + +```powershell +$paths = @( + "$env:USERPROFILE\.m2\repository\javax\servlet\javax.servlet-api\4.0.1", + "$env:USERPROFILE\.m2\repository\javax\servlet\jsp\javax.servlet.jsp-api\2.3.3", + "$env:USERPROFILE\.m2\repository\javax\servlet\jstl\1.2", + "$env:USERPROFILE\.m2\repository\mysql\mysql-connector-java\8.0.33", + "$env:USERPROFILE\.m2\repository\commons-fileupload\commons-fileupload\1.5", + "$env:USERPROFILE\.m2\repository\commons-io\commons-io\2.11.0" +) +foreach ($p in $paths) { if (Test-Path $p) { Remove-Item $p -Recurse -Force } } +Get-ChildItem "$env:USERPROFILE\.m2\repository" -Recurse -Filter "*.lastUpdated" | Remove-Item -Force +``` + +Mac / Linux: + +```bash +rm -rf ~/.m2/repository/javax/servlet/javax.servlet-api/4.0.1 +rm -rf ~/.m2/repository/javax/servlet/jsp/javax.servlet.jsp-api/2.3.3 +rm -rf ~/.m2/repository/javax/servlet/jstl/1.2 +rm -rf ~/.m2/repository/mysql/mysql-connector-java/8.0.33 +rm -rf ~/.m2/repository/commons-fileupload/commons-fileupload/1.5 +rm -rf ~/.m2/repository/commons-io/commons-io/2.11.0 +find ~/.m2/repository -name "*.lastUpdated" -delete +``` + +**步骤 2 — 强制重新下载** + +```bash +mvn -U clean compile +``` + +`-U` 表示强制更新,忽略失败缓存。 + +**步骤 3 — IDEA 刷新** + +右侧 Maven 面板 → 点击 **Reload All Maven Projects**(刷新按钮)。 + +--- + +### 错误 3:Tomcat 启动报 JAVA_HOME 未定义 + +**报错信息:** + +``` +Neither the JAVA_HOME nor the JRE_HOME environment variable is defined +At least one of these environment variable is needed to run this program +``` + +**原因:** + +系统未配置 `JAVA_HOME`,Tomcat 找不到 Java。 + +**解决方案:** + +**Windows — 永久配置:** + +1. `Win + R` → `sysdm.cpl` → **高级** → **环境变量** +2. 新建系统变量 `JAVA_HOME` = JDK 安装目录(如 `C:\Program Files\Java\jdk-21`) +3. `Path` 中添加 `%JAVA_HOME%\bin` +4. **重新打开** cmd,执行 `startup.bat` + +**Windows — 临时配置:** + +```cmd +set JAVA_HOME=C:\Program Files\Java\jdk-21 +set JRE_HOME=C:\Program Files\Java\jdk-21 +cd /d D:\apache-tomcat-9.0.98\bin +startup.bat +``` + +**Mac M1 — 永久配置:** + +```bash +# 查看 JDK 路径 +/usr/libexec/java_home -V + +# 写入 ~/.zshrc(以 OpenJDK 17 为例) +echo 'export JAVA_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"' >> ~/.zshrc +echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc + +# 验证 +echo $JAVA_HOME +java -version +``` + +**Mac — 临时配置:** + +```bash +export JAVA_HOME=$(/usr/libexec/java_home) +export JRE_HOME=$JAVA_HOME +~/apache-tomcat-9.0.98/bin/startup.sh +``` + +--- + +### 错误 4:页面 404 Not Found + +**原因:** + +- WAR 未放入 `webapps`,或 Tomcat 未完成解压 +- 访问 URL 缺少项目上下文路径 + +**解决方案:** + +1. 确认 `webapps` 下存在 `student-management.war` 或 `student-management/` 文件夹 +2. 访问:`http://localhost:8080/student-management/`(注意含项目名) +3. 查看 Tomcat 日志确认部署是否成功 + +--- + +### 错误 5:页面 500 / 数据库连接失败 + +**原因:** + +- MySQL 未启动 +- `db.properties` 账号密码错误 +- 未执行 `db_init.sql` 初始化数据库 + +**解决方案:** + +```bash +# 检查 MySQL 是否运行 +# Windows +sc query mysql + +# Mac +brew services list | grep mysql + +# 测试连接 +mysql -u root -p -e "USE student_db; SHOW TABLES;" +``` + +确认 `db.properties` 配置正确后,**重新打包并部署** WAR。 + +--- + +### 错误 6:8080 端口被占用 + +**报错信息(Tomcat 日志):** + +``` +Address already in use: bind +``` + +**解决方案:** + +**Windows:** + +```cmd +netstat -ano | findstr :8080 +taskkill /PID <进程号> /F +``` + +**Mac:** + +```bash +lsof -i :8080 +kill -9 +``` + +或修改 Tomcat `conf/server.xml` 中 `` 为其他端口(如 8081)。 + +--- + +## Maven 推荐配置 + +将以下内容写入 Maven 用户配置文件: + +- **Windows:** `C:\Users\你的用户名\.m2\settings.xml` +- **Mac:** `~/.m2/settings.xml` + +```xml + + + + aliyunmaven + central + Aliyun Maven + https://maven.aliyun.com/repository/public + + + + + + + http-proxy + false + http + 127.0.0.1 + 7890 + + + https-proxy + false + https + 127.0.0.1 + 7890 + + + +``` + +**IDEA 中确认 Maven 使用该配置:** + +`File → Settings → Build Tools → Maven → User settings file` 指向上述 `settings.xml`。 + +--- + +## 更新部署流程 + +代码或配置修改后,按以下步骤重新部署: + +### Windows + +```powershell +# 1. 重新打包 +cd D:\codefiles\javaweb-homework\student-management +mvn clean package + +# 2. 停止 Tomcat(shutdown.bat) + +# 3. 删除旧部署 +Remove-Item "D:\apache-tomcat-9.0.98\webapps\student-management" -Recurse -Force +Remove-Item "D:\apache-tomcat-9.0.98\webapps\student-management.war" -Force + +# 4. 复制新 WAR +Copy-Item "target\student-management.war" "D:\apache-tomcat-9.0.98\webapps\" + +# 5. 启动 Tomcat(startup.bat) +``` + +### Mac M1 + +```bash +# 1. 重新打包 +cd /path/to/student-management +mvn clean package + +# 2. 停止 Tomcat +brew services stop tomcat@9 + +# 3. 删除旧部署 +TOMCAT_HOME="/opt/homebrew/opt/tomcat@9/libexec" +rm -rf "$TOMCAT_HOME/webapps/student-management" +rm -f "$TOMCAT_HOME/webapps/student-management.war" + +# 4. 复制新 WAR +cp target/student-management.war "$TOMCAT_HOME/webapps/" + +# 5. 启动 Tomcat +brew services start tomcat@9 +``` + +--- + +## 快速排查清单 + +遇到问题时按顺序检查: + +- [ ] MySQL 已启动,`student_db` 数据库存在 +- [ ] `db.properties` 用户名密码正确 +- [ ] `JAVA_HOME` 已配置,`java -version` 正常 +- [ ] Maven `settings.xml` 代理已关闭或代理软件已启动 +- [ ] 依赖无红色报错(必要时 `mvn -U clean compile`) +- [ ] WAR 包已复制到 Tomcat `webapps/` +- [ ] Tomcat 已成功启动(日志无 ERROR) +- [ ] 浏览器访问 URL 含 `/student-management/` diff --git a/docs/EXTENSION_TASKS.md b/docs/EXTENSION_TASKS.md new file mode 100644 index 0000000..f2ffdf8 --- /dev/null +++ b/docs/EXTENSION_TASKS.md @@ -0,0 +1,231 @@ +# 学生信息管理系统 — 业务扩展任务表 + +> 基于现有 **JSP + Servlet + JavaBean + JDBC + DAO** 架构,按模块划分扩展任务。 +> 难度:⭐ 入门 · ⭐⭐ 中等 · ⭐⭐⭐ 较难 · ⭐⭐⭐⭐ 高级 +> 优先级:P0 必做推荐 · P1 建议做 · P2 进阶 · P3 选做 + +--- + +## 一、总览 + +| 阶段 | 目标 | 建议任务数 | 预估总工时 | +|------|------|-----------|-----------| +| 第一阶段 | 夯实基础安全与数据规范 | 5 项 | 15~20 h | +| 第二阶段 | 丰富核心业务实体 | 6 项 | 25~35 h | +| 第三阶段 | 流程、统计与高级功能 | 6 项 | 30~40 h | +| 第四阶段 | 架构升级与工程化 | 4 项 | 20~30 h | + +--- + +## 二、扩展任务明细 + +### 模块 A:安全与账户体系 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| A-01 | 密码加密存储 | 数据库不再明文存密码,提升安全性 | `User` 表、`UserDao`、登录/注册 Servlet | MD5 或 BCrypt;注册加密、登录比对 | ⭐⭐ | P0 | 3 h | +| A-02 | 登录失败锁定 | 连续输错密码 5 次锁定账户 30 分钟 | 新增 `login_log` 表、登录 Servlet | Session/Redis 计数;Ajax 提示剩余次数 | ⭐⭐ | P1 | 4 h | +| A-03 | 找回密码 | 用户通过邮箱验证码重置密码 | 邮件发送工具类、重置密码 Servlet/JSP | JavaMail 或模拟验证码;验证码有效期 | ⭐⭐⭐ | P1 | 6 h | +| A-04 | 记住我 / 自动登录 | 勾选后 7 天内免登录 | Cookie 工具类、LoginServlet、Filter | Token 存 Cookie + 数据库;HttpOnly | ⭐⭐⭐ | P2 | 5 h | +| A-05 | 在线用户管理 | 管理员查看当前在线用户并强制下线 | Session 监听器、在线用户列表页 | `HttpSessionListener`;Session 映射表 | ⭐⭐⭐ | P2 | 5 h | +| A-06 | 三级角色权限 | 新增「教师」角色,三类用户权限隔离 | `User.role` 扩展、RBAC 表、AuthFilter | 角色-权限-菜单;Filter 细粒度拦截 | ⭐⭐⭐⭐ | P1 | 8 h | + +--- + +### 模块 B:学生档案扩展 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| B-01 | 院系 / 专业 / 班级 | 学生归属组织结构,支持按班级查询 | 新增 `department`、`major`、`clazz` 表 | 多表关联;下拉级联 Ajax | ⭐⭐ | P0 | 6 h | +| B-02 | 学号自动生成 | 注册或录入时按规则生成唯一学号 | 学号生成工具类、新增用户逻辑 | 规则如 `2026CS001`;唯一约束 | ⭐⭐ | P1 | 3 h | +| B-03 | 学生状态管理 | 在读 / 休学 / 毕业 / 退学等状态流转 | `users` 增加 `status` 字段、状态变更 Servlet | 状态枚举;变更记录;列表筛选 | ⭐⭐ | P1 | 4 h | +| B-04 | 头像上传 | 用户上传个人头像并展示 | 文件上传 Servlet、存储路径配置 | `commons-fileupload`;大小/格式校验 | ⭐⭐ | P1 | 4 h | +| B-05 | 学生详情页 | 单独页面展示完整档案(含班级、状态等) | 详情 Servlet、详情 JSP | 多表 JOIN 查询;EL/JSTL 展示 | ⭐⭐ | P1 | 3 h | +| B-06 | 批量导出 Excel | 管理员按条件导出学生信息为 `.xlsx` | 导出 Servlet、Apache POI 依赖 | POI 写 Excel;条件筛选与 CSV 导出并存 | ⭐⭐⭐ | P1 | 5 h | +| B-07 | 高级组合搜索 | 多条件(班级+性别+年龄区间+状态)联合查询 | 列表 Servlet、DAO 动态 SQL | `StringBuilder` 拼接 SQL;防注入用 PreparedStatement | ⭐⭐⭐ | P2 | 6 h | + +--- + +### 模块 C:课程与成绩 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| C-01 | 课程管理 CRUD | 管理员维护课程(编号、名称、学分、授课教师) | `course` 表、CourseDao、Servlet、JSP | 标准 DAO CRUD;分页列表 | ⭐⭐ | P1 | 6 h | +| C-02 | 学生选课 | 一般用户在线选课,限制人数与重复选课 | `enrollment` 表、选课 Servlet | 事务控制;名额校验;已选课程列表 | ⭐⭐⭐ | P1 | 8 h | +| C-03 | 成绩录入 | 教师/管理员录入课程成绩 | `score` 表、成绩 Servlet | 权限校验;批量录入表单 | ⭐⭐⭐ | P1 | 6 h | +| C-04 | 成绩查询 | 学生查看本人成绩,管理员查看全班成绩 | 成绩列表 JSP、查询 Servlet | 按角色过滤数据;GPA 简单计算 | ⭐⭐ | P1 | 4 h | +| C-05 | 成绩统计报表 | 按课程统计平均分、最高/最低分、及格率 | 统计 DAO、报表 JSP | SQL 聚合函数 `AVG/MAX/MIN/COUNT` | ⭐⭐⭐ | P2 | 5 h | +| C-06 | 课表展示 | 学生个人课表(按周视图或列表) | 课表查询 Servlet、课表 JSP | 多表关联;前端表格布局 | ⭐⭐ | P2 | 4 h | + +--- + +### 模块 D:考勤与请假 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| D-01 | 考勤记录 | 教师记录学生出勤(出勤/迟到/缺勤) | `attendance` 表、考勤 Servlet | 按日期+课程+学生唯一;批量提交 | ⭐⭐⭐ | P2 | 6 h | +| D-02 | 请假申请 | 学生提交请假,填写起止时间与原因 | `leave_request` 表、申请 JSP/Servlet | 表单校验;状态:待审/通过/驳回 | ⭐⭐⭐ | P2 | 6 h | +| D-03 | 请假审批 | 教师/管理员审批请假申请 | 审批 Servlet、待办列表 JSP | 工作流状态机;审批意见字段 | ⭐⭐⭐ | P2 | 5 h | +| D-04 | 考勤统计 | 按学生/班级统计出勤率 | 统计 DAO、图表 JSP | 聚合查询;可选 ECharts 展示 | ⭐⭐⭐ | P3 | 6 h | + +--- + +### 模块 E:通知与日志 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| E-01 | 系统公告 | 管理员发布公告,登录后所有用户可见 | `announcement` 表、公告 CRUD | 置顶、有效期;首页滚动展示 | ⭐⭐ | P1 | 4 h | +| E-02 | 站内消息 | 管理员向指定用户/send 全体发消息 | `message` 表、收件箱/发件箱 JSP | 已读/未读;Ajax 未读数角标 | ⭐⭐⭐ | P2 | 6 h | +| E-03 | 操作日志 | 记录增删改等关键操作(谁、何时、做了什么) | `operation_log` 表、日志 Filter/AOP 式拦截 | Filter 或 Servlet 基类;日志分页查询 | ⭐⭐⭐ | P1 | 6 h | +| E-04 | 登录日志 | 记录每次登录 IP、时间、结果 | `login_log` 表、LoginServlet 埋点 | 获取客户端 IP;失败/成功分类 | ⭐⭐ | P1 | 3 h | + +--- + +### 模块 F:数据统计与可视化 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| F-01 | 管理员 Dashboard | 首页展示用户数、今日注册、班级分布等 | 仪表盘 JSP、统计 Servlet | 多指标卡片;SQL 聚合 | ⭐⭐ | P1 | 5 h | +| F-02 | 图表可视化 | 性别比例、年龄分布、院系人数饼图/柱状图 | 引入 ECharts、统计 API Servlet | Servlet 返回 JSON;Ajax 渲染图表 | ⭐⭐⭐ | P2 | 6 h | +| F-03 | 数据大屏(选做) | 全屏展示核心指标,适合答辩演示 | 单独大屏 JSP + ECharts | CSS 全屏布局;定时 Ajax 刷新 | ⭐⭐⭐ | P3 | 8 h | + +--- + +### 模块 G:流程与审批 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| G-01 | 信息变更审批 | 学生修改关键信息(姓名、学号)需管理员审核 | `change_request` 表、审批 Servlet | 暂存待审;通过后更新主表 | ⭐⭐⭐⭐ | P2 | 8 h | +| G-02 | 新生注册审核 | 注册后状态为「待审核」,管理员通过后激活 | 注册流程改造、审核列表 | 状态字段;邮件/站内信通知 | ⭐⭐⭐ | P2 | 5 h | +| G-03 | 批量导入审核 | CSV 导入先进临时表,确认后再写入正式表 | 临时表 + 确认 Servlet | 两阶段导入;预览与回滚 | ⭐⭐⭐⭐ | P3 | 8 h | + +--- + +### 模块 H:工程化与架构升级 + +| 编号 | 功能任务 | 业务说明 | 主要改动 | 技术要点 | 难度 | 优先级 | 工时 | +|------|----------|----------|----------|----------|------|--------|------| +| H-01 | 引入 Service 层 | DAO 之上增加业务层,Servlet 只调 Service | 新增 `service` 包、重构 Servlet | 分层解耦;事务放 Service | ⭐⭐⭐ | P1 | 8 h | +| H-02 | DBUtils / Druid 连接池 | 替换原生 JDBC 连接管理 | 引入依赖、改造 `DBUtil` | 连接池配置;SQL 性能提升 | ⭐⭐⭐ | P1 | 4 h | +| H-03 | 统一 JSON 接口 | 列表/删除等改为 REST 风格 Ajax API | 统一响应 JavaBean、JsonServlet 基类 | ` Gson/Jackson`;前后端分离雏形 | ⭐⭐⭐ | P2 | 6 h | +| H-04 | JSP 标签 / 公共片段 | 抽取 header、sidebar、分页为 `jsp:include` | `WEB-INF/jspf/` 公共片段 | 减少重复代码;统一布局 | ⭐⭐ | P0 | 4 h | + +--- + +## 三、推荐实施路线 + +### 路线 1:课程设计 / 作业答辩(约 2~3 周) + +| 顺序 | 任务编号 | 理由 | +|------|----------|------| +| 1 | A-01 密码加密 | 必讲安全,改动小见效快 | +| 2 | H-04 公共 JSP 片段 | 为后续页面扩展打基础 | +| 3 | B-01 院系班级 | 业务实体明显变复杂 | +| 4 | E-03 操作日志 | 体现系统完整性 | +| 5 | F-01 Dashboard | 答辩展示效果好 | +| 6 | C-01 + C-04 课程与成绩 | 核心业务闭环 | + +### 路线 2:功能丰富型(约 4~6 周) + +在路线 1 基础上增加: + +| 顺序 | 任务编号 | +|------|----------| +| 7 | A-06 三级角色 | +| 8 | C-02 学生选课 | +| 9 | D-02 + D-03 请假与审批 | +| 10 | E-01 系统公告 | +| 11 | F-02 图表可视化 | +| 12 | B-06 Excel 导出 | + +### 路线 3:架构进阶型(约 6~8 周) + +在路线 2 基础上增加: + +| 顺序 | 任务编号 | +|------|----------| +| 13 | H-01 Service 层 | +| 14 | H-02 连接池 | +| 15 | G-01 信息变更审批 | +| 16 | H-03 统一 JSON 接口 | + +--- + +## 四、任务依赖关系 + +``` +A-01 密码加密 + └── A-02 登录锁定 + └── A-03 找回密码 + +B-01 院系/班级 + └── B-02 学号生成 + └── B-07 高级搜索 + └── C-02 学生选课 + +C-01 课程管理 + └── C-02 选课 + └── C-03 成绩录入 + └── C-04 成绩查询 + └── D-01 考勤记录 + +E-04 登录日志 + └── A-02 登录锁定 + +H-04 JSP 公共片段 + └── F-01 Dashboard + └── 所有新增 JSP 页面 + +H-01 Service 层 + └── H-03 JSON 接口 + └── G-01 审批流程 +``` + +--- + +## 五、答辩可强调的「业务复杂度」亮点 + +| 亮点 | 对应任务 | 答辩话术要点 | +|------|----------|-------------| +| 安全体系 | A-01、A-02、E-04 | 密码加密、防暴力破解、审计追踪 | +| 组织模型 | B-01、B-03 | 院系-专业-班级三级结构,状态流转 | +| 教学业务 | C-01~C-05 | 选课-成绩完整链路,含统计报表 | +| 流程控制 | D-02、G-01、G-02 | 请假/变更/注册多级审批 | +| 数据可视化 | F-01、F-02 | Dashboard + ECharts 图表 | +| 架构演进 | H-01、H-02 | 三层架构 + 连接池,体现工程能力 | + +--- + +## 六、数据库扩展预览(核心新增表) + +| 表名 | 说明 | 关联任务 | +|------|------|----------| +| `department` | 院系 | B-01 | +| `major` | 专业 | B-01 | +| `clazz` | 班级 | B-01 | +| `course` | 课程 | C-01 | +| `enrollment` | 选课记录 | C-02 | +| `score` | 成绩 | C-03 | +| `attendance` | 考勤 | D-01 | +| `leave_request` | 请假 | D-02 | +| `announcement` | 公告 | E-01 | +| `message` | 站内消息 | E-02 | +| `operation_log` | 操作日志 | E-03 | +| `login_log` | 登录日志 | E-04 | +| `change_request` | 变更审批 | G-01 | + +--- + +## 七、选型建议 + +| 如果目标是… | 优先选这些任务 | +|------------|---------------| +| 快速加分 | A-01、F-01、E-01、B-04 | +| 业务深度 | B-01、C-01~C-04、D-02~D-03 | +| 技术深度 | H-01、H-02、H-03、A-06 | +| 演示效果好 | F-01、F-02、C-06、E-01 | +| 工作量可控 | 选 **路线 1** 的 6 项即可 | + +--- + +*可根据课程要求和时间,从表中勾选 5~10 项组成你的扩展计划。* diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0b1102a --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + com.student + student-management + 2.0-SNAPSHOT + war + student-management + + UTF-8 + 1.8 + 1.8 + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + javax.servlet.jsp + javax.servlet.jsp-api + 2.3.3 + provided + + + javax.servlet + jstl + 1.2 + + + com.mysql + mysql-connector-j + 8.0.33 + + + com.zaxxer + HikariCP + 4.0.3 + + + commons-fileupload + commons-fileupload + 1.5 + + + commons-io + commons-io + 2.11.0 + + + com.google.code.gson + gson + 2.10.1 + + + org.apache.poi + poi-ooxml + 5.2.3 + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + + + + student-management + + + org.apache.maven.plugins + maven-war-plugin + 3.3.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/sample_users.csv b/sample_users.csv new file mode 100644 index 0000000..3210f85 --- /dev/null +++ b/sample_users.csv @@ -0,0 +1,4 @@ +用户名,密码,真实姓名,性别,年龄,邮箱,电话,地址 +lisi,user123,李四,男,21,lisi@example.com,13900000002,上海市浦东新区 +wangwu,user123,王五,女,22,wangwu@example.com,13900000003,广州市天河区 +zhaoliu,user123,赵六,男,19,zhaoliu@example.com,13900000004,深圳市南山区 diff --git a/src/main/java/com/student/bean/Announcement.java b/src/main/java/com/student/bean/Announcement.java new file mode 100644 index 0000000..395b15c --- /dev/null +++ b/src/main/java/com/student/bean/Announcement.java @@ -0,0 +1,82 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class Announcement implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private String title; + private String content; + private Integer publisherId; + private Integer isTop; + private Date expireDate; + private Date createTime; + private String publisherName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getPublisherId() { + return publisherId; + } + + public void setPublisherId(Integer publisherId) { + this.publisherId = publisherId; + } + + public Integer getIsTop() { + return isTop; + } + + public void setIsTop(Integer isTop) { + this.isTop = isTop; + } + + public Date getExpireDate() { + return expireDate; + } + + public void setExpireDate(Date expireDate) { + this.expireDate = expireDate; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getPublisherName() { + return publisherName; + } + + public void setPublisherName(String publisherName) { + this.publisherName = publisherName; + } +} diff --git a/src/main/java/com/student/bean/ApiResult.java b/src/main/java/com/student/bean/ApiResult.java new file mode 100644 index 0000000..c3ecff3 --- /dev/null +++ b/src/main/java/com/student/bean/ApiResult.java @@ -0,0 +1,64 @@ +package com.student.bean; + +/** + * 统一 API 响应结构 + */ +public class ApiResult { + + private int code; + private String message; + private T data; + + public ApiResult() { + } + + public ApiResult(int code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static ApiResult ok() { + return ok(null); + } + + public static ApiResult ok(T data) { + return new ApiResult<>(200, "success", data); + } + + public static ApiResult ok(String message, T data) { + return new ApiResult<>(200, message, data); + } + + public static ApiResult fail(String message) { + return fail(500, message); + } + + public static ApiResult fail(int code, String message) { + return new ApiResult<>(code, message, null); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/src/main/java/com/student/bean/Attendance.java b/src/main/java/com/student/bean/Attendance.java new file mode 100644 index 0000000..514724b --- /dev/null +++ b/src/main/java/com/student/bean/Attendance.java @@ -0,0 +1,86 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class Attendance implements Serializable { + + public static final int STATUS_PRESENT = 1; + public static final int STATUS_LATE = 2; + public static final int STATUS_ABSENT = 3; + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer studentId; + private Integer courseId; + private Date attDate; + private Integer status; + private String remark; + private String studentName; + private String courseName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getStudentId() { + return studentId; + } + + public void setStudentId(Integer studentId) { + this.studentId = studentId; + } + + public Integer getCourseId() { + return courseId; + } + + public void setCourseId(Integer courseId) { + this.courseId = courseId; + } + + public Date getAttDate() { + return attDate; + } + + public void setAttDate(Date attDate) { + this.attDate = attDate; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } +} diff --git a/src/main/java/com/student/bean/AttendanceStats.java b/src/main/java/com/student/bean/AttendanceStats.java new file mode 100644 index 0000000..1c34044 --- /dev/null +++ b/src/main/java/com/student/bean/AttendanceStats.java @@ -0,0 +1,45 @@ +package com.student.bean; + +import java.io.Serializable; + +public class AttendanceStats implements Serializable { + + private static final long serialVersionUID = 1L; + + private int present; + private int late; + private int absent; + private int total; + + public int getPresent() { + return present; + } + + public void setPresent(int present) { + this.present = present; + } + + public int getLate() { + return late; + } + + public void setLate(int late) { + this.late = late; + } + + public int getAbsent() { + return absent; + } + + public void setAbsent(int absent) { + this.absent = absent; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } +} diff --git a/src/main/java/com/student/bean/ChangeRequest.java b/src/main/java/com/student/bean/ChangeRequest.java new file mode 100644 index 0000000..56cf06a --- /dev/null +++ b/src/main/java/com/student/bean/ChangeRequest.java @@ -0,0 +1,122 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class ChangeRequest implements Serializable { + + public static final int STATUS_PENDING = 0; + public static final int STATUS_APPROVED = 1; + public static final int STATUS_REJECTED = 2; + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer userId; + private String fieldName; + private String oldValue; + private String newValue; + private Integer status; + private Integer approverId; + private String approveNote; + private Date createTime; + private String username; + private String realName; + private String approverName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getOldValue() { + return oldValue; + } + + public void setOldValue(String oldValue) { + this.oldValue = oldValue; + } + + public String getNewValue() { + return newValue; + } + + public void setNewValue(String newValue) { + this.newValue = newValue; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getApproverId() { + return approverId; + } + + public void setApproverId(Integer approverId) { + this.approverId = approverId; + } + + public String getApproveNote() { + return approveNote; + } + + public void setApproveNote(String approveNote) { + this.approveNote = approveNote; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getApproverName() { + return approverName; + } + + public void setApproverName(String approverName) { + this.approverName = approverName; + } +} diff --git a/src/main/java/com/student/bean/Clazz.java b/src/main/java/com/student/bean/Clazz.java new file mode 100644 index 0000000..7560818 --- /dev/null +++ b/src/main/java/com/student/bean/Clazz.java @@ -0,0 +1,63 @@ +package com.student.bean; + +import java.io.Serializable; + +public class Clazz implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer majorId; + private String name; + private String grade; + private String majorName; + private String departmentName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getMajorId() { + return majorId; + } + + public void setMajorId(Integer majorId) { + this.majorId = majorId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGrade() { + return grade; + } + + public void setGrade(String grade) { + this.grade = grade; + } + + public String getMajorName() { + return majorName; + } + + public void setMajorName(String majorName) { + this.majorName = majorName; + } + + public String getDepartmentName() { + return departmentName; + } + + public void setDepartmentName(String departmentName) { + this.departmentName = departmentName; + } +} diff --git a/src/main/java/com/student/bean/Course.java b/src/main/java/com/student/bean/Course.java new file mode 100644 index 0000000..231317c --- /dev/null +++ b/src/main/java/com/student/bean/Course.java @@ -0,0 +1,101 @@ +package com.student.bean; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +public class Course implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private String courseNo; + private String name; + private BigDecimal credit; + private Integer teacherId; + private Integer maxStudents; + private String schedule; + private Date createTime; + private String teacherName; + private Integer enrolledCount; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCourseNo() { + return courseNo; + } + + public void setCourseNo(String courseNo) { + this.courseNo = courseNo; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public BigDecimal getCredit() { + return credit; + } + + public void setCredit(BigDecimal credit) { + this.credit = credit; + } + + public Integer getTeacherId() { + return teacherId; + } + + public void setTeacherId(Integer teacherId) { + this.teacherId = teacherId; + } + + public Integer getMaxStudents() { + return maxStudents; + } + + public void setMaxStudents(Integer maxStudents) { + this.maxStudents = maxStudents; + } + + public String getSchedule() { + return schedule; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getTeacherName() { + return teacherName; + } + + public void setTeacherName(String teacherName) { + this.teacherName = teacherName; + } + + public Integer getEnrolledCount() { + return enrolledCount; + } + + public void setEnrolledCount(Integer enrolledCount) { + this.enrolledCount = enrolledCount; + } +} diff --git a/src/main/java/com/student/bean/CourseStats.java b/src/main/java/com/student/bean/CourseStats.java new file mode 100644 index 0000000..b56e32b --- /dev/null +++ b/src/main/java/com/student/bean/CourseStats.java @@ -0,0 +1,64 @@ +package com.student.bean; + +import java.io.Serializable; +import java.math.BigDecimal; + +public class CourseStats implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer courseId; + private BigDecimal avg; + private BigDecimal max; + private BigDecimal min; + private BigDecimal passRate; + private int totalCount; + + public Integer getCourseId() { + return courseId; + } + + public void setCourseId(Integer courseId) { + this.courseId = courseId; + } + + public BigDecimal getAvg() { + return avg; + } + + public void setAvg(BigDecimal avg) { + this.avg = avg; + } + + public BigDecimal getMax() { + return max; + } + + public void setMax(BigDecimal max) { + this.max = max; + } + + public BigDecimal getMin() { + return min; + } + + public void setMin(BigDecimal min) { + this.min = min; + } + + public BigDecimal getPassRate() { + return passRate; + } + + public void setPassRate(BigDecimal passRate) { + this.passRate = passRate; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } +} diff --git a/src/main/java/com/student/bean/DashboardStats.java b/src/main/java/com/student/bean/DashboardStats.java new file mode 100644 index 0000000..ea5aaf4 --- /dev/null +++ b/src/main/java/com/student/bean/DashboardStats.java @@ -0,0 +1,83 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +public class DashboardStats implements Serializable { + + private static final long serialVersionUID = 1L; + + private int totalUsers; + private int todayRegister; + private int totalCourses; + private int pendingLeave; + private int pendingChange; + private int pendingRegister; + private Map genderStats = new HashMap<>(); + private Map deptStats = new HashMap<>(); + + public int getTotalUsers() { + return totalUsers; + } + + public void setTotalUsers(int totalUsers) { + this.totalUsers = totalUsers; + } + + public int getTodayRegister() { + return todayRegister; + } + + public void setTodayRegister(int todayRegister) { + this.todayRegister = todayRegister; + } + + public int getTotalCourses() { + return totalCourses; + } + + public void setTotalCourses(int totalCourses) { + this.totalCourses = totalCourses; + } + + public int getPendingLeave() { + return pendingLeave; + } + + public void setPendingLeave(int pendingLeave) { + this.pendingLeave = pendingLeave; + } + + public int getPendingChange() { + return pendingChange; + } + + public void setPendingChange(int pendingChange) { + this.pendingChange = pendingChange; + } + + public int getPendingRegister() { + return pendingRegister; + } + + public void setPendingRegister(int pendingRegister) { + this.pendingRegister = pendingRegister; + } + + public Map getGenderStats() { + return genderStats; + } + + public void setGenderStats(Map genderStats) { + this.genderStats = genderStats; + } + + public Map getDeptStats() { + return deptStats; + } + + public void setDeptStats(Map deptStats) { + this.deptStats = deptStats; + } +} diff --git a/src/main/java/com/student/bean/Department.java b/src/main/java/com/student/bean/Department.java new file mode 100644 index 0000000..9a73a1c --- /dev/null +++ b/src/main/java/com/student/bean/Department.java @@ -0,0 +1,36 @@ +package com.student.bean; + +import java.io.Serializable; + +public class Department implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private String name; + private String code; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/src/main/java/com/student/bean/Enrollment.java b/src/main/java/com/student/bean/Enrollment.java new file mode 100644 index 0000000..ad016a4 --- /dev/null +++ b/src/main/java/com/student/bean/Enrollment.java @@ -0,0 +1,82 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class Enrollment implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer studentId; + private Integer courseId; + private Date createTime; + private String studentName; + private String studentNo; + private String courseName; + private String courseNo; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getStudentId() { + return studentId; + } + + public void setStudentId(Integer studentId) { + this.studentId = studentId; + } + + public Integer getCourseId() { + return courseId; + } + + public void setCourseId(Integer courseId) { + this.courseId = courseId; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getStudentNo() { + return studentNo; + } + + public void setStudentNo(String studentNo) { + this.studentNo = studentNo; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public String getCourseNo() { + return courseNo; + } + + public void setCourseNo(String courseNo) { + this.courseNo = courseNo; + } +} diff --git a/src/main/java/com/student/bean/LeaveRequest.java b/src/main/java/com/student/bean/LeaveRequest.java new file mode 100644 index 0000000..eabafc9 --- /dev/null +++ b/src/main/java/com/student/bean/LeaveRequest.java @@ -0,0 +1,131 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class LeaveRequest implements Serializable { + + public static final int STATUS_PENDING = 0; + public static final int STATUS_APPROVED = 1; + public static final int STATUS_REJECTED = 2; + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer studentId; + private Date startDate; + private Date endDate; + private String reason; + private Integer status; + private Integer approverId; + private String approveNote; + private Date approveTime; + private Date createTime; + private String studentName; + private String studentNo; + private String approverName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getStudentId() { + return studentId; + } + + public void setStudentId(Integer studentId) { + this.studentId = studentId; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getApproverId() { + return approverId; + } + + public void setApproverId(Integer approverId) { + this.approverId = approverId; + } + + public String getApproveNote() { + return approveNote; + } + + public void setApproveNote(String approveNote) { + this.approveNote = approveNote; + } + + public Date getApproveTime() { + return approveTime; + } + + public void setApproveTime(Date approveTime) { + this.approveTime = approveTime; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getStudentNo() { + return studentNo; + } + + public void setStudentNo(String studentNo) { + this.studentNo = studentNo; + } + + public String getApproverName() { + return approverName; + } + + public void setApproverName(String approverName) { + this.approverName = approverName; + } +} diff --git a/src/main/java/com/student/bean/LoginLog.java b/src/main/java/com/student/bean/LoginLog.java new file mode 100644 index 0000000..6245e7e --- /dev/null +++ b/src/main/java/com/student/bean/LoginLog.java @@ -0,0 +1,73 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class LoginLog implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer userId; + private String username; + private String ip; + private Integer result; + private String message; + private Date createTime; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getResult() { + return result; + } + + public void setResult(Integer result) { + this.result = result; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/student/bean/Major.java b/src/main/java/com/student/bean/Major.java new file mode 100644 index 0000000..1ba7850 --- /dev/null +++ b/src/main/java/com/student/bean/Major.java @@ -0,0 +1,54 @@ +package com.student.bean; + +import java.io.Serializable; + +public class Major implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer departmentId; + private String name; + private String code; + private String departmentName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Integer departmentId) { + this.departmentId = departmentId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDepartmentName() { + return departmentName; + } + + public void setDepartmentName(String departmentName) { + this.departmentName = departmentName; + } +} diff --git a/src/main/java/com/student/bean/Message.java b/src/main/java/com/student/bean/Message.java new file mode 100644 index 0000000..33341b0 --- /dev/null +++ b/src/main/java/com/student/bean/Message.java @@ -0,0 +1,82 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class Message implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer senderId; + private Integer receiverId; + private String title; + private String content; + private Integer isRead; + private Date createTime; + private String senderName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getSenderId() { + return senderId; + } + + public void setSenderId(Integer senderId) { + this.senderId = senderId; + } + + public Integer getReceiverId() { + return receiverId; + } + + public void setReceiverId(Integer receiverId) { + this.receiverId = receiverId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getIsRead() { + return isRead; + } + + public void setIsRead(Integer isRead) { + this.isRead = isRead; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } +} diff --git a/src/main/java/com/student/bean/OperationLog.java b/src/main/java/com/student/bean/OperationLog.java new file mode 100644 index 0000000..77f2535 --- /dev/null +++ b/src/main/java/com/student/bean/OperationLog.java @@ -0,0 +1,73 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class OperationLog implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer userId; + private String username; + private String module; + private String action; + private String ip; + private Date createTime; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/student/bean/PageBean.java b/src/main/java/com/student/bean/PageBean.java new file mode 100644 index 0000000..884b4c9 --- /dev/null +++ b/src/main/java/com/student/bean/PageBean.java @@ -0,0 +1,78 @@ +package com.student.bean; + +import java.util.List; + +/** + * 分页 JavaBean + */ +public class PageBean { + + private int currentPage = 1; + private int pageSize = 10; + private int totalCount; + private int totalPage; + private List data; + + public PageBean() { + } + + public PageBean(int currentPage, int pageSize) { + this.currentPage = currentPage > 0 ? currentPage : 1; + this.pageSize = pageSize > 0 ? pageSize : 10; + } + + public int getCurrentPage() { + return currentPage; + } + + public void setCurrentPage(int currentPage) { + this.currentPage = currentPage; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + this.totalPage = (totalCount + pageSize - 1) / pageSize; + if (this.totalPage == 0) { + this.totalPage = 1; + } + if (this.currentPage > this.totalPage) { + this.currentPage = this.totalPage; + } + } + + public int getTotalPage() { + return totalPage; + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + public int getStartIndex() { + return (currentPage - 1) * pageSize; + } + + public boolean hasPrevious() { + return currentPage > 1; + } + + public boolean hasNext() { + return currentPage < totalPage; + } +} diff --git a/src/main/java/com/student/bean/RememberToken.java b/src/main/java/com/student/bean/RememberToken.java new file mode 100644 index 0000000..2215c58 --- /dev/null +++ b/src/main/java/com/student/bean/RememberToken.java @@ -0,0 +1,46 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class RememberToken implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer userId; + private String token; + private Date expireTime; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Date getExpireTime() { + return expireTime; + } + + public void setExpireTime(Date expireTime) { + this.expireTime = expireTime; + } +} diff --git a/src/main/java/com/student/bean/Score.java b/src/main/java/com/student/bean/Score.java new file mode 100644 index 0000000..d6b0730 --- /dev/null +++ b/src/main/java/com/student/bean/Score.java @@ -0,0 +1,82 @@ +package com.student.bean; + +import java.io.Serializable; +import java.math.BigDecimal; + +public class Score implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private Integer studentId; + private Integer courseId; + private BigDecimal score; + private String remark; + private String studentName; + private String studentNo; + private String courseName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getStudentId() { + return studentId; + } + + public void setStudentId(Integer studentId) { + this.studentId = studentId; + } + + public Integer getCourseId() { + return courseId; + } + + public void setCourseId(Integer courseId) { + this.courseId = courseId; + } + + public BigDecimal getScore() { + return score; + } + + public void setScore(BigDecimal score) { + this.score = score; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getStudentNo() { + return studentNo; + } + + public void setStudentNo(String studentNo) { + this.studentNo = studentNo; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } +} diff --git a/src/main/java/com/student/bean/User.java b/src/main/java/com/student/bean/User.java new file mode 100644 index 0000000..a3cb1dd --- /dev/null +++ b/src/main/java/com/student/bean/User.java @@ -0,0 +1,250 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +/** + * 用户 JavaBean + */ +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 0=学生 */ + public static final int ROLE_STUDENT = 0; + public static final int ROLE_USER = ROLE_STUDENT; + public static final int ROLE_ADMIN = 1; + public static final int ROLE_TEACHER = 2; + + /** 0=待审核 1=在读 2=休学 3=毕业 4=退学 */ + public static final int STATUS_PENDING = 0; + public static final int STATUS_ACTIVE = 1; + public static final int STATUS_SUSPEND = 2; + public static final int STATUS_GRAD = 3; + public static final int STATUS_GRADUATED = STATUS_GRAD; + public static final int STATUS_DROP = 4; + public static final int STATUS_DROPPED = STATUS_DROP; + + private Integer id; + private String username; + private String password; + private String salt; + private String studentNo; + private String realName; + private String gender; + private Integer age; + private String email; + private String phone; + private String address; + private String avatar; + private Integer clazzId; + private Integer role; + private Integer status; + private Integer failCount; + private Date lockUntil; + private Date createTime; + + private String clazzName; + private String majorName; + private String departmentName; + + public User() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getSalt() { + return salt; + } + + public void setSalt(String salt) { + this.salt = salt; + } + + public String getStudentNo() { + return studentNo; + } + + public void setStudentNo(String studentNo) { + this.studentNo = studentNo; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public Integer getClazzId() { + return clazzId; + } + + public void setClazzId(Integer clazzId) { + this.clazzId = clazzId; + } + + public Integer getRole() { + return role; + } + + public void setRole(Integer role) { + this.role = role; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getFailCount() { + return failCount; + } + + public void setFailCount(Integer failCount) { + this.failCount = failCount; + } + + public Date getLockUntil() { + return lockUntil; + } + + public void setLockUntil(Date lockUntil) { + this.lockUntil = lockUntil; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getClazzName() { + return clazzName; + } + + public void setClazzName(String clazzName) { + this.clazzName = clazzName; + } + + public String getMajorName() { + return majorName; + } + + public void setMajorName(String majorName) { + this.majorName = majorName; + } + + public String getDepartmentName() { + return departmentName; + } + + public void setDepartmentName(String departmentName) { + this.departmentName = departmentName; + } + + public String getDeptName() { + return departmentName; + } + + public void setDeptName(String deptName) { + this.departmentName = deptName; + } + + public boolean isAdmin() { + return role != null && role == ROLE_ADMIN; + } + + public boolean isTeacher() { + return role != null && role == ROLE_TEACHER; + } + + public boolean isStudent() { + return role != null && role == ROLE_STUDENT; + } + + public boolean isActive() { + return status != null && status == STATUS_ACTIVE; + } + + @Override + public String toString() { + return "User{id=" + id + ", username='" + username + "', realName='" + realName + "'}"; + } +} diff --git a/src/main/java/com/student/bean/UserImportTemp.java b/src/main/java/com/student/bean/UserImportTemp.java new file mode 100644 index 0000000..32df5df --- /dev/null +++ b/src/main/java/com/student/bean/UserImportTemp.java @@ -0,0 +1,109 @@ +package com.student.bean; + +import java.io.Serializable; +import java.util.Date; + +public class UserImportTemp implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer id; + private String batchId; + private String username; + private String password; + private String realName; + private String gender; + private Integer age; + private String email; + private String phone; + private String address; + private Date createTime; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getBatchId() { + return batchId; + } + + public void setBatchId(String batchId) { + this.batchId = batchId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/student/bean/UserQuery.java b/src/main/java/com/student/bean/UserQuery.java new file mode 100644 index 0000000..86eac8a --- /dev/null +++ b/src/main/java/com/student/bean/UserQuery.java @@ -0,0 +1,90 @@ +package com.student.bean; + +import java.io.Serializable; + +public class UserQuery implements Serializable { + + private static final long serialVersionUID = 1L; + + private String keyword; + private Integer clazzId; + private String gender; + private Integer ageMin; + private Integer ageMax; + private Integer status; + private Integer role; + private int currentPage = 1; + private int pageSize = 10; + + public String getKeyword() { + return keyword; + } + + public void setKeyword(String keyword) { + this.keyword = keyword; + } + + public Integer getClazzId() { + return clazzId; + } + + public void setClazzId(Integer clazzId) { + this.clazzId = clazzId; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Integer getAgeMin() { + return ageMin; + } + + public void setAgeMin(Integer ageMin) { + this.ageMin = ageMin; + } + + public Integer getAgeMax() { + return ageMax; + } + + public void setAgeMax(Integer ageMax) { + this.ageMax = ageMax; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getRole() { + return role; + } + + public void setRole(Integer role) { + this.role = role; + } + + public int getCurrentPage() { + return currentPage; + } + + public void setCurrentPage(int currentPage) { + this.currentPage = currentPage; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } +} diff --git a/src/main/java/com/student/dao/AnnouncementDao.java b/src/main/java/com/student/dao/AnnouncementDao.java new file mode 100644 index 0000000..01f8b2d --- /dev/null +++ b/src/main/java/com/student/dao/AnnouncementDao.java @@ -0,0 +1,20 @@ +package com.student.dao; + +import com.student.bean.Announcement; + +import java.util.List; + +public interface AnnouncementDao { + + Announcement findById(int id); + + boolean insert(Announcement announcement); + + boolean update(Announcement announcement); + + boolean delete(int id); + + List findAll(); + + List findActive(); +} diff --git a/src/main/java/com/student/dao/AttendanceDao.java b/src/main/java/com/student/dao/AttendanceDao.java new file mode 100644 index 0000000..2611c49 --- /dev/null +++ b/src/main/java/com/student/dao/AttendanceDao.java @@ -0,0 +1,23 @@ +package com.student.dao; + +import com.student.bean.Attendance; +import com.student.bean.AttendanceStats; + +import java.util.List; + +public interface AttendanceDao { + + boolean saveBatch(List records); + + List findByStudent(int studentId); + + List findByCourse(int courseId); + + List findRecent(int limit); + + AttendanceStats getStatsByStudent(int studentId); + + AttendanceStats getStatsByClazz(int clazzId); + + AttendanceStats getStatsByCourse(int courseId); +} diff --git a/src/main/java/com/student/dao/ChangeRequestDao.java b/src/main/java/com/student/dao/ChangeRequestDao.java new file mode 100644 index 0000000..909c3b3 --- /dev/null +++ b/src/main/java/com/student/dao/ChangeRequestDao.java @@ -0,0 +1,20 @@ +package com.student.dao; + +import com.student.bean.ChangeRequest; + +import java.util.List; + +public interface ChangeRequestDao { + + boolean submit(ChangeRequest request); + + boolean approve(int id, int approverId, String note); + + boolean reject(int id, int approverId, String note); + + List findPending(); + + ChangeRequest findById(int id); + + List findByUser(int userId); +} diff --git a/src/main/java/com/student/dao/CourseDao.java b/src/main/java/com/student/dao/CourseDao.java new file mode 100644 index 0000000..7775867 --- /dev/null +++ b/src/main/java/com/student/dao/CourseDao.java @@ -0,0 +1,29 @@ +package com.student.dao; + +import com.student.bean.Course; +import com.student.bean.PageBean; + +import java.util.List; + +public interface CourseDao { + + Course findById(int id); + + Course findByCourseNo(String courseNo); + + boolean insert(Course course); + + boolean update(Course course); + + boolean delete(int id); + + int count(String keyword); + + List findList(String keyword, int startIndex, int pageSize); + + PageBean findPage(String keyword, int currentPage, int pageSize); + + List findByTeacher(int teacherId); + + List findAll(); +} diff --git a/src/main/java/com/student/dao/EnrollmentDao.java b/src/main/java/com/student/dao/EnrollmentDao.java new file mode 100644 index 0000000..4cfcbbe --- /dev/null +++ b/src/main/java/com/student/dao/EnrollmentDao.java @@ -0,0 +1,20 @@ +package com.student.dao; + +import com.student.bean.Enrollment; + +import java.util.List; + +public interface EnrollmentDao { + + boolean enroll(int studentId, int courseId); + + boolean cancel(int studentId, int courseId); + + boolean exists(int studentId, int courseId); + + List findByStudent(int studentId); + + List findByCourse(int courseId); + + int countByCourse(int courseId); +} diff --git a/src/main/java/com/student/dao/ImportTempDao.java b/src/main/java/com/student/dao/ImportTempDao.java new file mode 100644 index 0000000..c573244 --- /dev/null +++ b/src/main/java/com/student/dao/ImportTempDao.java @@ -0,0 +1,16 @@ +package com.student.dao; + +import com.student.bean.UserImportTemp; + +import java.util.List; + +public interface ImportTempDao { + + boolean batchInsert(List records); + + List findByBatch(String batchId); + + boolean deleteBatch(String batchId); + + int confirmImport(String batchId); +} diff --git a/src/main/java/com/student/dao/LeaveDao.java b/src/main/java/com/student/dao/LeaveDao.java new file mode 100644 index 0000000..89e4448 --- /dev/null +++ b/src/main/java/com/student/dao/LeaveDao.java @@ -0,0 +1,20 @@ +package com.student.dao; + +import com.student.bean.LeaveRequest; + +import java.util.List; + +public interface LeaveDao { + + boolean submit(LeaveRequest request); + + boolean approve(int id, int approverId, String note); + + boolean reject(int id, int approverId, String note); + + List findPending(); + + List findByStudent(int studentId); + + LeaveRequest findById(int id); +} diff --git a/src/main/java/com/student/dao/LogDao.java b/src/main/java/com/student/dao/LogDao.java new file mode 100644 index 0000000..767cdba --- /dev/null +++ b/src/main/java/com/student/dao/LogDao.java @@ -0,0 +1,16 @@ +package com.student.dao; + +import com.student.bean.LoginLog; +import com.student.bean.OperationLog; +import com.student.bean.PageBean; + +public interface LogDao { + + boolean saveLoginLog(LoginLog log); + + boolean saveOperationLog(OperationLog log); + + PageBean findLoginLogs(int currentPage, int pageSize); + + PageBean findOperationLogs(int currentPage, int pageSize); +} diff --git a/src/main/java/com/student/dao/MessageDao.java b/src/main/java/com/student/dao/MessageDao.java new file mode 100644 index 0000000..dd744da --- /dev/null +++ b/src/main/java/com/student/dao/MessageDao.java @@ -0,0 +1,20 @@ +package com.student.dao; + +import com.student.bean.Message; + +import java.util.List; + +public interface MessageDao { + + boolean send(Message message); + + boolean sendAll(Message message); + + List findInbox(int receiverId); + + int countUnread(int receiverId); + + boolean markRead(int messageId); + + boolean markAllRead(int receiverId); +} diff --git a/src/main/java/com/student/dao/OrgDao.java b/src/main/java/com/student/dao/OrgDao.java new file mode 100644 index 0000000..8f959a9 --- /dev/null +++ b/src/main/java/com/student/dao/OrgDao.java @@ -0,0 +1,44 @@ +package com.student.dao; + +import com.student.bean.Clazz; +import com.student.bean.Department; +import com.student.bean.Major; + +import java.util.List; + +public interface OrgDao { + + List findAllDepts(); + + Department findDeptById(int id); + + boolean insertDept(Department dept); + + boolean updateDept(Department dept); + + boolean deleteDept(int id); + + List findMajorsByDept(int deptId); + + List findAllMajors(); + + Major findMajorById(int id); + + boolean insertMajor(Major major); + + boolean updateMajor(Major major); + + boolean deleteMajor(int id); + + List findClazzByMajor(int majorId); + + List findAllClazz(); + + Clazz findClazzById(int id); + + boolean insertClazz(Clazz clazz); + + boolean updateClazz(Clazz clazz); + + boolean deleteClazz(int id); +} diff --git a/src/main/java/com/student/dao/RememberTokenDao.java b/src/main/java/com/student/dao/RememberTokenDao.java new file mode 100644 index 0000000..eab227c --- /dev/null +++ b/src/main/java/com/student/dao/RememberTokenDao.java @@ -0,0 +1,14 @@ +package com.student.dao; + +import com.student.bean.RememberToken; + +public interface RememberTokenDao { + + boolean save(RememberToken token); + + RememberToken findByToken(String token); + + boolean deleteByUser(int userId); + + int deleteExpired(); +} diff --git a/src/main/java/com/student/dao/ScoreDao.java b/src/main/java/com/student/dao/ScoreDao.java new file mode 100644 index 0000000..559e433 --- /dev/null +++ b/src/main/java/com/student/dao/ScoreDao.java @@ -0,0 +1,19 @@ +package com.student.dao; + +import com.student.bean.CourseStats; +import com.student.bean.Score; + +import java.util.List; + +public interface ScoreDao { + + boolean saveOrUpdate(Score score); + + boolean saveOrUpdateBatch(List scores); + + List findByStudent(int studentId); + + List findByCourse(int courseId); + + CourseStats getCourseStats(int courseId); +} diff --git a/src/main/java/com/student/dao/StatDao.java b/src/main/java/com/student/dao/StatDao.java new file mode 100644 index 0000000..67f87bf --- /dev/null +++ b/src/main/java/com/student/dao/StatDao.java @@ -0,0 +1,16 @@ +package com.student.dao; + +import com.student.bean.DashboardStats; + +import java.util.Map; + +public interface StatDao { + + DashboardStats getDashboardStats(); + + Map genderDistribution(); + + Map ageDistribution(); + + Map deptDistribution(); +} diff --git a/src/main/java/com/student/dao/UserDao.java b/src/main/java/com/student/dao/UserDao.java new file mode 100644 index 0000000..5060181 --- /dev/null +++ b/src/main/java/com/student/dao/UserDao.java @@ -0,0 +1,52 @@ +package com.student.dao; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.bean.UserQuery; + +import java.util.List; + +/** + * 用户 DAO 接口 + */ +public interface UserDao { + + User findByUsername(String username); + + User findById(int id); + + User findByStudentNo(String studentNo); + + boolean insert(User user); + + boolean update(User user); + + boolean updateProfile(User user); + + boolean updateAvatar(int userId, String avatar); + + boolean delete(int id); + + int count(UserQuery query); + + List findList(UserQuery query, int startIndex, int pageSize); + + PageBean findPage(UserQuery query); + + PageBean findPendingRegister(int currentPage, int pageSize); + + boolean updatePassword(int userId, String password, String salt); + + boolean updateFailCount(int userId, int failCount); + + boolean lockUser(int userId, java.util.Date lockUntil); + + boolean updateStatus(int userId, int status); + + List findAllStudents(); + + List findAllTeachers(); + + /** 兼容旧版登录 */ + User findByUsernameAndPassword(String username, String password); +} diff --git a/src/main/java/com/student/dao/impl/AnnouncementDaoImpl.java b/src/main/java/com/student/dao/impl/AnnouncementDaoImpl.java new file mode 100644 index 0000000..fef335c --- /dev/null +++ b/src/main/java/com/student/dao/impl/AnnouncementDaoImpl.java @@ -0,0 +1,155 @@ +package com.student.dao.impl; + +import com.student.bean.Announcement; +import com.student.dao.AnnouncementDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class AnnouncementDaoImpl implements AnnouncementDao { + + private static final String BASE_SQL = + "SELECT a.id, a.title, a.content, a.publisher_id, a.is_top, a.expire_date, a.create_time, " + + "u.real_name AS publisher_name FROM announcement a " + + "LEFT JOIN users u ON a.publisher_id = u.id "; + + @Override + public Announcement findById(int id) { + List list = queryList(BASE_SQL + " WHERE a.id = ?", id); + return list.isEmpty() ? null : list.get(0); + } + + @Override + public boolean insert(Announcement announcement) { + String sql = "INSERT INTO announcement (title, content, publisher_id, is_top, expire_date) VALUES (?, ?, ?, ?, ?)"; + return executeUpdate(sql, announcement); + } + + @Override + public boolean update(Announcement announcement) { + String sql = "UPDATE announcement SET title=?, content=?, publisher_id=?, is_top=?, expire_date=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + fillParams(ps, announcement); + ps.setInt(6, announcement.getId()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新公告失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean delete(int id) { + String sql = "DELETE FROM announcement WHERE id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("删除公告失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findAll() { + return queryList(BASE_SQL + " ORDER BY a.is_top DESC, a.create_time DESC"); + } + + @Override + public List findActive() { + return queryList(BASE_SQL + " WHERE (a.expire_date IS NULL OR a.expire_date >= CURDATE()) " + + "ORDER BY a.is_top DESC, a.create_time DESC"); + } + + private boolean executeUpdate(String sql, Announcement announcement) { + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + fillParams(ps, announcement); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("保存公告失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + private void fillParams(PreparedStatement ps, Announcement a) throws SQLException { + ps.setString(1, a.getTitle()); + ps.setString(2, a.getContent()); + if (a.getPublisherId() != null) { + ps.setInt(3, a.getPublisherId()); + } else { + ps.setNull(3, Types.INTEGER); + } + ps.setInt(4, a.getIsTop() != null ? a.getIsTop() : 0); + if (a.getExpireDate() != null) { + ps.setDate(5, new Date(a.getExpireDate().getTime())); + } else { + ps.setNull(5, Types.DATE); + } + } + + private List queryList(String sql, Object... params) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询公告失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private Announcement mapRow(ResultSet rs) throws SQLException { + Announcement a = new Announcement(); + a.setId(rs.getInt("id")); + a.setTitle(rs.getString("title")); + a.setContent(rs.getString("content")); + int publisherId = rs.getInt("publisher_id"); + a.setPublisherId(rs.wasNull() ? null : publisherId); + a.setIsTop(rs.getInt("is_top")); + Date expireDate = rs.getDate("expire_date"); + if (expireDate != null) { + a.setExpireDate(new java.util.Date(expireDate.getTime())); + } + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + a.setCreateTime(new java.util.Date(createTime.getTime())); + } + a.setPublisherName(rs.getString("publisher_name")); + return a; + } +} diff --git a/src/main/java/com/student/dao/impl/AttendanceDaoImpl.java b/src/main/java/com/student/dao/impl/AttendanceDaoImpl.java new file mode 100644 index 0000000..f5a97f9 --- /dev/null +++ b/src/main/java/com/student/dao/impl/AttendanceDaoImpl.java @@ -0,0 +1,177 @@ +package com.student.dao.impl; + +import com.student.bean.Attendance; +import com.student.bean.AttendanceStats; +import com.student.dao.AttendanceDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class AttendanceDaoImpl implements AttendanceDao { + + private static final String BASE_SQL = + "SELECT a.id, a.student_id, a.course_id, a.att_date, a.status, a.remark, " + + "u.real_name AS student_name, c.name AS course_name " + + "FROM attendance a " + + "JOIN users u ON a.student_id = u.id " + + "JOIN course c ON a.course_id = c.id "; + + @Override + public boolean saveBatch(List records) { + if (records == null || records.isEmpty()) { + return true; + } + String sql = "INSERT INTO attendance (student_id, course_id, att_date, status, remark) " + + "VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE status=VALUES(status), remark=VALUES(remark)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (Attendance att : records) { + ps.setInt(1, att.getStudentId()); + ps.setInt(2, att.getCourseId()); + ps.setDate(3, new Date(att.getAttDate().getTime())); + ps.setInt(4, att.getStatus()); + ps.setString(5, att.getRemark()); + ps.addBatch(); + } + ps.executeBatch(); + return true; + } catch (SQLException e) { + throw new RuntimeException("批量保存考勤失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findByStudent(int studentId) { + return queryList(BASE_SQL + " WHERE a.student_id = ? ORDER BY a.att_date DESC", studentId); + } + + @Override + public List findByCourse(int courseId) { + return queryList(BASE_SQL + " WHERE a.course_id = ? ORDER BY a.att_date DESC", courseId); + } + + @Override + public List findRecent(int limit) { + int size = limit > 0 ? limit : 50; + List list = new ArrayList<>(); + String sql = BASE_SQL + " ORDER BY a.att_date DESC, a.id DESC LIMIT ?"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, size); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询最近考勤记录失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public AttendanceStats getStatsByStudent(int studentId) { + String sql = "SELECT status, COUNT(*) AS cnt FROM attendance WHERE student_id = ? GROUP BY status"; + return queryStats(sql, studentId); + } + + @Override + public AttendanceStats getStatsByClazz(int clazzId) { + String sql = "SELECT a.status, COUNT(*) AS cnt FROM attendance a " + + "JOIN users u ON a.student_id = u.id WHERE u.clazz_id = ? GROUP BY a.status"; + return queryStats(sql, clazzId); + } + + @Override + public AttendanceStats getStatsByCourse(int courseId) { + String sql = "SELECT status, COUNT(*) AS cnt FROM attendance WHERE course_id = ? GROUP BY status"; + return queryStats(sql, courseId); + } + + private AttendanceStats queryStats(String sql, int id) { + AttendanceStats stats = new AttendanceStats(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + int total = 0; + while (rs.next()) { + int status = rs.getInt("status"); + int cnt = rs.getInt("cnt"); + total += cnt; + if (status == Attendance.STATUS_PRESENT) { + stats.setPresent(cnt); + } else if (status == Attendance.STATUS_LATE) { + stats.setLate(cnt); + } else if (status == Attendance.STATUS_ABSENT) { + stats.setAbsent(cnt); + } + } + stats.setTotal(total); + } catch (SQLException e) { + throw new RuntimeException("查询考勤统计失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return stats; + } + + private List queryList(String sql, int id) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询考勤记录失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private Attendance mapRow(ResultSet rs) throws SQLException { + Attendance att = new Attendance(); + att.setId(rs.getInt("id")); + att.setStudentId(rs.getInt("student_id")); + att.setCourseId(rs.getInt("course_id")); + Date attDate = rs.getDate("att_date"); + if (attDate != null) { + att.setAttDate(new java.util.Date(attDate.getTime())); + } + att.setStatus(rs.getInt("status")); + att.setRemark(rs.getString("remark")); + att.setStudentName(rs.getString("student_name")); + att.setCourseName(rs.getString("course_name")); + return att; + } +} diff --git a/src/main/java/com/student/dao/impl/ChangeRequestDaoImpl.java b/src/main/java/com/student/dao/impl/ChangeRequestDaoImpl.java new file mode 100644 index 0000000..fc019f1 --- /dev/null +++ b/src/main/java/com/student/dao/impl/ChangeRequestDaoImpl.java @@ -0,0 +1,135 @@ +package com.student.dao.impl; + +import com.student.bean.ChangeRequest; +import com.student.dao.ChangeRequestDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class ChangeRequestDaoImpl implements ChangeRequestDao { + + private static final String BASE_SQL = + "SELECT cr.id, cr.user_id, cr.field_name, cr.old_value, cr.new_value, cr.status, " + + "cr.approver_id, cr.approve_note, cr.create_time, " + + "u.username, u.real_name, a.real_name AS approver_name " + + "FROM change_request cr " + + "JOIN users u ON cr.user_id = u.id " + + "LEFT JOIN users a ON cr.approver_id = a.id "; + + @Override + public boolean submit(ChangeRequest request) { + String sql = "INSERT INTO change_request (user_id, field_name, old_value, new_value, status) " + + "VALUES (?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, request.getUserId()); + ps.setString(2, request.getFieldName()); + ps.setString(3, request.getOldValue()); + ps.setString(4, request.getNewValue()); + ps.setInt(5, ChangeRequest.STATUS_PENDING); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("提交变更申请失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean approve(int id, int approverId, String note) { + return updateStatus(id, approverId, note, ChangeRequest.STATUS_APPROVED); + } + + @Override + public boolean reject(int id, int approverId, String note) { + return updateStatus(id, approverId, note, ChangeRequest.STATUS_REJECTED); + } + + private boolean updateStatus(int id, int approverId, String note, int status) { + String sql = "UPDATE change_request SET status=?, approver_id=?, approve_note=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, status); + ps.setInt(2, approverId); + ps.setString(3, note); + ps.setInt(4, id); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("审批变更申请失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findPending() { + return queryList(BASE_SQL + " WHERE cr.status = 0 ORDER BY cr.create_time ASC"); + } + + @Override + public ChangeRequest findById(int id) { + List list = queryList(BASE_SQL + " WHERE cr.id = ?", id); + return list.isEmpty() ? null : list.get(0); + } + + @Override + public List findByUser(int userId) { + return queryList(BASE_SQL + " WHERE cr.user_id = ? ORDER BY cr.create_time DESC", userId); + } + + private List queryList(String sql, Object... params) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询变更申请失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private ChangeRequest mapRow(ResultSet rs) throws SQLException { + ChangeRequest cr = new ChangeRequest(); + cr.setId(rs.getInt("id")); + cr.setUserId(rs.getInt("user_id")); + cr.setFieldName(rs.getString("field_name")); + cr.setOldValue(rs.getString("old_value")); + cr.setNewValue(rs.getString("new_value")); + cr.setStatus(rs.getInt("status")); + int approverId = rs.getInt("approver_id"); + cr.setApproverId(rs.wasNull() ? null : approverId); + cr.setApproveNote(rs.getString("approve_note")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + cr.setCreateTime(new java.util.Date(createTime.getTime())); + } + cr.setUsername(rs.getString("username")); + cr.setRealName(rs.getString("real_name")); + cr.setApproverName(rs.getString("approver_name")); + return cr; + } +} diff --git a/src/main/java/com/student/dao/impl/CourseDaoImpl.java b/src/main/java/com/student/dao/impl/CourseDaoImpl.java new file mode 100644 index 0000000..2fe9cd3 --- /dev/null +++ b/src/main/java/com/student/dao/impl/CourseDaoImpl.java @@ -0,0 +1,260 @@ +package com.student.dao.impl; + +import com.student.bean.Course; +import com.student.bean.PageBean; +import com.student.dao.CourseDao; +import com.student.util.DBUtil; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class CourseDaoImpl implements CourseDao { + + private static final String BASE_SQL = + "SELECT c.id, c.course_no, c.name, c.credit, c.teacher_id, c.max_students, c.schedule, c.create_time, " + + "u.real_name AS teacher_name, " + + "(SELECT COUNT(*) FROM enrollment e WHERE e.course_id = c.id) AS enrolled_count " + + "FROM course c LEFT JOIN users u ON c.teacher_id = u.id "; + + @Override + public Course findById(int id) { + return queryOne(BASE_SQL + " WHERE c.id = ?", id); + } + + @Override + public Course findByCourseNo(String courseNo) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(BASE_SQL + " WHERE c.course_no = ?"); + ps.setString(1, courseNo); + rs = ps.executeQuery(); + if (rs.next()) { + return mapRow(rs); + } + } catch (SQLException e) { + throw new RuntimeException("查询课程失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + @Override + public boolean insert(Course course) { + String sql = "INSERT INTO course (course_no, name, credit, teacher_id, max_students, schedule) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, course.getCourseNo()); + ps.setString(2, course.getName()); + ps.setBigDecimal(3, course.getCredit() != null ? course.getCredit() : new BigDecimal("2.0")); + if (course.getTeacherId() != null) { + ps.setInt(4, course.getTeacherId()); + } else { + ps.setNull(4, Types.INTEGER); + } + ps.setInt(5, course.getMaxStudents() != null ? course.getMaxStudents() : 50); + ps.setString(6, course.getSchedule()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("新增课程失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean update(Course course) { + String sql = "UPDATE course SET course_no=?, name=?, credit=?, teacher_id=?, max_students=?, schedule=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, course.getCourseNo()); + ps.setString(2, course.getName()); + ps.setBigDecimal(3, course.getCredit()); + if (course.getTeacherId() != null) { + ps.setInt(4, course.getTeacherId()); + } else { + ps.setNull(4, Types.INTEGER); + } + ps.setInt(5, course.getMaxStudents()); + ps.setString(6, course.getSchedule()); + ps.setInt(7, course.getId()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新课程失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean delete(int id) { + String sql = "DELETE FROM course WHERE id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("删除课程失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public int count(String keyword) { + String sql = "SELECT COUNT(*) FROM course c WHERE 1=1"; + if (keyword != null && !keyword.isEmpty()) { + sql += " AND (c.course_no LIKE ? OR c.name LIKE ?)"; + } + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (keyword != null && !keyword.isEmpty()) { + String pattern = "%" + keyword + "%"; + ps.setString(1, pattern); + ps.setString(2, pattern); + } + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计课程数量失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + @Override + public List findList(String keyword, int startIndex, int pageSize) { + StringBuilder sql = new StringBuilder(BASE_SQL + " WHERE 1=1"); + if (keyword != null && !keyword.isEmpty()) { + sql.append(" AND (c.course_no LIKE ? OR c.name LIKE ?)"); + } + sql.append(" ORDER BY c.id DESC LIMIT ?, ?"); + + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql.toString()); + int idx = 1; + if (keyword != null && !keyword.isEmpty()) { + String pattern = "%" + keyword + "%"; + ps.setString(idx++, pattern); + ps.setString(idx++, pattern); + } + ps.setInt(idx++, startIndex); + ps.setInt(idx, pageSize); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("分页查询课程失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public PageBean findPage(String keyword, int currentPage, int pageSize) { + PageBean page = new PageBean<>(currentPage, pageSize); + page.setTotalCount(count(keyword)); + page.setData(findList(keyword, page.getStartIndex(), page.getPageSize())); + return page; + } + + @Override + public List findByTeacher(int teacherId) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(BASE_SQL + " WHERE c.teacher_id = ? ORDER BY c.id"); + ps.setInt(1, teacherId); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询教师课程失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public List findAll() { + return findList(null, 0, Integer.MAX_VALUE); + } + + private Course queryOne(String sql, int id) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + if (rs.next()) { + return mapRow(rs); + } + } catch (SQLException e) { + throw new RuntimeException("查询课程失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + private Course mapRow(ResultSet rs) throws SQLException { + Course course = new Course(); + course.setId(rs.getInt("id")); + course.setCourseNo(rs.getString("course_no")); + course.setName(rs.getString("name")); + course.setCredit(rs.getBigDecimal("credit")); + int teacherId = rs.getInt("teacher_id"); + course.setTeacherId(rs.wasNull() ? null : teacherId); + course.setMaxStudents(rs.getInt("max_students")); + course.setSchedule(rs.getString("schedule")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + course.setCreateTime(new java.util.Date(createTime.getTime())); + } + course.setTeacherName(rs.getString("teacher_name")); + course.setEnrolledCount(rs.getInt("enrolled_count")); + return course; + } +} diff --git a/src/main/java/com/student/dao/impl/EnrollmentDaoImpl.java b/src/main/java/com/student/dao/impl/EnrollmentDaoImpl.java new file mode 100644 index 0000000..0e426eb --- /dev/null +++ b/src/main/java/com/student/dao/impl/EnrollmentDaoImpl.java @@ -0,0 +1,151 @@ +package com.student.dao.impl; + +import com.student.bean.Enrollment; +import com.student.dao.EnrollmentDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class EnrollmentDaoImpl implements EnrollmentDao { + + private static final String BASE_SQL = + "SELECT e.id, e.student_id, e.course_id, e.create_time, " + + "u.real_name AS student_name, u.student_no, c.name AS course_name, c.course_no " + + "FROM enrollment e " + + "JOIN users u ON e.student_id = u.id " + + "JOIN course c ON e.course_id = c.id "; + + @Override + public boolean enroll(int studentId, int courseId) { + String sql = "INSERT INTO enrollment (student_id, course_id) VALUES (?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, studentId); + ps.setInt(2, courseId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("选课失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean cancel(int studentId, int courseId) { + String sql = "DELETE FROM enrollment WHERE student_id = ? AND course_id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, studentId); + ps.setInt(2, courseId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("退课失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean exists(int studentId, int courseId) { + String sql = "SELECT COUNT(*) FROM enrollment WHERE student_id = ? AND course_id = ?"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, studentId); + ps.setInt(2, courseId); + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1) > 0; + } + } catch (SQLException e) { + throw new RuntimeException("查询选课记录失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return false; + } + + @Override + public List findByStudent(int studentId) { + return queryList(BASE_SQL + " WHERE e.student_id = ? ORDER BY e.create_time DESC", studentId); + } + + @Override + public List findByCourse(int courseId) { + return queryList(BASE_SQL + " WHERE e.course_id = ? ORDER BY e.create_time DESC", courseId); + } + + @Override + public int countByCourse(int courseId) { + String sql = "SELECT COUNT(*) FROM enrollment WHERE course_id = ?"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, courseId); + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计选课人数失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + private List queryList(String sql, int id) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询选课记录失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private Enrollment mapRow(ResultSet rs) throws SQLException { + Enrollment e = new Enrollment(); + e.setId(rs.getInt("id")); + e.setStudentId(rs.getInt("student_id")); + e.setCourseId(rs.getInt("course_id")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + e.setCreateTime(new java.util.Date(createTime.getTime())); + } + e.setStudentName(rs.getString("student_name")); + e.setStudentNo(rs.getString("student_no")); + e.setCourseName(rs.getString("course_name")); + e.setCourseNo(rs.getString("course_no")); + return e; + } +} diff --git a/src/main/java/com/student/dao/impl/ImportTempDaoImpl.java b/src/main/java/com/student/dao/impl/ImportTempDaoImpl.java new file mode 100644 index 0000000..d73bcdc --- /dev/null +++ b/src/main/java/com/student/dao/impl/ImportTempDaoImpl.java @@ -0,0 +1,184 @@ +package com.student.dao.impl; + +import com.student.bean.User; +import com.student.bean.UserImportTemp; +import com.student.dao.ImportTempDao; +import com.student.util.DBUtil; +import com.student.util.PasswordUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class ImportTempDaoImpl implements ImportTempDao { + + @Override + public boolean batchInsert(List records) { + if (records == null || records.isEmpty()) { + return true; + } + String sql = "INSERT INTO user_import_temp (batch_id, username, password, real_name, gender, age, email, phone, address) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (UserImportTemp temp : records) { + ps.setString(1, temp.getBatchId()); + ps.setString(2, temp.getUsername()); + ps.setString(3, temp.getPassword()); + ps.setString(4, temp.getRealName()); + ps.setString(5, temp.getGender()); + if (temp.getAge() != null) { + ps.setInt(6, temp.getAge()); + } else { + ps.setNull(6, Types.INTEGER); + } + ps.setString(7, temp.getEmail()); + ps.setString(8, temp.getPhone()); + ps.setString(9, temp.getAddress()); + ps.addBatch(); + } + ps.executeBatch(); + return true; + } catch (SQLException e) { + throw new RuntimeException("批量导入临时数据失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findByBatch(String batchId) { + String sql = "SELECT id, batch_id, username, password, real_name, gender, age, email, phone, address, create_time " + + "FROM user_import_temp WHERE batch_id = ? ORDER BY id"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, batchId); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询导入临时数据失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public boolean deleteBatch(String batchId) { + String sql = "DELETE FROM user_import_temp WHERE batch_id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, batchId); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + throw new RuntimeException("删除导入临时数据失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public int confirmImport(String batchId) { + List temps = findByBatch(batchId); + if (temps.isEmpty()) { + return 0; + } + String sql = "INSERT INTO users (username, password, salt, real_name, gender, age, email, phone, address, role, status) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + int count = 0; + try { + conn = DBUtil.getConnection(); + conn.setAutoCommit(false); + ps = conn.prepareStatement(sql); + for (UserImportTemp temp : temps) { + String salt = PasswordUtil.generateSalt(); + String hashed = PasswordUtil.hash(temp.getPassword(), salt); + ps.setString(1, temp.getUsername()); + ps.setString(2, hashed); + ps.setString(3, salt); + ps.setString(4, temp.getRealName()); + ps.setString(5, temp.getGender()); + if (temp.getAge() != null) { + ps.setInt(6, temp.getAge()); + } else { + ps.setNull(6, Types.INTEGER); + } + ps.setString(7, temp.getEmail()); + ps.setString(8, temp.getPhone()); + ps.setString(9, temp.getAddress()); + ps.setInt(10, User.ROLE_STUDENT); + ps.setInt(11, User.STATUS_ACTIVE); + ps.addBatch(); + } + int[] results = ps.executeBatch(); + for (int r : results) { + if (r > 0) { + count++; + } + } + PreparedStatement delPs = conn.prepareStatement("DELETE FROM user_import_temp WHERE batch_id = ?"); + delPs.setString(1, batchId); + delPs.executeUpdate(); + delPs.close(); + conn.commit(); + } catch (SQLException e) { + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ignored) { + } + } + throw new RuntimeException("确认导入失败", e); + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + } catch (SQLException ignored) { + } + } + DBUtil.close(conn, ps); + } + return count; + } + + private UserImportTemp mapRow(ResultSet rs) throws SQLException { + UserImportTemp temp = new UserImportTemp(); + temp.setId(rs.getInt("id")); + temp.setBatchId(rs.getString("batch_id")); + temp.setUsername(rs.getString("username")); + temp.setPassword(rs.getString("password")); + temp.setRealName(rs.getString("real_name")); + temp.setGender(rs.getString("gender")); + int age = rs.getInt("age"); + temp.setAge(rs.wasNull() ? null : age); + temp.setEmail(rs.getString("email")); + temp.setPhone(rs.getString("phone")); + temp.setAddress(rs.getString("address")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + temp.setCreateTime(new java.util.Date(createTime.getTime())); + } + return temp; + } +} diff --git a/src/main/java/com/student/dao/impl/LeaveDaoImpl.java b/src/main/java/com/student/dao/impl/LeaveDaoImpl.java new file mode 100644 index 0000000..6a5a267 --- /dev/null +++ b/src/main/java/com/student/dao/impl/LeaveDaoImpl.java @@ -0,0 +1,146 @@ +package com.student.dao.impl; + +import com.student.bean.LeaveRequest; +import com.student.dao.LeaveDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class LeaveDaoImpl implements LeaveDao { + + private static final String BASE_SQL = + "SELECT l.id, l.student_id, l.start_date, l.end_date, l.reason, l.status, " + + "l.approver_id, l.approve_note, l.approve_time, l.create_time, " + + "u.real_name AS student_name, u.student_no, a.real_name AS approver_name " + + "FROM leave_request l " + + "JOIN users u ON l.student_id = u.id " + + "LEFT JOIN users a ON l.approver_id = a.id "; + + @Override + public boolean submit(LeaveRequest request) { + String sql = "INSERT INTO leave_request (student_id, start_date, end_date, reason, status) " + + "VALUES (?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, request.getStudentId()); + ps.setDate(2, new Date(request.getStartDate().getTime())); + ps.setDate(3, new Date(request.getEndDate().getTime())); + ps.setString(4, request.getReason()); + ps.setInt(5, LeaveRequest.STATUS_PENDING); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("提交请假申请失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean approve(int id, int approverId, String note) { + return updateStatus(id, approverId, note, LeaveRequest.STATUS_APPROVED); + } + + @Override + public boolean reject(int id, int approverId, String note) { + return updateStatus(id, approverId, note, LeaveRequest.STATUS_REJECTED); + } + + private boolean updateStatus(int id, int approverId, String note, int status) { + String sql = "UPDATE leave_request SET status=?, approver_id=?, approve_note=?, approve_time=NOW() WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, status); + ps.setInt(2, approverId); + ps.setString(3, note); + ps.setInt(4, id); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("审批请假失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findPending() { + return queryList(BASE_SQL + " WHERE l.status = 0 ORDER BY l.create_time ASC"); + } + + @Override + public List findByStudent(int studentId) { + return queryList(BASE_SQL + " WHERE l.student_id = ? ORDER BY l.create_time DESC", studentId); + } + + @Override + public LeaveRequest findById(int id) { + List list = queryList(BASE_SQL + " WHERE l.id = ?", id); + return list.isEmpty() ? null : list.get(0); + } + + private List queryList(String sql, Object... params) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询请假记录失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private LeaveRequest mapRow(ResultSet rs) throws SQLException { + LeaveRequest req = new LeaveRequest(); + req.setId(rs.getInt("id")); + req.setStudentId(rs.getInt("student_id")); + Date startDate = rs.getDate("start_date"); + if (startDate != null) { + req.setStartDate(new java.util.Date(startDate.getTime())); + } + Date endDate = rs.getDate("end_date"); + if (endDate != null) { + req.setEndDate(new java.util.Date(endDate.getTime())); + } + req.setReason(rs.getString("reason")); + req.setStatus(rs.getInt("status")); + int approverId = rs.getInt("approver_id"); + req.setApproverId(rs.wasNull() ? null : approverId); + req.setApproveNote(rs.getString("approve_note")); + Timestamp approveTime = rs.getTimestamp("approve_time"); + if (approveTime != null) { + req.setApproveTime(new java.util.Date(approveTime.getTime())); + } + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + req.setCreateTime(new java.util.Date(createTime.getTime())); + } + req.setStudentName(rs.getString("student_name")); + req.setStudentNo(rs.getString("student_no")); + req.setApproverName(rs.getString("approver_name")); + return req; + } +} diff --git a/src/main/java/com/student/dao/impl/LogDaoImpl.java b/src/main/java/com/student/dao/impl/LogDaoImpl.java new file mode 100644 index 0000000..9601976 --- /dev/null +++ b/src/main/java/com/student/dao/impl/LogDaoImpl.java @@ -0,0 +1,184 @@ +package com.student.dao.impl; + +import com.student.bean.LoginLog; +import com.student.bean.OperationLog; +import com.student.bean.PageBean; +import com.student.dao.LogDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class LogDaoImpl implements LogDao { + + @Override + public boolean saveLoginLog(LoginLog log) { + String sql = "INSERT INTO login_log (user_id, username, ip, result, message) VALUES (?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (log.getUserId() != null) { + ps.setInt(1, log.getUserId()); + } else { + ps.setNull(1, Types.INTEGER); + } + ps.setString(2, log.getUsername()); + ps.setString(3, log.getIp()); + ps.setInt(4, log.getResult()); + ps.setString(5, log.getMessage()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("保存登录日志失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean saveOperationLog(OperationLog log) { + String sql = "INSERT INTO operation_log (user_id, username, module, action, ip) VALUES (?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (log.getUserId() != null) { + ps.setInt(1, log.getUserId()); + } else { + ps.setNull(1, Types.INTEGER); + } + ps.setString(2, log.getUsername()); + ps.setString(3, log.getModule()); + ps.setString(4, log.getAction()); + ps.setString(5, log.getIp()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("保存操作日志失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public PageBean findLoginLogs(int currentPage, int pageSize) { + PageBean page = new PageBean<>(currentPage, pageSize); + page.setTotalCount(countLoginLogs()); + page.setData(queryLoginLogs(page.getStartIndex(), page.getPageSize())); + return page; + } + + @Override + public PageBean findOperationLogs(int currentPage, int pageSize) { + PageBean page = new PageBean<>(currentPage, pageSize); + page.setTotalCount(countOperationLogs()); + page.setData(queryOperationLogs(page.getStartIndex(), page.getPageSize())); + return page; + } + + private int countLoginLogs() { + return count("SELECT COUNT(*) FROM login_log"); + } + + private int countOperationLogs() { + return count("SELECT COUNT(*) FROM operation_log"); + } + + private int count(String sql) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计日志数量失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + private List queryLoginLogs(int startIndex, int pageSize) { + String sql = "SELECT id, user_id, username, ip, result, message, create_time " + + "FROM login_log ORDER BY create_time DESC LIMIT ?, ?"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, startIndex); + ps.setInt(2, pageSize); + rs = ps.executeQuery(); + while (rs.next()) { + LoginLog log = new LoginLog(); + log.setId(rs.getInt("id")); + int userId = rs.getInt("user_id"); + log.setUserId(rs.wasNull() ? null : userId); + log.setUsername(rs.getString("username")); + log.setIp(rs.getString("ip")); + log.setResult(rs.getInt("result")); + log.setMessage(rs.getString("message")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + log.setCreateTime(new java.util.Date(createTime.getTime())); + } + list.add(log); + } + } catch (SQLException e) { + throw new RuntimeException("查询登录日志失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private List queryOperationLogs(int startIndex, int pageSize) { + String sql = "SELECT id, user_id, username, module, action, ip, create_time " + + "FROM operation_log ORDER BY create_time DESC LIMIT ?, ?"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, startIndex); + ps.setInt(2, pageSize); + rs = ps.executeQuery(); + while (rs.next()) { + OperationLog log = new OperationLog(); + log.setId(rs.getInt("id")); + int userId = rs.getInt("user_id"); + log.setUserId(rs.wasNull() ? null : userId); + log.setUsername(rs.getString("username")); + log.setModule(rs.getString("module")); + log.setAction(rs.getString("action")); + log.setIp(rs.getString("ip")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + log.setCreateTime(new java.util.Date(createTime.getTime())); + } + list.add(log); + } + } catch (SQLException e) { + throw new RuntimeException("查询操作日志失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } +} diff --git a/src/main/java/com/student/dao/impl/MessageDaoImpl.java b/src/main/java/com/student/dao/impl/MessageDaoImpl.java new file mode 100644 index 0000000..50bb4d2 --- /dev/null +++ b/src/main/java/com/student/dao/impl/MessageDaoImpl.java @@ -0,0 +1,173 @@ +package com.student.dao.impl; + +import com.student.bean.Message; +import com.student.dao.MessageDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class MessageDaoImpl implements MessageDao { + + private static final String BASE_SQL = + "SELECT m.id, m.sender_id, m.receiver_id, m.title, m.content, m.is_read, m.create_time, " + + "u.real_name AS sender_name FROM message m " + + "LEFT JOIN users u ON m.sender_id = u.id "; + + @Override + public boolean send(Message message) { + String sql = "INSERT INTO message (sender_id, receiver_id, title, content, is_read) VALUES (?, ?, ?, ?, 0)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (message.getSenderId() != null) { + ps.setInt(1, message.getSenderId()); + } else { + ps.setNull(1, Types.INTEGER); + } + ps.setInt(2, message.getReceiverId()); + ps.setString(3, message.getTitle()); + ps.setString(4, message.getContent()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("发送消息失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean sendAll(Message message) { + String sql = "INSERT INTO message (sender_id, receiver_id, title, content, is_read) " + + "SELECT ?, id, ?, ?, 0 FROM users WHERE role = 0 AND status = 1"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (message.getSenderId() != null) { + ps.setInt(1, message.getSenderId()); + } else { + ps.setNull(1, Types.INTEGER); + } + ps.setString(2, message.getTitle()); + ps.setString(3, message.getContent()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("群发消息失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findInbox(int receiverId) { + return queryList(BASE_SQL + " WHERE m.receiver_id = ? OR m.receiver_id IS NULL " + + "ORDER BY m.create_time DESC", receiverId); + } + + @Override + public int countUnread(int receiverId) { + String sql = "SELECT COUNT(*) FROM message WHERE is_read = 0 AND (receiver_id = ? OR receiver_id IS NULL)"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, receiverId); + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计未读消息失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + @Override + public boolean markRead(int messageId) { + String sql = "UPDATE message SET is_read = 1 WHERE id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, messageId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("标记已读失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean markAllRead(int receiverId) { + String sql = "UPDATE message SET is_read = 1 WHERE is_read = 0 AND (receiver_id = ? OR receiver_id IS NULL)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, receiverId); + return ps.executeUpdate() >= 0; + } catch (SQLException e) { + throw new RuntimeException("全部标记已读失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + private List queryList(String sql, Object... params) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询消息失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private Message mapRow(ResultSet rs) throws SQLException { + Message m = new Message(); + m.setId(rs.getInt("id")); + int senderId = rs.getInt("sender_id"); + m.setSenderId(rs.wasNull() ? null : senderId); + int receiverId = rs.getInt("receiver_id"); + m.setReceiverId(rs.wasNull() ? null : receiverId); + m.setTitle(rs.getString("title")); + m.setContent(rs.getString("content")); + m.setIsRead(rs.getInt("is_read")); + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + m.setCreateTime(new java.util.Date(createTime.getTime())); + } + m.setSenderName(rs.getString("sender_name")); + return m; + } +} diff --git a/src/main/java/com/student/dao/impl/OrgDaoImpl.java b/src/main/java/com/student/dao/impl/OrgDaoImpl.java new file mode 100644 index 0000000..4857ae1 --- /dev/null +++ b/src/main/java/com/student/dao/impl/OrgDaoImpl.java @@ -0,0 +1,323 @@ +package com.student.dao.impl; + +import com.student.bean.Clazz; +import com.student.bean.Department; +import com.student.bean.Major; +import com.student.dao.OrgDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class OrgDaoImpl implements OrgDao { + + @Override + public List findAllDepts() { + String sql = "SELECT id, name, code FROM department ORDER BY id"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapDept(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询院系列表失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public Department findDeptById(int id) { + String sql = "SELECT id, name, code FROM department WHERE id = ?"; + return queryDept(sql, id); + } + + @Override + public boolean insertDept(Department dept) { + String sql = "INSERT INTO department (name, code) VALUES (?, ?)"; + return executeUpdate(sql, dept.getName(), dept.getCode()); + } + + @Override + public boolean updateDept(Department dept) { + String sql = "UPDATE department SET name=?, code=? WHERE id=?"; + return executeUpdate(sql, dept.getName(), dept.getCode(), dept.getId()); + } + + @Override + public boolean deleteDept(int id) { + String sql = "DELETE FROM department WHERE id = ?"; + return executeUpdate(sql, id); + } + + @Override + public List findMajorsByDept(int deptId) { + String sql = "SELECT m.id, m.department_id, m.name, m.code, d.name AS department_name " + + "FROM major m LEFT JOIN department d ON m.department_id = d.id " + + "WHERE m.department_id = ? ORDER BY m.id"; + return queryMajors(sql, deptId); + } + + @Override + public List findAllMajors() { + String sql = "SELECT m.id, m.department_id, m.name, m.code, d.name AS department_name " + + "FROM major m LEFT JOIN department d ON m.department_id = d.id ORDER BY m.id"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapMajor(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询专业列表失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public Major findMajorById(int id) { + String sql = "SELECT m.id, m.department_id, m.name, m.code, d.name AS department_name " + + "FROM major m LEFT JOIN department d ON m.department_id = d.id WHERE m.id = ?"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + if (rs.next()) { + return mapMajor(rs); + } + } catch (SQLException e) { + throw new RuntimeException("查询专业失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + @Override + public boolean insertMajor(Major major) { + String sql = "INSERT INTO major (department_id, name, code) VALUES (?, ?, ?)"; + return executeUpdate(sql, major.getDepartmentId(), major.getName(), major.getCode()); + } + + @Override + public boolean updateMajor(Major major) { + String sql = "UPDATE major SET department_id=?, name=?, code=? WHERE id=?"; + return executeUpdate(sql, major.getDepartmentId(), major.getName(), major.getCode(), major.getId()); + } + + @Override + public boolean deleteMajor(int id) { + String sql = "DELETE FROM major WHERE id = ?"; + return executeUpdate(sql, id); + } + + @Override + public List findClazzByMajor(int majorId) { + String sql = "SELECT c.id, c.major_id, c.name, c.grade, m.name AS major_name, d.name AS department_name " + + "FROM clazz c LEFT JOIN major m ON c.major_id = m.id " + + "LEFT JOIN department d ON m.department_id = d.id " + + "WHERE c.major_id = ? ORDER BY c.id"; + return queryClazzList(sql, majorId); + } + + @Override + public List findAllClazz() { + String sql = "SELECT c.id, c.major_id, c.name, c.grade, m.name AS major_name, d.name AS department_name " + + "FROM clazz c LEFT JOIN major m ON c.major_id = m.id " + + "LEFT JOIN department d ON m.department_id = d.id ORDER BY c.id"; + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapClazz(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询班级列表失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public Clazz findClazzById(int id) { + String sql = "SELECT c.id, c.major_id, c.name, c.grade, m.name AS major_name, d.name AS department_name " + + "FROM clazz c LEFT JOIN major m ON c.major_id = m.id " + + "LEFT JOIN department d ON m.department_id = d.id WHERE c.id = ?"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + if (rs.next()) { + return mapClazz(rs); + } + } catch (SQLException e) { + throw new RuntimeException("查询班级失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + @Override + public boolean insertClazz(Clazz clazz) { + String sql = "INSERT INTO clazz (major_id, name, grade) VALUES (?, ?, ?)"; + return executeUpdate(sql, clazz.getMajorId(), clazz.getName(), clazz.getGrade()); + } + + @Override + public boolean updateClazz(Clazz clazz) { + String sql = "UPDATE clazz SET major_id=?, name=?, grade=? WHERE id=?"; + return executeUpdate(sql, clazz.getMajorId(), clazz.getName(), clazz.getGrade(), clazz.getId()); + } + + @Override + public boolean deleteClazz(int id) { + String sql = "DELETE FROM clazz WHERE id = ?"; + return executeUpdate(sql, id); + } + + private Department queryDept(String sql, int id) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + if (rs.next()) { + return mapDept(rs); + } + } catch (SQLException e) { + throw new RuntimeException("查询院系失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + private List queryMajors(String sql, int deptId) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, deptId); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapMajor(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询专业列表失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private List queryClazzList(String sql, int majorId) { + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, majorId); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapClazz(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询班级列表失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private boolean executeUpdate(String sql, Object... params) { + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + Object p = params[i]; + if (p instanceof Integer) { + ps.setInt(i + 1, (Integer) p); + } else if (p instanceof String) { + ps.setString(i + 1, (String) p); + } + } + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("组织数据操作失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + private Department mapDept(ResultSet rs) throws SQLException { + Department dept = new Department(); + dept.setId(rs.getInt("id")); + dept.setName(rs.getString("name")); + dept.setCode(rs.getString("code")); + return dept; + } + + private Major mapMajor(ResultSet rs) throws SQLException { + Major major = new Major(); + major.setId(rs.getInt("id")); + major.setDepartmentId(rs.getInt("department_id")); + major.setName(rs.getString("name")); + major.setCode(rs.getString("code")); + major.setDepartmentName(rs.getString("department_name")); + return major; + } + + private Clazz mapClazz(ResultSet rs) throws SQLException { + Clazz clazz = new Clazz(); + clazz.setId(rs.getInt("id")); + clazz.setMajorId(rs.getInt("major_id")); + clazz.setName(rs.getString("name")); + clazz.setGrade(rs.getString("grade")); + clazz.setMajorName(rs.getString("major_name")); + clazz.setDepartmentName(rs.getString("department_name")); + return clazz; + } +} diff --git a/src/main/java/com/student/dao/impl/RememberTokenDaoImpl.java b/src/main/java/com/student/dao/impl/RememberTokenDaoImpl.java new file mode 100644 index 0000000..c7a57c8 --- /dev/null +++ b/src/main/java/com/student/dao/impl/RememberTokenDaoImpl.java @@ -0,0 +1,97 @@ +package com.student.dao.impl; + +import com.student.bean.RememberToken; +import com.student.dao.RememberTokenDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; + +public class RememberTokenDaoImpl implements RememberTokenDao { + + @Override + public boolean save(RememberToken token) { + String sql = "INSERT INTO remember_token (user_id, token, expire_time) VALUES (?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, token.getUserId()); + ps.setString(2, token.getToken()); + ps.setTimestamp(3, new Timestamp(token.getExpireTime().getTime())); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("保存记住登录令牌失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public RememberToken findByToken(String token) { + String sql = "SELECT id, user_id, token, expire_time FROM remember_token WHERE token = ? AND expire_time > NOW()"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, token); + rs = ps.executeQuery(); + if (rs.next()) { + RememberToken rt = new RememberToken(); + rt.setId(rs.getInt("id")); + rt.setUserId(rs.getInt("user_id")); + rt.setToken(rs.getString("token")); + Timestamp expireTime = rs.getTimestamp("expire_time"); + if (expireTime != null) { + rt.setExpireTime(new java.util.Date(expireTime.getTime())); + } + return rt; + } + } catch (SQLException e) { + throw new RuntimeException("查询记住登录令牌失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + @Override + public boolean deleteByUser(int userId) { + String sql = "DELETE FROM remember_token WHERE user_id = ?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, userId); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + throw new RuntimeException("删除用户令牌失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public int deleteExpired() { + String sql = "DELETE FROM remember_token WHERE expire_time <= NOW()"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + return ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("清理过期令牌失败", e); + } finally { + DBUtil.close(conn, ps); + } + } +} diff --git a/src/main/java/com/student/dao/impl/ScoreDaoImpl.java b/src/main/java/com/student/dao/impl/ScoreDaoImpl.java new file mode 100644 index 0000000..a6c3c68 --- /dev/null +++ b/src/main/java/com/student/dao/impl/ScoreDaoImpl.java @@ -0,0 +1,161 @@ +package com.student.dao.impl; + +import com.student.bean.CourseStats; +import com.student.bean.Score; +import com.student.dao.ScoreDao; +import com.student.util.DBUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class ScoreDaoImpl implements ScoreDao { + + private static final String BASE_SQL = + "SELECT s.id, s.student_id, s.course_id, s.score, s.remark, " + + "u.real_name AS student_name, u.student_no, c.name AS course_name " + + "FROM score s " + + "JOIN users u ON s.student_id = u.id " + + "JOIN course c ON s.course_id = c.id "; + + @Override + public boolean saveOrUpdate(Score score) { + String sql = "INSERT INTO score (student_id, course_id, score, remark) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE score=VALUES(score), remark=VALUES(remark)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, score.getStudentId()); + ps.setInt(2, score.getCourseId()); + ps.setBigDecimal(3, score.getScore()); + ps.setString(4, score.getRemark()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("保存成绩失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean saveOrUpdateBatch(List scores) { + if (scores == null || scores.isEmpty()) { + return true; + } + String sql = "INSERT INTO score (student_id, course_id, score, remark) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE score=VALUES(score), remark=VALUES(remark)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (Score score : scores) { + ps.setInt(1, score.getStudentId()); + ps.setInt(2, score.getCourseId()); + ps.setBigDecimal(3, score.getScore()); + ps.setString(4, score.getRemark()); + ps.addBatch(); + } + ps.executeBatch(); + return true; + } catch (SQLException e) { + throw new RuntimeException("批量保存成绩失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findByStudent(int studentId) { + return queryList(BASE_SQL + " WHERE s.student_id = ? ORDER BY s.course_id", studentId); + } + + @Override + public List findByCourse(int courseId) { + return queryList(BASE_SQL + " WHERE s.course_id = ? ORDER BY u.student_no", courseId); + } + + @Override + public CourseStats getCourseStats(int courseId) { + String sql = "SELECT COUNT(*) AS total_count, AVG(score) AS avg_score, " + + "MAX(score) AS max_score, MIN(score) AS min_score, " + + "SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS pass_count " + + "FROM score WHERE course_id = ? AND score IS NOT NULL"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, courseId); + rs = ps.executeQuery(); + CourseStats stats = new CourseStats(); + stats.setCourseId(courseId); + if (rs.next()) { + int total = rs.getInt("total_count"); + stats.setTotalCount(total); + stats.setAvg(round(rs.getBigDecimal("avg_score"))); + stats.setMax(round(rs.getBigDecimal("max_score"))); + stats.setMin(round(rs.getBigDecimal("min_score"))); + int passCount = rs.getInt("pass_count"); + if (total > 0) { + stats.setPassRate(new BigDecimal(passCount * 100.0 / total).setScale(1, RoundingMode.HALF_UP)); + } else { + stats.setPassRate(BigDecimal.ZERO); + } + } + return stats; + } catch (SQLException e) { + throw new RuntimeException("查询课程成绩统计失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + } + + private BigDecimal round(BigDecimal value) { + if (value == null) { + return BigDecimal.ZERO; + } + return value.setScale(1, RoundingMode.HALF_UP); + } + + private List queryList(String sql, int id) { + java.util.List list = new java.util.ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("查询成绩失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + private Score mapRow(ResultSet rs) throws SQLException { + Score score = new Score(); + score.setId(rs.getInt("id")); + score.setStudentId(rs.getInt("student_id")); + score.setCourseId(rs.getInt("course_id")); + score.setScore(rs.getBigDecimal("score")); + score.setRemark(rs.getString("remark")); + score.setStudentName(rs.getString("student_name")); + score.setStudentNo(rs.getString("student_no")); + score.setCourseName(rs.getString("course_name")); + return score; + } +} diff --git a/src/main/java/com/student/dao/impl/StatDaoImpl.java b/src/main/java/com/student/dao/impl/StatDaoImpl.java new file mode 100644 index 0000000..c51b4e5 --- /dev/null +++ b/src/main/java/com/student/dao/impl/StatDaoImpl.java @@ -0,0 +1,102 @@ +package com.student.dao.impl; + +import com.student.bean.DashboardStats; +import com.student.bean.User; +import com.student.dao.StatDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +public class StatDaoImpl implements StatDao { + + @Override + public DashboardStats getDashboardStats() { + DashboardStats stats = new DashboardStats(); + stats.setTotalUsers(queryInt("SELECT COUNT(*) FROM users WHERE role = 0")); + stats.setTodayRegister(queryInt("SELECT COUNT(*) FROM users WHERE role = 0 AND DATE(create_time) = CURDATE()")); + stats.setTotalCourses(queryInt("SELECT COUNT(*) FROM course")); + stats.setPendingLeave(queryInt("SELECT COUNT(*) FROM leave_request WHERE status = 0")); + stats.setPendingChange(queryInt("SELECT COUNT(*) FROM change_request WHERE status = 0")); + stats.setPendingRegister(queryInt("SELECT COUNT(*) FROM users WHERE role = 0 AND status = ?", User.STATUS_PENDING)); + stats.setGenderStats(genderDistribution()); + stats.setDeptStats(deptDistribution()); + return stats; + } + + @Override + public Map genderDistribution() { + String sql = "SELECT IFNULL(gender, '未知') AS label, COUNT(*) AS cnt " + + "FROM users WHERE role = 0 GROUP BY gender"; + return queryMap(sql); + } + + @Override + public Map ageDistribution() { + String sql = "SELECT CASE " + + "WHEN age IS NULL THEN '未知' " + + "WHEN age < 18 THEN '18岁以下' " + + "WHEN age BETWEEN 18 AND 22 THEN '18-22岁' " + + "WHEN age BETWEEN 23 AND 25 THEN '23-25岁' " + + "ELSE '25岁以上' END AS label, COUNT(*) AS cnt " + + "FROM users WHERE role = 0 GROUP BY label"; + return queryMap(sql); + } + + @Override + public Map deptDistribution() { + String sql = "SELECT IFNULL(d.name, '未分配') AS label, COUNT(*) AS cnt " + + "FROM users u " + + "LEFT JOIN clazz c ON u.clazz_id = c.id " + + "LEFT JOIN major m ON c.major_id = m.id " + + "LEFT JOIN department d ON m.department_id = d.id " + + "WHERE u.role = 0 GROUP BY d.name"; + return queryMap(sql); + } + + private int queryInt(String sql, Object... params) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计查询失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + private Map queryMap(String sql) { + Map map = new HashMap<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + while (rs.next()) { + map.put(rs.getString("label"), rs.getInt("cnt")); + } + } catch (SQLException e) { + throw new RuntimeException("分布统计查询失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return map; + } +} diff --git a/src/main/java/com/student/dao/impl/UserDaoImpl.java b/src/main/java/com/student/dao/impl/UserDaoImpl.java new file mode 100644 index 0000000..33e170f --- /dev/null +++ b/src/main/java/com/student/dao/impl/UserDaoImpl.java @@ -0,0 +1,475 @@ +package com.student.dao.impl; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.bean.UserQuery; +import com.student.dao.UserDao; +import com.student.util.DBUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +public class UserDaoImpl implements UserDao { + + private static final String BASE_COLUMNS = + "u.id, u.username, u.password, u.salt, u.student_no, u.real_name, u.gender, u.age, " + + "u.email, u.phone, u.address, u.avatar, u.clazz_id, u.role, u.status, " + + "u.fail_count, u.lock_until, u.create_time"; + + private static final String JOIN_CLAZZ = + " FROM users u LEFT JOIN clazz c ON u.clazz_id = c.id " + + "LEFT JOIN major m ON c.major_id = m.id " + + "LEFT JOIN department d ON m.department_id = d.id "; + + @Override + public User findByUsernameAndPassword(String username, String password) { + String sql = "SELECT " + BASE_COLUMNS + JOIN_CLAZZ + " WHERE u.username = ? AND u.password = ?"; + return queryOne(sql, ps -> { + ps.setString(1, username); + ps.setString(2, password); + }); + } + + @Override + public User findByUsername(String username) { + String sql = "SELECT " + BASE_COLUMNS + ", c.name AS clazz_name, m.name AS major_name, d.name AS dept_name " + + JOIN_CLAZZ + " WHERE u.username = ?"; + return queryOne(sql, ps -> ps.setString(1, username)); + } + + @Override + public User findById(int id) { + String sql = "SELECT " + BASE_COLUMNS + ", c.name AS clazz_name, m.name AS major_name, d.name AS dept_name " + + JOIN_CLAZZ + " WHERE u.id = ?"; + return queryOne(sql, ps -> ps.setInt(1, id)); + } + + @Override + public User findByStudentNo(String studentNo) { + String sql = "SELECT " + BASE_COLUMNS + ", c.name AS clazz_name, m.name AS major_name, d.name AS dept_name " + + JOIN_CLAZZ + " WHERE u.student_no = ?"; + return queryOne(sql, ps -> ps.setString(1, studentNo)); + } + + @Override + public boolean insert(User user) { + String sql = "INSERT INTO users (username, password, salt, student_no, real_name, gender, age, " + + "email, phone, address, avatar, clazz_id, role, status) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + fillInsertParams(ps, user); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("新增用户失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean update(User user) { + String sql = "UPDATE users SET username=?, student_no=?, real_name=?, gender=?, age=?, email=?, " + + "phone=?, address=?, avatar=?, clazz_id=?, role=?, status=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, user.getUsername()); + setStringOrNull(ps, 2, user.getStudentNo()); + ps.setString(3, user.getRealName()); + ps.setString(4, user.getGender()); + setIntOrNull(ps, 5, user.getAge()); + ps.setString(6, user.getEmail()); + ps.setString(7, user.getPhone()); + ps.setString(8, user.getAddress()); + setStringOrNull(ps, 9, user.getAvatar()); + setIntOrNull(ps, 10, user.getClazzId()); + ps.setInt(11, user.getRole() != null ? user.getRole() : User.ROLE_STUDENT); + ps.setInt(12, user.getStatus() != null ? user.getStatus() : User.STATUS_ACTIVE); + ps.setInt(13, user.getId()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新用户失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean updateProfile(User user) { + String sql = "UPDATE users SET real_name=?, gender=?, age=?, email=?, phone=?, address=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, user.getRealName()); + ps.setString(2, user.getGender()); + setIntOrNull(ps, 3, user.getAge()); + ps.setString(4, user.getEmail()); + ps.setString(5, user.getPhone()); + ps.setString(6, user.getAddress()); + ps.setInt(7, user.getId()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新用户资料失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean updateAvatar(int userId, String avatar) { + String sql = "UPDATE users SET avatar=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, avatar); + ps.setInt(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新头像失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean delete(int id) { + String sql = "DELETE FROM users WHERE id = ? AND role = 0"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("删除用户失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public int count(UserQuery query) { + StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM users u WHERE 1=1"); + List params = appendQueryConditions(sql, query); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql.toString()); + setParams(ps, params); + rs = ps.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + } catch (SQLException e) { + throw new RuntimeException("统计用户数量失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return 0; + } + + @Override + public List findList(UserQuery query, int startIndex, int pageSize) { + StringBuilder sql = new StringBuilder("SELECT " + BASE_COLUMNS + + ", c.name AS clazz_name, m.name AS major_name, d.name AS dept_name " + JOIN_CLAZZ + " WHERE 1=1"); + List params = appendQueryConditions(sql, query); + sql.append(" ORDER BY u.id DESC LIMIT ?, ?"); + params.add(startIndex); + params.add(pageSize); + + List list = new ArrayList<>(); + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql.toString()); + setParams(ps, params); + rs = ps.executeQuery(); + while (rs.next()) { + list.add(mapRow(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("分页查询用户失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return list; + } + + @Override + public PageBean findPage(UserQuery query) { + PageBean page = new PageBean<>(query.getCurrentPage(), query.getPageSize()); + page.setTotalCount(count(query)); + page.setData(findList(query, page.getStartIndex(), page.getPageSize())); + return page; + } + + @Override + public PageBean findPendingRegister(int currentPage, int pageSize) { + UserQuery query = new UserQuery(); + query.setStatus(User.STATUS_PENDING); + query.setRole(User.ROLE_STUDENT); + query.setCurrentPage(currentPage); + query.setPageSize(pageSize); + return findPage(query); + } + + @Override + public boolean updatePassword(int userId, String password, String salt) { + String sql = "UPDATE users SET password=?, salt=?, fail_count=0, lock_until=NULL WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, password); + ps.setString(2, salt); + ps.setInt(3, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新密码失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean updateFailCount(int userId, int failCount) { + String sql = "UPDATE users SET fail_count=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, failCount); + ps.setInt(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新失败次数失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean lockUser(int userId, java.util.Date lockUntil) { + String sql = "UPDATE users SET lock_until=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + if (lockUntil != null) { + ps.setTimestamp(1, new Timestamp(lockUntil.getTime())); + } else { + ps.setNull(1, Types.TIMESTAMP); + } + ps.setInt(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("锁定用户失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public boolean updateStatus(int userId, int status) { + String sql = "UPDATE users SET status=? WHERE id=?"; + Connection conn = null; + PreparedStatement ps = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setInt(1, status); + ps.setInt(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + throw new RuntimeException("更新用户状态失败", e); + } finally { + DBUtil.close(conn, ps); + } + } + + @Override + public List findAllStudents() { + UserQuery query = new UserQuery(); + query.setRole(User.ROLE_STUDENT); + query.setCurrentPage(1); + query.setPageSize(Integer.MAX_VALUE); + return findList(query, 0, Integer.MAX_VALUE); + } + + @Override + public List findAllTeachers() { + UserQuery query = new UserQuery(); + query.setRole(User.ROLE_TEACHER); + query.setCurrentPage(1); + query.setPageSize(Integer.MAX_VALUE); + return findList(query, 0, Integer.MAX_VALUE); + } + + private List appendQueryConditions(StringBuilder sql, UserQuery query) { + List params = new ArrayList<>(); + if (query.getRole() != null) { + sql.append(" AND u.role = ?"); + params.add(query.getRole()); + } + if (query.getStatus() != null) { + sql.append(" AND u.status = ?"); + params.add(query.getStatus()); + } + if (query.getClazzId() != null) { + sql.append(" AND u.clazz_id = ?"); + params.add(query.getClazzId()); + } + if (query.getGender() != null && !query.getGender().isEmpty()) { + sql.append(" AND u.gender = ?"); + params.add(query.getGender()); + } + if (query.getAgeMin() != null) { + sql.append(" AND u.age >= ?"); + params.add(query.getAgeMin()); + } + if (query.getAgeMax() != null) { + sql.append(" AND u.age <= ?"); + params.add(query.getAgeMax()); + } + if (query.getKeyword() != null && !query.getKeyword().isEmpty()) { + sql.append(" AND (u.username LIKE ? OR u.real_name LIKE ? OR u.student_no LIKE ? " + + "OR u.email LIKE ? OR u.phone LIKE ?)"); + String pattern = "%" + query.getKeyword() + "%"; + for (int i = 0; i < 5; i++) { + params.add(pattern); + } + } + return params; + } + + private void fillInsertParams(PreparedStatement ps, User user) throws SQLException { + ps.setString(1, user.getUsername()); + ps.setString(2, user.getPassword()); + ps.setString(3, user.getSalt()); + setStringOrNull(ps, 4, user.getStudentNo()); + ps.setString(5, user.getRealName()); + ps.setString(6, user.getGender()); + setIntOrNull(ps, 7, user.getAge()); + ps.setString(8, user.getEmail()); + ps.setString(9, user.getPhone()); + ps.setString(10, user.getAddress()); + setStringOrNull(ps, 11, user.getAvatar()); + setIntOrNull(ps, 12, user.getClazzId()); + ps.setInt(13, user.getRole() != null ? user.getRole() : User.ROLE_STUDENT); + ps.setInt(14, user.getStatus() != null ? user.getStatus() : User.STATUS_ACTIVE); + } + + private User mapRow(ResultSet rs) throws SQLException { + User user = new User(); + user.setId(rs.getInt("id")); + user.setUsername(rs.getString("username")); + user.setPassword(rs.getString("password")); + user.setSalt(rs.getString("salt")); + user.setStudentNo(rs.getString("student_no")); + user.setRealName(rs.getString("real_name")); + user.setGender(rs.getString("gender")); + int age = rs.getInt("age"); + user.setAge(rs.wasNull() ? null : age); + user.setEmail(rs.getString("email")); + user.setPhone(rs.getString("phone")); + user.setAddress(rs.getString("address")); + user.setAvatar(rs.getString("avatar")); + int clazzId = rs.getInt("clazz_id"); + user.setClazzId(rs.wasNull() ? null : clazzId); + user.setRole(rs.getInt("role")); + user.setStatus(rs.getInt("status")); + int failCount = rs.getInt("fail_count"); + user.setFailCount(rs.wasNull() ? 0 : failCount); + Timestamp lockUntil = rs.getTimestamp("lock_until"); + if (lockUntil != null) { + user.setLockUntil(new java.util.Date(lockUntil.getTime())); + } + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + user.setCreateTime(new java.util.Date(createTime.getTime())); + } + try { + user.setClazzName(rs.getString("clazz_name")); + user.setMajorName(rs.getString("major_name")); + user.setDeptName(rs.getString("dept_name")); + } catch (SQLException ignored) { + } + return user; + } + + private void setIntOrNull(PreparedStatement ps, int index, Integer value) throws SQLException { + if (value != null) { + ps.setInt(index, value); + } else { + ps.setNull(index, Types.INTEGER); + } + } + + private void setStringOrNull(PreparedStatement ps, int index, String value) throws SQLException { + if (value != null) { + ps.setString(index, value); + } else { + ps.setNull(index, Types.VARCHAR); + } + } + + private void setParams(PreparedStatement ps, List params) throws SQLException { + for (int i = 0; i < params.size(); i++) { + Object param = params.get(i); + if (param instanceof Integer) { + ps.setInt(i + 1, (Integer) param); + } else if (param instanceof String) { + ps.setString(i + 1, (String) param); + } + } + } + + @FunctionalInterface + private interface ParamSetter { + void set(PreparedStatement ps) throws SQLException; + } + + private User queryOne(String sql, ParamSetter setter) { + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + setter.set(ps); + rs = ps.executeQuery(); + if (rs.next()) { + return mapRow(rs); + } + } catch (SQLException e) { + throw new RuntimeException("用户查询失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } +} diff --git a/src/main/java/com/student/filter/AuthFilter.java b/src/main/java/com/student/filter/AuthFilter.java new file mode 100644 index 0000000..1edab83 --- /dev/null +++ b/src/main/java/com/student/filter/AuthFilter.java @@ -0,0 +1,81 @@ +package com.student.filter; + +import com.student.bean.User; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.net.URLEncoder; + +/** + * 登录认证与角色权限过滤器 + */ +public class AuthFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + HttpSession session = req.getSession(false); + + User loginUser = null; + if (session != null) { + loginUser = (User) session.getAttribute("loginUser"); + } + + if (loginUser == null) { + resp.sendRedirect(req.getContextPath() + "/login.jsp?msg=" + + URLEncoder.encode("请先登录", "UTF-8")); + return; + } + + String uri = req.getRequestURI(); + String ctx = req.getContextPath(); + String path = uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri; + + if (path.startsWith("/admin/")) { + if (!loginUser.isAdmin()) { + resp.sendRedirect(ctx + defaultHome(loginUser) + "?msg=" + + URLEncoder.encode("无权访问管理员功能", "UTF-8")); + return; + } + } else if (path.startsWith("/teacher/")) { + if (!loginUser.isAdmin() && !loginUser.isTeacher()) { + resp.sendRedirect(ctx + defaultHome(loginUser) + "?msg=" + + URLEncoder.encode("无权访问教师功能", "UTF-8")); + return; + } + } else if (path.startsWith("/user/")) { + // 所有已登录用户均可访问 + } else if (path.startsWith("/api/")) { + if (!loginUser.isAdmin()) { + resp.sendRedirect(ctx + defaultHome(loginUser) + "?msg=" + + URLEncoder.encode("无权访问 API", "UTF-8")); + return; + } + } + + chain.doFilter(request, response); + } + + private String defaultHome(User user) { + if (user.isAdmin()) { + return "/admin/dashboard"; + } + if (user.isTeacher()) { + return "/teacher/home"; + } + return "/user/profile"; + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/com/student/filter/CharacterEncodingFilter.java b/src/main/java/com/student/filter/CharacterEncodingFilter.java new file mode 100644 index 0000000..5c5b5c6 --- /dev/null +++ b/src/main/java/com/student/filter/CharacterEncodingFilter.java @@ -0,0 +1,38 @@ +package com.student.filter; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 字符编码过滤器 + */ +public class CharacterEncodingFilter implements Filter { + + private String encoding = "UTF-8"; + + @Override + public void init(javax.servlet.FilterConfig filterConfig) { + String configEncoding = filterConfig.getInitParameter("encoding"); + if (configEncoding != null && !configEncoding.isEmpty()) { + encoding = configEncoding; + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + request.setCharacterEncoding(encoding); + response.setCharacterEncoding(encoding); + // 不在这里设置 Content-Type,避免覆盖 CSS/JS/图片的 MIME 类型 + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/com/student/filter/OperationLogFilter.java b/src/main/java/com/student/filter/OperationLogFilter.java new file mode 100644 index 0000000..319d4e8 --- /dev/null +++ b/src/main/java/com/student/filter/OperationLogFilter.java @@ -0,0 +1,65 @@ +package com.student.filter; + +import com.student.bean.OperationLog; +import com.student.bean.User; +import com.student.dao.LogDao; +import com.student.dao.impl.LogDaoImpl; +import com.student.util.IpUtil; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * 操作日志过滤器:记录已登录用户的 POST 请求 + */ +public class OperationLogFilter implements Filter { + + private final LogDao logDao = new LogDaoImpl(); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + if ("POST".equalsIgnoreCase(req.getMethod())) { + HttpSession session = req.getSession(false); + if (session != null) { + Object obj = session.getAttribute("loginUser"); + if (obj instanceof User) { + User user = (User) obj; + String uri = req.getRequestURI(); + String ctx = req.getContextPath(); + String path = uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri; + OperationLog log = new OperationLog(); + log.setUserId(user.getId()); + log.setUsername(user.getUsername()); + log.setModule(extractModule(path)); + log.setAction(path); + log.setIp(IpUtil.getClientIp(req)); + logDao.saveOperationLog(log); + } + } + } + chain.doFilter(request, response); + } + + private String extractModule(String path) { + if (path == null || path.isEmpty()) { + return "unknown"; + } + String[] parts = path.split("/"); + if (parts.length >= 2 && !parts[1].isEmpty()) { + return parts[1]; + } + return "unknown"; + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/com/student/filter/RememberMeFilter.java b/src/main/java/com/student/filter/RememberMeFilter.java new file mode 100644 index 0000000..45050d6 --- /dev/null +++ b/src/main/java/com/student/filter/RememberMeFilter.java @@ -0,0 +1,117 @@ +package com.student.filter; + +import com.student.bean.User; +import com.student.listener.SessionListener; +import com.student.util.CookieUtil; +import com.student.util.DBUtil; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; + +/** + * 记住我过滤器:校验 remember_token Cookie 并自动登录 + */ +public class RememberMeFilter implements Filter { + + public static final String REMEMBER_COOKIE_NAME = "remember_token"; + + private static final String USER_COLUMNS = + "u.id, u.username, u.password, u.salt, u.student_no, u.real_name, u.gender, u.age, " + + "u.email, u.phone, u.address, u.avatar, u.clazz_id, u.role, u.status, " + + "u.fail_count, u.lock_until, u.create_time"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + HttpSession session = req.getSession(false); + if (session == null || session.getAttribute("loginUser") == null) { + String token = CookieUtil.getCookie(req, REMEMBER_COOKIE_NAME); + if (token != null && !token.isEmpty()) { + User user = findUserByRememberToken(token); + if (user != null) { + HttpSession newSession = req.getSession(true); + newSession.setAttribute("loginUser", user); + SessionListener.registerSession(user, newSession); + } else { + CookieUtil.deleteCookie(resp, REMEMBER_COOKIE_NAME); + } + } + } + + chain.doFilter(request, response); + } + + private User findUserByRememberToken(String token) { + String sql = "SELECT " + USER_COLUMNS + " FROM remember_token rt " + + "JOIN users u ON rt.user_id = u.id " + + "WHERE rt.token = ? AND rt.expire_time > NOW() LIMIT 1"; + Connection conn = null; + PreparedStatement ps = null; + ResultSet rs = null; + try { + conn = DBUtil.getConnection(); + ps = conn.prepareStatement(sql); + ps.setString(1, token); + rs = ps.executeQuery(); + if (rs.next()) { + return mapUser(rs); + } + } catch (SQLException e) { + throw new RuntimeException("记住我自动登录查询失败", e); + } finally { + DBUtil.close(conn, ps, rs); + } + return null; + } + + private User mapUser(ResultSet rs) throws SQLException { + User user = new User(); + user.setId(rs.getInt("id")); + user.setUsername(rs.getString("username")); + user.setPassword(rs.getString("password")); + user.setSalt(rs.getString("salt")); + user.setStudentNo(rs.getString("student_no")); + user.setRealName(rs.getString("real_name")); + user.setGender(rs.getString("gender")); + int age = rs.getInt("age"); + user.setAge(rs.wasNull() ? null : age); + user.setEmail(rs.getString("email")); + user.setPhone(rs.getString("phone")); + user.setAddress(rs.getString("address")); + user.setAvatar(rs.getString("avatar")); + int clazzId = rs.getInt("clazz_id"); + user.setClazzId(rs.wasNull() ? null : clazzId); + user.setRole(rs.getInt("role")); + user.setStatus(rs.getInt("status")); + int failCount = rs.getInt("fail_count"); + user.setFailCount(rs.wasNull() ? null : failCount); + Timestamp lockUntil = rs.getTimestamp("lock_until"); + if (lockUntil != null) { + user.setLockUntil(new java.util.Date(lockUntil.getTime())); + } + Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + user.setCreateTime(new java.util.Date(createTime.getTime())); + } + return user; + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/com/student/filter/UserContextFilter.java b/src/main/java/com/student/filter/UserContextFilter.java new file mode 100644 index 0000000..0b32162 --- /dev/null +++ b/src/main/java/com/student/filter/UserContextFilter.java @@ -0,0 +1,57 @@ +package com.student.filter; + +import com.student.bean.DashboardStats; +import com.student.bean.User; +import com.student.service.MessageService; +import com.student.service.StatService; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * 为页面注入侧边栏所需的上下文数据(未读消息、待办数量等) + */ +public class UserContextFilter implements Filter { + + private final MessageService messageService = new MessageService(); + private final StatService statService = new StatService(); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpSession session = req.getSession(false); + User user = null; + if (session != null) { + Object obj = session.getAttribute("loginUser"); + if (obj instanceof User) { + user = (User) obj; + } + } + + String path = req.getRequestURI().substring(req.getContextPath().length()); + + if (user != null && path.startsWith("/user/")) { + request.setAttribute("unreadCount", messageService.countUnread(user.getId())); + } + + if (user != null && user.isAdmin() && path.startsWith("/admin/")) { + DashboardStats stats = statService.getDashboard(); + request.setAttribute("pendingRegister", stats.getPendingRegister()); + request.setAttribute("pendingChange", stats.getPendingChange()); + request.setAttribute("pendingLeave", stats.getPendingLeave()); + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/com/student/listener/SessionListener.java b/src/main/java/com/student/listener/SessionListener.java new file mode 100644 index 0000000..2ecaef3 --- /dev/null +++ b/src/main/java/com/student/listener/SessionListener.java @@ -0,0 +1,79 @@ +package com.student.listener; + +import com.student.bean.User; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Session 监听器,维护在线用户会话 + */ +public class SessionListener implements HttpSessionListener { + + private static final Map ONLINE_SESSIONS = new ConcurrentHashMap<>(); + + @Override + public void sessionCreated(HttpSessionEvent se) { + } + + @Override + public void sessionDestroyed(HttpSessionEvent se) { + HttpSession session = se.getSession(); + User user = getLoginUser(session); + if (user != null && user.getId() != null) { + ONLINE_SESSIONS.remove(String.valueOf(user.getId())); + } + } + + public static void registerSession(User user, HttpSession session) { + if (user == null || user.getId() == null || session == null) { + return; + } + ONLINE_SESSIONS.put(String.valueOf(user.getId()), session); + } + + public static int getOnlineCount() { + return ONLINE_SESSIONS.size(); + } + + public static List getOnlineUsers() { + List users = new ArrayList<>(); + for (HttpSession session : ONLINE_SESSIONS.values()) { + User user = getLoginUser(session); + if (user != null) { + users.add(user); + } + } + return users; + } + + public static boolean invalidateSession(int userId) { + HttpSession session = ONLINE_SESSIONS.remove(String.valueOf(userId)); + if (session != null) { + try { + session.invalidate(); + return true; + } catch (IllegalStateException ignored) { + return true; + } + } + return false; + } + + private static User getLoginUser(HttpSession session) { + if (session == null) { + return null; + } + try { + Object obj = session.getAttribute("loginUser"); + return obj instanceof User ? (User) obj : null; + } catch (IllegalStateException e) { + return null; + } + } +} diff --git a/src/main/java/com/student/service/AnnouncementService.java b/src/main/java/com/student/service/AnnouncementService.java new file mode 100644 index 0000000..31d43cd --- /dev/null +++ b/src/main/java/com/student/service/AnnouncementService.java @@ -0,0 +1,44 @@ +package com.student.service; + +import com.student.bean.Announcement; +import com.student.dao.AnnouncementDao; +import com.student.dao.impl.AnnouncementDaoImpl; + +import java.util.List; + +public class AnnouncementService { + + private final AnnouncementDao announcementDao; + + public AnnouncementService() { + this(new AnnouncementDaoImpl()); + } + + AnnouncementService(AnnouncementDao announcementDao) { + this.announcementDao = announcementDao; + } + + public Announcement findById(int id) { + return announcementDao.findById(id); + } + + public boolean add(Announcement announcement) { + return announcementDao.insert(announcement); + } + + public boolean update(Announcement announcement) { + return announcementDao.update(announcement); + } + + public boolean delete(int id) { + return announcementDao.delete(id); + } + + public List findAll() { + return announcementDao.findAll(); + } + + public List findActive() { + return announcementDao.findActive(); + } +} diff --git a/src/main/java/com/student/service/AttendanceService.java b/src/main/java/com/student/service/AttendanceService.java new file mode 100644 index 0000000..6515037 --- /dev/null +++ b/src/main/java/com/student/service/AttendanceService.java @@ -0,0 +1,49 @@ +package com.student.service; + +import com.student.bean.Attendance; +import com.student.bean.AttendanceStats; +import com.student.dao.AttendanceDao; +import com.student.dao.impl.AttendanceDaoImpl; + +import java.util.List; + +public class AttendanceService { + + private final AttendanceDao attendanceDao; + + public AttendanceService() { + this(new AttendanceDaoImpl()); + } + + AttendanceService(AttendanceDao attendanceDao) { + this.attendanceDao = attendanceDao; + } + + public boolean recordBatch(List records) { + return attendanceDao.saveBatch(records); + } + + public List findByStudent(int studentId) { + return attendanceDao.findByStudent(studentId); + } + + public List findByCourse(int courseId) { + return attendanceDao.findByCourse(courseId); + } + + public List findRecent(int limit) { + return attendanceDao.findRecent(limit); + } + + public AttendanceStats getStatsByStudent(int studentId) { + return attendanceDao.getStatsByStudent(studentId); + } + + public AttendanceStats getStatsByClazz(int clazzId) { + return attendanceDao.getStatsByClazz(clazzId); + } + + public AttendanceStats getStatsByCourse(int courseId) { + return attendanceDao.getStatsByCourse(courseId); + } +} diff --git a/src/main/java/com/student/service/AuthService.java b/src/main/java/com/student/service/AuthService.java new file mode 100644 index 0000000..afe240a --- /dev/null +++ b/src/main/java/com/student/service/AuthService.java @@ -0,0 +1,124 @@ +package com.student.service; + +import com.student.bean.LoginLog; +import com.student.bean.RememberToken; +import com.student.bean.User; +import com.student.dao.LogDao; +import com.student.dao.RememberTokenDao; +import com.student.dao.UserDao; +import com.student.dao.impl.LogDaoImpl; +import com.student.dao.impl.RememberTokenDaoImpl; +import com.student.dao.impl.UserDaoImpl; +import com.student.util.PasswordUtil; + +import java.util.Calendar; +import java.util.Date; + +public class AuthService { + + private static final int MAX_FAIL_COUNT = 5; + private static final int LOCK_MINUTES = 30; + private static final int REMEMBER_DAYS = 7; + + private final UserDao userDao; + private final LogDao logDao; + private final RememberTokenDao tokenDao; + + public AuthService() { + this(new UserDaoImpl(), new LogDaoImpl(), new RememberTokenDaoImpl()); + } + + AuthService(UserDao userDao, LogDao logDao, RememberTokenDao tokenDao) { + this.userDao = userDao; + this.logDao = logDao; + this.tokenDao = tokenDao; + } + + public User login(String username, String password, String ip) { + User user = userDao.findByUsername(username); + if (user == null) { + saveLoginLog(null, username, ip, 0, "用户不存在"); + return null; + } + if (isLocked(user)) { + saveLoginLog(user.getId(), username, ip, 0, "账户已锁定"); + throw new RuntimeException("账户已锁定,请稍后再试"); + } + if (!PasswordUtil.verify(password, user.getSalt(), user.getPassword())) { + int failCount = (user.getFailCount() != null ? user.getFailCount() : 0) + 1; + userDao.updateFailCount(user.getId(), failCount); + if (failCount >= MAX_FAIL_COUNT) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, LOCK_MINUTES); + userDao.lockUser(user.getId(), cal.getTime()); + saveLoginLog(user.getId(), username, ip, 0, "密码错误,账户已锁定"); + throw new RuntimeException("密码错误次数过多,账户已锁定30分钟"); + } + saveLoginLog(user.getId(), username, ip, 0, "密码错误"); + return null; + } + if (user.getStatus() != null && user.getStatus() == User.STATUS_PENDING) { + saveLoginLog(user.getId(), username, ip, 0, "账户待审核"); + throw new RuntimeException("账户待审核,请等待管理员审批"); + } + if (user.getStatus() != null && (user.getStatus() == User.STATUS_SUSPEND + || user.getStatus() == User.STATUS_DROPPED)) { + saveLoginLog(user.getId(), username, ip, 0, "账户已停用"); + throw new RuntimeException("账户已停用"); + } + userDao.updateFailCount(user.getId(), 0); + userDao.lockUser(user.getId(), null); + saveLoginLog(user.getId(), username, ip, 1, "登录成功"); + user.setPassword(null); + return user; + } + + public String createRememberToken(int userId) { + tokenDao.deleteByUser(userId); + RememberToken token = new RememberToken(); + token.setUserId(userId); + token.setToken(PasswordUtil.generateToken()); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, REMEMBER_DAYS); + token.setExpireTime(cal.getTime()); + tokenDao.save(token); + return token.getToken(); + } + + public User loginByToken(String token) { + RememberToken rt = tokenDao.findByToken(token); + if (rt == null) { + return null; + } + User user = userDao.findById(rt.getUserId()); + if (user != null) { + user.setPassword(null); + } + return user; + } + + public void logoutRemember(int userId) { + tokenDao.deleteByUser(userId); + } + + public void cleanExpiredTokens() { + tokenDao.deleteExpired(); + } + + private boolean isLocked(User user) { + if (user.getLockUntil() == null) { + return false; + } + return user.getLockUntil().after(new Date()); + } + + private void saveLoginLog(Integer userId, String username, String ip, int result, String message) { + LoginLog log = new LoginLog(); + log.setUserId(userId); + log.setUsername(username); + log.setIp(ip); + log.setResult(result); + log.setMessage(message); + logDao.saveLoginLog(log); + } +} diff --git a/src/main/java/com/student/service/ChangeRequestService.java b/src/main/java/com/student/service/ChangeRequestService.java new file mode 100644 index 0000000..c87f932 --- /dev/null +++ b/src/main/java/com/student/service/ChangeRequestService.java @@ -0,0 +1,83 @@ +package com.student.service; + +import com.student.bean.ChangeRequest; +import com.student.bean.User; +import com.student.dao.ChangeRequestDao; +import com.student.dao.UserDao; +import com.student.dao.impl.ChangeRequestDaoImpl; +import com.student.dao.impl.UserDaoImpl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ChangeRequestService { + + private static final Set ALLOWED_FIELDS = new HashSet<>( + Arrays.asList("realName", "studentNo")); + + private final ChangeRequestDao changeRequestDao; + private final UserDao userDao; + + public ChangeRequestService() { + this(new ChangeRequestDaoImpl(), new UserDaoImpl()); + } + + ChangeRequestService(ChangeRequestDao changeRequestDao, UserDao userDao) { + this.changeRequestDao = changeRequestDao; + this.userDao = userDao; + } + + public boolean submit(ChangeRequest request) { + if (!ALLOWED_FIELDS.contains(request.getFieldName())) { + throw new RuntimeException("不允许修改该字段"); + } + User user = userDao.findById(request.getUserId()); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + if ("realName".equals(request.getFieldName())) { + request.setOldValue(user.getRealName()); + } else if ("studentNo".equals(request.getFieldName())) { + request.setOldValue(user.getStudentNo()); + if (userDao.findByStudentNo(request.getNewValue()) != null) { + throw new RuntimeException("学号已存在"); + } + } + return changeRequestDao.submit(request); + } + + public boolean approve(int id, int approverId, String note) { + ChangeRequest cr = changeRequestDao.findById(id); + if (cr == null) { + throw new RuntimeException("申请不存在"); + } + if (cr.getStatus() != ChangeRequest.STATUS_PENDING) { + throw new RuntimeException("申请已处理"); + } + User user = userDao.findById(cr.getUserId()); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + if ("realName".equals(cr.getFieldName())) { + user.setRealName(cr.getNewValue()); + } else if ("studentNo".equals(cr.getFieldName())) { + user.setStudentNo(cr.getNewValue()); + } + userDao.update(user); + return changeRequestDao.approve(id, approverId, note); + } + + public boolean reject(int id, int approverId, String note) { + return changeRequestDao.reject(id, approverId, note); + } + + public List findPending() { + return changeRequestDao.findPending(); + } + + public List findByUser(int userId) { + return changeRequestDao.findByUser(userId); + } +} diff --git a/src/main/java/com/student/service/CourseService.java b/src/main/java/com/student/service/CourseService.java new file mode 100644 index 0000000..0944309 --- /dev/null +++ b/src/main/java/com/student/service/CourseService.java @@ -0,0 +1,56 @@ +package com.student.service; + +import com.student.bean.Course; +import com.student.bean.PageBean; +import com.student.dao.CourseDao; +import com.student.dao.impl.CourseDaoImpl; + +import java.util.List; + +public class CourseService { + + private final CourseDao courseDao; + + public CourseService() { + this(new CourseDaoImpl()); + } + + CourseService(CourseDao courseDao) { + this.courseDao = courseDao; + } + + public Course findById(int id) { + return courseDao.findById(id); + } + + public boolean add(Course course) { + if (courseDao.findByCourseNo(course.getCourseNo()) != null) { + throw new RuntimeException("课程编号已存在"); + } + return courseDao.insert(course); + } + + public boolean update(Course course) { + Course existing = courseDao.findByCourseNo(course.getCourseNo()); + if (existing != null && !existing.getId().equals(course.getId())) { + throw new RuntimeException("课程编号已存在"); + } + return courseDao.update(course); + } + + public boolean delete(int id) { + return courseDao.delete(id); + } + + public PageBean findPage(String keyword, int currentPage, int pageSize) { + return courseDao.findPage(keyword, currentPage, pageSize); + } + + public List findByTeacher(int teacherId) { + return courseDao.findByTeacher(teacherId); + } + + public List findAll() { + return courseDao.findAll(); + } +} diff --git a/src/main/java/com/student/service/EnrollmentService.java b/src/main/java/com/student/service/EnrollmentService.java new file mode 100644 index 0000000..19ac28b --- /dev/null +++ b/src/main/java/com/student/service/EnrollmentService.java @@ -0,0 +1,56 @@ +package com.student.service; + +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.dao.CourseDao; +import com.student.dao.EnrollmentDao; +import com.student.dao.impl.CourseDaoImpl; +import com.student.dao.impl.EnrollmentDaoImpl; + +import java.util.List; + +public class EnrollmentService { + + private final EnrollmentDao enrollmentDao; + private final CourseDao courseDao; + + public EnrollmentService() { + this(new EnrollmentDaoImpl(), new CourseDaoImpl()); + } + + EnrollmentService(EnrollmentDao enrollmentDao, CourseDao courseDao) { + this.enrollmentDao = enrollmentDao; + this.courseDao = courseDao; + } + + public boolean enroll(int studentId, int courseId) { + if (enrollmentDao.exists(studentId, courseId)) { + throw new RuntimeException("已选过该课程"); + } + Course course = courseDao.findById(courseId); + if (course == null) { + throw new RuntimeException("课程不存在"); + } + int count = enrollmentDao.countByCourse(courseId); + if (course.getMaxStudents() != null && count >= course.getMaxStudents()) { + throw new RuntimeException("课程已满"); + } + return enrollmentDao.enroll(studentId, courseId); + } + + public boolean cancel(int studentId, int courseId) { + return enrollmentDao.cancel(studentId, courseId); + } + + public List findByStudent(int studentId) { + return enrollmentDao.findByStudent(studentId); + } + + public List findByCourse(int courseId) { + return enrollmentDao.findByCourse(courseId); + } + + public int countByCourse(int courseId) { + return enrollmentDao.countByCourse(courseId); + } +} diff --git a/src/main/java/com/student/service/ImportService.java b/src/main/java/com/student/service/ImportService.java new file mode 100644 index 0000000..c2d0bdd --- /dev/null +++ b/src/main/java/com/student/service/ImportService.java @@ -0,0 +1,47 @@ +package com.student.service; + +import com.student.bean.UserImportTemp; +import com.student.dao.ImportTempDao; +import com.student.dao.impl.ImportTempDaoImpl; + +import java.util.List; + +public class ImportService { + + private final ImportTempDao importTempDao; + + public ImportService() { + this(new ImportTempDaoImpl()); + } + + ImportService(ImportTempDao importTempDao) { + this.importTempDao = importTempDao; + } + + public String uploadToTemp(List records) { + if (records == null || records.isEmpty()) { + throw new RuntimeException("导入数据为空"); + } + String batchId = java.util.UUID.randomUUID().toString().replace("-", ""); + for (UserImportTemp temp : records) { + temp.setBatchId(batchId); + if (temp.getPassword() == null || temp.getPassword().isEmpty()) { + temp.setPassword("123456"); + } + } + importTempDao.batchInsert(records); + return batchId; + } + + public List preview(String batchId) { + return importTempDao.findByBatch(batchId); + } + + public int confirm(String batchId) { + return importTempDao.confirmImport(batchId); + } + + public boolean rollback(String batchId) { + return importTempDao.deleteBatch(batchId); + } +} diff --git a/src/main/java/com/student/service/LeaveService.java b/src/main/java/com/student/service/LeaveService.java new file mode 100644 index 0000000..07d55dc --- /dev/null +++ b/src/main/java/com/student/service/LeaveService.java @@ -0,0 +1,50 @@ +package com.student.service; + +import com.student.bean.LeaveRequest; +import com.student.dao.LeaveDao; +import com.student.dao.impl.LeaveDaoImpl; + +import java.util.List; + +public class LeaveService { + + private final LeaveDao leaveDao; + + public LeaveService() { + this(new LeaveDaoImpl()); + } + + LeaveService(LeaveDao leaveDao) { + this.leaveDao = leaveDao; + } + + public boolean apply(LeaveRequest request) { + if (request.getStartDate() == null || request.getEndDate() == null) { + throw new RuntimeException("请假日期不能为空"); + } + if (request.getStartDate().after(request.getEndDate())) { + throw new RuntimeException("开始日期不能晚于结束日期"); + } + return leaveDao.submit(request); + } + + public boolean approve(int id, int approverId, String note) { + return leaveDao.approve(id, approverId, note); + } + + public boolean reject(int id, int approverId, String note) { + return leaveDao.reject(id, approverId, note); + } + + public List findPending() { + return leaveDao.findPending(); + } + + public List findByStudent(int studentId) { + return leaveDao.findByStudent(studentId); + } + + public LeaveRequest findById(int id) { + return leaveDao.findById(id); + } +} diff --git a/src/main/java/com/student/service/MessageService.java b/src/main/java/com/student/service/MessageService.java new file mode 100644 index 0000000..50bece5 --- /dev/null +++ b/src/main/java/com/student/service/MessageService.java @@ -0,0 +1,47 @@ +package com.student.service; + +import com.student.bean.Message; +import com.student.dao.MessageDao; +import com.student.dao.impl.MessageDaoImpl; + +import java.util.List; + +public class MessageService { + + private final MessageDao messageDao; + + public MessageService() { + this(new MessageDaoImpl()); + } + + MessageService(MessageDao messageDao) { + this.messageDao = messageDao; + } + + public boolean send(Message message) { + if (message.getReceiverId() == null) { + throw new RuntimeException("接收者不能为空"); + } + return messageDao.send(message); + } + + public boolean sendAll(Message message) { + return messageDao.sendAll(message); + } + + public List findInbox(int receiverId) { + return messageDao.findInbox(receiverId); + } + + public int countUnread(int receiverId) { + return messageDao.countUnread(receiverId); + } + + public boolean markRead(int messageId) { + return messageDao.markRead(messageId); + } + + public boolean markAllRead(int receiverId) { + return messageDao.markAllRead(receiverId); + } +} diff --git a/src/main/java/com/student/service/OrgService.java b/src/main/java/com/student/service/OrgService.java new file mode 100644 index 0000000..9102b1f --- /dev/null +++ b/src/main/java/com/student/service/OrgService.java @@ -0,0 +1,90 @@ +package com.student.service; + +import com.student.bean.Clazz; +import com.student.bean.Department; +import com.student.bean.Major; +import com.student.dao.OrgDao; +import com.student.dao.impl.OrgDaoImpl; + +import java.util.List; + +public class OrgService { + + private final OrgDao orgDao; + + public OrgService() { + this(new OrgDaoImpl()); + } + + OrgService(OrgDao orgDao) { + this.orgDao = orgDao; + } + + public List listDepts() { + return orgDao.findAllDepts(); + } + + public Department findDept(int id) { + return orgDao.findDeptById(id); + } + + public boolean addDept(Department dept) { + return orgDao.insertDept(dept); + } + + public boolean updateDept(Department dept) { + return orgDao.updateDept(dept); + } + + public boolean deleteDept(int id) { + return orgDao.deleteDept(id); + } + + public List listMajorsByDept(int deptId) { + return orgDao.findMajorsByDept(deptId); + } + + public List listAllMajors() { + return orgDao.findAllMajors(); + } + + public Major findMajor(int id) { + return orgDao.findMajorById(id); + } + + public boolean addMajor(Major major) { + return orgDao.insertMajor(major); + } + + public boolean updateMajor(Major major) { + return orgDao.updateMajor(major); + } + + public boolean deleteMajor(int id) { + return orgDao.deleteMajor(id); + } + + public List listClazzByMajor(int majorId) { + return orgDao.findClazzByMajor(majorId); + } + + public List listAllClazz() { + return orgDao.findAllClazz(); + } + + public Clazz findClazz(int id) { + return orgDao.findClazzById(id); + } + + public boolean addClazz(Clazz clazz) { + return orgDao.insertClazz(clazz); + } + + public boolean updateClazz(Clazz clazz) { + return orgDao.updateClazz(clazz); + } + + public boolean deleteClazz(int id) { + return orgDao.deleteClazz(id); + } +} diff --git a/src/main/java/com/student/service/ScoreService.java b/src/main/java/com/student/service/ScoreService.java new file mode 100644 index 0000000..1823e2d --- /dev/null +++ b/src/main/java/com/student/service/ScoreService.java @@ -0,0 +1,41 @@ +package com.student.service; + +import com.student.bean.CourseStats; +import com.student.bean.Score; +import com.student.dao.ScoreDao; +import com.student.dao.impl.ScoreDaoImpl; + +import java.util.List; + +public class ScoreService { + + private final ScoreDao scoreDao; + + public ScoreService() { + this(new ScoreDaoImpl()); + } + + ScoreService(ScoreDao scoreDao) { + this.scoreDao = scoreDao; + } + + public boolean save(Score score) { + return scoreDao.saveOrUpdate(score); + } + + public boolean saveBatch(List scores) { + return scoreDao.saveOrUpdateBatch(scores); + } + + public List findByStudent(int studentId) { + return scoreDao.findByStudent(studentId); + } + + public List findByCourse(int courseId) { + return scoreDao.findByCourse(courseId); + } + + public CourseStats getCourseStats(int courseId) { + return scoreDao.getCourseStats(courseId); + } +} diff --git a/src/main/java/com/student/service/StatService.java b/src/main/java/com/student/service/StatService.java new file mode 100644 index 0000000..4e1e395 --- /dev/null +++ b/src/main/java/com/student/service/StatService.java @@ -0,0 +1,36 @@ +package com.student.service; + +import com.student.bean.DashboardStats; +import com.student.dao.StatDao; +import com.student.dao.impl.StatDaoImpl; + +import java.util.Map; + +public class StatService { + + private final StatDao statDao; + + public StatService() { + this(new StatDaoImpl()); + } + + StatService(StatDao statDao) { + this.statDao = statDao; + } + + public DashboardStats getDashboard() { + return statDao.getDashboardStats(); + } + + public Map genderDistribution() { + return statDao.genderDistribution(); + } + + public Map ageDistribution() { + return statDao.ageDistribution(); + } + + public Map deptDistribution() { + return statDao.deptDistribution(); + } +} diff --git a/src/main/java/com/student/service/UserService.java b/src/main/java/com/student/service/UserService.java new file mode 100644 index 0000000..b0090f7 --- /dev/null +++ b/src/main/java/com/student/service/UserService.java @@ -0,0 +1,133 @@ +package com.student.service; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.bean.UserQuery; +import com.student.dao.UserDao; +import com.student.dao.impl.UserDaoImpl; +import com.student.util.PasswordUtil; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class UserService { + + private final UserDao userDao; + + public UserService() { + this(new UserDaoImpl()); + } + + UserService(UserDao userDao) { + this.userDao = userDao; + } + + public boolean register(User user) { + if (userDao.findByUsername(user.getUsername()) != null) { + throw new RuntimeException("用户名已存在"); + } + String salt = PasswordUtil.generateSalt(); + user.setSalt(salt); + user.setPassword(PasswordUtil.hash(user.getPassword(), salt)); + user.setRole(User.ROLE_STUDENT); + user.setStatus(User.STATUS_PENDING); + user.setFailCount(0); + return userDao.insert(user); + } + + public boolean adminAdd(User user) { + if (userDao.findByUsername(user.getUsername()) != null) { + throw new RuntimeException("用户名已存在"); + } + String salt = PasswordUtil.generateSalt(); + user.setSalt(salt); + if (user.getPassword() != null && !user.getPassword().isEmpty()) { + user.setPassword(PasswordUtil.hash(user.getPassword(), salt)); + } else { + user.setPassword(PasswordUtil.hash("123456", salt)); + } + if (user.getRole() == null) { + user.setRole(User.ROLE_STUDENT); + } + if (user.getStatus() == null) { + user.setStatus(User.STATUS_ACTIVE); + } + if (user.getRole() == User.ROLE_STUDENT && user.getStudentNo() == null) { + user.setStudentNo(generateStudentNo(user.getClazzId())); + } + user.setFailCount(0); + return userDao.insert(user); + } + + public boolean update(User user) { + return userDao.update(user); + } + + public boolean updateAvatar(int userId, String avatar) { + return userDao.updateAvatar(userId, avatar); + } + + public User findById(int id) { + return userDao.findById(id); + } + + public User findByUsername(String username) { + return userDao.findByUsername(username); + } + + public PageBean search(UserQuery query) { + if (query.getRole() == null) { + query.setRole(User.ROLE_STUDENT); + } + return userDao.findPage(query); + } + + public PageBean findPendingRegister(int currentPage, int pageSize) { + return userDao.findPendingRegister(currentPage, pageSize); + } + + public boolean changeStatus(int userId, int status) { + return userDao.updateStatus(userId, status); + } + + public boolean delete(int id) { + return userDao.delete(id); + } + + public List findAllStudents() { + return userDao.findAllStudents(); + } + + public List findAllTeachers() { + return userDao.findAllTeachers(); + } + + public String generateStudentNo(Integer clazzId) { + String prefix = new SimpleDateFormat("yyyy").format(new Date()); + if (clazzId != null) { + prefix = prefix + String.format("%03d", clazzId); + } + int seq = 1; + String studentNo; + do { + studentNo = prefix + String.format("%03d", seq++); + } while (userDao.findByStudentNo(studentNo) != null && seq < 1000); + return studentNo; + } + + public boolean approveRegister(int userId, Integer clazzId) { + User user = userDao.findById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + if (user.getStudentNo() == null) { + user.setStudentNo(generateStudentNo(clazzId)); + } + if (clazzId != null) { + user.setClazzId(clazzId); + } + user.setStatus(User.STATUS_ACTIVE); + return userDao.update(user); + } +} diff --git a/src/main/java/com/student/servlet/BaseServlet.java b/src/main/java/com/student/servlet/BaseServlet.java new file mode 100644 index 0000000..e44c81c --- /dev/null +++ b/src/main/java/com/student/servlet/BaseServlet.java @@ -0,0 +1,68 @@ +package com.student.servlet; + +import com.student.bean.ApiResult; +import com.student.bean.OperationLog; +import com.student.bean.User; +import com.student.dao.LogDao; +import com.student.dao.impl.LogDaoImpl; +import com.student.util.JsonUtil; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.net.URLEncoder; + +/** + * Servlet 基类,提供 JSON 输出、登录用户获取、操作日志等公共方法 + */ +public abstract class BaseServlet extends HttpServlet { + + private final LogDao logDao = new LogDaoImpl(); + + protected void writeJson(HttpServletResponse response, ApiResult result) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(JsonUtil.toJson(result)); + } + + protected User getLoginUser(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return null; + } + Object obj = session.getAttribute("loginUser"); + return obj instanceof User ? (User) obj : null; + } + + protected void logOperation(User user, String module, String action, String ip) { + if (user == null) { + return; + } + OperationLog log = new OperationLog(); + log.setUserId(user.getId()); + log.setUsername(user.getUsername()); + log.setModule(module); + log.setAction(action); + log.setIp(ip); + logDao.saveOperationLog(log); + } + + protected void redirectWithMsg(HttpServletResponse response, HttpServletRequest request, + String path, String msg) throws IOException { + redirectWithMsg(response, request, path, msg, "info"); + } + + protected void redirectWithMsg(HttpServletResponse response, HttpServletRequest request, + String path, String msg, String type) throws IOException { + String url = request.getContextPath() + path; + if (msg != null && !msg.isEmpty()) { + url += (path.contains("?") ? "&" : "?") + "msg=" + URLEncoder.encode(msg, "UTF-8"); + if (type != null && !type.isEmpty()) { + url += "&type=" + type; + } + } + response.sendRedirect(url); + } +} diff --git a/src/main/java/com/student/servlet/CaptchaServlet.java b/src/main/java/com/student/servlet/CaptchaServlet.java new file mode 100644 index 0000000..bb873c0 --- /dev/null +++ b/src/main/java/com/student/servlet/CaptchaServlet.java @@ -0,0 +1,34 @@ +package com.student.servlet; + +import com.student.util.CaptchaUtil; + +import javax.imageio.ImageIO; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * 图形验证码 Servlet + */ +@WebServlet("/captcha") +public class CaptchaServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String code = CaptchaUtil.generateCode(); + HttpSession session = request.getSession(); + session.setAttribute("captcha", code); + + response.setContentType("image/jpeg"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + + ImageIO.write(CaptchaUtil.generateImage(code), "JPEG", response.getOutputStream()); + } +} diff --git a/src/main/java/com/student/servlet/CheckUsernameServlet.java b/src/main/java/com/student/servlet/CheckUsernameServlet.java new file mode 100644 index 0000000..73062e6 --- /dev/null +++ b/src/main/java/com/student/servlet/CheckUsernameServlet.java @@ -0,0 +1,37 @@ +package com.student.servlet; + +import com.student.service.UserService; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Ajax 校验用户名是否可用 + */ +@WebServlet("/checkUsername") +public class CheckUsernameServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = ParamUtil.getString(request, "username"); + + if (username.isEmpty()) { + writeJson(response, com.student.bean.ApiResult.fail("用户名不能为空")); + return; + } + + boolean available = userService.findByUsername(username) == null; + if (available) { + writeJson(response, com.student.bean.ApiResult.ok("用户名可用", true)); + } else { + writeJson(response, com.student.bean.ApiResult.fail("用户名已被占用")); + } + } +} diff --git a/src/main/java/com/student/servlet/ForgotPasswordServlet.java b/src/main/java/com/student/servlet/ForgotPasswordServlet.java new file mode 100644 index 0000000..966470c --- /dev/null +++ b/src/main/java/com/student/servlet/ForgotPasswordServlet.java @@ -0,0 +1,92 @@ +package com.student.servlet; + +import com.student.dao.UserDao; +import com.student.dao.impl.UserDaoImpl; +import com.student.service.UserService; +import com.student.util.ParamUtil; +import com.student.util.PasswordUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.Random; + +/** + * 忘记密码(A-03 演示模式:验证码存入 Session) + */ +@WebServlet("/forgotPassword") +public class ForgotPasswordServlet extends BaseServlet { + + private static final String SESSION_CODE_KEY = "resetCode"; + private static final String SESSION_USERNAME_KEY = "resetUsername"; + + private final UserService userService = new UserService(); + private final UserDao userDao = new UserDaoImpl(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.getRequestDispatcher("/forgot_password.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String step = ParamUtil.getString(request, "step"); + HttpSession session = request.getSession(); + + if ("sendCode".equals(step)) { + String username = ParamUtil.getString(request, "username"); + com.student.bean.User user = userService.findByUsername(username); + if (user == null) { + redirectWithMsg(response, request, "/forgotPassword", "用户不存在"); + return; + } + String code = String.format("%06d", new Random().nextInt(999999)); + session.setAttribute(SESSION_CODE_KEY, code); + session.setAttribute(SESSION_USERNAME_KEY, username); + redirectWithMsg(response, request, "/forgotPassword?step=reset", + "演示模式:验证码为 " + code + "(已存入Session)"); + return; + } + + if ("reset".equals(step)) { + String username = ParamUtil.getString(request, "username"); + String code = ParamUtil.getString(request, "code"); + String newPassword = ParamUtil.getString(request, "newPassword"); + String confirmPassword = ParamUtil.getString(request, "confirmPassword"); + + String sessionCode = (String) session.getAttribute(SESSION_CODE_KEY); + String sessionUsername = (String) session.getAttribute(SESSION_USERNAME_KEY); + + if (sessionCode == null || !sessionCode.equals(code) + || sessionUsername == null || !sessionUsername.equals(username)) { + redirectWithMsg(response, request, "/forgotPassword?step=reset", "验证码错误"); + return; + } + + if (newPassword.isEmpty() || !newPassword.equals(confirmPassword)) { + redirectWithMsg(response, request, "/forgotPassword?step=reset", "两次密码不一致"); + return; + } + + com.student.bean.User user = userService.findByUsername(username); + if (user == null) { + redirectWithMsg(response, request, "/forgotPassword", "用户不存在"); + return; + } + + String salt = PasswordUtil.generateSalt(); + if (userDao.updatePassword(user.getId(), PasswordUtil.hash(newPassword, salt), salt)) { + session.removeAttribute(SESSION_CODE_KEY); + session.removeAttribute(SESSION_USERNAME_KEY); + redirectWithMsg(response, request, "/login.jsp", "密码重置成功,请登录"); + } else { + redirectWithMsg(response, request, "/forgotPassword?step=reset", "重置失败"); + } + } + } +} diff --git a/src/main/java/com/student/servlet/LoginServlet.java b/src/main/java/com/student/servlet/LoginServlet.java new file mode 100644 index 0000000..ff66467 --- /dev/null +++ b/src/main/java/com/student/servlet/LoginServlet.java @@ -0,0 +1,84 @@ +package com.student.servlet; + +import com.student.bean.User; +import com.student.filter.RememberMeFilter; +import com.student.listener.SessionListener; +import com.student.service.AuthService; +import com.student.util.CookieUtil; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * 用户登录 Servlet + */ +@WebServlet("/login") +public class LoginServlet extends BaseServlet { + + private static final int REMEMBER_MAX_AGE = 7 * 24 * 3600; + + private final AuthService authService = new AuthService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.sendRedirect(request.getContextPath() + "/login.jsp"); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = ParamUtil.getString(request, "username"); + String password = ParamUtil.getString(request, "password"); + String captcha = ParamUtil.getString(request, "captcha"); + boolean rememberMe = "on".equals(request.getParameter("rememberMe")) + || "true".equals(request.getParameter("rememberMe")); + + HttpSession session = request.getSession(); + String sessionCaptcha = (String) session.getAttribute("captcha"); + session.removeAttribute("captcha"); + + if (username.isEmpty() || password.isEmpty()) { + redirectWithMsg(response, request, "/login.jsp", "用户名和密码不能为空"); + return; + } + + if (sessionCaptcha == null || !sessionCaptcha.equalsIgnoreCase(captcha)) { + redirectWithMsg(response, request, "/login.jsp", "验证码错误"); + return; + } + + String ip = IpUtil.getClientIp(request); + try { + User user = authService.login(username, password, ip); + if (user == null) { + redirectWithMsg(response, request, "/login.jsp", "用户名或密码错误"); + return; + } + + session.setAttribute("loginUser", user); + SessionListener.registerSession(user, session); + + if (rememberMe) { + String token = authService.createRememberToken(user.getId()); + CookieUtil.setCookie(response, RememberMeFilter.REMEMBER_COOKIE_NAME, token, REMEMBER_MAX_AGE); + } + + if (user.isAdmin()) { + response.sendRedirect(request.getContextPath() + "/admin/dashboard"); + } else if (user.isTeacher()) { + response.sendRedirect(request.getContextPath() + "/teacher/home"); + } else { + response.sendRedirect(request.getContextPath() + "/user/profile"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/login.jsp", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/LogoutServlet.java b/src/main/java/com/student/servlet/LogoutServlet.java new file mode 100644 index 0000000..fc2dbb8 --- /dev/null +++ b/src/main/java/com/student/servlet/LogoutServlet.java @@ -0,0 +1,37 @@ +package com.student.servlet; + +import com.student.bean.User; +import com.student.filter.RememberMeFilter; +import com.student.service.AuthService; +import com.student.util.CookieUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * 退出登录 Servlet + */ +@WebServlet("/logout") +public class LogoutServlet extends BaseServlet { + + private final AuthService authService = new AuthService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(false); + User loginUser = getLoginUser(request); + if (loginUser != null) { + authService.logoutRemember(loginUser.getId()); + } + CookieUtil.deleteCookie(response, RememberMeFilter.REMEMBER_COOKIE_NAME); + if (session != null) { + session.invalidate(); + } + response.sendRedirect(request.getContextPath() + "/login.jsp"); + } +} diff --git a/src/main/java/com/student/servlet/PublicOrgServlet.java b/src/main/java/com/student/servlet/PublicOrgServlet.java new file mode 100644 index 0000000..b073505 --- /dev/null +++ b/src/main/java/com/student/servlet/PublicOrgServlet.java @@ -0,0 +1,33 @@ +package com.student.servlet; + +import com.student.bean.ApiResult; +import com.student.service.OrgService; +import com.student.util.ParamUtil; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 公开组织级联 API(注册页使用,无需登录) + */ +@WebServlet(urlPatterns = {"/public/org/majors", "/public/org/clazz", "/public/org/depts"}) +public class PublicOrgServlet extends BaseServlet { + + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + String uri = request.getRequestURI(); + if (uri.endsWith("/depts")) { + writeJson(response, ApiResult.ok(orgService.listDepts())); + } else if (uri.endsWith("/majors")) { + writeJson(response, ApiResult.ok(orgService.listMajorsByDept(ParamUtil.getInt(request, "deptId", 0)))); + } else if (uri.endsWith("/clazz")) { + writeJson(response, ApiResult.ok(orgService.listClazzByMajor(ParamUtil.getInt(request, "majorId", 0)))); + } else { + writeJson(response, ApiResult.fail("无效路径")); + } + } +} diff --git a/src/main/java/com/student/servlet/RegisterServlet.java b/src/main/java/com/student/servlet/RegisterServlet.java new file mode 100644 index 0000000..493b00a --- /dev/null +++ b/src/main/java/com/student/servlet/RegisterServlet.java @@ -0,0 +1,81 @@ +package com.student.servlet; + +import com.student.bean.User; +import com.student.service.OrgService; +import com.student.service.UserService; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 用户注册 Servlet(G-02 待审核) + */ +@WebServlet("/register") +public class RegisterServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("deptList", orgService.listDepts()); + request.getRequestDispatcher("/register.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = ParamUtil.getString(request, "username"); + String password = ParamUtil.getString(request, "password"); + String confirmPassword = ParamUtil.getString(request, "confirmPassword"); + String realName = ParamUtil.getString(request, "realName"); + String gender = ParamUtil.getString(request, "gender"); + Integer age = ParamUtil.getInteger(request, "age"); + String email = ParamUtil.getString(request, "email"); + String phone = ParamUtil.getString(request, "phone"); + String address = ParamUtil.getString(request, "address"); + Integer clazzId = ParamUtil.getInteger(request, "clazzId"); + + if (username.isEmpty() || password.isEmpty()) { + redirectWithMsg(response, request, "/register", "用户名和密码不能为空", "error"); + return; + } + + if (username.length() < 3 || username.length() > 20) { + redirectWithMsg(response, request, "/register", "用户名长度应在3-20个字符之间", "error"); + return; + } + + if (!password.equals(confirmPassword)) { + redirectWithMsg(response, request, "/register", "两次输入的密码不一致", "error"); + return; + } + + User user = new User(); + user.setUsername(username); + user.setPassword(password); + user.setRealName(realName); + user.setGender(gender); + user.setAge(age); + user.setEmail(email); + user.setPhone(phone); + user.setAddress(address); + user.setClazzId(clazzId); + user.setStatus(User.STATUS_PENDING); + + try { + if (userService.register(user)) { + redirectWithMsg(response, request, "/login.jsp", "注册成功,请等待管理员审核", "success"); + } else { + redirectWithMsg(response, request, "/register", "注册失败,请重试", "error"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/register", e.getMessage(), "error"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/AnnouncementServlet.java b/src/main/java/com/student/servlet/admin/AnnouncementServlet.java new file mode 100644 index 0000000..011a602 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/AnnouncementServlet.java @@ -0,0 +1,80 @@ +package com.student.servlet.admin; + +import com.student.bean.Announcement; +import com.student.service.AnnouncementService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +/** + * 管理员 - 公告 CRUD + */ +@WebServlet("/admin/announcement") +public class AnnouncementServlet extends BaseServlet { + + private final AnnouncementService announcementService = new AnnouncementService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + if (id > 0) { + request.setAttribute("announcement", announcementService.findById(id)); + } + request.setAttribute("annList", announcementService.findAll()); + request.getRequestDispatcher("/admin/announcement.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String action = ParamUtil.getString(request, "action"); + int id = ParamUtil.getInt(request, "id", 0); + + if ("delete".equals(action)) { + if (announcementService.delete(id)) { + logOperation(getLoginUser(request), "admin", "删除公告:" + id, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/announcement", "删除成功"); + } else { + redirectWithMsg(response, request, "/admin/announcement", "删除失败"); + } + return; + } + + Announcement ann = new Announcement(); + ann.setTitle(ParamUtil.getString(request, "title")); + ann.setContent(ParamUtil.getString(request, "content")); + ann.setIsTop(ParamUtil.getInt(request, "isTop", 0)); + String expireStr = ParamUtil.getString(request, "expireDate"); + if (!expireStr.isEmpty()) { + try { + ann.setExpireDate(new SimpleDateFormat("yyyy-MM-dd").parse(expireStr)); + } catch (ParseException ignored) { + } + } + + boolean success; + if (id > 0) { + ann.setId(id); + success = announcementService.update(ann); + } else { + ann.setPublisherId(getLoginUser(request).getId()); + success = announcementService.add(ann); + } + + if (success) { + logOperation(getLoginUser(request), "admin", "保存公告", IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/announcement", "保存成功"); + } else { + redirectWithMsg(response, request, "/admin/announcement", "保存失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/AttendanceManageServlet.java b/src/main/java/com/student/servlet/admin/AttendanceManageServlet.java new file mode 100644 index 0000000..75fa5fb --- /dev/null +++ b/src/main/java/com/student/servlet/admin/AttendanceManageServlet.java @@ -0,0 +1,80 @@ +package com.student.servlet.admin; + +import com.student.bean.Attendance; +import com.student.service.AttendanceService; +import com.student.service.CourseService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 管理员 - 考勤记录 + */ +@WebServlet("/admin/attendance") +public class AttendanceManageServlet extends BaseServlet { + + private final AttendanceService attendanceService = new AttendanceService(); + private final CourseService courseService = new CourseService(); + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("courseList", courseService.findAll()); + request.setAttribute("studentList", userService.findAllStudents()); + request.setAttribute("attList", attendanceService.findRecent(50)); + request.getRequestDispatcher("/admin/attendance.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + int studentId = ParamUtil.getInt(request, "studentId", 0); + String attDateStr = ParamUtil.getString(request, "attDate"); + int status = ParamUtil.getInt(request, "status", 1); + String remark = ParamUtil.getString(request, "remark"); + + if (courseId <= 0 || studentId <= 0 || attDateStr.isEmpty()) { + redirectWithMsg(response, request, "/admin/attendance", "参数无效"); + return; + } + + Date attDate; + try { + attDate = new SimpleDateFormat("yyyy-MM-dd").parse(attDateStr); + } catch (ParseException e) { + redirectWithMsg(response, request, "/admin/attendance", "日期格式错误"); + return; + } + + Attendance att = new Attendance(); + att.setStudentId(studentId); + att.setCourseId(courseId); + att.setAttDate(attDate); + att.setStatus(status); + att.setRemark(remark); + + List records = new ArrayList<>(); + records.add(att); + + if (attendanceService.recordBatch(records)) { + logOperation(getLoginUser(request), "admin", "记录考勤:course=" + courseId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/attendance", "保存成功"); + } else { + redirectWithMsg(response, request, "/admin/attendance", "保存失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/AttendanceStatsServlet.java b/src/main/java/com/student/servlet/admin/AttendanceStatsServlet.java new file mode 100644 index 0000000..929e6fa --- /dev/null +++ b/src/main/java/com/student/servlet/admin/AttendanceStatsServlet.java @@ -0,0 +1,40 @@ +package com.student.servlet.admin; + +import com.student.bean.AttendanceStats; +import com.student.bean.Course; +import com.student.service.AttendanceService; +import com.student.service.CourseService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * 管理员 - 考勤统计 + */ +@WebServlet("/admin/attendanceStats") +public class AttendanceStatsServlet extends BaseServlet { + + private final AttendanceService attendanceService = new AttendanceService(); + private final CourseService courseService = new CourseService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + List courseList = courseService.findAll(); + request.setAttribute("courseList", courseList); + AttendanceStats stats = new AttendanceStats(); + if (courseId > 0) { + request.setAttribute("courseId", courseId); + stats = attendanceService.getStatsByCourse(courseId); + } + request.setAttribute("stats", stats); + request.getRequestDispatcher("/admin/attendance_stats.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/BigScreenServlet.java b/src/main/java/com/student/servlet/admin/BigScreenServlet.java new file mode 100644 index 0000000..2bcc775 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/BigScreenServlet.java @@ -0,0 +1,26 @@ +package com.student.servlet.admin; + +import com.student.service.StatService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 数据大屏 + */ +@WebServlet("/admin/bigscreen") +public class BigScreenServlet extends BaseServlet { + + private final StatService statService = new StatService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("stats", statService.getDashboard()); + request.getRequestDispatcher("/admin/bigscreen.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/ChangeAuditServlet.java b/src/main/java/com/student/servlet/admin/ChangeAuditServlet.java new file mode 100644 index 0000000..8e647b1 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/ChangeAuditServlet.java @@ -0,0 +1,58 @@ +package com.student.servlet.admin; + +import com.student.service.ChangeRequestService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 变更申请审核(G-01) + */ +@WebServlet("/admin/changeAudit") +public class ChangeAuditServlet extends BaseServlet { + + private final ChangeRequestService changeRequestService = new ChangeRequestService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("changeList", changeRequestService.findPending()); + request.getRequestDispatcher("/admin/change_audit.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + String action = ParamUtil.getString(request, "action"); + String note = ParamUtil.getString(request, "note"); + int approverId = getLoginUser(request).getId(); + + try { + boolean success; + if ("approve".equals(action)) { + success = changeRequestService.approve(id, approverId, note); + } else if ("reject".equals(action)) { + success = changeRequestService.reject(id, approverId, note); + } else { + redirectWithMsg(response, request, "/admin/changeAudit", "无效操作"); + return; + } + + if (success) { + logOperation(getLoginUser(request), "admin", "变更审核:" + action, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/changeAudit", "操作成功"); + } else { + redirectWithMsg(response, request, "/admin/changeAudit", "操作失败"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/admin/changeAudit", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/CourseDeleteServlet.java b/src/main/java/com/student/servlet/admin/CourseDeleteServlet.java new file mode 100644 index 0000000..c0dcbde --- /dev/null +++ b/src/main/java/com/student/servlet/admin/CourseDeleteServlet.java @@ -0,0 +1,33 @@ +package com.student.servlet.admin; + +import com.student.service.CourseService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 删除课程 + */ +@WebServlet("/admin/courseDelete") +public class CourseDeleteServlet extends BaseServlet { + + private final CourseService courseService = new CourseService(); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + if (id > 0 && courseService.delete(id)) { + logOperation(getLoginUser(request), "admin", "删除课程:" + id, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/courseList", "删除成功"); + } else { + redirectWithMsg(response, request, "/admin/courseList", "删除失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/CourseEditServlet.java b/src/main/java/com/student/servlet/admin/CourseEditServlet.java new file mode 100644 index 0000000..3d299f0 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/CourseEditServlet.java @@ -0,0 +1,71 @@ +package com.student.servlet.admin; + +import com.student.bean.Course; +import com.student.service.CourseService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; + +/** + * 管理员 - 课程编辑 + */ +@WebServlet("/admin/courseEdit") +public class CourseEditServlet extends BaseServlet { + + private final CourseService courseService = new CourseService(); + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + if (id > 0) { + request.setAttribute("course", courseService.findById(id)); + } + request.setAttribute("teacherList", userService.findAllTeachers()); + request.getRequestDispatcher("/admin/course_edit.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + Course course = new Course(); + course.setId(id > 0 ? id : null); + course.setCourseNo(ParamUtil.getString(request, "courseNo")); + course.setName(ParamUtil.getString(request, "name")); + String creditStr = ParamUtil.getString(request, "credit"); + if (!creditStr.isEmpty()) { + course.setCredit(new BigDecimal(creditStr)); + } + course.setTeacherId(ParamUtil.getInteger(request, "teacherId")); + course.setMaxStudents(ParamUtil.getInteger(request, "maxStudents")); + course.setSchedule(ParamUtil.getString(request, "schedule")); + + try { + boolean success; + if (id > 0) { + course.setId(id); + success = courseService.update(course); + } else { + success = courseService.add(course); + } + if (success) { + logOperation(getLoginUser(request), "admin", "编辑课程:" + course.getCourseNo(), IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/courseList", "保存成功"); + } else { + redirectWithMsg(response, request, "/admin/courseEdit" + (id > 0 ? "?id=" + id : ""), "保存失败"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/admin/courseEdit" + (id > 0 ? "?id=" + id : ""), e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/CourseListServlet.java b/src/main/java/com/student/servlet/admin/CourseListServlet.java new file mode 100644 index 0000000..1d23574 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/CourseListServlet.java @@ -0,0 +1,33 @@ +package com.student.servlet.admin; + +import com.student.service.CourseService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 课程列表 + */ +@WebServlet("/admin/courseList") +public class CourseListServlet extends BaseServlet { + + private final CourseService courseService = new CourseService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String keyword = ParamUtil.getString(request, "keyword"); + int page = ParamUtil.getInt(request, "page", 1); + int pageSize = ParamUtil.getInt(request, "pageSize", 10); + com.student.bean.PageBean pageBean = courseService.findPage(keyword, page, pageSize); + request.setAttribute("pageBean", pageBean); + request.setAttribute("courseList", pageBean.getData()); + request.setAttribute("keyword", keyword); + request.getRequestDispatcher("/admin/course_list.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/DashboardServlet.java b/src/main/java/com/student/servlet/admin/DashboardServlet.java new file mode 100644 index 0000000..1a94398 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/DashboardServlet.java @@ -0,0 +1,29 @@ +package com.student.servlet.admin; + +import com.student.service.AnnouncementService; +import com.student.service.StatService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 仪表盘 + */ +@WebServlet("/admin/dashboard") +public class DashboardServlet extends BaseServlet { + + private final StatService statService = new StatService(); + private final AnnouncementService announcementService = new AnnouncementService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("stats", statService.getDashboard()); + request.setAttribute("announcements", announcementService.findActive()); + request.getRequestDispatcher("/admin/dashboard.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/ImportPreviewServlet.java b/src/main/java/com/student/servlet/admin/ImportPreviewServlet.java new file mode 100644 index 0000000..2554733 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/ImportPreviewServlet.java @@ -0,0 +1,66 @@ +package com.student.servlet.admin; + +import com.student.service.ImportService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 导入预览(G-03) + */ +@WebServlet("/admin/importPreview") +public class ImportPreviewServlet extends BaseServlet { + + private final ImportService importService = new ImportService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String batchId = ParamUtil.getString(request, "batchId"); + if (batchId.isEmpty()) { + Object lastBatch = request.getSession().getAttribute("lastImportBatchId"); + if (lastBatch instanceof String && !((String) lastBatch).isEmpty()) { + batchId = (String) lastBatch; + } + } + if (batchId.isEmpty()) { + request.setAttribute("msg", "请先上传 CSV 文件"); + request.getRequestDispatcher("/admin/user_upload.jsp").forward(request, response); + return; + } + request.setAttribute("batchId", batchId); + request.setAttribute("importList", importService.preview(batchId)); + request.getRequestDispatcher("/admin/import_preview.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String batchId = ParamUtil.getString(request, "batchId"); + String action = ParamUtil.getString(request, "action"); + + if (batchId.isEmpty()) { + redirectWithMsg(response, request, "/admin/importPreview", "批次ID无效"); + return; + } + + if ("confirm".equals(action)) { + int count = importService.confirm(batchId); + request.getSession().removeAttribute("lastImportBatchId"); + logOperation(getLoginUser(request), "admin", "确认导入:" + count + "条", IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/userList", "成功导入 " + count + " 条记录"); + } else if ("cancel".equals(action)) { + importService.rollback(batchId); + request.getSession().removeAttribute("lastImportBatchId"); + redirectWithMsg(response, request, "/admin/importPreview", "已取消导入"); + } else { + redirectWithMsg(response, request, "/admin/importPreview", "无效操作"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/LeaveApproveServlet.java b/src/main/java/com/student/servlet/admin/LeaveApproveServlet.java new file mode 100644 index 0000000..155575a --- /dev/null +++ b/src/main/java/com/student/servlet/admin/LeaveApproveServlet.java @@ -0,0 +1,54 @@ +package com.student.servlet.admin; + +import com.student.service.LeaveService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 请假审批 + */ +@WebServlet("/admin/leaveApprove") +public class LeaveApproveServlet extends BaseServlet { + + private final LeaveService leaveService = new LeaveService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("leaveList", leaveService.findPending()); + request.getRequestDispatcher("/admin/leave_approve.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + String action = ParamUtil.getString(request, "action"); + String note = ParamUtil.getString(request, "note"); + int approverId = getLoginUser(request).getId(); + + boolean success; + if ("approve".equals(action)) { + success = leaveService.approve(id, approverId, note); + } else if ("reject".equals(action)) { + success = leaveService.reject(id, approverId, note); + } else { + redirectWithMsg(response, request, "/admin/leaveApprove", "无效操作"); + return; + } + + if (success) { + logOperation(getLoginUser(request), "admin", "请假审批:" + action + ",id=" + id, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/leaveApprove", "操作成功"); + } else { + redirectWithMsg(response, request, "/admin/leaveApprove", "操作失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/LogViewServlet.java b/src/main/java/com/student/servlet/admin/LogViewServlet.java new file mode 100644 index 0000000..201336b --- /dev/null +++ b/src/main/java/com/student/servlet/admin/LogViewServlet.java @@ -0,0 +1,40 @@ +package com.student.servlet.admin; + +import com.student.dao.LogDao; +import com.student.dao.impl.LogDaoImpl; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 登录/操作日志查看 + */ +@WebServlet("/admin/logs") +public class LogViewServlet extends BaseServlet { + + private final LogDao logDao = new LogDaoImpl(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String tab = ParamUtil.getString(request, "tab"); + if (tab.isEmpty()) { + tab = "login"; + } + int page = ParamUtil.getInt(request, "page", 1); + int pageSize = ParamUtil.getInt(request, "pageSize", 20); + + request.setAttribute("tab", tab); + if ("operation".equals(tab)) { + request.setAttribute("opLogs", logDao.findOperationLogs(page, pageSize).getData()); + } else { + request.setAttribute("loginLogs", logDao.findLoginLogs(page, pageSize).getData()); + } + request.getRequestDispatcher("/admin/logs.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/MessageSendServlet.java b/src/main/java/com/student/servlet/admin/MessageSendServlet.java new file mode 100644 index 0000000..0d8f10a --- /dev/null +++ b/src/main/java/com/student/servlet/admin/MessageSendServlet.java @@ -0,0 +1,60 @@ +package com.student.servlet.admin; + +import com.student.bean.Message; +import com.student.service.MessageService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 发送消息 + */ +@WebServlet("/admin/messageSend") +public class MessageSendServlet extends BaseServlet { + + private final MessageService messageService = new MessageService(); + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("userList", userService.findAllStudents()); + request.getRequestDispatcher("/admin/message_send.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String title = ParamUtil.getString(request, "title"); + String content = ParamUtil.getString(request, "content"); + String sendType = ParamUtil.getString(request, "sendType"); + Integer receiverId = ParamUtil.getInteger(request, "receiverId"); + + Message message = new Message(); + message.setSenderId(getLoginUser(request).getId()); + message.setTitle(title); + message.setContent(content); + + boolean success; + if ("all".equals(sendType)) { + success = messageService.sendAll(message); + } else { + message.setReceiverId(receiverId); + success = messageService.send(message); + } + + if (success) { + logOperation(getLoginUser(request), "admin", "发送消息:" + title, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/messageSend", "发送成功"); + } else { + redirectWithMsg(response, request, "/admin/messageSend", "发送失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/OnlineUserServlet.java b/src/main/java/com/student/servlet/admin/OnlineUserServlet.java new file mode 100644 index 0000000..ab3598e --- /dev/null +++ b/src/main/java/com/student/servlet/admin/OnlineUserServlet.java @@ -0,0 +1,39 @@ +package com.student.servlet.admin; + +import com.student.listener.SessionListener; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 在线用户管理 + */ +@WebServlet("/admin/online") +public class OnlineUserServlet extends BaseServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("onlineList", SessionListener.getOnlineUsers()); + request.setAttribute("onlineCount", SessionListener.getOnlineCount()); + request.getRequestDispatcher("/admin/online.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int userId = ParamUtil.getInt(request, "userId", 0); + if (userId > 0 && SessionListener.invalidateSession(userId)) { + logOperation(getLoginUser(request), "admin", "踢出用户:" + userId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/online", "已强制下线"); + } else { + redirectWithMsg(response, request, "/admin/online", "操作失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/OrgApiServlet.java b/src/main/java/com/student/servlet/admin/OrgApiServlet.java new file mode 100644 index 0000000..fdd29ff --- /dev/null +++ b/src/main/java/com/student/servlet/admin/OrgApiServlet.java @@ -0,0 +1,36 @@ +package com.student.servlet.admin; + +import com.student.bean.ApiResult; +import com.student.service.OrgService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 组织级联 API - 专业/班级 JSON + */ +@WebServlet(urlPatterns = {"/api/org/majors", "/api/org/clazz"}) +public class OrgApiServlet extends BaseServlet { + + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String uri = request.getRequestURI(); + if (uri.endsWith("/majors")) { + int deptId = ParamUtil.getInt(request, "deptId", 0); + writeJson(response, ApiResult.ok(orgService.listMajorsByDept(deptId))); + } else if (uri.endsWith("/clazz")) { + int majorId = ParamUtil.getInt(request, "majorId", 0); + writeJson(response, ApiResult.ok(orgService.listClazzByMajor(majorId))); + } else { + writeJson(response, ApiResult.fail("无效路径")); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/OrgManageServlet.java b/src/main/java/com/student/servlet/admin/OrgManageServlet.java new file mode 100644 index 0000000..67eb6e5 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/OrgManageServlet.java @@ -0,0 +1,73 @@ +package com.student.servlet.admin; + +import com.student.bean.Clazz; +import com.student.bean.Department; +import com.student.bean.Major; +import com.student.service.OrgService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 组织架构管理 + */ +@WebServlet("/admin/org") +public class OrgManageServlet extends BaseServlet { + + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("deptList", orgService.listDepts()); + request.setAttribute("majorList", orgService.listAllMajors()); + request.setAttribute("clazzList", orgService.listAllClazz()); + request.getRequestDispatcher("/admin/org.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String type = ParamUtil.getString(request, "type"); + String name = ParamUtil.getString(request, "name"); + String code = ParamUtil.getString(request, "code"); + boolean success = false; + + try { + if ("dept".equals(type)) { + Department dept = new Department(); + dept.setName(name); + dept.setCode(code); + success = orgService.addDept(dept); + } else if ("major".equals(type)) { + Major major = new Major(); + major.setName(name); + major.setCode(code); + major.setDepartmentId(ParamUtil.getInt(request, "deptId", 0)); + success = orgService.addMajor(major); + } else if ("clazz".equals(type)) { + Clazz clazz = new Clazz(); + clazz.setName(name); + clazz.setMajorId(ParamUtil.getInt(request, "majorId", 0)); + clazz.setGrade(ParamUtil.getString(request, "grade")); + success = orgService.addClazz(clazz); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/admin/org", e.getMessage()); + return; + } + + if (success) { + logOperation(getLoginUser(request), "admin", "添加组织:" + type, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/org", "添加成功"); + } else { + redirectWithMsg(response, request, "/admin/org", "添加失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/RegAuditServlet.java b/src/main/java/com/student/servlet/admin/RegAuditServlet.java new file mode 100644 index 0000000..45e2243 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/RegAuditServlet.java @@ -0,0 +1,66 @@ +package com.student.servlet.admin; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 注册审核(G-02) + */ +@WebServlet("/admin/regAudit") +public class RegAuditServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int page = ParamUtil.getInt(request, "page", 1); + int pageSize = ParamUtil.getInt(request, "pageSize", 10); + PageBean pageBean = userService.findPendingRegister(page, pageSize); + request.setAttribute("pageBean", pageBean); + request.setAttribute("pendingList", pageBean.getData()); + request.getRequestDispatcher("/admin/reg_audit.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int userId = ParamUtil.getInt(request, "userId", 0); + if (userId <= 0) { + userId = ParamUtil.getInt(request, "id", 0); + } + String action = ParamUtil.getString(request, "action"); + Integer clazzId = ParamUtil.getInteger(request, "clazzId"); + + if (userId <= 0) { + redirectWithMsg(response, request, "/admin/regAudit", "参数无效"); + return; + } + + try { + if ("approve".equals(action)) { + userService.approveRegister(userId, clazzId); + logOperation(getLoginUser(request), "admin", "批准注册:" + userId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/regAudit", "审核通过"); + } else if ("reject".equals(action)) { + userService.changeStatus(userId, com.student.bean.User.STATUS_DROPPED); + logOperation(getLoginUser(request), "admin", "拒绝注册:" + userId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/regAudit", "已拒绝"); + } else { + redirectWithMsg(response, request, "/admin/regAudit", "无效操作"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/admin/regAudit", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/ScoreManageServlet.java b/src/main/java/com/student/servlet/admin/ScoreManageServlet.java new file mode 100644 index 0000000..82f6bcd --- /dev/null +++ b/src/main/java/com/student/servlet/admin/ScoreManageServlet.java @@ -0,0 +1,89 @@ +package com.student.servlet.admin; + +import com.student.bean.Enrollment; +import com.student.bean.Score; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.service.ScoreService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 管理员 - 批量录入成绩 + */ +@WebServlet("/admin/scoreManage") +public class ScoreManageServlet extends BaseServlet { + + private final ScoreService scoreService = new ScoreService(); + private final CourseService courseService = new CourseService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + request.setAttribute("courseList", courseService.findAll()); + if (courseId > 0) { + request.setAttribute("courseId", courseId); + List studentList = enrollmentService.findByCourse(courseId); + request.setAttribute("studentList", studentList); + Map scoreMap = new HashMap<>(); + Map remarkMap = new HashMap<>(); + for (Score score : scoreService.findByCourse(courseId)) { + scoreMap.put(score.getStudentId(), score.getScore()); + remarkMap.put(score.getStudentId(), score.getRemark()); + } + request.setAttribute("scoreMap", scoreMap); + request.setAttribute("remarkMap", remarkMap); + } + request.getRequestDispatcher("/admin/score_manage.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + String[] studentIds = request.getParameterValues("studentIds"); + String[] scoreValues = request.getParameterValues("scores"); + + if (courseId <= 0 || studentIds == null) { + redirectWithMsg(response, request, "/admin/scoreManage", "参数无效"); + return; + } + + List scores = new ArrayList<>(); + for (int i = 0; i < studentIds.length; i++) { + try { + String val = scoreValues != null && i < scoreValues.length ? scoreValues[i] : ""; + if (val.isEmpty()) { + continue; + } + Score score = new Score(); + score.setStudentId(Integer.parseInt(studentIds[i])); + score.setCourseId(courseId); + score.setScore(new BigDecimal(val.trim())); + scores.add(score); + } catch (NumberFormatException ignored) { + } + } + + if (scoreService.saveBatch(scores)) { + logOperation(getLoginUser(request), "admin", "批量录入成绩:course=" + courseId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/scoreManage?courseId=" + courseId, "保存成功"); + } else { + redirectWithMsg(response, request, "/admin/scoreManage?courseId=" + courseId, "保存失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/ScoreStatsServlet.java b/src/main/java/com/student/servlet/admin/ScoreStatsServlet.java new file mode 100644 index 0000000..c84ea9c --- /dev/null +++ b/src/main/java/com/student/servlet/admin/ScoreStatsServlet.java @@ -0,0 +1,46 @@ +package com.student.servlet.admin; + +import com.student.bean.Course; +import com.student.bean.CourseStats; +import com.student.service.CourseService; +import com.student.service.ScoreService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 管理员 - 课程成绩统计报告 + */ +@WebServlet("/admin/scoreStats") +public class ScoreStatsServlet extends BaseServlet { + + private final ScoreService scoreService = new ScoreService(); + private final CourseService courseService = new CourseService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + List courseList = courseService.findAll(); + request.setAttribute("courseList", courseList); + + List statsList = new ArrayList<>(); + if (courseId > 0) { + request.setAttribute("courseId", courseId); + statsList.add(scoreService.getCourseStats(courseId)); + } else { + for (Course course : courseList) { + statsList.add(scoreService.getCourseStats(course.getId())); + } + } + request.setAttribute("statsList", statsList); + request.getRequestDispatcher("/admin/score_stats.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/StatApiServlet.java b/src/main/java/com/student/servlet/admin/StatApiServlet.java new file mode 100644 index 0000000..099ceeb --- /dev/null +++ b/src/main/java/com/student/servlet/admin/StatApiServlet.java @@ -0,0 +1,35 @@ +package com.student.servlet.admin; + +import com.student.bean.ApiResult; +import com.student.listener.SessionListener; +import com.student.service.StatService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * 统计 API - JSON 图表数据 + */ +@WebServlet("/api/stats") +public class StatApiServlet extends BaseServlet { + + private final StatService statService = new StatService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + Map data = new HashMap<>(); + data.put("dashboard", statService.getDashboard()); + data.put("gender", statService.genderDistribution()); + data.put("age", statService.ageDistribution()); + data.put("dept", statService.deptDistribution()); + data.put("onlineCount", SessionListener.getOnlineCount()); + writeJson(response, ApiResult.ok(data)); + } +} diff --git a/src/main/java/com/student/servlet/admin/UserAddServlet.java b/src/main/java/com/student/servlet/admin/UserAddServlet.java new file mode 100644 index 0000000..a763b7d --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserAddServlet.java @@ -0,0 +1,74 @@ +package com.student.servlet.admin; + +import com.student.bean.User; +import com.student.service.OrgService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 新增用户 + */ +@WebServlet("/admin/userAdd") +public class UserAddServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("clazzList", orgService.listAllClazz()); + request.getRequestDispatcher("/admin/user_add.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = ParamUtil.getString(request, "username"); + String password = ParamUtil.getString(request, "password"); + String realName = ParamUtil.getString(request, "realName"); + String gender = ParamUtil.getString(request, "gender"); + Integer age = ParamUtil.getInteger(request, "age"); + String email = ParamUtil.getString(request, "email"); + String phone = ParamUtil.getString(request, "phone"); + String address = ParamUtil.getString(request, "address"); + Integer clazzId = ParamUtil.getInteger(request, "clazzId"); + + if (username.isEmpty()) { + redirectWithMsg(response, request, "/admin/userAdd", "用户名不能为空"); + return; + } + + User user = new User(); + user.setUsername(username); + user.setPassword(password); + user.setRealName(realName); + user.setGender(gender); + user.setAge(age); + user.setEmail(email); + user.setPhone(phone); + user.setAddress(address); + user.setClazzId(clazzId); + user.setRole(User.ROLE_STUDENT); + user.setStatus(User.STATUS_ACTIVE); + + try { + if (userService.adminAdd(user)) { + logOperation(getLoginUser(request), "admin", "新增用户:" + username, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/userList", "新增用户成功"); + } else { + redirectWithMsg(response, request, "/admin/userAdd", "新增用户失败"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/admin/userAdd", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/UserDeleteServlet.java b/src/main/java/com/student/servlet/admin/UserDeleteServlet.java new file mode 100644 index 0000000..b2b2dd4 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserDeleteServlet.java @@ -0,0 +1,45 @@ +package com.student.servlet.admin; + +import com.student.bean.ApiResult; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 删除用户(支持 Ajax) + */ +@WebServlet("/admin/userDelete") +public class UserDeleteServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + String ajax = ParamUtil.getString(request, "ajax"); + + boolean success = id > 0 && userService.delete(id); + + if ("true".equals(ajax)) { + if (success) { + writeJson(response, ApiResult.ok("删除成功")); + } else { + writeJson(response, ApiResult.fail("删除失败,用户不存在或为管理员")); + } + return; + } + + if (success) { + logOperation(getLoginUser(request), "admin", "删除用户:" + id, IpUtil.getClientIp(request)); + } + redirectWithMsg(response, request, "/admin/userList", success ? "删除成功" : "删除失败"); + } +} diff --git a/src/main/java/com/student/servlet/admin/UserDetailServlet.java b/src/main/java/com/student/servlet/admin/UserDetailServlet.java new file mode 100644 index 0000000..07c15cf --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserDetailServlet.java @@ -0,0 +1,50 @@ +package com.student.servlet.admin; + +import com.student.bean.User; +import com.student.service.EnrollmentService; +import com.student.service.ScoreService; +import com.student.service.AttendanceService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 学生详情(B-05) + */ +@WebServlet("/admin/userDetail") +public class UserDetailServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + private final ScoreService scoreService = new ScoreService(); + private final AttendanceService attendanceService = new AttendanceService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + if (id <= 0) { + redirectWithMsg(response, request, "/admin/userList", "用户ID无效"); + return; + } + + User user = userService.findById(id); + if (user == null || user.isAdmin()) { + redirectWithMsg(response, request, "/admin/userList", "用户不存在"); + return; + } + + request.setAttribute("user", user); + request.setAttribute("enrollments", enrollmentService.findByStudent(id)); + request.setAttribute("scores", scoreService.findByStudent(id)); + request.setAttribute("attendances", attendanceService.findByStudent(id)); + request.setAttribute("attStats", attendanceService.getStatsByStudent(id)); + request.getRequestDispatcher("/admin/user_detail.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/UserDownloadServlet.java b/src/main/java/com/student/servlet/admin/UserDownloadServlet.java new file mode 100644 index 0000000..7967057 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserDownloadServlet.java @@ -0,0 +1,66 @@ +package com.student.servlet.admin; + +import com.student.bean.User; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * 管理员 - 下载用户信息(CSV 格式) + */ +@WebServlet("/admin/userDownload") +public class UserDownloadServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + List users = userService.findAllStudents(); + + response.setContentType("text/csv;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment;filename=users.csv"); + + PrintWriter writer = new PrintWriter( + new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8)); + + writer.write('\ufeff'); + writer.println("ID,用户名,学号,真实姓名,性别,年龄,邮箱,电话,地址,注册时间"); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for (User user : users) { + writer.printf("%d,%s,%s,%s,%s,%s,%s,%s,%s,%s%n", + user.getId(), + escapeCsv(user.getUsername()), + escapeCsv(user.getStudentNo()), + escapeCsv(user.getRealName()), + escapeCsv(user.getGender()), + user.getAge() != null ? user.getAge().toString() : "", + escapeCsv(user.getEmail()), + escapeCsv(user.getPhone()), + escapeCsv(user.getAddress()), + user.getCreateTime() != null ? sdf.format(user.getCreateTime()) : ""); + } + writer.flush(); + } + + private String escapeCsv(String value) { + if (value == null) { + return ""; + } + if (value.contains(",") || value.contains("\"") || value.contains("\n")) { + return "\"" + value.replace("\"", "\"\"") + "\""; + } + return value; + } +} diff --git a/src/main/java/com/student/servlet/admin/UserEditServlet.java b/src/main/java/com/student/servlet/admin/UserEditServlet.java new file mode 100644 index 0000000..7c6ba2a --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserEditServlet.java @@ -0,0 +1,89 @@ +package com.student.servlet.admin; + +import com.student.bean.User; +import com.student.dao.UserDao; +import com.student.dao.impl.UserDaoImpl; +import com.student.service.OrgService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; +import com.student.util.PasswordUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 编辑用户 + */ +@WebServlet("/admin/userEdit") +public class UserEditServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final UserDao userDao = new UserDaoImpl(); + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + if (id <= 0) { + response.sendRedirect(request.getContextPath() + "/admin/userList"); + return; + } + + User user = userService.findById(id); + if (user == null || user.isAdmin()) { + redirectWithMsg(response, request, "/admin/userList", "用户不存在或不可编辑"); + return; + } + + request.setAttribute("user", user); + request.setAttribute("clazzList", orgService.listAllClazz()); + request.getRequestDispatcher("/admin/user_edit.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + String password = ParamUtil.getString(request, "password"); + String realName = ParamUtil.getString(request, "realName"); + String gender = ParamUtil.getString(request, "gender"); + Integer age = ParamUtil.getInteger(request, "age"); + String email = ParamUtil.getString(request, "email"); + String phone = ParamUtil.getString(request, "phone"); + String address = ParamUtil.getString(request, "address"); + Integer clazzId = ParamUtil.getInteger(request, "clazzId"); + + User existing = userService.findById(id); + if (existing == null || existing.isAdmin()) { + redirectWithMsg(response, request, "/admin/userList", "用户不存在或不可编辑"); + return; + } + + existing.setRealName(realName); + existing.setGender(gender); + existing.setAge(age); + existing.setEmail(email); + existing.setPhone(phone); + existing.setAddress(address); + existing.setClazzId(clazzId); + + boolean success = userService.update(existing); + if (success && !password.isEmpty()) { + String salt = PasswordUtil.generateSalt(); + success = userDao.updatePassword(id, PasswordUtil.hash(password, salt), salt); + } + + if (success) { + logOperation(getLoginUser(request), "admin", "编辑用户:" + id, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/userList", "修改用户成功"); + } else { + redirectWithMsg(response, request, "/admin/userEdit?id=" + id, "修改用户失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/UserExcelServlet.java b/src/main/java/com/student/servlet/admin/UserExcelServlet.java new file mode 100644 index 0000000..22c5de8 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserExcelServlet.java @@ -0,0 +1,83 @@ +package com.student.servlet.admin; + +import com.student.bean.User; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * 管理员 - Excel 导出用户(B-06) + */ +@WebServlet("/admin/userExcel") +public class UserExcelServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + List users = userService.findAllStudents(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("学生列表"); + Row header = sheet.createRow(0); + String[] titles = {"ID", "用户名", "学号", "真实姓名", "性别", "年龄", "邮箱", "电话", "地址", "班级", "状态", "注册时间"}; + for (int i = 0; i < titles.length; i++) { + header.createCell(i).setCellValue(titles[i]); + } + + int rowNum = 1; + for (User user : users) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(user.getId()); + row.createCell(1).setCellValue(nullToEmpty(user.getUsername())); + row.createCell(2).setCellValue(nullToEmpty(user.getStudentNo())); + row.createCell(3).setCellValue(nullToEmpty(user.getRealName())); + row.createCell(4).setCellValue(nullToEmpty(user.getGender())); + row.createCell(5).setCellValue(user.getAge() != null ? user.getAge() : 0); + row.createCell(6).setCellValue(nullToEmpty(user.getEmail())); + row.createCell(7).setCellValue(nullToEmpty(user.getPhone())); + row.createCell(8).setCellValue(nullToEmpty(user.getAddress())); + row.createCell(9).setCellValue(nullToEmpty(user.getClazzName())); + row.createCell(10).setCellValue(statusText(user.getStatus())); + row.createCell(11).setCellValue(user.getCreateTime() != null ? sdf.format(user.getCreateTime()) : ""); + } + + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=users.xlsx"); + workbook.write(response.getOutputStream()); + logOperation(getLoginUser(request), "admin", "导出Excel", IpUtil.getClientIp(request)); + } + } + + private String nullToEmpty(String s) { + return s == null ? "" : s; + } + + private String statusText(Integer status) { + if (status == null) { + return ""; + } + switch (status) { + case User.STATUS_PENDING: return "待审核"; + case User.STATUS_ACTIVE: return "在读"; + case User.STATUS_SUSPEND: return "休学"; + case User.STATUS_GRAD: return "毕业"; + case User.STATUS_DROP: return "退学"; + default: return String.valueOf(status); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/UserListServlet.java b/src/main/java/com/student/servlet/admin/UserListServlet.java new file mode 100644 index 0000000..f957590 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserListServlet.java @@ -0,0 +1,46 @@ +package com.student.servlet.admin; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.bean.UserQuery; +import com.student.service.OrgService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 用户列表(B-07 高级搜索) + */ +@WebServlet("/admin/userList") +public class UserListServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final OrgService orgService = new OrgService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + UserQuery query = new UserQuery(); + query.setKeyword(ParamUtil.getString(request, "keyword")); + query.setClazzId(ParamUtil.getInteger(request, "clazzId")); + query.setGender(ParamUtil.getString(request, "gender")); + query.setAgeMin(ParamUtil.getInteger(request, "ageMin")); + query.setAgeMax(ParamUtil.getInteger(request, "ageMax")); + query.setStatus(ParamUtil.getInteger(request, "status")); + query.setRole(User.ROLE_STUDENT); + query.setCurrentPage(ParamUtil.getInt(request, "page", 1)); + query.setPageSize(ParamUtil.getInt(request, "pageSize", 10)); + + PageBean pageBean = userService.search(query); + request.setAttribute("pageBean", pageBean); + request.setAttribute("query", query); + request.setAttribute("clazzList", orgService.listAllClazz()); + request.getRequestDispatcher("/admin/user_list.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/admin/UserStatusServlet.java b/src/main/java/com/student/servlet/admin/UserStatusServlet.java new file mode 100644 index 0000000..108da18 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserStatusServlet.java @@ -0,0 +1,43 @@ +package com.student.servlet.admin; + +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 管理员 - 变更学生状态(B-03) + */ +@WebServlet("/admin/userStatus") +public class UserStatusServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int userId = ParamUtil.getInt(request, "userId", 0); + if (userId <= 0) { + userId = ParamUtil.getInt(request, "id", 0); + } + int status = ParamUtil.getInt(request, "status", -1); + + if (userId <= 0 || status < 0) { + redirectWithMsg(response, request, "/admin/userList", "参数无效"); + return; + } + + if (userService.changeStatus(userId, status)) { + logOperation(getLoginUser(request), "admin", "变更状态:userId=" + userId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/admin/userDetail?id=" + userId, "状态变更成功"); + } else { + redirectWithMsg(response, request, "/admin/userDetail?id=" + userId, "状态变更失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/admin/UserUploadServlet.java b/src/main/java/com/student/servlet/admin/UserUploadServlet.java new file mode 100644 index 0000000..02f8385 --- /dev/null +++ b/src/main/java/com/student/servlet/admin/UserUploadServlet.java @@ -0,0 +1,131 @@ +package com.student.servlet.admin; + +import com.student.bean.UserImportTemp; +import com.student.service.ImportService; +import com.student.servlet.BaseServlet; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 管理员 - 上传用户信息(G-03 导入预览) + */ +@WebServlet("/admin/userUpload") +public class UserUploadServlet extends BaseServlet { + + private final ImportService importService = new ImportService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.getRequestDispatcher("/admin/user_upload.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + if (!ServletFileUpload.isMultipartContent(request)) { + redirectWithMsg(response, request, "/admin/userUpload", "请上传 CSV 文件"); + return; + } + + try { + ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); + List items = upload.parseRequest(request); + + FileItem fileItem = null; + for (FileItem item : items) { + if (!item.isFormField() && "file".equals(item.getFieldName())) { + fileItem = item; + break; + } + } + + if (fileItem == null || fileItem.getSize() == 0) { + redirectWithMsg(response, request, "/admin/userUpload", "请选择要上传的文件"); + return; + } + + List records = parseCsv(fileItem); + if (records.isEmpty()) { + redirectWithMsg(response, request, "/admin/userUpload", "未解析到有效数据"); + return; + } + + String batchId = importService.uploadToTemp(records); + request.getSession().setAttribute("lastImportBatchId", batchId); + response.sendRedirect(request.getContextPath() + "/admin/importPreview?batchId=" + batchId); + } catch (Exception e) { + redirectWithMsg(response, request, "/admin/userUpload", "文件上传失败:" + e.getMessage()); + } + } + + private List parseCsv(FileItem fileItem) throws IOException { + List records = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(fileItem.getInputStream(), StandardCharsets.UTF_8))) { + String line; + boolean firstLine = true; + while ((line = reader.readLine()) != null) { + if (firstLine) { + firstLine = false; + if (line.startsWith("\ufeff")) { + line = line.substring(1); + } + if (line.contains("用户名") || line.toLowerCase().contains("username")) { + continue; + } + } + if (line.trim().isEmpty()) { + continue; + } + String[] fields = line.split(",", -1); + if (fields.length < 2) { + continue; + } + String username = fields[0].trim(); + String password = fields[1].trim(); + if (username.isEmpty()) { + continue; + } + UserImportTemp temp = new UserImportTemp(); + temp.setUsername(username); + temp.setPassword(password.isEmpty() ? "123456" : password); + if (fields.length > 2) { + temp.setRealName(fields[2].trim()); + } + if (fields.length > 3) { + temp.setGender(fields[3].trim()); + } + if (fields.length > 4 && !fields[4].trim().isEmpty()) { + try { + temp.setAge(Integer.parseInt(fields[4].trim())); + } catch (NumberFormatException ignored) { + } + } + if (fields.length > 5) { + temp.setEmail(fields[5].trim()); + } + if (fields.length > 6) { + temp.setPhone(fields[6].trim()); + } + if (fields.length > 7) { + temp.setAddress(fields[7].trim()); + } + records.add(temp); + } + } + return records; + } +} diff --git a/src/main/java/com/student/servlet/teacher/TeacherAttendanceServlet.java b/src/main/java/com/student/servlet/teacher/TeacherAttendanceServlet.java new file mode 100644 index 0000000..877949f --- /dev/null +++ b/src/main/java/com/student/servlet/teacher/TeacherAttendanceServlet.java @@ -0,0 +1,108 @@ +package com.student.servlet.teacher; + +import com.student.bean.Attendance; +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.service.AttendanceService; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 教师 - 考勤记录 + */ +@WebServlet("/teacher/attendance") +public class TeacherAttendanceServlet extends BaseServlet { + + private final AttendanceService attendanceService = new AttendanceService(); + private final CourseService courseService = new CourseService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int teacherId = getLoginUser(request).getId(); + List courseList = courseService.findByTeacher(teacherId); + request.setAttribute("courseList", courseList); + + Set seen = new HashSet<>(); + List studentList = new ArrayList<>(); + for (Course course : courseList) { + for (Enrollment enrollment : enrollmentService.findByCourse(course.getId())) { + if (enrollment.getStudentId() != null && seen.add(enrollment.getStudentId())) { + studentList.add(enrollment); + } + } + } + request.setAttribute("studentList", studentList); + + List attList = new ArrayList<>(); + for (Course course : courseList) { + attList.addAll(attendanceService.findByCourse(course.getId())); + } + attList.sort((a, b) -> { + if (a.getAttDate() == null || b.getAttDate() == null) { + return 0; + } + return b.getAttDate().compareTo(a.getAttDate()); + }); + if (attList.size() > 50) { + attList = attList.subList(0, 50); + } + request.setAttribute("attList", attList); + request.getRequestDispatcher("/teacher/attendance.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + int studentId = ParamUtil.getInt(request, "studentId", 0); + String attDateStr = ParamUtil.getString(request, "attDate"); + int status = ParamUtil.getInt(request, "status", 1); + + if (courseId <= 0 || studentId <= 0 || attDateStr.isEmpty()) { + redirectWithMsg(response, request, "/teacher/attendance", "参数无效"); + return; + } + + Date attDate; + try { + attDate = new SimpleDateFormat("yyyy-MM-dd").parse(attDateStr); + } catch (ParseException e) { + redirectWithMsg(response, request, "/teacher/attendance", "日期格式错误"); + return; + } + + Attendance att = new Attendance(); + att.setStudentId(studentId); + att.setCourseId(courseId); + att.setAttDate(attDate); + att.setStatus(status); + + List records = new ArrayList<>(); + records.add(att); + + if (attendanceService.recordBatch(records)) { + logOperation(getLoginUser(request), "teacher", "记录考勤:course=" + courseId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/teacher/attendance", "保存成功"); + } else { + redirectWithMsg(response, request, "/teacher/attendance", "保存失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/teacher/TeacherHomeServlet.java b/src/main/java/com/student/servlet/teacher/TeacherHomeServlet.java new file mode 100644 index 0000000..8d9291a --- /dev/null +++ b/src/main/java/com/student/servlet/teacher/TeacherHomeServlet.java @@ -0,0 +1,48 @@ +package com.student.servlet.teacher; + +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.service.LeaveService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 教师 - 首页 + */ +@WebServlet("/teacher/home") +public class TeacherHomeServlet extends BaseServlet { + + private final CourseService courseService = new CourseService(); + private final LeaveService leaveService = new LeaveService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int teacherId = getLoginUser(request).getId(); + List myCourses = courseService.findByTeacher(teacherId); + Set studentIds = new HashSet<>(); + for (Course course : myCourses) { + for (Enrollment enrollment : enrollmentService.findByCourse(course.getId())) { + if (enrollment.getStudentId() != null) { + studentIds.add(enrollment.getStudentId()); + } + } + } + request.setAttribute("myCourses", myCourses); + request.setAttribute("courseCount", myCourses.size()); + request.setAttribute("pendingLeaveCount", leaveService.findPending().size()); + request.setAttribute("studentCount", studentIds.size()); + request.getRequestDispatcher("/teacher/home.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/teacher/TeacherLeaveServlet.java b/src/main/java/com/student/servlet/teacher/TeacherLeaveServlet.java new file mode 100644 index 0000000..ae07f9c --- /dev/null +++ b/src/main/java/com/student/servlet/teacher/TeacherLeaveServlet.java @@ -0,0 +1,54 @@ +package com.student.servlet.teacher; + +import com.student.service.LeaveService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 教师 - 请假审批 + */ +@WebServlet("/teacher/leave") +public class TeacherLeaveServlet extends BaseServlet { + + private final LeaveService leaveService = new LeaveService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.setAttribute("leaveList", leaveService.findPending()); + request.getRequestDispatcher("/teacher/leave.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int id = ParamUtil.getInt(request, "id", 0); + String action = ParamUtil.getString(request, "action"); + String note = ParamUtil.getString(request, "note"); + int approverId = getLoginUser(request).getId(); + + boolean success; + if ("approve".equals(action)) { + success = leaveService.approve(id, approverId, note); + } else if ("reject".equals(action)) { + success = leaveService.reject(id, approverId, note); + } else { + redirectWithMsg(response, request, "/teacher/leave", "无效操作"); + return; + } + + if (success) { + logOperation(getLoginUser(request), "teacher", "请假审批:" + action, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/teacher/leave", "操作成功"); + } else { + redirectWithMsg(response, request, "/teacher/leave", "操作失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/teacher/TeacherScoreServlet.java b/src/main/java/com/student/servlet/teacher/TeacherScoreServlet.java new file mode 100644 index 0000000..ff5ff69 --- /dev/null +++ b/src/main/java/com/student/servlet/teacher/TeacherScoreServlet.java @@ -0,0 +1,89 @@ +package com.student.servlet.teacher; + +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.bean.Score; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.service.ScoreService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 教师 - 成绩录入 + */ +@WebServlet("/teacher/score") +public class TeacherScoreServlet extends BaseServlet { + + private final ScoreService scoreService = new ScoreService(); + private final CourseService courseService = new CourseService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int teacherId = getLoginUser(request).getId(); + int courseId = ParamUtil.getInt(request, "courseId", 0); + List courseList = courseService.findByTeacher(teacherId); + request.setAttribute("courseList", courseList); + if (courseId > 0) { + request.setAttribute("courseId", courseId); + List studentList = enrollmentService.findByCourse(courseId); + request.setAttribute("studentList", studentList); + Map scoreMap = new HashMap<>(); + for (Score score : scoreService.findByCourse(courseId)) { + scoreMap.put(score.getStudentId(), score.getScore()); + } + request.setAttribute("scoreMap", scoreMap); + } + request.getRequestDispatcher("/teacher/score.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + String[] studentIds = request.getParameterValues("studentIds"); + String[] scoreValues = request.getParameterValues("scores"); + + if (courseId <= 0 || studentIds == null) { + redirectWithMsg(response, request, "/teacher/score", "参数无效"); + return; + } + + List scores = new ArrayList<>(); + for (int i = 0; i < studentIds.length; i++) { + try { + String val = scoreValues != null && i < scoreValues.length ? scoreValues[i] : ""; + if (val.isEmpty()) { + continue; + } + Score score = new Score(); + score.setStudentId(Integer.parseInt(studentIds[i])); + score.setCourseId(courseId); + score.setScore(new BigDecimal(val.trim())); + scores.add(score); + } catch (NumberFormatException ignored) { + } + } + + if (scoreService.saveBatch(scores)) { + logOperation(getLoginUser(request), "teacher", "录入成绩:course=" + courseId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/teacher/score?courseId=" + courseId, "保存成功"); + } else { + redirectWithMsg(response, request, "/teacher/score?courseId=" + courseId, "保存失败"); + } + } +} diff --git a/src/main/java/com/student/servlet/user/AvatarUploadServlet.java b/src/main/java/com/student/servlet/user/AvatarUploadServlet.java new file mode 100644 index 0000000..afc4a13 --- /dev/null +++ b/src/main/java/com/student/servlet/user/AvatarUploadServlet.java @@ -0,0 +1,86 @@ +package com.student.servlet.user; + +import com.student.bean.User; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +/** + * 学生 - 头像上传 + */ +@WebServlet("/user/avatar") +public class AvatarUploadServlet extends BaseServlet { + + private final UserService userService = new UserService(); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + if (!ServletFileUpload.isMultipartContent(request)) { + redirectWithMsg(response, request, "/user/profile", "请上传图片文件"); + return; + } + + try { + ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); + List items = upload.parseRequest(request); + + FileItem fileItem = null; + for (FileItem item : items) { + if (!item.isFormField() && "avatar".equals(item.getFieldName())) { + fileItem = item; + break; + } + } + + if (fileItem == null || fileItem.getSize() == 0) { + redirectWithMsg(response, request, "/user/profile", "请选择头像文件"); + return; + } + + String ext = ""; + String fileName = fileItem.getName(); + int dot = fileName.lastIndexOf('.'); + if (dot > 0) { + ext = fileName.substring(dot); + } + + String newName = UUID.randomUUID().toString().replace("-", "") + ext; + String uploadDir = getServletContext().getRealPath("/uploads/avatars"); + File dir = new File(uploadDir); + if (!dir.exists()) { + dir.mkdirs(); + } + File dest = new File(dir, newName); + fileItem.write(dest); + + User loginUser = getLoginUser(request); + String avatarPath = "/uploads/avatars/" + newName; + if (!userService.updateAvatar(loginUser.getId(), avatarPath)) { + redirectWithMsg(response, request, "/user/profile", "头像保存失败"); + return; + } + + HttpSession session = request.getSession(); + session.setAttribute("loginUser", userService.findById(loginUser.getId())); + + logOperation(loginUser, "user", "上传头像", IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/user/profile", "头像上传成功"); + } catch (Exception e) { + redirectWithMsg(response, request, "/user/profile", "上传失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/user/EnrollServlet.java b/src/main/java/com/student/servlet/user/EnrollServlet.java new file mode 100644 index 0000000..44dd53f --- /dev/null +++ b/src/main/java/com/student/servlet/user/EnrollServlet.java @@ -0,0 +1,59 @@ +package com.student.servlet.user; + +import com.student.bean.Enrollment; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * 学生 - 选课 + */ +@WebServlet("/user/enroll") +public class EnrollServlet extends BaseServlet { + + private final CourseService courseService = new CourseService(); + private final EnrollmentService enrollmentService = new EnrollmentService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int studentId = getLoginUser(request).getId(); + request.setAttribute("courseList", courseService.findAll()); + Set enrolledIds = new HashSet<>(); + for (Enrollment enrollment : enrollmentService.findByStudent(studentId)) { + if (enrollment.getCourseId() != null) { + enrolledIds.add(enrollment.getCourseId()); + } + } + request.setAttribute("enrolledIds", enrolledIds); + request.getRequestDispatcher("/user/enroll.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int courseId = ParamUtil.getInt(request, "courseId", 0); + int studentId = getLoginUser(request).getId(); + + try { + if (enrollmentService.enroll(studentId, courseId)) { + logOperation(getLoginUser(request), "user", "选课:course=" + courseId, IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/user/enroll", "选课成功"); + } else { + redirectWithMsg(response, request, "/user/enroll", "选课失败"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/user/enroll", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/user/LeaveApplyServlet.java b/src/main/java/com/student/servlet/user/LeaveApplyServlet.java new file mode 100644 index 0000000..ff64952 --- /dev/null +++ b/src/main/java/com/student/servlet/user/LeaveApplyServlet.java @@ -0,0 +1,66 @@ +package com.student.servlet.user; + +import com.student.bean.LeaveRequest; +import com.student.service.LeaveService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * 学生 - 请假申请 + */ +@WebServlet("/user/leaveApply") +public class LeaveApplyServlet extends BaseServlet { + + private final LeaveService leaveService = new LeaveService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + List historyList = leaveService.findByStudent(getLoginUser(request).getId()); + request.setAttribute("historyList", historyList); + request.setAttribute("leaveList", historyList); + request.getRequestDispatcher("/user/leave_apply.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String startStr = ParamUtil.getString(request, "startDate"); + String endStr = ParamUtil.getString(request, "endDate"); + String reason = ParamUtil.getString(request, "reason"); + + LeaveRequest lr = new LeaveRequest(); + lr.setStudentId(getLoginUser(request).getId()); + lr.setReason(reason); + + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + lr.setStartDate(sdf.parse(startStr)); + lr.setEndDate(sdf.parse(endStr)); + } catch (ParseException e) { + redirectWithMsg(response, request, "/user/leaveApply", "日期格式错误"); + return; + } + + try { + if (leaveService.apply(lr)) { + logOperation(getLoginUser(request), "user", "提交请假", IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/user/leaveApply", "申请已提交"); + } else { + redirectWithMsg(response, request, "/user/leaveApply", "提交失败"); + } + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/user/leaveApply", e.getMessage()); + } + } +} diff --git a/src/main/java/com/student/servlet/user/MessageInboxServlet.java b/src/main/java/com/student/servlet/user/MessageInboxServlet.java new file mode 100644 index 0000000..a56825e --- /dev/null +++ b/src/main/java/com/student/servlet/user/MessageInboxServlet.java @@ -0,0 +1,84 @@ +package com.student.servlet.user; + +import com.student.service.MessageService; + +import com.student.servlet.BaseServlet; + +import com.student.util.ParamUtil; + + + +import javax.servlet.ServletException; + +import javax.servlet.annotation.WebServlet; + +import javax.servlet.http.HttpServletRequest; + +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + + + +/** + + * 学生 - 消息收件箱 + + */ + +@WebServlet("/user/messages") + +public class MessageInboxServlet extends BaseServlet { + + + + private final MessageService messageService = new MessageService(); + + + + @Override + + protected void doGet(HttpServletRequest request, HttpServletResponse response) + + throws ServletException, IOException { + + int userId = getLoginUser(request).getId(); + + request.setAttribute("messageList", messageService.findInbox(userId)); + + request.setAttribute("unreadCount", messageService.countUnread(userId)); + + request.getRequestDispatcher("/user/messages.jsp").forward(request, response); + + } + + + + @Override + + protected void doPost(HttpServletRequest request, HttpServletResponse response) + + throws ServletException, IOException { + + String action = ParamUtil.getString(request, "action"); + + int userId = getLoginUser(request).getId(); + + + + if ("read".equals(action)) { + + messageService.markRead(ParamUtil.getInt(request, "id", 0)); + + } else if ("readAll".equals(action)) { + + messageService.markAllRead(userId); + + } + + response.sendRedirect(request.getContextPath() + "/user/messages"); + + } + +} + diff --git a/src/main/java/com/student/servlet/user/MyCourseServlet.java b/src/main/java/com/student/servlet/user/MyCourseServlet.java new file mode 100644 index 0000000..01df750 --- /dev/null +++ b/src/main/java/com/student/servlet/user/MyCourseServlet.java @@ -0,0 +1,41 @@ +package com.student.servlet.user; + +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.service.CourseService; +import com.student.service.EnrollmentService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 学生 - 我的课程与课表(C-06) + */ +@WebServlet("/user/myCourses") +public class MyCourseServlet extends BaseServlet { + + private final EnrollmentService enrollmentService = new EnrollmentService(); + private final CourseService courseService = new CourseService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int studentId = getLoginUser(request).getId(); + List scheduleList = new ArrayList<>(); + for (Enrollment enrollment : enrollmentService.findByStudent(studentId)) { + Course course = courseService.findById(enrollment.getCourseId()); + if (course != null) { + course.setCreateTime(enrollment.getCreateTime()); + scheduleList.add(course); + } + } + request.setAttribute("scheduleList", scheduleList); + request.getRequestDispatcher("/user/my_courses.jsp").forward(request, response); + } +} diff --git a/src/main/java/com/student/servlet/user/MyScoreServlet.java b/src/main/java/com/student/servlet/user/MyScoreServlet.java new file mode 100644 index 0000000..9e60112 --- /dev/null +++ b/src/main/java/com/student/servlet/user/MyScoreServlet.java @@ -0,0 +1,49 @@ +package com.student.servlet.user; + +import com.student.bean.Score; +import com.student.service.ScoreService; +import com.student.servlet.BaseServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * 学生 - 我的成绩 + */ +@WebServlet("/user/myScores") +public class MyScoreServlet extends BaseServlet { + + private final ScoreService scoreService = new ScoreService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + int studentId = getLoginUser(request).getId(); + List scoreList = scoreService.findByStudent(studentId); + request.setAttribute("scoreList", scoreList); + request.setAttribute("gpa", calculateGpa(scoreList)); + request.getRequestDispatcher("/user/my_scores.jsp").forward(request, response); + } + + private double calculateGpa(List scoreList) { + if (scoreList == null || scoreList.isEmpty()) { + return 0; + } + double sum = 0; + int count = 0; + for (Score score : scoreList) { + if (score.getScore() != null) { + sum += score.getScore().doubleValue(); + count++; + } + } + if (count == 0) { + return 0; + } + return Math.round(sum / count * 100.0) / 100.0; + } +} diff --git a/src/main/java/com/student/servlet/user/UserProfileServlet.java b/src/main/java/com/student/servlet/user/UserProfileServlet.java new file mode 100644 index 0000000..dab161d --- /dev/null +++ b/src/main/java/com/student/servlet/user/UserProfileServlet.java @@ -0,0 +1,107 @@ +package com.student.servlet.user; + +import com.student.bean.ChangeRequest; +import com.student.bean.User; +import com.student.dao.UserDao; +import com.student.dao.impl.UserDaoImpl; +import com.student.service.ChangeRequestService; +import com.student.service.UserService; +import com.student.servlet.BaseServlet; +import com.student.util.IpUtil; +import com.student.util.ParamUtil; +import com.student.util.PasswordUtil; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.List; + +/** + * 用户 - 个人信息(G-01 真实姓名变更需审核) + */ +@WebServlet("/user/profile") +public class UserProfileServlet extends BaseServlet { + + private final UserService userService = new UserService(); + private final UserDao userDao = new UserDaoImpl(); + private final ChangeRequestService changeRequestService = new ChangeRequestService(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + User loginUser = getLoginUser(request); + User user = userService.findById(loginUser.getId()); + List changeRequests = changeRequestService.findByUser(loginUser.getId()); + request.setAttribute("user", user); + request.setAttribute("changeRequests", changeRequests); + request.setAttribute("pendingChange", findPendingChange(changeRequests)); + request.getRequestDispatcher("/user/profile.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(); + User loginUser = getLoginUser(request); + + String password = ParamUtil.getString(request, "password"); + String realName = ParamUtil.getString(request, "realName"); + String gender = ParamUtil.getString(request, "gender"); + Integer age = ParamUtil.getInteger(request, "age"); + String email = ParamUtil.getString(request, "email"); + String phone = ParamUtil.getString(request, "phone"); + String address = ParamUtil.getString(request, "address"); + + User user = userService.findById(loginUser.getId()); + boolean success = true; + + if (!realName.isEmpty() && !realName.equals(user.getRealName())) { + ChangeRequest cr = new ChangeRequest(); + cr.setUserId(loginUser.getId()); + cr.setFieldName("realName"); + cr.setNewValue(realName); + try { + changeRequestService.submit(cr); + } catch (RuntimeException e) { + redirectWithMsg(response, request, "/user/profile", e.getMessage()); + return; + } + } + + user.setGender(gender); + user.setAge(age); + user.setEmail(email); + user.setPhone(phone); + user.setAddress(address); + success = userDao.updateProfile(user); + + if (success && !password.isEmpty()) { + String salt = PasswordUtil.generateSalt(); + success = userDao.updatePassword(loginUser.getId(), PasswordUtil.hash(password, salt), salt); + } + + if (success) { + User updated = userService.findById(loginUser.getId()); + session.setAttribute("loginUser", updated); + logOperation(loginUser, "user", "更新个人信息", IpUtil.getClientIp(request)); + redirectWithMsg(response, request, "/user/profile", "个人信息修改成功"); + } else { + redirectWithMsg(response, request, "/user/profile", "修改失败,请重试"); + } + } + + private ChangeRequest findPendingChange(List changeRequests) { + if (changeRequests == null) { + return null; + } + for (ChangeRequest cr : changeRequests) { + if (cr.getStatus() != null && cr.getStatus() == ChangeRequest.STATUS_PENDING) { + return cr; + } + } + return null; + } +} diff --git a/src/main/java/com/student/util/CaptchaUtil.java b/src/main/java/com/student/util/CaptchaUtil.java new file mode 100644 index 0000000..0f78919 --- /dev/null +++ b/src/main/java/com/student/util/CaptchaUtil.java @@ -0,0 +1,50 @@ +package com.student.util; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.Random; + +/** + * 图形验证码工具类 + */ +public class CaptchaUtil { + + private static final int WIDTH = 120; + private static final int HEIGHT = 40; + private static final int CODE_LENGTH = 4; + private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; + private static final Random RANDOM = new Random(); + + public static BufferedImage generateImage(String code) { + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + + g.setColor(new Color(240, 240, 240)); + g.fillRect(0, 0, WIDTH, HEIGHT); + + g.setFont(new Font("Arial", Font.BOLD, 28)); + for (int i = 0; i < code.length(); i++) { + g.setColor(new Color(RANDOM.nextInt(150), RANDOM.nextInt(150), RANDOM.nextInt(150))); + g.drawString(String.valueOf(code.charAt(i)), 20 + i * 25, 30); + } + + for (int i = 0; i < 6; i++) { + g.setColor(new Color(RANDOM.nextInt(200), RANDOM.nextInt(200), RANDOM.nextInt(200))); + g.drawLine(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT), + RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT)); + } + + g.dispose(); + return image; + } + + public static String generateCode() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length()))); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/student/util/CookieUtil.java b/src/main/java/com/student/util/CookieUtil.java new file mode 100644 index 0000000..9c468b8 --- /dev/null +++ b/src/main/java/com/student/util/CookieUtil.java @@ -0,0 +1,41 @@ +package com.student.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie 工具类 + */ +public final class CookieUtil { + + public static final String PATH_ROOT = "/"; + + private CookieUtil() { + } + + public static void setCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath(PATH_ROOT); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static String getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies == null || name == null) { + return null; + } + for (Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } + + public static void deleteCookie(HttpServletResponse response, String name) { + setCookie(response, name, "", 0); + } +} diff --git a/src/main/java/com/student/util/DBUtil.java b/src/main/java/com/student/util/DBUtil.java new file mode 100644 index 0000000..a49a485 --- /dev/null +++ b/src/main/java/com/student/util/DBUtil.java @@ -0,0 +1,102 @@ +package com.student.util; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +/** + * JDBC 数据库工具类(HikariCP 连接池) + */ +public final class DBUtil { + + private static final HikariDataSource DATA_SOURCE; + + static { + try { + Properties props = new Properties(); + InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties"); + if (in == null) { + throw new RuntimeException("db.properties not found"); + } + props.load(in); + in.close(); + + HikariConfig config = new HikariConfig(); + config.setDriverClassName(props.getProperty("jdbc.driver")); + config.setJdbcUrl(props.getProperty("jdbc.url")); + config.setUsername(props.getProperty("jdbc.username")); + config.setPassword(props.getProperty("jdbc.password")); + config.setMaximumPoolSize(parseInt(props.getProperty("jdbc.pool.maxSize"), 10)); + config.setMinimumIdle(parseInt(props.getProperty("jdbc.pool.minIdle"), 2)); + config.setConnectionTimeout(parseLong(props.getProperty("jdbc.pool.connectionTimeout"), 30000)); + config.setIdleTimeout(parseLong(props.getProperty("jdbc.pool.idleTimeout"), 600000)); + config.setMaxLifetime(parseLong(props.getProperty("jdbc.pool.maxLifetime"), 1800000)); + config.setPoolName("StudentManagementPool"); + + DATA_SOURCE = new HikariDataSource(config); + } catch (IOException e) { + throw new RuntimeException("数据库配置加载失败", e); + } + } + + private DBUtil() { + } + + public static Connection getConnection() throws SQLException { + return DATA_SOURCE.getConnection(); + } + + public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException ignored) { + } + try { + if (ps != null) { + ps.close(); + } + } catch (SQLException ignored) { + } + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException ignored) { + } + } + + public static void close(Connection conn, PreparedStatement ps) { + close(conn, ps, null); + } + + private static int parseInt(String value, int defaultValue) { + if (value == null || value.trim().isEmpty()) { + return defaultValue; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static long parseLong(String value, long defaultValue) { + if (value == null || value.trim().isEmpty()) { + return defaultValue; + } + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/src/main/java/com/student/util/IpUtil.java b/src/main/java/com/student/util/IpUtil.java new file mode 100644 index 0000000..aeb6a7a --- /dev/null +++ b/src/main/java/com/student/util/IpUtil.java @@ -0,0 +1,39 @@ +package com.student.util; + +import javax.servlet.http.HttpServletRequest; + +/** + * 客户端 IP 工具类 + */ +public final class IpUtil { + + private IpUtil() { + } + + public static String getClientIp(HttpServletRequest request) { + if (request == null) { + return ""; + } + String[] headers = { + "X-Forwarded-For", + "X-Real-IP", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + for (String header : headers) { + String ip = request.getHeader(header); + if (isValidIp(ip)) { + int commaIndex = ip.indexOf(','); + return commaIndex > 0 ? ip.substring(0, commaIndex).trim() : ip.trim(); + } + } + String remoteAddr = request.getRemoteAddr(); + return remoteAddr != null ? remoteAddr : ""; + } + + private static boolean isValidIp(String ip) { + return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip); + } +} diff --git a/src/main/java/com/student/util/JsonUtil.java b/src/main/java/com/student/util/JsonUtil.java new file mode 100644 index 0000000..41cb962 --- /dev/null +++ b/src/main/java/com/student/util/JsonUtil.java @@ -0,0 +1,25 @@ +package com.student.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Gson 封装工具类 + */ +public final class JsonUtil { + + private static final Gson GSON = new GsonBuilder() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .create(); + + private JsonUtil() { + } + + public static String toJson(Object obj) { + return GSON.toJson(obj); + } + + public static T fromJson(String json, Class clazz) { + return GSON.fromJson(json, clazz); + } +} diff --git a/src/main/java/com/student/util/ParamUtil.java b/src/main/java/com/student/util/ParamUtil.java new file mode 100644 index 0000000..d4c693b --- /dev/null +++ b/src/main/java/com/student/util/ParamUtil.java @@ -0,0 +1,41 @@ +package com.student.util; + +import javax.servlet.http.HttpServletRequest; + +/** + * 请求参数工具类 + */ +public class ParamUtil { + + private ParamUtil() { + } + + public static String getString(HttpServletRequest request, String name) { + String value = request.getParameter(name); + return value == null ? "" : value.trim(); + } + + public static int getInt(HttpServletRequest request, String name, int defaultValue) { + String value = request.getParameter(name); + if (value == null || value.trim().isEmpty()) { + return defaultValue; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static Integer getInteger(HttpServletRequest request, String name) { + String value = request.getParameter(name); + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/src/main/java/com/student/util/PasswordUtil.java b/src/main/java/com/student/util/PasswordUtil.java new file mode 100644 index 0000000..f73c75a --- /dev/null +++ b/src/main/java/com/student/util/PasswordUtil.java @@ -0,0 +1,89 @@ +package com.student.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * 密码工具:SHA-256(salt + password),十六进制存储。 + */ +public final class PasswordUtil { + + /** db_init.sql 迁移前的占位哈希(64 位十六进制) */ + public static final String LEGACY_PLACEHOLDER_HASH = + "8f4e3b2a1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2"; + + private static final SecureRandom RANDOM = new SecureRandom(); + + private PasswordUtil() { + } + + public static String generateSalt() { + byte[] bytes = new byte[16]; + RANDOM.nextBytes(bytes); + return bytesToHex(bytes); + } + + public static String generateToken() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return bytesToHex(bytes); + } + + public static String hash(String password, String salt) { + if (password == null || salt == null) { + throw new IllegalArgumentException("password and salt must not be null"); + } + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashed = digest.digest((salt + password).getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hashed); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 not available", e); + } + } + + public static boolean verify(String password, String salt, String storedHash) { + if (password == null || salt == null || storedHash == null) { + return false; + } + String computed = hash(password, salt); + if (computed.equalsIgnoreCase(storedHash)) { + return true; + } + // 兼容 init SQL 迁移前占位符:库中仍为占位哈希时,回退为 hash(password,salt) 算法校验并拒绝登录,提示迁移 + if (isLegacyPlaceholder(storedHash)) { + return false; + } + return false; + } + + /** 判断是否为迁移前的 64 位十六进制占位哈希 */ + public static boolean isLegacyPlaceholder(String storedHash) { + return storedHash != null + && storedHash.length() == 64 + && storedHash.matches("[0-9a-fA-F]{64}") + && LEGACY_PLACEHOLDER_HASH.equalsIgnoreCase(storedHash); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * 生成 db_init.sql 所用密码哈希。 + * 算法:SHA-256(salt + password),salt=abc123 + * 例:SHA256("abc123admin123") = hash("admin123", "abc123") + */ + public static void main(String[] args) { + String salt = "abc123"; + System.out.println("SHA256(\"abc123admin123\") = " + hash("admin123", salt)); + System.out.println("SHA256(\"abc123user123\") = " + hash("user123", salt)); + System.out.println("SHA256(\"abc123teacher123\") = " + hash("teacher123", salt)); + } +} diff --git a/src/main/java/com/student/util/StudentNoUtil.java b/src/main/java/com/student/util/StudentNoUtil.java new file mode 100644 index 0000000..d0a501f --- /dev/null +++ b/src/main/java/com/student/util/StudentNoUtil.java @@ -0,0 +1,26 @@ +package com.student.util; + +import java.time.Year; + +/** + * 学号生成工具 + */ +public final class StudentNoUtil { + + private StudentNoUtil() { + } + + /** + * 生成学号,格式:年份 + 专业代码 + 三位序号,例如 2026SE001 + */ + public static String generateStudentNo(String majorCode, int seq) { + if (majorCode == null || majorCode.isEmpty()) { + throw new IllegalArgumentException("majorCode must not be empty"); + } + if (seq < 1) { + throw new IllegalArgumentException("seq must be >= 1"); + } + int year = Year.now().getValue(); + return year + majorCode.toUpperCase() + String.format("%03d", seq); + } +} diff --git a/src/main/resources/db.properties b/src/main/resources/db.properties new file mode 100644 index 0000000..f0298ef --- /dev/null +++ b/src/main/resources/db.properties @@ -0,0 +1,4 @@ +jdbc.driver=com.mysql.cj.jdbc.Driver +jdbc.url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false +jdbc.username=root +jdbc.password=admin diff --git a/src/main/resources/db_init.sql b/src/main/resources/db_init.sql new file mode 100644 index 0000000..0c8e973 --- /dev/null +++ b/src/main/resources/db_init.sql @@ -0,0 +1,221 @@ +CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE student_db; + +DROP TABLE IF EXISTS user_import_temp; +DROP TABLE IF EXISTS change_request; +DROP TABLE IF EXISTS message; +DROP TABLE IF EXISTS announcement; +DROP TABLE IF EXISTS leave_request; +DROP TABLE IF EXISTS attendance; +DROP TABLE IF EXISTS score; +DROP TABLE IF EXISTS enrollment; +DROP TABLE IF EXISTS course; +DROP TABLE IF EXISTS operation_log; +DROP TABLE IF EXISTS login_log; +DROP TABLE IF EXISTS remember_token; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS clazz; +DROP TABLE IF EXISTS major; +DROP TABLE IF EXISTS department; + +CREATE TABLE department ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + code VARCHAR(20) NOT NULL UNIQUE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE major ( + id INT PRIMARY KEY AUTO_INCREMENT, + department_id INT NOT NULL, + name VARCHAR(100) NOT NULL, + code VARCHAR(20) NOT NULL, + FOREIGN KEY (department_id) REFERENCES department(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE clazz ( + id INT PRIMARY KEY AUTO_INCREMENT, + major_id INT NOT NULL, + name VARCHAR(100) NOT NULL, + grade VARCHAR(10) NOT NULL, + FOREIGN KEY (major_id) REFERENCES major(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(128) NOT NULL, + salt VARCHAR(64) NOT NULL, + student_no VARCHAR(20) UNIQUE, + real_name VARCHAR(50), + gender VARCHAR(10), + age INT, + email VARCHAR(100), + phone VARCHAR(20), + address VARCHAR(200), + avatar VARCHAR(255), + clazz_id INT, + role TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1管理员 2教师', + status TINYINT NOT NULL DEFAULT 1 COMMENT '0待审核 1在读 2休学 3毕业 4退学', + fail_count INT DEFAULT 0, + lock_until DATETIME, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (clazz_id) REFERENCES clazz(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE remember_token ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + token VARCHAR(64) NOT NULL UNIQUE, + expire_time DATETIME NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE login_log ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT, + username VARCHAR(50), + ip VARCHAR(50), + result TINYINT NOT NULL COMMENT '0失败 1成功', + message VARCHAR(200), + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE operation_log ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT, + username VARCHAR(50), + module VARCHAR(50), + action VARCHAR(100), + ip VARCHAR(50), + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE course ( + id INT PRIMARY KEY AUTO_INCREMENT, + course_no VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, + teacher_id INT, + max_students INT NOT NULL DEFAULT 50, + schedule VARCHAR(100), + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (teacher_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE enrollment ( + id INT PRIMARY KEY AUTO_INCREMENT, + student_id INT NOT NULL, + course_id INT NOT NULL, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_student_course (student_id, course_id), + FOREIGN KEY (student_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES course(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE score ( + id INT PRIMARY KEY AUTO_INCREMENT, + student_id INT NOT NULL, + course_id INT NOT NULL, + score DECIMAL(5,1), + remark VARCHAR(200), + UNIQUE KEY uk_score (student_id, course_id), + FOREIGN KEY (student_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES course(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE attendance ( + id INT PRIMARY KEY AUTO_INCREMENT, + student_id INT NOT NULL, + course_id INT NOT NULL, + att_date DATE NOT NULL, + status TINYINT NOT NULL COMMENT '1出勤 2迟到 3缺勤', + remark VARCHAR(200), + UNIQUE KEY uk_att (student_id, course_id, att_date), + FOREIGN KEY (student_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES course(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE leave_request ( + id INT PRIMARY KEY AUTO_INCREMENT, + student_id INT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + reason VARCHAR(500) NOT NULL, + status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回', + approver_id INT, + approve_note VARCHAR(200), + approve_time DATETIME, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (student_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE announcement ( + id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + publisher_id INT, + is_top TINYINT DEFAULT 0, + expire_date DATE, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE message ( + id INT PRIMARY KEY AUTO_INCREMENT, + sender_id INT, + receiver_id INT COMMENT 'NULL表示全体', + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + is_read TINYINT DEFAULT 0, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE change_request ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + field_name VARCHAR(50) NOT NULL, + old_value VARCHAR(200), + new_value VARCHAR(200) NOT NULL, + status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回', + approver_id INT, + approve_note VARCHAR(200), + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE user_import_temp ( + id INT PRIMARY KEY AUTO_INCREMENT, + batch_id VARCHAR(32) NOT NULL, + username VARCHAR(50) NOT NULL, + password VARCHAR(100) NOT NULL, + real_name VARCHAR(50), + gender VARCHAR(10), + age INT, + email VARCHAR(100), + phone VARCHAR(20), + address VARCHAR(200), + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 初始化组织数据 +INSERT INTO department (name, code) VALUES ('计算机学院', 'CS'), ('经济管理学院', 'EM'); +INSERT INTO major (department_id, name, code) VALUES (1, '软件工程', 'SE'), (1, '计算机科学', 'CS'), (2, '工商管理', 'BA'); +INSERT INTO clazz (major_id, name, grade) VALUES (1, '软件2201', '2022'), (1, '软件2202', '2022'), (2, '计科2201', '2022'); + +-- 密码均为 admin123 / user123 / teacher123,salt=abc123 +-- 算法:SHA-256(salt + password),由 PasswordUtil.main 生成 +INSERT INTO users (username, password, salt, student_no, real_name, gender, age, email, phone, address, clazz_id, role, status) VALUES +('admin', '48250b108313b04cd3761a733d4c44ca83c7a6a74cdb698b3f82799ef399e288', 'abc123', NULL, '系统管理员', '男', 30, 'admin@system.com', '13800000000', '管理中心', NULL, 1, 1), +('teacher', '5c401e9b501004ae42a8102df24703b08c7f9a4b157ed54fe4c1eade27408298', 'abc123', 'T2026001', '李老师', '女', 35, 'teacher@school.com', '13800000001', '教师办公室', NULL, 2, 1), +('zhangsan','00ad616e86966743329f8cb7e7bc29aa03a4d9af1aeeb6d7c732a4f18c1dcfc1', 'abc123', '2022SE001','张三', '男', 20, 'zhangsan@stu.com', '13900000001', '北京市', 1, 0, 1); + +INSERT INTO course (course_no, name, credit, teacher_id, max_students, schedule) VALUES +('CS101', 'Java程序设计', 3.0, 2, 50, '周一 1-2节'), +('CS102', '数据库原理', 3.0, 2, 50, '周三 3-4节'), +('EM101', '管理学基础', 2.0, 2, 60, '周五 1-2节'); + +INSERT INTO enrollment (student_id, course_id) VALUES (3, 1), (3, 2); +INSERT INTO score (student_id, course_id, score) VALUES (3, 1, 88.5), (3, 2, 92.0); +INSERT INTO attendance (student_id, course_id, att_date, status) VALUES (3, 1, CURDATE(), 1); + +INSERT INTO announcement (title, content, publisher_id, is_top) VALUES +('欢迎使用学生信息管理系统', '系统已升级,支持选课、成绩、考勤、请假等功能。', 1, 1); diff --git a/src/main/webapp/WEB-INF/jspf/admin-sidebar.jspf b/src/main/webapp/WEB-INF/jspf/admin-sidebar.jspf new file mode 100644 index 0000000..01bc4db --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/admin-sidebar.jspf @@ -0,0 +1,83 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + diff --git a/src/main/webapp/WEB-INF/jspf/admin-topbar.jspf b/src/main/webapp/WEB-INF/jspf/admin-topbar.jspf new file mode 100644 index 0000000..10efd41 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/admin-topbar.jspf @@ -0,0 +1,20 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + +
+
+ +
+

${param.title}

+ +

${param.subtitle}

+
+
+
+
+ + 退出 +
+
diff --git a/src/main/webapp/WEB-INF/jspf/alert.jspf b/src/main/webapp/WEB-INF/jspf/alert.jspf new file mode 100644 index 0000000..de48d07 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/alert.jspf @@ -0,0 +1,9 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + +
+ ${alertMsg} + +
+
diff --git a/src/main/webapp/WEB-INF/jspf/empty-state.jspf b/src/main/webapp/WEB-INF/jspf/empty-state.jspf new file mode 100644 index 0000000..2bc5c02 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/empty-state.jspf @@ -0,0 +1,5 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +
+
${empty param.icon ? '📭' : param.icon}
+

${empty param.text ? '暂无数据' : param.text}

+
diff --git a/src/main/webapp/WEB-INF/jspf/head.jspf b/src/main/webapp/WEB-INF/jspf/head.jspf new file mode 100644 index 0000000..e857c84 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/head.jspf @@ -0,0 +1,6 @@ + + +${param.title} - 学生信息管理系统 + + + diff --git a/src/main/webapp/WEB-INF/jspf/pagination.jspf b/src/main/webapp/WEB-INF/jspf/pagination.jspf new file mode 100644 index 0000000..b600c70 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/pagination.jspf @@ -0,0 +1,38 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + diff --git a/src/main/webapp/WEB-INF/jspf/stat-num.jspf b/src/main/webapp/WEB-INF/jspf/stat-num.jspf new file mode 100644 index 0000000..23de221 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/stat-num.jspf @@ -0,0 +1,5 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + ${param.value} + 0 + diff --git a/src/main/webapp/WEB-INF/jspf/teacher-sidebar.jspf b/src/main/webapp/WEB-INF/jspf/teacher-sidebar.jspf new file mode 100644 index 0000000..940d4df --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/teacher-sidebar.jspf @@ -0,0 +1,29 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + diff --git a/src/main/webapp/WEB-INF/jspf/teacher-topbar.jspf b/src/main/webapp/WEB-INF/jspf/teacher-topbar.jspf new file mode 100644 index 0000000..456e5f4 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/teacher-topbar.jspf @@ -0,0 +1,15 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + +
+
+ +

${param.title}

+
+
+ + 退出 +
+
diff --git a/src/main/webapp/WEB-INF/jspf/user-avatar.jspf b/src/main/webapp/WEB-INF/jspf/user-avatar.jspf new file mode 100644 index 0000000..b2f0470 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/user-avatar.jspf @@ -0,0 +1,16 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + 头像 + + ${avatarUser.username.substring(0,1)} + diff --git a/src/main/webapp/WEB-INF/jspf/user-sidebar.jspf b/src/main/webapp/WEB-INF/jspf/user-sidebar.jspf new file mode 100644 index 0000000..37f15e2 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/user-sidebar.jspf @@ -0,0 +1,38 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + diff --git a/src/main/webapp/WEB-INF/jspf/user-topbar.jspf b/src/main/webapp/WEB-INF/jspf/user-topbar.jspf new file mode 100644 index 0000000..1a91138 --- /dev/null +++ b/src/main/webapp/WEB-INF/jspf/user-topbar.jspf @@ -0,0 +1,32 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + +
+ +
+ + + +

${param.title}

+ +
+ +
+ + + + 退出 + +
+ +
+ diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..119c161 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,105 @@ + + + + Student Management System + + + login.jsp + + + + + CharacterEncodingFilter + com.student.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + + CharacterEncodingFilter + /* + + + + + RememberMeFilter + com.student.filter.RememberMeFilter + + + RememberMeFilter + /* + + + + + AuthFilter + com.student.filter.AuthFilter + + + AuthFilter + /admin/* + /teacher/* + /user/* + /api/* + + + + + OperationLogFilter + com.student.filter.OperationLogFilter + + + OperationLogFilter + /admin/* + /teacher/* + /user/* + + + + + UserContextFilter + com.student.filter.UserContextFilter + + + UserContextFilter + /admin/* + /user/* + + + + + com.student.listener.SessionListener + + + + + + *.jsp + *.jspf + false + false + UTF-8 + + + + + + + 30 + + + + 404 + /error.jsp + + + 500 + /error.jsp + + diff --git a/src/main/webapp/admin/announcement.jsp b/src/main/webapp/admin/announcement.jsp new file mode 100644 index 0000000..1792457 --- /dev/null +++ b/src/main/webapp/admin/announcement.jsp @@ -0,0 +1,91 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

公告管理

+
+ 退出 +
+
+
+ + +
+

发布公告

+
+
+ +
+ + +
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+
+
+ +
+

公告列表

+
+ + +
+ + + + + + + + + + + + + + + +
标题发布人置顶发布时间操作
${ann.title}${ann.publisherName}${ann.isTop == 1 ? '是' : '否'} +
+ + + +
+
+
+
+ +

暂无公告

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/attendance.jsp b/src/main/webapp/admin/attendance.jsp new file mode 100644 index 0000000..e4a67af --- /dev/null +++ b/src/main/webapp/admin/attendance.jsp @@ -0,0 +1,111 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

考勤管理

+
+ 退出 +
+
+
+ + +
+

登记考勤

+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+

最近考勤记录

+
+ + +
+ + + + + + + + + + + + + + + +
学生课程日期状态备注
${a.studentName}${a.courseName} + + 出勤 + 迟到 + 缺勤 + + ${empty a.remark ? '—' : a.remark}
+
+
+ +

暂无考勤记录

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/attendance_stats.jsp b/src/main/webapp/admin/attendance_stats.jsp new file mode 100644 index 0000000..3cea47e --- /dev/null +++ b/src/main/webapp/admin/attendance_stats.jsp @@ -0,0 +1,66 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + +
+
+

考勤统计

+
+ 退出 +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
出勤
${empty stats ? 0 : stats.present}
+
迟到
${empty stats ? 0 : stats.late}
+
缺勤
${empty stats ? 0 : stats.absent}
+
总计
${empty stats ? 0 : stats.total}
+
+ +
+

考勤分布

+
+
+
+
+ + + diff --git a/src/main/webapp/admin/bigscreen.jsp b/src/main/webapp/admin/bigscreen.jsp new file mode 100644 index 0000000..d8e2b95 --- /dev/null +++ b/src/main/webapp/admin/bigscreen.jsp @@ -0,0 +1,140 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + 数据大屏 - 学生信息管理系统 + + + + + +
+
+

学生信息管理系统 · 数据大屏

+

实时数据监控 · 每 30 秒自动刷新

+
+
+
+

核心指标

+
+
用户总数
+
课程总数
+
今日注册
+
+
+
待审注册
+
待审变更
+
待审请假
+
+
+
+

院系学生分布

+
+
+
+

性别比例

+
+
+
+

趋势概览

+
+
+
+

在线状态

+
+
+
+ + + diff --git a/src/main/webapp/admin/change_audit.jsp b/src/main/webapp/admin/change_audit.jsp new file mode 100644 index 0000000..560fb57 --- /dev/null +++ b/src/main/webapp/admin/change_audit.jsp @@ -0,0 +1,69 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

变更审核

+
+ 退出 +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + +
用户变更字段原值新值申请时间操作
${cr.realName} (${cr.username})${cr.fieldName}${cr.oldValue}${cr.newValue} +
+ + + +
+
+ + + + +
+
+
+
+ +

暂无待审核变更申请

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/course_edit.jsp b/src/main/webapp/admin/course_edit.jsp new file mode 100644 index 0000000..ef8bbbe --- /dev/null +++ b/src/main/webapp/admin/course_edit.jsp @@ -0,0 +1,74 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

${empty course ? '新增课程' : '编辑课程'}

+ +
+
+
+
+ +
${param.msg}
+
+
+ + + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + 取消 +
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/course_list.jsp b/src/main/webapp/admin/course_list.jsp new file mode 100644 index 0000000..c093022 --- /dev/null +++ b/src/main/webapp/admin/course_list.jsp @@ -0,0 +1,65 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

课程管理

+ +
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + +
课程编号课程名称学分授课教师容量已选上课时间操作
${c.courseNo}${c.name}${c.credit}${empty c.teacherName ? '—' : c.teacherName}${c.maxStudents}${c.enrolledCount}${empty c.schedule ? '—' : c.schedule} + 编辑 +
+ + +
+
+
+
+ +

暂无课程数据

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/dashboard.jsp b/src/main/webapp/admin/dashboard.jsp new file mode 100644 index 0000000..421ced5 --- /dev/null +++ b/src/main/webapp/admin/dashboard.jsp @@ -0,0 +1,133 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + + +
+ + + + + +
+ + + + +
+
+
用户总数
+
${stats.totalUsers}
+
+
+
今日注册
+
${stats.todayRegister}
+
+
+
课程总数
+
${stats.totalCourses}
+
+
+
待审注册
+
${stats.pendingRegister}
+
+
+
待审变更
+
${stats.pendingChange}
+
+
+
待审请假
+
${stats.pendingLeave}
+
+
+ +
+
+

性别分布

+
+
+
+

院系分布

+
+
+
+ +
+
+

最新公告

+ 管理公告 +
+
+ + +
+ + + + + + + + + + + +
标题发布人发布时间
+ 置顶 + ${ann.title} + ${empty ann.publisherName ? '—' : ann.publisherName}
+
+
+ +
📭

暂无公告

+
+
+
+
+
+
+ + + diff --git a/src/main/webapp/admin/import_preview.jsp b/src/main/webapp/admin/import_preview.jsp new file mode 100644 index 0000000..5fa44b6 --- /dev/null +++ b/src/main/webapp/admin/import_preview.jsp @@ -0,0 +1,76 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

导入预览

+ +
+
+ + +
+
+

待导入数据(共 ${empty importList ? 0 : importList.size()} 条)

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
用户名真实姓名性别年龄邮箱电话地址
${row.username}${row.realName}${row.gender}${row.age}${row.email}${row.phone}${row.address}
+
+
+
+ + + +
+
+ + + +
+
+
+ +

暂无待导入数据,请先上传 CSV 文件

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/leave_approve.jsp b/src/main/webapp/admin/leave_approve.jsp new file mode 100644 index 0000000..18e46dc --- /dev/null +++ b/src/main/webapp/admin/leave_approve.jsp @@ -0,0 +1,78 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

请假审批

+
+ 退出 +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
学号学生开始日期结束日期原因状态操作
${lv.studentNo}${lv.studentName}${lv.reason} + + 待审批 + 已通过 + 已拒绝 + + + +
+ + + +
+
+ + + + +
+
+
+
+
+ +

暂无请假申请

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/logs.jsp b/src/main/webapp/admin/logs.jsp new file mode 100644 index 0000000..358257c --- /dev/null +++ b/src/main/webapp/admin/logs.jsp @@ -0,0 +1,83 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + + +
+
+

系统日志

+
+ 退出 +
+
+
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + + +
用户模块操作IP时间
${log.username}${log.module}${log.action}${log.ip}
+
+
+ +
+ + + + + + + + + + + + + + +
用户名IP结果时间
${log.username}${log.ip}${log.result == 1 ? '成功' : '失败'}
+
+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/message_send.jsp b/src/main/webapp/admin/message_send.jsp new file mode 100644 index 0000000..881d62b --- /dev/null +++ b/src/main/webapp/admin/message_send.jsp @@ -0,0 +1,51 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

消息发送

+
+ 退出 +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + diff --git a/src/main/webapp/admin/online.jsp b/src/main/webapp/admin/online.jsp new file mode 100644 index 0000000..0d5a591 --- /dev/null +++ b/src/main/webapp/admin/online.jsp @@ -0,0 +1,68 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

在线用户

+
+ 当前在线:${onlineCount} 人 + 退出 +
+
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + +
用户名姓名角色操作
${u.username}${u.realName} + + 管理员 + 教师 + 学生 + + +
+ + + +
+
+
+
+ +

当前无在线用户

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/org.jsp b/src/main/webapp/admin/org.jsp new file mode 100644 index 0000000..f62be25 --- /dev/null +++ b/src/main/webapp/admin/org.jsp @@ -0,0 +1,99 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

组织管理

+
+ 退出 +
+
+
+ + +
+
+

院系管理

+
+
+ + +
+ +
+ +
+
    + +
  • ${dept.name}
  • +
    +
+
+
+
+

专业管理

+
+
+ + +
+ +
+
+ +
+ +
+
    + +
  • ${major.name}
  • +
    +
+
+
+
+

班级管理

+
+
+ + +
+ +
+
+ +
+ +
+
    + +
  • ${clazz.name}
  • +
    +
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/reg_audit.jsp b/src/main/webapp/admin/reg_audit.jsp new file mode 100644 index 0000000..aa8b8a3 --- /dev/null +++ b/src/main/webapp/admin/reg_audit.jsp @@ -0,0 +1,68 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

注册审核

+
+ 退出 +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + +
用户名姓名性别班级注册时间操作
${u.username}${u.realName}${u.gender}${empty u.clazzName ? '—' : u.clazzName} +
+ + + +
+
+ + + +
+
+
+
+ +
📭

暂无待审核注册申请

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/score_manage.jsp b/src/main/webapp/admin/score_manage.jsp new file mode 100644 index 0000000..0c51c27 --- /dev/null +++ b/src/main/webapp/admin/score_manage.jsp @@ -0,0 +1,80 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

成绩管理

+
+ 退出 +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+

批量录入成绩

+
+ + +
+ +
+ + + + + + + + + + + + + + +
学号姓名成绩备注
${s.studentNo}${s.studentName}
+
+
+ +
+
+
+ + + + + +
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/score_stats.jsp b/src/main/webapp/admin/score_stats.jsp new file mode 100644 index 0000000..f61e7c8 --- /dev/null +++ b/src/main/webapp/admin/score_stats.jsp @@ -0,0 +1,101 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + + +
+
+

成绩统计

+
+ 退出 +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + +
课程平均分最高分最低分及格率人数
+ + ${c.name} + + ${st.avg}${st.max}${st.min}${st.passRate}%${st.totalCount}
+
+
+ + + + + +
+
+
+ +
+

成绩分布图

+
+
+
+
+ + + diff --git a/src/main/webapp/admin/user_add.jsp b/src/main/webapp/admin/user_add.jsp new file mode 100644 index 0000000..fe5d6c2 --- /dev/null +++ b/src/main/webapp/admin/user_add.jsp @@ -0,0 +1,114 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

新增用户

+ +
+
+
+
+ +
${param.msg}
+
+
+

账号信息

+
+ + +
+
+
+ + +
+
+ + +
+

个人信息

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + 取消 +
+
+
+
+
+
+ + + diff --git a/src/main/webapp/admin/user_detail.jsp b/src/main/webapp/admin/user_detail.jsp new file mode 100644 index 0000000..410f149 --- /dev/null +++ b/src/main/webapp/admin/user_detail.jsp @@ -0,0 +1,90 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

学生详情 · ${user.realName}

+ +
+
+ + +
+
+
+
+ + + 头像 + + ${user.username.substring(0,1)} + +
+
+

${user.realName} (${user.username})

+

学号:${empty user.studentNo ? '未分配' : user.studentNo}

+
+
+ +
+
+
院系
${empty user.departmentName ? '—' : user.departmentName}
+
专业
${empty user.majorName ? '—' : user.majorName}
+
班级
${empty user.clazzName ? '—' : user.clazzName}
+
性别
${empty user.gender ? '—' : user.gender}
+
年龄
${empty user.age ? '—' : user.age}
+
状态
+
+ + 待审核 + 在读 + 休学 + 毕业 + 退学 + + +
+
+
邮箱
${empty user.email ? '—' : user.email}
+
电话
${empty user.phone ? '—' : user.phone}
+
地址
${empty user.address ? '—' : user.address}
+
注册时间
+
+
+ +
+ +

变更学籍状态

+
+
+ + +
+
+ +
+
+
+
+
+ + diff --git a/src/main/webapp/admin/user_edit.jsp b/src/main/webapp/admin/user_edit.jsp new file mode 100644 index 0000000..3821dc8 --- /dev/null +++ b/src/main/webapp/admin/user_edit.jsp @@ -0,0 +1,106 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

编辑用户 · ${user.username}

+ +
+
+
+
+ +
${param.msg}
+
+
+ +

账号信息

+
+ + +
+
+ + +
+
+ + +
+

个人信息

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + 取消 +
+
+
+
+
+
+ + diff --git a/src/main/webapp/admin/user_list.jsp b/src/main/webapp/admin/user_list.jsp new file mode 100644 index 0000000..36e7129 --- /dev/null +++ b/src/main/webapp/admin/user_list.jsp @@ -0,0 +1,148 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+ + + + + +
+ + +
+
+
匹配用户
+
${pageBean.totalCount}
+
+
+
当前页码
+
${pageBean.currentPage} / ${pageBean.totalPage}
+
+
+ +
+
+
+ + + + + + + + 重置 +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
学号用户名姓名性别年龄班级状态注册时间操作
${empty u.studentNo ? '—' : u.studentNo}${u.username}${empty u.realName ? '—' : u.realName} + + + + + + ${empty u.age ? '—' : u.age}${empty u.clazzName ? '—' : u.clazzName} + + 待审核 + 在读 + 休学 + 毕业 + 退学 + + + +
+ 详情 + 编辑 + +
+
+
+ + +
+ +
📭

暂无用户数据

+
+
+
+
+
+
+ + + diff --git a/src/main/webapp/admin/user_upload.jsp b/src/main/webapp/admin/user_upload.jsp new file mode 100644 index 0000000..dd3883d --- /dev/null +++ b/src/main/webapp/admin/user_upload.jsp @@ -0,0 +1,47 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

批量上传用户

+ +
+
+ +
+
+
+ CSV 文件格式说明
+ 第一行为表头(可选),数据行格式:
+ 用户名,密码,真实姓名,性别,年龄,邮箱,电话,地址

+ 示例:
+ lisi,user123,李四,男,21,lisi@example.com,13900000002,上海市 +
+
+
+ + +
+
+ + 取消 +
+
+
+
+
+
+ + diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css new file mode 100644 index 0000000..e93b313 --- /dev/null +++ b/src/main/webapp/css/style.css @@ -0,0 +1,1362 @@ +/* ============================================================ + 学生信息管理系统 — 设计系统 + 风格:简约、专业、管理系统 + ============================================================ */ + +:root { + --primary: #2563eb; + --primary-hover: #1d4ed8; + --primary-light: #eff6ff; + --success: #059669; + --success-hover: #047857; + --danger: #dc2626; + --danger-hover: #b91c1c; + --warning: #d97706; + --info: #0891b2; + + --bg: #f1f5f9; + --surface: #ffffff; + --border: #e2e8f0; + --border-light: #f1f5f9; + + --text: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + + --sidebar-bg: #0f172a; + --sidebar-text: #94a3b8; + --sidebar-active: #ffffff; + --sidebar-hover: #1e293b; + + --radius: 8px; + --radius-lg: 12px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04); + + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + --sidebar-width: 240px; + --topbar-height: 60px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font); + color: var(--text); + background: var(--bg); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--primary); + text-decoration: none; +} + +a:hover { + color: var(--primary-hover); +} + +/* ============================================================ + 认证页面(登录 / 注册) + ============================================================ */ + +.auth-body { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg); + padding: 24px; +} + +.auth-layout { + display: flex; + width: 100%; + max-width: 960px; + min-height: 560px; + background: var(--surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.auth-brand { + flex: 1; + background: linear-gradient(145deg, #1e40af 0%, #2563eb 50%, #3b82f6 100%); + padding: 48px 40px; + display: flex; + flex-direction: column; + justify-content: center; + color: #fff; +} + +.auth-brand-logo { + width: 48px; + height: 48px; + background: rgba(255, 255, 255, 0.2); + border-radius: var(--radius); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + margin-bottom: 24px; +} + +.auth-brand h1 { + font-size: 28px; + font-weight: 700; + margin-bottom: 12px; + letter-spacing: -0.02em; +} + +.auth-brand p { + font-size: 15px; + opacity: 0.85; + line-height: 1.7; + max-width: 280px; +} + +.auth-brand-features { + margin-top: 40px; + list-style: none; +} + +.auth-brand-features li { + font-size: 13px; + opacity: 0.8; + padding: 6px 0; + padding-left: 20px; + position: relative; +} + +.auth-brand-features li::before { + content: "✓"; + position: absolute; + left: 0; + font-weight: 600; +} + +.auth-panel { + flex: 1; + padding: 48px 40px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.auth-panel-wide { + max-width: 520px; + margin: 0 auto; + width: 100%; +} + +.auth-panel h2 { + font-size: 22px; + font-weight: 600; + color: var(--text); + margin-bottom: 6px; +} + +.auth-subtitle { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 28px; +} + +.auth-card-single { + width: 100%; + max-width: 420px; + background: var(--surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + padding: 40px; +} + +/* ============================================================ + 管理后台布局 + ============================================================ */ + +.admin-body { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: var(--sidebar-width); + background: var(--sidebar-bg); + color: var(--sidebar-text); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 100; +} + +.sidebar-brand { + padding: 20px 20px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.sidebar-brand a { + display: flex; + align-items: center; + gap: 12px; + color: #fff; + text-decoration: none; +} + +.sidebar-brand a:hover { + color: #fff; +} + +.sidebar-logo { + width: 36px; + height: 36px; + background: var(--primary); + border-radius: var(--radius); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.sidebar-title { + font-size: 15px; + font-weight: 600; + line-height: 1.3; +} + +.sidebar-subtitle { + font-size: 11px; + color: var(--sidebar-text); + font-weight: 400; +} + +.sidebar-nav { + flex: 1; + padding: 16px 12px; + overflow-y: auto; +} + +.sidebar-section { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #475569; + padding: 8px 12px 6px; +} + +.sidebar-nav a { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: var(--radius); + color: var(--sidebar-text); + font-size: 14px; + text-decoration: none; + transition: all 0.15s; + margin-bottom: 2px; +} + +.sidebar-nav a:hover { + background: var(--sidebar-hover); + color: var(--sidebar-active); +} + +.sidebar-nav a.active { + background: var(--primary); + color: #fff; +} + +.sidebar-nav .nav-icon { + width: 20px; + text-align: center; + font-size: 15px; + opacity: 0.9; +} + +.main-wrapper { + flex: 1; + margin-left: var(--sidebar-width); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.topbar { + height: var(--topbar-height); + background: var(--surface); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px; + position: sticky; + top: 0; + z-index: 50; +} + +.topbar-left h2 { + font-size: 18px; + font-weight: 600; + color: var(--text); +} + +.topbar-right { + display: flex; + align-items: center; + gap: 16px; +} + +.user-info { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: var(--text-secondary); +} + +.user-avatar { + width: 34px; + height: 34px; + background: var(--primary-light); + color: var(--primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; +} + +.page-content { + flex: 1; + padding: 24px 28px 32px; +} + +/* ============================================================ + 卡片 / 统计 + ============================================================ */ + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.card-body { + padding: 24px; +} + +.card-header { + padding: 20px 24px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-header h3 { + font-size: 16px; + font-weight: 600; +} + +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.stat-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px; + box-shadow: var(--shadow-sm); +} + +.stat-card .stat-label { + font-size: 13px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.stat-card .stat-value { + font-size: 28px; + font-weight: 700; + color: var(--text); +} + +.stat-card .stat-value.primary { + color: var(--primary); +} + +/* ============================================================ + 表单 + ============================================================ */ + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 500; + color: var(--text); +} + +.form-group label .required { + color: var(--danger); + margin-left: 2px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 9px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 14px; + font-family: var(--font); + color: var(--text); + background: var(--surface); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.form-group input::placeholder { + color: var(--text-muted); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +.form-group input[type="file"] { + padding: 8px; + font-size: 13px; + color: var(--text-secondary); +} + +.form-group input[type="file"]::file-selector-button { + padding: 6px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 13px; + cursor: pointer; + margin-right: 12px; + transition: background 0.15s; +} + +.form-group input[type="file"]::file-selector-button:hover { + background: var(--border-light); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid var(--border-light); +} + +.captcha-row { + display: flex; + gap: 10px; + align-items: center; +} + +.captcha-row input { + flex: 1; +} + +.captcha-row img { + height: 40px; + border-radius: var(--radius); + cursor: pointer; + border: 1px solid var(--border); + transition: opacity 0.15s; +} + +.captcha-row img:hover { + opacity: 0.85; +} + +/* ============================================================ + 按钮 + ============================================================ */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 18px; + border: 1px solid transparent; + border-radius: var(--radius); + font-size: 14px; + font-weight: 500; + font-family: var(--font); + cursor: pointer; + text-decoration: none; + transition: all 0.15s; + white-space: nowrap; + line-height: 1.4; +} + +.btn-primary { + background: var(--primary); + color: #fff; +} + +.btn-primary:hover { + background: var(--primary-hover); + color: #fff; +} + +.btn-success { + background: var(--success); + color: #fff; +} + +.btn-success:hover { + background: var(--success-hover); + color: #fff; +} + +.btn-danger { + background: var(--danger); + color: #fff; +} + +.btn-danger:hover { + background: var(--danger-hover); + color: #fff; +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + border-color: var(--border); +} + +.btn-secondary:hover { + background: var(--bg); + color: var(--text); +} + +.btn-outline { + background: transparent; + color: var(--primary); + border-color: var(--primary); +} + +.btn-outline:hover { + background: var(--primary-light); + color: var(--primary); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border: none; + padding: 9px 12px; +} + +.btn-ghost:hover { + background: var(--bg); + color: var(--text); +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +.btn-block { + width: 100%; +} + +.btn-icon { + padding: 6px 10px; +} + +/* ============================================================ + 工具栏 / 搜索 + ============================================================ */ + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 12px; +} + +.search-form { + display: flex; + gap: 8px; +} + +.search-form input { + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 14px; + width: 260px; + font-family: var(--font); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.search-form input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +.toolbar-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* ============================================================ + 表格 + ============================================================ */ + +.table-wrap { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.data-table th { + padding: 12px 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.03em; + background: var(--bg); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.data-table td { + padding: 14px 16px; + border-bottom: 1px solid var(--border-light); + color: var(--text); + vertical-align: middle; +} + +.data-table tbody tr { + transition: background 0.1s; +} + +.data-table tbody tr:hover { + background: #f8fafc; +} + +.data-table tbody tr:last-child td { + border-bottom: none; +} + +.data-table .actions { + white-space: nowrap; + display: flex; + gap: 6px; +} + +.text-muted { + color: var(--text-muted); +} + +/* 标签 */ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; +} + +.badge-blue { + background: #dbeafe; + color: #1d4ed8; +} + +.badge-pink { + background: #fce7f3; + color: #be185d; +} + +.badge-gray { + background: #f1f5f9; + color: #64748b; +} + +/* ============================================================ + 分页 + ============================================================ */ + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--border-light); +} + +.pagination a, +.pagination span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 36px; + height: 36px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: var(--radius); + text-decoration: none; + color: var(--text-secondary); + font-size: 13px; + transition: all 0.15s; +} + +.pagination a:hover { + border-color: var(--primary); + color: var(--primary); + background: var(--primary-light); +} + +.pagination span.current { + background: var(--primary); + color: #fff; + border-color: var(--primary); +} + +.page-info { + color: var(--text-muted); + font-size: 13px; + margin-left: 12px; +} + +/* ============================================================ + 提示 / 空状态 + ============================================================ */ + +.alert { + padding: 12px 16px; + border-radius: var(--radius); + margin-bottom: 20px; + font-size: 14px; + display: flex; + align-items: flex-start; + gap: 10px; + border: 1px solid; +} + +.alert-success { + background: #ecfdf5; + color: #065f46; + border-color: #a7f3d0; +} + +.alert-error { + background: #fef2f2; + color: #991b1b; + border-color: #fecaca; +} + +.alert-info { + background: #eff6ff; + color: #1e40af; + border-color: #bfdbfe; +} + +.tip-text { + font-size: 12px; + margin-top: 4px; +} + +.tip-success { + color: var(--success); +} + +.tip-error { + color: var(--danger); +} + +.link-text { + text-align: center; + margin-top: 20px; + font-size: 14px; + color: var(--text-secondary); +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--text-muted); +} + +.empty-state:not(:has(.empty-state-icon))::before { + content: '📭'; + display: block; + font-size: 48px; + margin-bottom: 12px; + opacity: 0.4; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 12px; + opacity: 0.4; +} + +.empty-state p { + font-size: 15px; +} + +/* 信息面板 */ +.info-panel { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 20px; + margin-bottom: 24px; +} + +.info-panel-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.info-item { + font-size: 14px; +} + +.info-item .label { + color: var(--text-secondary); + font-size: 12px; + margin-bottom: 2px; +} + +.info-item .value { + color: var(--text); + font-weight: 500; +} + +.upload-tip { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 20px; + margin-bottom: 24px; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.8; +} + +.upload-tip strong { + color: var(--text); +} + +.upload-tip code { + background: var(--surface); + border: 1px solid var(--border); + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + color: var(--primary); +} + +.demo-account { + margin-top: 24px; + padding: 14px 16px; + background: var(--bg); + border-radius: var(--radius); + font-size: 12px; + color: var(--text-secondary); + line-height: 1.8; + border: 1px solid var(--border-light); +} + +.demo-account strong { + color: var(--text); + font-size: 13px; +} + +.form-section-title { + font-size: 14px; + font-weight: 600; + color: var(--text); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-light); +} + +.form-card-narrow { + max-width: 640px; +} + +/* ============================================================ + 响应式 + ============================================================ */ + +@media (max-width: 768px) { + .auth-layout { + flex-direction: column; + max-width: 440px; + } + + .auth-brand { + padding: 32px 28px; + } + + .auth-brand-features { + display: none; + } + + .auth-panel { + padding: 32px 28px; + } + + .sidebar { + display: none; + } + + .main-wrapper { + margin-left: 0; + } + + .form-row { + grid-template-columns: 1fr; + } + + .info-panel-grid { + grid-template-columns: 1fr; + } + + .search-form input { + width: 180px; + } + + .page-content { + padding: 16px; + } +} + +/* 兼容旧类名 */ +.container { max-width: none; padding: 0; } +.main-card { background: transparent; box-shadow: none; padding: 0; border: none; } +.header { display: none; } +.form-card { max-width: 640px; margin: 0 auto; } + +.profile-info { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 20px; + margin-bottom: 24px; +} + +.profile-info p { + margin: 4px 0; + font-size: 14px; + color: var(--text-secondary); +} + +.profile-info strong { + color: var(--text); + font-weight: 500; + min-width: 80px; + display: inline-block; +} + +.empty-tip { + text-align: center; + padding: 60px 20px; + color: var(--text-muted); +} + +/* 数据大屏 F-03 */ +.bigscreen-body { + background: #0b1120; + color: #e2e8f0; + min-height: 100vh; +} + +.bigscreen-header { + text-align: center; + padding: 24px; + font-size: 28px; + font-weight: 700; + letter-spacing: 4px; + color: #38bdf8; +} + +.bigscreen-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding: 0 20px 20px; +} + +.bigscreen-panel { + background: rgba(30, 41, 59, 0.8); + border: 1px solid rgba(56, 189, 248, 0.2); + border-radius: 8px; + padding: 16px; +} + +.bigscreen-panel h3 { + font-size: 14px; + color: #94a3b8; + margin-bottom: 12px; +} + +.bigscreen-chart { + height: 260px; +} + +.tabs { + display: flex; + gap: 4px; + margin-bottom: 20px; + border-bottom: 1px solid var(--border); +} + +.tab-btn { + padding: 10px 18px; + border: none; + background: none; + cursor: pointer; + font-size: 14px; + color: var(--text-secondary); + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} + +.tab-btn.active { + color: var(--primary); + border-bottom-color: var(--primary); + font-weight: 600; +} + +.avatar-preview { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--border); +} + +/* ========== v3 优化 ========== */ + +.page-content { + animation: fadeIn 0.25s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.topbar-subtitle { + font-size: 12px; + color: var(--text-muted); + margin-top: 2px; + font-weight: 400; +} + +.sidebar-toggle { + display: none; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 10px; + font-size: 18px; + cursor: pointer; + margin-right: 12px; + color: var(--text); +} + +.nav-badge { + margin-left: auto; + background: var(--danger); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 1px 7px; + border-radius: 999px; + min-width: 20px; + text-align: center; +} + +.sidebar-nav a { + position: relative; +} + +.search-panel { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 16px; + background: var(--bg); + border: 1px solid var(--border-light); + border-radius: var(--radius); + margin-bottom: 16px; +} + +.search-panel input, +.search-panel select { + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 14px; + font-family: var(--font); + background: var(--surface); +} + +.search-panel input:focus, +.search-panel select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +.search-panel .keyword-input { + flex: 1; + min-width: 180px; +} + +.chart-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 20px; +} + +.stat-card { + position: relative; + overflow: hidden; + transition: transform 0.15s, box-shadow 0.15s; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow); +} + +.stat-card::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: var(--primary); + opacity: 0; + transition: opacity 0.15s; +} + +.stat-card:hover::after { + opacity: 1; +} + +.alert-dismissible { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.alert-close { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + opacity: 0.5; + line-height: 1; + padding: 0 4px; +} + +.alert-close:hover { + opacity: 1; +} + +.alert-error { + background: #fef2f2; + color: #991b1b; + border-color: #fecaca; +} + +#toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + padding: 12px 20px; + border-radius: var(--radius); + color: #fff; + font-size: 14px; + box-shadow: var(--shadow-md); + opacity: 0; + transform: translateX(40px); + transition: all 0.3s ease; + max-width: 360px; +} + +.toast.show { + opacity: 1; + transform: translateX(0); +} + +.toast-success { background: var(--success); } +.toast-error { background: var(--danger); } +.toast-info { background: var(--primary); } + +.page-ellipsis { + padding: 0 6px; + color: var(--text-muted); +} + +.user-avatar { + background: linear-gradient(135deg, #2563eb, #7c3aed); + color: #fff; + font-weight: 600; +} + +.badge-warning { + background: #fef3c7; + color: #b45309; +} + +.badge-success { + background: #d1fae5; + color: #047857; +} + +.quick-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.quick-action-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + text-decoration: none; + font-size: 13px; + transition: all 0.15s; +} + +.quick-action-btn:hover { + border-color: var(--primary); + color: var(--primary); + background: var(--primary-light); +} + +@media (max-width: 768px) { + .sidebar-toggle { + display: inline-block; + } + + .sidebar { + transform: translateX(-100%); + transition: transform 0.25s ease; + } + + .sidebar.open { + transform: translateX(0); + } + + .chart-grid { + grid-template-columns: 1fr; + } + + #toast-container { + left: 16px; + right: 16px; + } + + .toast { + max-width: none; + } +} + +.remember-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.remember-label { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-weight: 400; + cursor: pointer; + font-size: 14px; +} + +.remember-label input { + width: auto; +} + +.forgot-link { + font-size: 13px; +} diff --git a/src/main/webapp/error.jsp b/src/main/webapp/error.jsp new file mode 100644 index 0000000..455ac7a --- /dev/null +++ b/src/main/webapp/error.jsp @@ -0,0 +1,40 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + 错误 - 学生信息管理系统 + + + +
+
+
⚠️
+
+

页面出错了

+

抱歉,系统遇到了一个问题

+
+ 请稍后重试,或联系管理员。 + +
${exception.message} +
+
+ + + 返回主页 + + + 返回主页 + + + 返回主页 + + + 返回登录 + + +
+ + diff --git a/src/main/webapp/forgot_password.jsp b/src/main/webapp/forgot_password.jsp new file mode 100644 index 0000000..f63245c --- /dev/null +++ b/src/main/webapp/forgot_password.jsp @@ -0,0 +1,67 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + +
+

找回密码

+

通过用户名和验证码重置密码(演示模式)

+ + +
${param.msg}
+
+ +
${error}
+
+ + + +
+ + +
+ + +
+
+ + +
演示模式:验证码已存入 Session,请查看控制台或提示信息
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ + +
+ +
+
+
+ + +
+ + + diff --git a/src/main/webapp/js/common.js b/src/main/webapp/js/common.js new file mode 100644 index 0000000..22bbfa7 --- /dev/null +++ b/src/main/webapp/js/common.js @@ -0,0 +1,134 @@ +/** + * 学生信息管理系统 - 公共脚本 + */ +var StudentApp = { + contextPath: '', + + init: function (contextPath) { + this.contextPath = contextPath; + this.bindSidebarToggle(); + this.autoDismissAlerts(); + this.showUrlToast(); + }, + + bindSidebarToggle: function () { + $('#sidebarToggle').on('click', function () { + $('#sidebar').toggleClass('open'); + }); + $(document).on('click', function (e) { + if ($(window).width() <= 768 && !$(e.target).closest('#sidebar, #sidebarToggle').length) { + $('#sidebar').removeClass('open'); + } + }); + }, + + autoDismissAlerts: function () { + $('[data-auto-dismiss="true"]').each(function () { + var el = $(this); + setTimeout(function () { + el.fadeOut(300, function () { el.remove(); }); + }, 4000); + }); + }, + + showUrlToast: function () { + var params = new URLSearchParams(window.location.search); + var msg = params.get('msg'); + if (msg) { + this.toast(decodeURIComponent(msg), params.get('type') || 'success'); + } + }, + + toast: function (message, type) { + type = type || 'info'; + var container = $('#toast-container'); + if (!container.length) { + container = $('
').appendTo('body'); + } + var toast = $('
' + message + '
'); + container.append(toast); + setTimeout(function () { + toast.addClass('show'); + }, 10); + setTimeout(function () { + toast.removeClass('show'); + setTimeout(function () { toast.remove(); }, 300); + }, 3000); + }, + + refreshCaptcha: function () { + $('#captchaImg').attr('src', this.contextPath + '/captcha?t=' + new Date().getTime()); + }, + + confirmDelete: function (id, username, callback) { + if (confirm('确定要删除用户 "' + username + '" 吗?此操作不可恢复。')) { + var self = this; + $.ajax({ + url: this.contextPath + '/admin/userDelete', + type: 'POST', + data: { id: id, ajax: 'true' }, + dataType: 'json', + success: function (result) { + self.toast(result.message, result.success ? 'success' : 'error'); + if (result.success && callback) callback(); + }, + error: function () { + self.toast('删除请求失败,请重试', 'error'); + } + }); + } + }, + + checkUsername: function (username, tipElement) { + if (!username || username.length < 3) { + tipElement.text('用户名至少3个字符').removeClass('tip-success').addClass('tip-error'); + return; + } + $.getJSON(this.contextPath + '/checkUsername', { username: username }, function (result) { + tipElement.text(result.message); + tipElement.toggleClass('tip-success', result.available).toggleClass('tip-error', !result.available); + }); + }, + + initOrgCascade: function (deptSel, majorSel, clazzSel) { + var self = this; + var $dept = $(deptSel), $major = $(majorSel), $clazz = $(clazzSel); + + function reset($el, placeholder) { + $el.empty().append(''); + } + + $dept.on('change', function () { + reset($major, '请选择专业'); + reset($clazz, '请选择班级'); + var deptId = $(this).val(); + if (!deptId) return; + $.getJSON(self.contextPath + '/public/org/majors', { deptId: deptId }, function (res) { + if (res.code === 200 && res.data) { + $.each(res.data, function (_, m) { + $major.append(''); + }); + } + }); + }); + + $major.on('change', function () { + reset($clazz, '请选择班级'); + var majorId = $(this).val(); + if (!majorId) return; + $.getJSON(self.contextPath + '/public/org/clazz', { majorId: majorId }, function (res) { + if (res.code === 200 && res.data) { + $.each(res.data, function (_, c) { + $clazz.append(''); + }); + } + }); + }); + } +}; + +$(function () { + if (typeof contextPath !== 'undefined') { + StudentApp.init(contextPath); + } +}); diff --git a/src/main/webapp/login.jsp b/src/main/webapp/login.jsp new file mode 100644 index 0000000..89f539d --- /dev/null +++ b/src/main/webapp/login.jsp @@ -0,0 +1,78 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + 用户登录 - 学生信息管理系统 + + + + + +
+
+ +

学生信息管理系统

+

简洁高效的学生信息管理平台,支持用户注册、信息维护与管理员集中管理。

+
    +
  • 安全的登录验证与图形验证码
  • +
  • 管理员用户增删改查与批量导入
  • +
  • 个人信息自助查看与修改
  • +
+
+
+
+

欢迎回来

+

请输入您的账号信息登录系统

+ + +
${param.msg}
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + 验证码 +
+
+
+ + 忘记密码? +
+ +
+ + + + +
+
+
+ + + diff --git a/src/main/webapp/register.jsp b/src/main/webapp/register.jsp new file mode 100644 index 0000000..6bce9b2 --- /dev/null +++ b/src/main/webapp/register.jsp @@ -0,0 +1,134 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + 用户注册 - 学生信息管理系统 + + + + + +
+
+ +

创建账号

+

注册成为系统用户,审核通过后即可登录使用。

+
    +
  • 实时校验用户名是否可用
  • +
  • 院系 / 专业 / 班级级联选择
  • +
  • 注册后等待管理员审核
  • +
+
+
+
+

用户注册

+

请填写以下信息完成注册

+ + +
${param.msg}
+
+ +
+

账号信息

+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +

个人信息

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +

所属组织(可选)

+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+ + + diff --git a/src/main/webapp/teacher/attendance.jsp b/src/main/webapp/teacher/attendance.jsp new file mode 100644 index 0000000..ee2fceb --- /dev/null +++ b/src/main/webapp/teacher/attendance.jsp @@ -0,0 +1,103 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

考勤登记

+
+ 退出 +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+

今日考勤

+
+ + +
+ + + + + + + + + + + + +
学生课程日期状态
${a.studentName}${a.courseName} + + 出勤 + 迟到 + 缺勤 + +
+
+
+ +
📭

暂无记录

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/teacher/home.jsp b/src/main/webapp/teacher/home.jsp new file mode 100644 index 0000000..41888df --- /dev/null +++ b/src/main/webapp/teacher/home.jsp @@ -0,0 +1,65 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

工作台

+
+ + 退出 +
+
+
+ + +
+
我的课程
${empty courseCount ? 0 : courseCount}
+
待审请假
${empty pendingLeaveCount ? 0 : pendingLeaveCount}
+
学生总数
${empty studentCount ? 0 : studentCount}
+
+ +
+

我的课程

+
+ + +
+ + + + + + + + + + + + + +
课程编号课程名称学分已选人数上课时间
${c.courseNo}${c.name}${c.credit}${c.enrolledCount}/${c.maxStudents}${c.schedule}
+
+
+ +
📭

暂无授课课程

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/teacher/leave.jsp b/src/main/webapp/teacher/leave.jsp new file mode 100644 index 0000000..62563e8 --- /dev/null +++ b/src/main/webapp/teacher/leave.jsp @@ -0,0 +1,77 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

请假审批

+
+ 退出 +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
学号学生开始结束原因状态操作
${lv.studentNo}${lv.studentName}${lv.reason} + + 待审批 + 已通过 + 已拒绝 + + + +
+ + + +
+
+ + + +
+
+
+
+
+ +

暂无请假申请

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/teacher/score.jsp b/src/main/webapp/teacher/score.jsp new file mode 100644 index 0000000..43ee0e6 --- /dev/null +++ b/src/main/webapp/teacher/score.jsp @@ -0,0 +1,77 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

成绩录入

+
+ 退出 +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ + + + + + + + + + + + +
学号姓名成绩备注
${s.studentNo}${s.studentName}
+
+
+ +
+
+
+ + + + + +
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/enroll.jsp b/src/main/webapp/user/enroll.jsp new file mode 100644 index 0000000..cf7e4cc --- /dev/null +++ b/src/main/webapp/user/enroll.jsp @@ -0,0 +1,72 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

选课报名

+
+ 退出 +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
课程编号课程名称学分教师已选/容量上课时间操作
${c.courseNo}${c.name}${c.credit}${empty c.teacherName ? '—' : c.teacherName}${c.enrolledCount}/${c.maxStudents}${empty c.schedule ? '—' : c.schedule} + + + 已选 + + + 已满 + + +
+ + +
+
+
+
+
+
+ +

暂无可选课程

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/leave_apply.jsp b/src/main/webapp/user/leave_apply.jsp new file mode 100644 index 0000000..3c89ffc --- /dev/null +++ b/src/main/webapp/user/leave_apply.jsp @@ -0,0 +1,88 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

请假申请

+
+ 退出 +
+
+
+ + +
+

提交请假

+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+

请假记录

+
+ + +
+ + + + + + + + + + + + + + + + +
开始日期结束日期原因状态审批意见申请时间
${lv.reason} + + 待审批 + 已通过 + 已拒绝 + + ${empty lv.approveNote ? '—' : lv.approveNote}
+
+
+ +
📭

暂无请假记录

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/messages.jsp b/src/main/webapp/user/messages.jsp new file mode 100644 index 0000000..7098f0e --- /dev/null +++ b/src/main/webapp/user/messages.jsp @@ -0,0 +1,67 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+
+

消息中心 + + ${unreadCount} 未读 + +

+
+
+ 退出 +
+
+
+ + +
+
+ + + +
+
+
+ + 未读 + ${msg.title} + +
来自:${msg.senderName}
+
+ +
+

${msg.content}

+ +
+ + + +
+
+
+
+
+ +

暂无消息

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/my_courses.jsp b/src/main/webapp/user/my_courses.jsp new file mode 100644 index 0000000..8d84773 --- /dev/null +++ b/src/main/webapp/user/my_courses.jsp @@ -0,0 +1,60 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+
+

我的课程

+ +
+
+ + +
+

课程表

+
+ + +
+ + + + + + + + + + + + + + + + +
课程编号课程名称学分授课教师上课时间选课时间
${c.courseNo}${c.name}${c.credit}${empty c.teacherName ? '—' : c.teacherName}${empty c.schedule ? '—' : c.schedule}
+
+
+ +

尚未选课,去选课

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/my_scores.jsp b/src/main/webapp/user/my_scores.jsp new file mode 100644 index 0000000..981ba80 --- /dev/null +++ b/src/main/webapp/user/my_scores.jsp @@ -0,0 +1,71 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + +
+
+

我的成绩

+
+ 退出 +
+
+
+ + +
+
+
已修课程
+
${empty scoreList ? 0 : scoreList.size()}
+
+
+
平均绩点 GPA
+
${empty gpa ? 0 : gpa}
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + +
课程名称成绩备注
${s.courseName} + + ${s.score} + ${s.score} + ${s.score} + + ${empty s.remark ? '—' : s.remark}
+
+
+ +

暂无成绩记录

+
+
+
+
+
+
+ + diff --git a/src/main/webapp/user/profile.jsp b/src/main/webapp/user/profile.jsp new file mode 100644 index 0000000..138e380 --- /dev/null +++ b/src/main/webapp/user/profile.jsp @@ -0,0 +1,101 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + + + + + + + +
+ + + +
+ + +
+
+
+
+ + + +
+
+
+ +
+ +
+
+ +
+
+
用户名
${user.username}
+
学号
${empty user.studentNo ? '—' : user.studentNo}
+
班级
${empty user.clazzName ? '—' : user.clazzName}
+
注册时间
+
+
+ +
+

修改信息

+
+ + +
+
+
+ + +
修改姓名需提交变更申请,等待管理员审核
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ 您有一条待审核的姓名变更申请:${pendingChange.oldValue} → ${pendingChange.newValue} +
+
+
+
+
+
+ + diff --git a/src/test/java/com/student/service/AnnouncementServiceTest.java b/src/test/java/com/student/service/AnnouncementServiceTest.java new file mode 100644 index 0000000..f6d68d8 --- /dev/null +++ b/src/test/java/com/student/service/AnnouncementServiceTest.java @@ -0,0 +1,67 @@ +package com.student.service; + +import com.student.bean.Announcement; +import com.student.dao.AnnouncementDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AnnouncementServiceTest { + + @Mock + private AnnouncementDao announcementDao; + + private AnnouncementService announcementService; + + @BeforeEach + void setUp() { + announcementService = new AnnouncementService(announcementDao); + } + + @Test + void findById_delegates() { + Announcement a = new Announcement(); + when(announcementDao.findById(1)).thenReturn(a); + assertSame(a, announcementService.findById(1)); + } + + @Test + void add_delegates() { + Announcement a = new Announcement(); + when(announcementDao.insert(a)).thenReturn(true); + assertTrue(announcementService.add(a)); + } + + @Test + void update_delegates() { + Announcement a = new Announcement(); + when(announcementDao.update(a)).thenReturn(true); + assertTrue(announcementService.update(a)); + } + + @Test + void delete_delegates() { + when(announcementDao.delete(1)).thenReturn(true); + assertTrue(announcementService.delete(1)); + } + + @Test + void findAll_delegates() { + when(announcementDao.findAll()).thenReturn(Collections.emptyList()); + assertTrue(announcementService.findAll().isEmpty()); + } + + @Test + void findActive_delegates() { + when(announcementDao.findActive()).thenReturn(Collections.emptyList()); + assertTrue(announcementService.findActive().isEmpty()); + } +} diff --git a/src/test/java/com/student/service/AttendanceServiceTest.java b/src/test/java/com/student/service/AttendanceServiceTest.java new file mode 100644 index 0000000..ede577a --- /dev/null +++ b/src/test/java/com/student/service/AttendanceServiceTest.java @@ -0,0 +1,76 @@ +package com.student.service; + +import com.student.bean.Attendance; +import com.student.bean.AttendanceStats; +import com.student.dao.AttendanceDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AttendanceServiceTest { + + @Mock + private AttendanceDao attendanceDao; + + private AttendanceService attendanceService; + + @BeforeEach + void setUp() { + attendanceService = new AttendanceService(attendanceDao); + } + + @Test + void recordBatch_delegates() { + List records = Collections.singletonList(new Attendance()); + when(attendanceDao.saveBatch(records)).thenReturn(true); + assertTrue(attendanceService.recordBatch(records)); + } + + @Test + void findByStudent_delegates() { + when(attendanceDao.findByStudent(1)).thenReturn(Collections.emptyList()); + assertTrue(attendanceService.findByStudent(1).isEmpty()); + } + + @Test + void findByCourse_delegates() { + when(attendanceDao.findByCourse(10)).thenReturn(Collections.emptyList()); + assertTrue(attendanceService.findByCourse(10).isEmpty()); + } + + @Test + void findRecent_delegates() { + when(attendanceDao.findRecent(5)).thenReturn(Collections.emptyList()); + assertTrue(attendanceService.findRecent(5).isEmpty()); + } + + @Test + void getStatsByStudent_delegates() { + AttendanceStats stats = new AttendanceStats(); + when(attendanceDao.getStatsByStudent(1)).thenReturn(stats); + assertSame(stats, attendanceService.getStatsByStudent(1)); + } + + @Test + void getStatsByClazz_delegates() { + AttendanceStats stats = new AttendanceStats(); + when(attendanceDao.getStatsByClazz(1)).thenReturn(stats); + assertSame(stats, attendanceService.getStatsByClazz(1)); + } + + @Test + void getStatsByCourse_delegates() { + AttendanceStats stats = new AttendanceStats(); + when(attendanceDao.getStatsByCourse(10)).thenReturn(stats); + assertSame(stats, attendanceService.getStatsByCourse(10)); + } +} diff --git a/src/test/java/com/student/service/AuthServiceTest.java b/src/test/java/com/student/service/AuthServiceTest.java new file mode 100644 index 0000000..0e3e00f --- /dev/null +++ b/src/test/java/com/student/service/AuthServiceTest.java @@ -0,0 +1,160 @@ +package com.student.service; + +import com.student.bean.LoginLog; +import com.student.bean.RememberToken; +import com.student.bean.User; +import com.student.dao.LogDao; +import com.student.dao.RememberTokenDao; +import com.student.dao.UserDao; +import com.student.util.PasswordUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Calendar; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @Mock + private UserDao userDao; + @Mock + private LogDao logDao; + @Mock + private RememberTokenDao tokenDao; + + private AuthService authService; + + @BeforeEach + void setUp() { + authService = new AuthService(userDao, logDao, tokenDao); + } + + private User activeUser(String password) { + String salt = PasswordUtil.generateSalt(); + User user = new User(); + user.setId(1); + user.setUsername("zhangsan"); + user.setSalt(salt); + user.setPassword(PasswordUtil.hash(password, salt)); + user.setStatus(User.STATUS_ACTIVE); + user.setFailCount(0); + return user; + } + + @Test + void login_userNotFound() { + when(userDao.findByUsername("nobody")).thenReturn(null); + assertNull(authService.login("nobody", "pwd", "127.0.0.1")); + verify(logDao).saveLoginLog(any(LoginLog.class)); + } + + @Test + void login_success() { + User user = activeUser("user123"); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + User result = authService.login("zhangsan", "user123", "127.0.0.1"); + assertNotNull(result); + assertNull(result.getPassword()); + verify(userDao).updateFailCount(1, 0); + verify(userDao).lockUser(1, null); + } + + @Test + void login_wrongPassword() { + User user = activeUser("user123"); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + assertNull(authService.login("zhangsan", "wrong", "127.0.0.1")); + verify(userDao).updateFailCount(1, 1); + } + + @Test + void login_lockAfterMaxFails() { + User user = activeUser("user123"); + user.setFailCount(4); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + RuntimeException ex = assertThrows(RuntimeException.class, + () -> authService.login("zhangsan", "wrong", "127.0.0.1")); + assertTrue(ex.getMessage().contains("锁定")); + verify(userDao).lockUser(eq(1), any(Date.class)); + } + + @Test + void login_alreadyLocked() { + User user = activeUser("user123"); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, 10); + user.setLockUntil(cal.getTime()); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + assertThrows(RuntimeException.class, + () -> authService.login("zhangsan", "user123", "127.0.0.1")); + } + + @Test + void login_pendingStatus() { + User user = activeUser("user123"); + user.setStatus(User.STATUS_PENDING); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + assertThrows(RuntimeException.class, + () -> authService.login("zhangsan", "user123", "127.0.0.1")); + } + + @Test + void login_suspendedStatus() { + User user = activeUser("user123"); + user.setStatus(User.STATUS_SUSPEND); + when(userDao.findByUsername("zhangsan")).thenReturn(user); + assertThrows(RuntimeException.class, + () -> authService.login("zhangsan", "user123", "127.0.0.1")); + } + + @Test + void createRememberToken() { + when(tokenDao.save(any(RememberToken.class))).thenReturn(true); + String token = authService.createRememberToken(1); + assertNotNull(token); + verify(tokenDao).deleteByUser(1); + ArgumentCaptor captor = ArgumentCaptor.forClass(RememberToken.class); + verify(tokenDao).save(captor.capture()); + assertEquals(1, captor.getValue().getUserId()); + } + + @Test + void loginByToken_valid() { + RememberToken rt = new RememberToken(); + rt.setUserId(1); + when(tokenDao.findByToken("tok")).thenReturn(rt); + User user = new User(); + user.setId(1); + when(userDao.findById(1)).thenReturn(user); + User result = authService.loginByToken("tok"); + assertNotNull(result); + assertNull(result.getPassword()); + } + + @Test + void loginByToken_invalid() { + when(tokenDao.findByToken("bad")).thenReturn(null); + assertNull(authService.loginByToken("bad")); + } + + @Test + void logoutRemember() { + authService.logoutRemember(1); + verify(tokenDao).deleteByUser(1); + } + + @Test + void cleanExpiredTokens() { + authService.cleanExpiredTokens(); + verify(tokenDao).deleteExpired(); + } +} diff --git a/src/test/java/com/student/service/ChangeRequestServiceTest.java b/src/test/java/com/student/service/ChangeRequestServiceTest.java new file mode 100644 index 0000000..a97fecb --- /dev/null +++ b/src/test/java/com/student/service/ChangeRequestServiceTest.java @@ -0,0 +1,119 @@ +package com.student.service; + +import com.student.bean.ChangeRequest; +import com.student.bean.User; +import com.student.dao.ChangeRequestDao; +import com.student.dao.UserDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ChangeRequestServiceTest { + + @Mock + private ChangeRequestDao changeRequestDao; + @Mock + private UserDao userDao; + + private ChangeRequestService changeRequestService; + + @BeforeEach + void setUp() { + changeRequestService = new ChangeRequestService(changeRequestDao, userDao); + } + + @Test + void submit_disallowedField() { + ChangeRequest cr = new ChangeRequest(); + cr.setFieldName("password"); + assertThrows(RuntimeException.class, () -> changeRequestService.submit(cr)); + } + + @Test + void submit_userNotFound() { + ChangeRequest cr = new ChangeRequest(); + cr.setUserId(1); + cr.setFieldName("realName"); + when(userDao.findById(1)).thenReturn(null); + assertThrows(RuntimeException.class, () -> changeRequestService.submit(cr)); + } + + @Test + void submit_realName_success() { + ChangeRequest cr = new ChangeRequest(); + cr.setUserId(1); + cr.setFieldName("realName"); + cr.setNewValue("新名字"); + User user = new User(); + user.setRealName("旧名字"); + when(userDao.findById(1)).thenReturn(user); + when(changeRequestDao.submit(cr)).thenReturn(true); + assertTrue(changeRequestService.submit(cr)); + assertEquals("旧名字", cr.getOldValue()); + } + + @Test + void submit_studentNo_duplicate() { + ChangeRequest cr = new ChangeRequest(); + cr.setUserId(1); + cr.setFieldName("studentNo"); + cr.setNewValue("2026001"); + User user = new User(); + user.setStudentNo("2025001"); + when(userDao.findById(1)).thenReturn(user); + when(userDao.findByStudentNo("2026001")).thenReturn(new User()); + assertThrows(RuntimeException.class, () -> changeRequestService.submit(cr)); + } + + @Test + void approve_notFound() { + when(changeRequestDao.findById(1)).thenReturn(null); + assertThrows(RuntimeException.class, () -> changeRequestService.approve(1, 2, "ok")); + } + + @Test + void approve_alreadyProcessed() { + ChangeRequest cr = new ChangeRequest(); + cr.setStatus(ChangeRequest.STATUS_APPROVED); + when(changeRequestDao.findById(1)).thenReturn(cr); + assertThrows(RuntimeException.class, () -> changeRequestService.approve(1, 2, "ok")); + } + + @Test + void approve_realName_success() { + ChangeRequest cr = new ChangeRequest(); + cr.setId(1); + cr.setUserId(10); + cr.setFieldName("realName"); + cr.setNewValue("新名字"); + cr.setStatus(ChangeRequest.STATUS_PENDING); + User user = new User(); + user.setId(10); + when(changeRequestDao.findById(1)).thenReturn(cr); + when(userDao.findById(10)).thenReturn(user); + when(userDao.update(user)).thenReturn(true); + when(changeRequestDao.approve(1, 2, "ok")).thenReturn(true); + assertTrue(changeRequestService.approve(1, 2, "ok")); + assertEquals("新名字", user.getRealName()); + } + + @Test + void reject_delegates() { + when(changeRequestDao.reject(1, 2, "no")).thenReturn(true); + assertTrue(changeRequestService.reject(1, 2, "no")); + } + + @Test + void findPending_delegates() { + when(changeRequestDao.findPending()).thenReturn(Collections.emptyList()); + assertTrue(changeRequestService.findPending().isEmpty()); + } +} diff --git a/src/test/java/com/student/service/CourseServiceTest.java b/src/test/java/com/student/service/CourseServiceTest.java new file mode 100644 index 0000000..056ebdf --- /dev/null +++ b/src/test/java/com/student/service/CourseServiceTest.java @@ -0,0 +1,98 @@ +package com.student.service; + +import com.student.bean.Course; +import com.student.bean.PageBean; +import com.student.dao.CourseDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CourseServiceTest { + + @Mock + private CourseDao courseDao; + + private CourseService courseService; + + @BeforeEach + void setUp() { + courseService = new CourseService(courseDao); + } + + @Test + void add_duplicateCourseNo() { + Course course = new Course(); + course.setCourseNo("CS101"); + when(courseDao.findByCourseNo("CS101")).thenReturn(new Course()); + assertThrows(RuntimeException.class, () -> courseService.add(course)); + } + + @Test + void add_success() { + Course course = new Course(); + course.setCourseNo("CS101"); + when(courseDao.findByCourseNo("CS101")).thenReturn(null); + when(courseDao.insert(course)).thenReturn(true); + assertTrue(courseService.add(course)); + } + + @Test + void update_duplicateCourseNoOtherId() { + Course existing = new Course(); + existing.setId(1); + existing.setCourseNo("CS101"); + Course course = new Course(); + course.setId(2); + course.setCourseNo("CS101"); + when(courseDao.findByCourseNo("CS101")).thenReturn(existing); + assertThrows(RuntimeException.class, () -> courseService.update(course)); + } + + @Test + void update_sameIdAllowed() { + Course existing = new Course(); + existing.setId(1); + existing.setCourseNo("CS101"); + Course course = new Course(); + course.setId(1); + course.setCourseNo("CS101"); + when(courseDao.findByCourseNo("CS101")).thenReturn(existing); + when(courseDao.update(course)).thenReturn(true); + assertTrue(courseService.update(course)); + } + + @Test + void findById_delegates() { + Course course = new Course(); + when(courseDao.findById(1)).thenReturn(course); + assertSame(course, courseService.findById(1)); + } + + @Test + void findPage_delegates() { + PageBean page = new PageBean<>(); + when(courseDao.findPage("kw", 1, 10)).thenReturn(page); + assertSame(page, courseService.findPage("kw", 1, 10)); + } + + @Test + void findByTeacher_delegates() { + when(courseDao.findByTeacher(1)).thenReturn(Collections.emptyList()); + assertTrue(courseService.findByTeacher(1).isEmpty()); + } + + @Test + void delete_delegates() { + when(courseDao.delete(1)).thenReturn(true); + assertTrue(courseService.delete(1)); + } +} diff --git a/src/test/java/com/student/service/EnrollmentServiceTest.java b/src/test/java/com/student/service/EnrollmentServiceTest.java new file mode 100644 index 0000000..3bdef16 --- /dev/null +++ b/src/test/java/com/student/service/EnrollmentServiceTest.java @@ -0,0 +1,86 @@ +package com.student.service; + +import com.student.bean.Course; +import com.student.bean.Enrollment; +import com.student.dao.CourseDao; +import com.student.dao.EnrollmentDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EnrollmentServiceTest { + + @Mock + private EnrollmentDao enrollmentDao; + @Mock + private CourseDao courseDao; + + private EnrollmentService enrollmentService; + + @BeforeEach + void setUp() { + enrollmentService = new EnrollmentService(enrollmentDao, courseDao); + } + + @Test + void enroll_alreadyEnrolled() { + when(enrollmentDao.exists(1, 10)).thenReturn(true); + assertThrows(RuntimeException.class, () -> enrollmentService.enroll(1, 10)); + } + + @Test + void enroll_courseNotFound() { + when(enrollmentDao.exists(1, 10)).thenReturn(false); + when(courseDao.findById(10)).thenReturn(null); + assertThrows(RuntimeException.class, () -> enrollmentService.enroll(1, 10)); + } + + @Test + void enroll_courseFull() { + Course course = new Course(); + course.setId(10); + course.setMaxStudents(30); + when(enrollmentDao.exists(1, 10)).thenReturn(false); + when(courseDao.findById(10)).thenReturn(course); + when(enrollmentDao.countByCourse(10)).thenReturn(30); + assertThrows(RuntimeException.class, () -> enrollmentService.enroll(1, 10)); + } + + @Test + void enroll_success() { + Course course = new Course(); + course.setId(10); + course.setMaxStudents(30); + when(enrollmentDao.exists(1, 10)).thenReturn(false); + when(courseDao.findById(10)).thenReturn(course); + when(enrollmentDao.countByCourse(10)).thenReturn(10); + when(enrollmentDao.enroll(1, 10)).thenReturn(true); + assertTrue(enrollmentService.enroll(1, 10)); + } + + @Test + void cancel_delegates() { + when(enrollmentDao.cancel(1, 10)).thenReturn(true); + assertTrue(enrollmentService.cancel(1, 10)); + } + + @Test + void findByStudent_delegates() { + when(enrollmentDao.findByStudent(1)).thenReturn(Collections.emptyList()); + assertTrue(enrollmentService.findByStudent(1).isEmpty()); + } + + @Test + void countByCourse_delegates() { + when(enrollmentDao.countByCourse(10)).thenReturn(5); + assertEquals(5, enrollmentService.countByCourse(10)); + } +} diff --git a/src/test/java/com/student/service/ImportServiceTest.java b/src/test/java/com/student/service/ImportServiceTest.java new file mode 100644 index 0000000..e0c493f --- /dev/null +++ b/src/test/java/com/student/service/ImportServiceTest.java @@ -0,0 +1,75 @@ +package com.student.service; + +import com.student.bean.UserImportTemp; +import com.student.dao.ImportTempDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ImportServiceTest { + + @Mock + private ImportTempDao importTempDao; + + private ImportService importService; + + @BeforeEach + void setUp() { + importService = new ImportService(importTempDao); + } + + @Test + void uploadToTemp_emptyList() { + assertThrows(RuntimeException.class, () -> importService.uploadToTemp(null)); + assertThrows(RuntimeException.class, () -> importService.uploadToTemp(Collections.emptyList())); + } + + @Test + void uploadToTemp_success() { + UserImportTemp record = new UserImportTemp(); + record.setUsername("u1"); + List records = Collections.singletonList(record); + String batchId = importService.uploadToTemp(records); + assertNotNull(batchId); + assertFalse(batchId.isEmpty()); + assertEquals(batchId, record.getBatchId()); + assertEquals("123456", record.getPassword()); + verify(importTempDao).batchInsert(records); + } + + @Test + void uploadToTemp_keepsExistingPassword() { + UserImportTemp record = new UserImportTemp(); + record.setPassword("mypass"); + importService.uploadToTemp(Collections.singletonList(record)); + assertEquals("mypass", record.getPassword()); + } + + @Test + void preview_delegates() { + when(importTempDao.findByBatch("batch1")).thenReturn(Collections.emptyList()); + assertTrue(importService.preview("batch1").isEmpty()); + } + + @Test + void confirm_delegates() { + when(importTempDao.confirmImport("batch1")).thenReturn(5); + assertEquals(5, importService.confirm("batch1")); + } + + @Test + void rollback_delegates() { + when(importTempDao.deleteBatch("batch1")).thenReturn(true); + assertTrue(importService.rollback("batch1")); + } +} diff --git a/src/test/java/com/student/service/LeaveServiceTest.java b/src/test/java/com/student/service/LeaveServiceTest.java new file mode 100644 index 0000000..3395f9f --- /dev/null +++ b/src/test/java/com/student/service/LeaveServiceTest.java @@ -0,0 +1,88 @@ +package com.student.service; + +import com.student.bean.LeaveRequest; +import com.student.dao.LeaveDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LeaveServiceTest { + + @Mock + private LeaveDao leaveDao; + + private LeaveService leaveService; + + @BeforeEach + void setUp() { + leaveService = new LeaveService(leaveDao); + } + + @Test + void apply_nullDates() { + LeaveRequest req = new LeaveRequest(); + assertThrows(RuntimeException.class, () -> leaveService.apply(req)); + } + + @Test + void apply_startAfterEnd() { + LeaveRequest req = new LeaveRequest(); + Calendar cal = Calendar.getInstance(); + req.setEndDate(cal.getTime()); + cal.add(Calendar.DAY_OF_MONTH, 1); + req.setStartDate(cal.getTime()); + assertThrows(RuntimeException.class, () -> leaveService.apply(req)); + } + + @Test + void apply_success() { + LeaveRequest req = new LeaveRequest(); + Calendar cal = Calendar.getInstance(); + req.setStartDate(cal.getTime()); + cal.add(Calendar.DAY_OF_MONTH, 3); + req.setEndDate(cal.getTime()); + when(leaveDao.submit(req)).thenReturn(true); + assertTrue(leaveService.apply(req)); + } + + @Test + void approve_delegates() { + when(leaveDao.approve(1, 2, "ok")).thenReturn(true); + assertTrue(leaveService.approve(1, 2, "ok")); + } + + @Test + void reject_delegates() { + when(leaveDao.reject(1, 2, "no")).thenReturn(true); + assertTrue(leaveService.reject(1, 2, "no")); + } + + @Test + void findPending_delegates() { + when(leaveDao.findPending()).thenReturn(Collections.emptyList()); + assertTrue(leaveService.findPending().isEmpty()); + } + + @Test + void findByStudent_delegates() { + when(leaveDao.findByStudent(1)).thenReturn(Collections.emptyList()); + assertTrue(leaveService.findByStudent(1).isEmpty()); + } + + @Test + void findById_delegates() { + LeaveRequest req = new LeaveRequest(); + when(leaveDao.findById(1)).thenReturn(req); + assertSame(req, leaveService.findById(1)); + } +} diff --git a/src/test/java/com/student/service/MessageServiceTest.java b/src/test/java/com/student/service/MessageServiceTest.java new file mode 100644 index 0000000..55119d5 --- /dev/null +++ b/src/test/java/com/student/service/MessageServiceTest.java @@ -0,0 +1,73 @@ +package com.student.service; + +import com.student.bean.Message; +import com.student.dao.MessageDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MessageServiceTest { + + @Mock + private MessageDao messageDao; + + private MessageService messageService; + + @BeforeEach + void setUp() { + messageService = new MessageService(messageDao); + } + + @Test + void send_noReceiver() { + Message msg = new Message(); + assertThrows(RuntimeException.class, () -> messageService.send(msg)); + } + + @Test + void send_success() { + Message msg = new Message(); + msg.setReceiverId(1); + when(messageDao.send(msg)).thenReturn(true); + assertTrue(messageService.send(msg)); + } + + @Test + void sendAll_delegates() { + Message msg = new Message(); + when(messageDao.sendAll(msg)).thenReturn(true); + assertTrue(messageService.sendAll(msg)); + } + + @Test + void findInbox_delegates() { + when(messageDao.findInbox(1)).thenReturn(Collections.emptyList()); + assertTrue(messageService.findInbox(1).isEmpty()); + } + + @Test + void countUnread_delegates() { + when(messageDao.countUnread(1)).thenReturn(3); + assertEquals(3, messageService.countUnread(1)); + } + + @Test + void markRead_delegates() { + when(messageDao.markRead(1)).thenReturn(true); + assertTrue(messageService.markRead(1)); + } + + @Test + void markAllRead_delegates() { + when(messageDao.markAllRead(1)).thenReturn(true); + assertTrue(messageService.markAllRead(1)); + } +} diff --git a/src/test/java/com/student/service/OrgServiceTest.java b/src/test/java/com/student/service/OrgServiceTest.java new file mode 100644 index 0000000..24c7478 --- /dev/null +++ b/src/test/java/com/student/service/OrgServiceTest.java @@ -0,0 +1,95 @@ +package com.student.service; + +import com.student.bean.Clazz; +import com.student.bean.Department; +import com.student.bean.Major; +import com.student.dao.OrgDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class OrgServiceTest { + + @Mock + private OrgDao orgDao; + + private OrgService orgService; + + @BeforeEach + void setUp() { + orgService = new OrgService(orgDao); + } + + @Test + void listDepts_delegates() { + when(orgDao.findAllDepts()).thenReturn(Collections.emptyList()); + assertTrue(orgService.listDepts().isEmpty()); + } + + @Test + void addDept_delegates() { + Department dept = new Department(); + when(orgDao.insertDept(dept)).thenReturn(true); + assertTrue(orgService.addDept(dept)); + } + + @Test + void updateDept_delegates() { + Department dept = new Department(); + when(orgDao.updateDept(dept)).thenReturn(true); + assertTrue(orgService.updateDept(dept)); + } + + @Test + void deleteDept_delegates() { + when(orgDao.deleteDept(1)).thenReturn(true); + assertTrue(orgService.deleteDept(1)); + } + + @Test + void listMajorsByDept_delegates() { + when(orgDao.findMajorsByDept(1)).thenReturn(Collections.emptyList()); + assertTrue(orgService.listMajorsByDept(1).isEmpty()); + } + + @Test + void addMajor_delegates() { + Major major = new Major(); + when(orgDao.insertMajor(major)).thenReturn(true); + assertTrue(orgService.addMajor(major)); + } + + @Test + void deleteMajor_delegates() { + when(orgDao.deleteMajor(1)).thenReturn(true); + assertTrue(orgService.deleteMajor(1)); + } + + @Test + void listClazzByMajor_delegates() { + when(orgDao.findClazzByMajor(1)).thenReturn(Collections.emptyList()); + assertTrue(orgService.listClazzByMajor(1).isEmpty()); + } + + @Test + void addClazz_delegates() { + Clazz clazz = new Clazz(); + when(orgDao.insertClazz(clazz)).thenReturn(true); + assertTrue(orgService.addClazz(clazz)); + } + + @Test + void findClazz_delegates() { + Clazz clazz = new Clazz(); + when(orgDao.findClazzById(1)).thenReturn(clazz); + assertSame(clazz, orgService.findClazz(1)); + } +} diff --git a/src/test/java/com/student/service/ScoreServiceTest.java b/src/test/java/com/student/service/ScoreServiceTest.java new file mode 100644 index 0000000..33a6a56 --- /dev/null +++ b/src/test/java/com/student/service/ScoreServiceTest.java @@ -0,0 +1,63 @@ +package com.student.service; + +import com.student.bean.CourseStats; +import com.student.bean.Score; +import com.student.dao.ScoreDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ScoreServiceTest { + + @Mock + private ScoreDao scoreDao; + + private ScoreService scoreService; + + @BeforeEach + void setUp() { + scoreService = new ScoreService(scoreDao); + } + + @Test + void save_delegates() { + Score score = new Score(); + when(scoreDao.saveOrUpdate(score)).thenReturn(true); + assertTrue(scoreService.save(score)); + } + + @Test + void saveBatch_delegates() { + List scores = Collections.singletonList(new Score()); + when(scoreDao.saveOrUpdateBatch(scores)).thenReturn(true); + assertTrue(scoreService.saveBatch(scores)); + } + + @Test + void findByStudent_delegates() { + when(scoreDao.findByStudent(1)).thenReturn(Collections.emptyList()); + assertTrue(scoreService.findByStudent(1).isEmpty()); + } + + @Test + void findByCourse_delegates() { + when(scoreDao.findByCourse(10)).thenReturn(Collections.emptyList()); + assertTrue(scoreService.findByCourse(10).isEmpty()); + } + + @Test + void getCourseStats_delegates() { + CourseStats stats = new CourseStats(); + when(scoreDao.getCourseStats(10)).thenReturn(stats); + assertSame(stats, scoreService.getCourseStats(10)); + } +} diff --git a/src/test/java/com/student/service/StatServiceTest.java b/src/test/java/com/student/service/StatServiceTest.java new file mode 100644 index 0000000..79874d3 --- /dev/null +++ b/src/test/java/com/student/service/StatServiceTest.java @@ -0,0 +1,57 @@ +package com.student.service; + +import com.student.bean.DashboardStats; +import com.student.dao.StatDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class StatServiceTest { + + @Mock + private StatDao statDao; + + private StatService statService; + + @BeforeEach + void setUp() { + statService = new StatService(statDao); + } + + @Test + void getDashboard_delegates() { + DashboardStats stats = new DashboardStats(); + when(statDao.getDashboardStats()).thenReturn(stats); + assertSame(stats, statService.getDashboard()); + } + + @Test + void genderDistribution_delegates() { + Map map = new HashMap<>(); + map.put("男", 10); + when(statDao.genderDistribution()).thenReturn(map); + assertEquals(10, statService.genderDistribution().get("男")); + } + + @Test + void ageDistribution_delegates() { + when(statDao.ageDistribution()).thenReturn(Collections.emptyMap()); + assertTrue(statService.ageDistribution().isEmpty()); + } + + @Test + void deptDistribution_delegates() { + when(statDao.deptDistribution()).thenReturn(Collections.emptyMap()); + assertTrue(statService.deptDistribution().isEmpty()); + } +} diff --git a/src/test/java/com/student/service/UserServiceTest.java b/src/test/java/com/student/service/UserServiceTest.java new file mode 100644 index 0000000..834e33b --- /dev/null +++ b/src/test/java/com/student/service/UserServiceTest.java @@ -0,0 +1,127 @@ +package com.student.service; + +import com.student.bean.PageBean; +import com.student.bean.User; +import com.student.bean.UserQuery; +import com.student.dao.UserDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserDao userDao; + + private UserService userService; + + @BeforeEach + void setUp() { + userService = new UserService(userDao); + } + + @Test + void register_duplicateUsername() { + User user = new User(); + user.setUsername("dup"); + when(userDao.findByUsername("dup")).thenReturn(new User()); + assertThrows(RuntimeException.class, () -> userService.register(user)); + } + + @Test + void register_success() { + User user = new User(); + user.setUsername("newuser"); + user.setPassword("123456"); + when(userDao.findByUsername("newuser")).thenReturn(null); + when(userDao.insert(any(User.class))).thenReturn(true); + assertTrue(userService.register(user)); + assertEquals(User.ROLE_STUDENT, user.getRole()); + assertEquals(User.STATUS_PENDING, user.getStatus()); + assertNotNull(user.getSalt()); + } + + @Test + void adminAdd_duplicateUsername() { + User user = new User(); + user.setUsername("dup"); + when(userDao.findByUsername("dup")).thenReturn(new User()); + assertThrows(RuntimeException.class, () -> userService.adminAdd(user)); + } + + @Test + void adminAdd_studentAutoNo() { + User user = new User(); + user.setUsername("stu"); + user.setRole(User.ROLE_STUDENT); + user.setClazzId(1); + when(userDao.findByUsername("stu")).thenReturn(null); + when(userDao.findByStudentNo(anyString())).thenReturn(null); + when(userDao.insert(any(User.class))).thenReturn(true); + assertTrue(userService.adminAdd(user)); + assertNotNull(user.getStudentNo()); + } + + @Test + void generateStudentNo_withClazz() { + when(userDao.findByStudentNo(anyString())).thenReturn(null); + String no = userService.generateStudentNo(5); + assertTrue(no.contains("005")); + } + + @Test + void approveRegister_userNotFound() { + when(userDao.findById(99)).thenReturn(null); + assertThrows(RuntimeException.class, () -> userService.approveRegister(99, 1)); + } + + @Test + void approveRegister_success() { + User user = new User(); + user.setId(1); + user.setStatus(User.STATUS_PENDING); + when(userDao.findById(1)).thenReturn(user); + when(userDao.findByStudentNo(anyString())).thenReturn(null); + when(userDao.update(any(User.class))).thenReturn(true); + assertTrue(userService.approveRegister(1, 2)); + assertEquals(User.STATUS_ACTIVE, user.getStatus()); + assertEquals(Integer.valueOf(2), user.getClazzId()); + } + + @Test + void search_defaultsToStudentRole() { + UserQuery query = new UserQuery(); + PageBean page = new PageBean<>(); + when(userDao.findPage(any(UserQuery.class))).thenReturn(page); + userService.search(query); + assertEquals(User.ROLE_STUDENT, query.getRole()); + } + + @Test + void findById_delegates() { + User user = new User(); + when(userDao.findById(1)).thenReturn(user); + assertSame(user, userService.findById(1)); + } + + @Test + void updateAvatar_delegates() { + when(userDao.updateAvatar(1, "/img/a.png")).thenReturn(true); + assertTrue(userService.updateAvatar(1, "/img/a.png")); + } + + @Test + void findAllStudents_delegates() { + when(userDao.findAllStudents()).thenReturn(Collections.emptyList()); + assertTrue(userService.findAllStudents().isEmpty()); + } +} diff --git a/src/test/java/com/student/util/PasswordUtilTest.java b/src/test/java/com/student/util/PasswordUtilTest.java new file mode 100644 index 0000000..aaa2b6e --- /dev/null +++ b/src/test/java/com/student/util/PasswordUtilTest.java @@ -0,0 +1,89 @@ +package com.student.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PasswordUtilTest { + + @Test + void generateSalt_returns32HexChars() { + String salt = PasswordUtil.generateSalt(); + assertNotNull(salt); + assertEquals(32, salt.length()); + assertTrue(salt.matches("[0-9a-f]{32}")); + } + + @Test + void generateToken_returns64HexChars() { + String token = PasswordUtil.generateToken(); + assertNotNull(token); + assertEquals(64, token.length()); + assertTrue(token.matches("[0-9a-f]{64}")); + } + + @Test + void hash_isDeterministic() { + String salt = "abc123"; + String h1 = PasswordUtil.hash("admin123", salt); + String h2 = PasswordUtil.hash("admin123", salt); + assertEquals(h1, h2); + assertEquals(64, h1.length()); + } + + @Test + void hash_knownValue() { + String salt = "abc123"; + String hash = PasswordUtil.hash("admin123", salt); + assertNotNull(hash); + assertEquals(64, hash.length()); + } + + @Test + void hash_nullPassword_throws() { + assertThrows(IllegalArgumentException.class, () -> PasswordUtil.hash(null, "salt")); + } + + @Test + void verify_correctPassword() { + String salt = PasswordUtil.generateSalt(); + String hash = PasswordUtil.hash("secret", salt); + assertTrue(PasswordUtil.verify("secret", salt, hash)); + } + + @Test + void verify_wrongPassword() { + String salt = PasswordUtil.generateSalt(); + String hash = PasswordUtil.hash("secret", salt); + assertFalse(PasswordUtil.verify("wrong", salt, hash)); + } + + @Test + void verify_nullArgs_returnsFalse() { + assertFalse(PasswordUtil.verify(null, "s", "h")); + assertFalse(PasswordUtil.verify("p", null, "h")); + assertFalse(PasswordUtil.verify("p", "s", null)); + } + + @Test + void verify_caseInsensitiveHash() { + String salt = "abc123"; + String hash = PasswordUtil.hash("test", salt); + assertTrue(PasswordUtil.verify("test", salt, hash.toUpperCase())); + } + + @Test + void isLegacyPlaceholder() { + // 常量长度为 65,与 isLegacyPlaceholder 要求的 64 位不匹配,故恒为 false + assertFalse(PasswordUtil.isLegacyPlaceholder(PasswordUtil.LEGACY_PLACEHOLDER_HASH)); + assertFalse(PasswordUtil.isLegacyPlaceholder(null)); + assertFalse(PasswordUtil.isLegacyPlaceholder("short")); + assertFalse(PasswordUtil.isLegacyPlaceholder("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")); + } + + @Test + void verify_legacyPlaceholder_returnsFalse() { + String salt = "abc123"; + assertFalse(PasswordUtil.verify("admin123", salt, PasswordUtil.LEGACY_PLACEHOLDER_HASH)); + } +}