Java EE网页开发课程期末大作业——教室借用申请平台
·
这是一个基于Spring Boot开发的Web应用程序,专门用于管理教室资源和处理教室借用申请。系统提供了完整的教室查询、预订和审批功能,适用于学校或其他教育机构的教室资源管理。
一、核心功能
- 教室管理 - 管理员可以添加、编辑和删除教室信息,包括教室编号、类型、容量和位置等详细信息。
- 在线预订 - 普通用户可以浏览所有可用教室,并提交教室借用申请,选择使用时间和用途。
- 审批流程 - 管理员可以查看和审批用户的教室借用申请,决定是否批准或拒绝申请。
- 权限控制 - 系统设有三种用户角色:普通用户、管理员和超级管理员,每种角色有不同的操作权限。
- 用户管理 - 管理员可以管理普通用户账户,包括添加新用户、编辑用户信息和重置密码等。
- 系统日志 - 记录所有重要操作,如创建教室、审批申请等,方便追踪和审计。
二、采用技术及技术特点
- 采用Spring Boot框架,简化了配置和部署过程
- 使用Spring Data JPA进行数据持久化操作
- 基于Spring Security实现安全认证和权限控制
- 前端使用Thymeleaf模板引擎渲染页面,结合Bootstrap实现响应式设计
- 数据库使用MySQL存储系统数据
- 使用Maven进行项目构建和依赖管理
三、系统使用环境
1、Java环境
JDK版本:JDK17.0
2、后端框架
Spring Boot:2.7.0
Spring Data JPA + Hibernate:与Spring Boot 2.7.0配套版本
Spring Security:与Spring Boot 2.7.0配套版本
3、前端技术
Thymeleaf模板引擎:与Spring Boot 2.7.0配套版本
Bootstrap:5.1.3(已嵌入,用于前端样式设计)
4、数据库
MySQL:8.0.30
5、构建工具
Maven:用于项目构建和依赖管理
构建命令:mvn clean package
6、核心依赖组件
spring-boot-starter-data-jpa:提供JPA数据访问功能
hibernate-core:Hibernate核心组件,用于ORM映射
7、开发工具
IntelliJ IDEA 2025.3
四、主要功能及核心代码
1、用户认证与权限管理
(1)登录功能
- 处理用户登录请求
- 验证用户名、密码和验证码
- 根据用户角色重定向到相应的仪表板
@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout,
HttpSession session,
Map<String, Object> model) {
if (error != null) {
// 检查是否是因为验证码错误
String captchaError = (String) session.getAttribute("captchaError");
if (captchaError != null) {
model.put("captchaError", captchaError);
session.removeAttribute("captchaError");
} else {
model.put("error", "登录失败,请检查用户名和密码");
}
}
if (logout != null) {
model.put("logout", "您已成功退出登录");
}
return "login";
}
(2)验证码生成
- 生成图形验证码
- 将验证码文本存储在会话中用于后续验证
@GetMapping("/captcha")
@ResponseBody
public Map<String, String> getCaptcha(HttpSession session) {
CaptchaService.CaptchaDto captcha = captchaService.generateCaptcha();
// 将验证码文本存储在会话中用于验证
session.setAttribute("captcha", captcha.getText());
// 返回验证码图片的Base64编码
Map<String, String> response = new HashMap<>();
response.put("image", "data:image/png;base64," + captcha.getImageBase64());
return response;
}
(3)角色权限控制
- 配置Spring Security安全策略
- 定义不同角色的访问权限
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login", "/captcha").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/super-admin/**").hasRole("SUPER_ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.successHandler(authenticationSuccessHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.and()
.csrf().disable();
}
2、教室管理功能
(1)查看教室列表
- 分页显示所有教室
- 支持搜索功能
@GetMapping("/classrooms")
public String viewClassrooms(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<Classroom> classroomsPage;
if (search != null && !search.isEmpty()) {
classroomsPage = classroomService.findClassroomsWithSearch(search, pageable);
} else {
classroomsPage = classroomService.findAllClassrooms(pageable);
}
model.addAttribute("classroomsPage", classroomsPage);
model.addAttribute("currentPage", page);
model.addAttribute("search", search);
return "user/classrooms";
}
(2)教室详情查看
- 显示特定教室的详细信息
- 展示该教室已批准的预订记录
@GetMapping("/classrooms/{id}")
public String viewClassroomDetails(@PathVariable Long id, Model model) {
Classroom classroom = classroomService.findById(id).orElse(null);
if (classroom == null) {
return "redirect:/user/classrooms";
}
// 获取该教室的预订记录,只显示已批准的预订
List<Booking> bookings = bookingService.findBookingsByClassroom(classroom)
.stream()
.filter(booking -> booking.getStatus() == Booking.Status.APPROVED)
.sorted((b1, b2) -> b1.getStartTime().compareTo(b2.getStartTime()))
.collect(Collectors.toList());
model.addAttribute("classroom", classroom);
model.addAttribute("bookings", bookings);
return "user/classroom-details";
}
3、预订管理功能
(1)提交预订申请
- 处理用户提交的教室预订请求
- 检查时间冲突
- 创建待审批的预订记录
@PostMapping("/book-classroom")
public String bookClassroom(
@RequestParam Long classroomId,
@RequestParam String startTime,
@RequestParam String endTime,
@RequestParam String reason,
Model model,
Authentication authentication) {
// 获取当前用户
String username = authentication.getName();
User user = userService.findByUsername(username).orElse(null);
if (user == null) {
return "redirect:/login";
}
// 解析时间参数
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
LocalDateTime endDateTime = LocalDateTime.parse(endTime, formatter);
// 验证时间有效性
if (startDateTime.isAfter(endDateTime) || startDateTime.isEqual(endDateTime)) {
model.addAttribute("error", "结束时间必须晚于开始时间");
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
return "user/book-classroom";
}
// 检查所选教室是否存在
Classroom selectedClassroom = classroomService.findById(classroomId).orElse(null);
if (selectedClassroom == null) {
model.addAttribute("error", "所选教室不存在");
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
return "user/book-classroom";
}
// 检查是否有已批准的时间冲突
boolean conflict = bookingService.hasConflictingBooking(selectedClassroom, startDateTime, endDateTime);
// 检查是否有待审核的时间冲突
boolean pendingConflict = bookingService.hasPendingConflict(selectedClassroom, startDateTime, endDateTime);
if (conflict) {
// 对于已批准的冲突,直接提示并推荐替代教室
// 查找类似教室推荐给用户
List<Classroom> similarClassrooms = classroomService.findSimilarClassrooms(selectedClassroom);
// 过滤出在指定时间段内未被占用的教室
List<Classroom> availableClassrooms = similarClassrooms.stream()
.filter(c -> !bookingService.hasConflictingBooking(c, startDateTime, endDateTime) &&
!bookingService.hasPendingConflict(c, startDateTime, endDateTime))
.collect(Collectors.toList());
model.addAttribute("conflict", true);
model.addAttribute("availableClassrooms", availableClassrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedClassroomId", classroomId);
return "user/book-classroom";
}
// 创建临时预订对象供确认页面显示
Booking booking = new Booking(user, selectedClassroom, startDateTime, endDateTime, reason);
model.addAttribute("booking", booking);
// 检查是否有待处理的冲突预订(仅用于确认页面的警告)
boolean conflictWarning = pendingConflict;
model.addAttribute("conflictWarning", conflictWarning);
if (conflictWarning) {
// 查找类似教室推荐给用户
List<Classroom> similarClassrooms = classroomService.findSimilarClassrooms(selectedClassroom);
// 过滤出在指定时间段内未被占用的教室
List<Classroom> availableClassrooms = similarClassrooms.stream()
.filter(c -> !bookingService.hasConflictingBooking(c, startDateTime, endDateTime) &&
!bookingService.hasPendingConflict(c, startDateTime, endDateTime))
.collect(Collectors.toList());
model.addAttribute("availableClassrooms", availableClassrooms);
}
return "user/confirm-booking";
}
(2)确认预订申请
- 最终确认并创建预订记录
- 记录系统日志
@PostMapping("/confirm-booking")
public String confirmBooking(
@RequestParam Long classroomId,
@RequestParam String startTime,
@RequestParam String endTime,
@RequestParam String reason,
Authentication authentication,
Model model) {
// 获取当前用户
String username = authentication.getName();
User user = userService.findByUsername(username).orElse(null);
if (user == null) {
return "redirect:/login";
}
// 解析时间参数
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
LocalDateTime endDateTime = LocalDateTime.parse(endTime, formatter);
// 获取教室
Classroom classroom = classroomService.findById(classroomId).orElse(null);
if (classroom == null) {
return "redirect:/user/book-classroom";
}
// 创建预订
Booking booking = bookingService.createBooking(user, classroom, startDateTime, endDateTime, reason);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("提交教室申请: 教室=%s, 时间=%s至%s",
classroom.getRoomNumber(),
startDateTime.toString(),
endDateTime.toString());
systemLogService.createLog("提交教室申请", operator, details);
return "redirect:/user/bookings";
}
4、预订审批功能
(1)查看待审批预订
- 管理员查看所有待审批的预订申请
- 支持按状态筛选
@GetMapping("/bookings")
public String reviewBookings(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "pending") String filter,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<Booking> bookingsPage;
if (search != null && !search.isEmpty()) {
Page<Booking> allBookingsWithSearch = bookingService.findAllBookingsWithSearch(search, pageable);
// 根据过滤器筛选预订
if ("pending".equals(filter)) {
// 只显示待审核的申请
List<Booking> pendingBookings = allBookingsWithSearch.getContent().stream()
.filter(b -> b.getStatus() == Booking.Status.PENDING)
.collect(Collectors.toList());
bookingsPage = new PageImpl<>(pendingBookings, pageable, pendingBookings.size());
} else if ("processed".equals(filter)) {
// 显示已处理的申请(已通过、已驳回,但不包括已删除的)
List<Booking> processedBookings = allBookingsWithSearch.getContent().stream()
.filter(b -> b.getStatus() != Booking.Status.PENDING && b.getStatus() != Booking.Status.DELETED)
.collect(Collectors.toList());
bookingsPage = new PageImpl<>(processedBookings, pageable, processedBookings.size());
} else {
bookingsPage = allBookingsWithSearch;
}
} else {
// 根据过滤器筛选预订
if ("pending".equals(filter)) {
// 只显示待审核的申请
bookingsPage = bookingService.findPendingBookings(pageable);
} else if ("processed".equals(filter)) {
// 显示已处理的申请(已通过、已驳回,但不包括已删除的)
bookingsPage = bookingService.findProcessedBookings(pageable);
} else {
bookingsPage = bookingService.findAllBookings(pageable);
}
}
model.addAttribute("bookingsPage", bookingsPage);
model.addAttribute("currentPage", page);
model.addAttribute("search", search);
model.addAttribute("filter", filter);
return "admin/bookings";
}
(2)审批预订申请
- 管理员审批预订申请(通过或驳回)
- 记录审批人信息和系统日志
@PostMapping("/bookings/approve/{id}")
public String approveBooking(@PathVariable Long id, Authentication authentication) {
Booking booking = bookingService.findAllBookings().stream()
.filter(b -> b.getId().equals(id))
.findFirst()
.orElse(null);
if (booking != null) {
// 设置审批人信息
booking.setApprovedBy(authentication.getName());
bookingService.approveBooking(booking);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("审批教室申请: 申请人=%s, 教室=%s, 时间=%s至%s, 结果=通过",
booking.getUser().getName(),
booking.getClassroom().getRoomNumber(),
booking.getStartTime().toString(),
booking.getEndTime().toString());
systemLogService.createLog("审批教室申请", operator, details);
}
return "redirect:/admin/bookings";
}
@PostMapping("/bookings/reject/{id}")
public String rejectBooking(@PathVariable Long id, @RequestParam("reason") String reason, Authentication authentication) {
Booking booking = bookingService.findAllBookings().stream()
.filter(b -> b.getId().equals(id))
.findFirst()
.orElse(null);
if (booking != null) {
// 设置审批人信息
booking.setApprovedBy(authentication.getName());
bookingService.rejectBooking(booking, reason);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("审批教室申请: 申请人=%s, 教室=%s, 时间=%s至%s, 结果=驳回, 原因=%s",
booking.getUser().getName(),
booking.getClassroom().getRoomNumber(),
booking.getStartTime().toString(),
booking.getEndTime().toString(),
reason);
systemLogService.createLog("审批教室申请", operator, details);
}
return "redirect:/admin/bookings";
}
5、系统管理功能
(1)用户管理
- 管理员管理普通用户(增删改查)
- 重置用户密码
@PostMapping("/users/add")
public String addUser(@ModelAttribute User user, Model model, Authentication authentication) {
// 验证必填字段
if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
model.addAttribute("error", "用户名不能为空");
model.addAttribute("user", user);
return "admin/add-user";
}
// 验证用户名是否已经存在
if (userService.existsByUsername(user.getUsername())) {
model.addAttribute("error", "用户名已存在,请使用其他用户名");
model.addAttribute("user", user);
return "admin/add-user";
}
if (user.getName() == null || user.getName().trim().isEmpty()) {
model.addAttribute("error", "姓名不能为空");
model.addAttribute("user", user);
return "admin/add-user";
}
// 验证电话号码格式
if (user.getPhone() == null || user.getPhone().trim().isEmpty()) {
model.addAttribute("error", "电话号码不能为空");
model.addAttribute("user", user);
return "admin/add-user";
}
if (!PHONE_PATTERN.matcher(user.getPhone()).matches()) {
model.addAttribute("error", "电话号码格式不正确,请输入正确的11位手机号码");
model.addAttribute("user", user);
return "admin/add-user";
}
User newUser = userService.createUser(
user.getUsername(),
user.getName(),
user.getPhone(),
"123456",
User.Role.USER);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("创建用户: 用户名=%s, 姓名=%s, 电话=%s",
newUser.getUsername(), newUser.getName(), newUser.getPhone());
systemLogService.createLog("创建用户", operator, details);
return "redirect:/admin/users";
}
(2)系统日志导出
- 超级管理员导出系统日志为CSV文件
@PostMapping("/logs/export")
public String exportLogs(HttpServletResponse response) throws IOException {
List<SystemLog> logs = systemLogService.findAllLogs();
// 设置响应头
response.setContentType("text/csv;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"system-logs.csv\"");
// 写入CSV数据
PrintWriter writer = response.getWriter();
writer.println("操作时间,操作人,操作类型,操作详情");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (SystemLog log : logs) {
writer.printf("%s,%s,%s,\"%s\"%n",
log.getTimestamp().format(formatter),
log.getOperator(),
log.getAction(),
log.getDetails().replace("\"", "\"\"")); // 转义双引号
}
writer.flush();
return null; // 不需要重定向,直接返回文件
}
五、前后端接口和代码示例
1、登录验证码接口
接口说明:
- URL: /captcha
- 方法: GET
- 功能: 生成验证码图片并返回Base64编码
后端代码:
@GetMapping("/captcha")
@ResponseBody
public Map<String, String> getCaptcha(HttpSession session) {
CaptchaService.CaptchaDto captcha = captchaService.generateCaptcha();
// 将验证码文本存储在会话中用于验证
session.setAttribute("captcha", captcha.getText());
// 返回验证码图片的Base64编码
Map<String, String> response = new HashMap<>();
response.put("image", "data:image/png;base64," + captcha.getImageBase64());
return response;
}
前端代码:
<div class="form-group">
<label for="captcha">验证码:</label>
<div class="input-group">
<input type="text" class="form-control" id="captcha" name="captcha" required>
<div class="input-group-append">
<img id="captchaImage" src="/captcha" alt="验证码" onclick="refreshCaptcha()" style="cursor: pointer;">
</div>
</div>
</div>
<script>
function refreshCaptcha() {
document.getElementById('captchaImage').src = '/captcha?' + Math.random();
}
</script>
2、教室列表接口
接口说明:
- URL: /user/classrooms
- 方法: GET
- 功能: 获取教室列表,支持分页和搜索
后端代码:
@GetMapping("/classrooms")
public String viewClassrooms(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String search,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<Classroom> classroomsPage;
if (search != null && !search.isEmpty()) {
classroomsPage = classroomService.findClassroomsWithSearch(search, pageable);
} else {
classroomsPage = classroomService.findAllClassrooms(pageable);
}
model.addAttribute("classroomsPage", classroomsPage);
model.addAttribute("currentPage", page);
model.addAttribute("search", search);
return "user/classrooms";
}
前端代码:
<div class="container mt-4">
<h2>教室列表</h2>
<!-- 搜索框 -->
<form th:action="@{/user/classrooms}" method="get" class="mb-3">
<div class="input-group">
<input type="text" class="form-control" name="search" th:value="${search}" placeholder="搜索教室编号或类型...">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">搜索</button>
</div>
</div>
</form>
<!-- 教室表格 -->
<table class="table table-striped">
<thead>
<tr>
<th>教室编号</th>
<th>教室类型</th>
<th>容量</th>
<th>位置</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="classroom : ${classroomsPage.content}">
<td th:text="${classroom.roomNumber}"></td>
<td th:text="${classroom.roomType}"></td>
<td th:text="${classroom.capacity}"></td>
<td th:text="${classroom.location}"></td>
<td>
<a th:href="@{/user/classrooms/{id}(id=${classroom.id})}" class="btn btn-info btn-sm">查看详情</a>
<a th:href="@{/user/book-classroom/{id}(id=${classroom.id})}" class="btn btn-primary btn-sm">申请借用</a>
</td>
</tr>
</tbody>
</table>
<!-- 分页导航 -->
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item" th:classappend="${currentPage == 0} ? 'disabled'">
<a class="page-link"
th:href="@{/user/classrooms(page=${currentPage - 1}, search=${search})}">上一页</a>
</li>
<li class="page-item" th:each="i : ${#numbers.sequence(0, classroomsPage.totalPages - 1)}"
th:classappend="${i == currentPage} ? 'active'">
<a class="page-link"
th:href="@{/user/classrooms(page=${i}, search=${search})}"
th:text="${i + 1}"></a>
</li>
<li class="page-item" th:classappend="${currentPage + 1 >= classroomsPage.totalPages} ? 'disabled'">
<a class="page-link"
th:href="@{/user/classrooms(page=${currentPage + 1}, search=${search})}">下一页</a>
</li>
</ul>
</nav>
</div>
3、提交教室预订接口
接口说明:
- URL: /user/book-classroom
- 方法: POST
- 功能: 提交教室预订申请
后端代码:
@PostMapping("/book-classroom")
public String bookClassroom(
@RequestParam Long classroomId,
@RequestParam String startTime,
@RequestParam String endTime,
@RequestParam String reason,
Model model,
Authentication authentication) {
// 获取当前用户
String username = authentication.getName();
User user = userService.findByUsername(username).orElse(null);
if (user == null) {
return "redirect:/login";
}
// 解析时间参数
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
LocalDateTime endDateTime = LocalDateTime.parse(endTime, formatter);
// 验证时间有效性
if (startDateTime.isAfter(endDateTime) || startDateTime.isEqual(endDateTime)) {
model.addAttribute("error", "结束时间必须晚于开始时间");
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
return "user/book-classroom";
}
// 检查所选教室是否存在
Classroom selectedClassroom = classroomService.findById(classroomId).orElse(null);
if (selectedClassroom == null) {
model.addAttribute("error", "所选教室不存在");
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
return "user/book-classroom";
}
// 检查是否有已批准的时间冲突
boolean conflict = bookingService.hasConflictingBooking(selectedClassroom, startDateTime, endDateTime);
// 检查是否有待审核的时间冲突
boolean pendingConflict = bookingService.hasPendingConflict(selectedClassroom, startDateTime, endDateTime);
if (conflict) {
// 对于已批准的冲突,直接提示并推荐替代教室
// 查找类似教室推荐给用户
List<Classroom> similarClassrooms = classroomService.findSimilarClassrooms(selectedClassroom);
// 过滤出在指定时间段内未被占用的教室
List<Classroom> availableClassrooms = similarClassrooms.stream()
.filter(c -> !bookingService.hasConflictingBooking(c, startDateTime, endDateTime) &&
!bookingService.hasPendingConflict(c, startDateTime, endDateTime))
.collect(Collectors.toList());
model.addAttribute("conflict", true);
model.addAttribute("availableClassrooms", availableClassrooms);
model.addAttribute("requestedStartTime", startTime);
model.addAttribute("requestedEndTime", endTime);
model.addAttribute("requestedReason", reason);
List<Classroom> classrooms = classroomService.findAllClassrooms()
.stream()
.sorted((c1, c2) -> c1.getRoomNumber().compareTo(c2.getRoomNumber()))
.collect(Collectors.toList());
model.addAttribute("classrooms", classrooms);
model.addAttribute("requestedClassroomId", classroomId);
return "user/book-classroom";
}
// 创建临时预订对象供确认页面显示
Booking booking = new Booking(user, selectedClassroom, startDateTime, endDateTime, reason);
model.addAttribute("booking", booking);
// 检查是否有待处理的冲突预订(仅用于确认页面的警告)
boolean conflictWarning = pendingConflict;
model.addAttribute("conflictWarning", conflictWarning);
if (conflictWarning) {
// 查找类似教室推荐给用户
List<Classroom> similarClassrooms = classroomService.findSimilarClassrooms(selectedClassroom);
// 过滤出在指定时间段内未被占用的教室
List<Classroom> availableClassrooms = similarClassrooms.stream()
.filter(c -> !bookingService.hasConflictingBooking(c, startDateTime, endDateTime) &&
!bookingService.hasPendingConflict(c, startDateTime, endDateTime))
.collect(Collectors.toList());
model.addAttribute("availableClassrooms", availableClassrooms);
}
return "user/confirm-booking";
}
前端代码:
<div class="container mt-4">
<h2>申请借用教室</h2>
<form th:action="@{/user/book-classroom}" method="post" id="bookingForm">
<input type="hidden" name="classroomId" th:value="${requestedClassroomId ?: preselectedClassroomId}">
<div class="form-group">
<label for="classroomSelect">选择教室:</label>
<select class="form-control" id="classroomSelect" name="classroomId" required>
<option value="">请选择教室</option>
<option th:each="classroom : ${classrooms}"
th:value="${classroom.id}"
th:text="${classroom.roomNumber + ' (' + classroom.roomType + ', 容量:' + classroom.capacity + ')'}"
th:selected="${(requestedClassroomId != null and requestedClassroomId == classroom.id) or (preselectedClassroomId != null and preselectedClassroomId == classroom.id)}">
</option>
</select>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="startTime">开始时间:</label>
<input type="datetime-local" class="form-control" id="startTime" name="startTime"
th:value="${requestedStartTime}" required>
</div>
<div class="form-group col-md-6">
<label for="endTime">结束时间:</label>
<input type="datetime-local" class="form-control" id="endTime" name="endTime"
th:value="${requestedEndTime}" required>
</div>
</div>
<div class="form-group">
<label for="reason">申请理由:</label>
<textarea class="form-control" id="reason" name="reason" rows="3"
th:text="${requestedReason}" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交申请</button>
<a th:href="@{/user/classrooms}" class="btn btn-secondary">取消</a>
</form>
</div>
<script>
// 设置最小时间为当前时间
document.addEventListener('DOMContentLoaded', function() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const minDateTime = `${year}-${month}-${day}T${hours}:${minutes}`;
document.getElementById('startTime').min = minDateTime;
document.getElementById('endTime').min = minDateTime;
});
</script>
4、审批预订申请接口
接口说明:
- URL: /admin/bookings/approve/{id} 和 /admin/bookings/reject/{id}
- 方法: POST
- 功能: 管理员审批教室预订申请(通过或驳回)
后端代码:
@PostMapping("/bookings/approve/{id}")
public String approveBooking(@PathVariable Long id, Authentication authentication) {
Booking booking = bookingService.findAllBookings().stream()
.filter(b -> b.getId().equals(id))
.findFirst()
.orElse(null);
if (booking != null) {
// 设置审批人信息
booking.setApprovedBy(authentication.getName());
bookingService.approveBooking(booking);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("审批教室申请: 申请人=%s, 教室=%s, 时间=%s至%s, 结果=通过",
booking.getUser().getName(),
booking.getClassroom().getRoomNumber(),
booking.getStartTime().toString(),
booking.getEndTime().toString());
systemLogService.createLog("审批教室申请", operator, details);
}
return "redirect:/admin/bookings";
}
@PostMapping("/bookings/reject/{id}")
public String rejectBooking(@PathVariable Long id, @RequestParam("reason") String reason, Authentication authentication) {
Booking booking = bookingService.findAllBookings().stream()
.filter(b -> b.getId().equals(id))
.findFirst()
.orElse(null);
if (booking != null) {
// 设置审批人信息
booking.setApprovedBy(authentication.getName());
bookingService.rejectBooking(booking, reason);
// 记录系统日志
String operator = authentication.getName();
String details = String.format("审批教室申请: 申请人=%s, 教室=%s, 时间=%s至%s, 结果=驳回, 原因=%s",
booking.getUser().getName(),
booking.getClassroom().getRoomNumber(),
booking.getStartTime().toString(),
booking.getEndTime().toString(),
reason);
systemLogService.createLog("审批教室申请", operator, details);
}
return "redirect:/admin/bookings";
}
前端代码:
<div class="container mt-4">
<h2>预订申请管理</h2>
<!-- 筛选按钮 -->
<div class="mb-3">
<a th:href="@{/admin/bookings(filter='pending')}" class="btn btn-warning">待审核</a>
<a th:href="@{/admin/bookings(filter='processed')}" class="btn btn-success">已处理</a>
<a th:href="@{/admin/bookings}" class="btn btn-secondary">全部</a>
</div>
<!-- 预订表格 -->
<table class="table table-striped">
<thead>
<tr>
<th>申请人</th>
<th>教室</th>
<th>开始时间</th>
<th>结束时间</th>
<th>申请理由</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="booking : ${bookingsPage.content}">
<td th:text="${booking.user.name}"></td>
<td th:text="${booking.classroom.roomNumber}"></td>
<td th:text="${#temporals.format(booking.startTime, 'yyyy-MM-dd HH:mm')}"></td>
<td th:text="${#temporals.format(booking.endTime, 'yyyy-MM-dd HH:mm')}"></td>
<td th:text="${booking.reason}"></td>
<td>
<span th:if="${booking.status == T(com.classroombookingsystem.model.Booking.Status).PENDING}"
class="badge badge-warning">待审核</span>
<span th:if="${booking.status == T(com.classroombookingsystem.model.Booking.Status).APPROVED}"
class="badge badge-success">已批准</span>
<span th:if="${booking.status == T(com.classroombookingsystem.model.Booking.Status).REJECTED}"
class="badge badge-danger">已驳回</span>
<span th:if="${booking.status == T(com.classroombookingsystem.model.Booking.Status).DELETED}"
class="badge badge-secondary">已删除</span>
</td>
<td>
<a th:href="@{/admin/bookings/details/{id}(id=${booking.id})}" class="btn btn-info btn-sm">详情</a>
<span th:if="${booking.status == T(com.classroombookingsystem.model.Booking.Status).PENDING}">
<button type="button" class="btn btn-success btn-sm"
th:onclick="'approveBooking(' + ${booking.id} + ')'">通过</button>
<button type="button" class="btn btn-danger btn-sm"
th:onclick="'rejectBooking(' + ${booking.id} + ')'">驳回</button>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 驳回模态框 -->
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="rejectForm" method="post">
<div class="modal-header">
<h5 class="modal-title">驳回申请</h5>
<button type="button" class="close" data-dismiss="modal">
<span>×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="rejectReason">驳回原因:</label>
<textarea class="form-control" id="rejectReason" name="reason" rows="3" required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-danger">确认驳回</button>
</div>
</form>
</div>
</div>
</div>
<script>
function approveBooking(bookingId) {
if (confirm('确定要通过此申请吗?')) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/admin/bookings/approve/' + bookingId;
document.body.appendChild(form);
form.submit();
}
}
function rejectBooking(bookingId) {
const modal = $('#rejectModal');
const form = document.getElementById('rejectForm');
form.action = '/admin/bookings/reject/' + bookingId;
modal.modal('show');
}
</script>
六、项目截图
1、登录界面

2、超级管理员界面

3、管理员界面

4、用户界面

5、用户教室申请情况界面

6、教室申请审核管理界面


更多推荐

所有评论(0)