这是一个基于Spring Boot开发的Web应用程序,专门用于管理教室资源和处理教室借用申请。系统提供了完整的教室查询、预订和审批功能,适用于学校或其他教育机构的教室资源管理。

一、核心功能

  1. 教室管理 - 管理员可以添加、编辑和删除教室信息,包括教室编号、类型、容量和位置等详细信息。
  2. 在线预订 - 普通用户可以浏览所有可用教室,并提交教室借用申请,选择使用时间和用途。
  3. 审批流程 - 管理员可以查看和审批用户的教室借用申请,决定是否批准或拒绝申请。
  4. 权限控制 - 系统设有三种用户角色:普通用户、管理员和超级管理员,每种角色有不同的操作权限。
  5. 用户管理 - 管理员可以管理普通用户账户,包括添加新用户、编辑用户信息和重置密码等。
  6. 系统日志 - 记录所有重要操作,如创建教室、审批申请等,方便追踪和审计。

二、采用技术及技术特点

  • 采用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>&times;</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、教室申请审核管理界面

教室申请审核管理界面-1
教室申请审核管理界面-2

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐