Java全栈项目-学生考勤请假一体化系统

项目介绍

本文将详细介绍一个基于Java全栈技术开发的学生考勤请假一体化系统。该系统旨在为学校提供一个现代化的考勤管理解决方案,实现学生考勤记录、请假申请、审批流程的一体化管理。

技术栈

后端技术

  • Spring Boot 2.6.x:核心框架
  • Spring Security:安全框架
  • MyBatis-Plus:ORM框架
  • MySQL 8.0:数据库
  • Redis:缓存服务
  • JWT:用户认证

前端技术

  • Vue 3:前端框架
  • Element Plus:UI组件库
  • Axios:HTTP客户端
  • Vuex:状态管理
  • Vue Router:路由管理

核心功能模块

用户管理模块

  • 用户角色:管理员、教师、学生
  • 账号管理:注册、登录、密码修改
  • 权限控制:基于RBAC的权限管理

考勤管理模块

  • 考勤记录:支持教师发起签到
  • 考勤统计:按班级、学生、时间段统计
  • 异常记录:迟到、早退、旷课标记

请假管理模块

  • 请假申请:学生在线提交请假申请
  • 审批流程:教师审批、管理员备案
  • 请假记录:历史请假记录查询

系统管理模块

  • 班级管理:班级信息维护
  • 课程管理:课程信息维护
  • 教师管理:教师信息维护
  • 学生管理:学生信息维护

数据库设计

核心表结构

  • user:用户信息表
  • role:角色表
  • permission:权限表
  • attendance:考勤记录表
  • leave_application:请假申请表
  • class:班级信息表
  • course:课程信息表

接口设计

RESTful API

  • 用户接口:/api/user/**
  • 考勤接口:/api/attendance/**
  • 请假接口:/api/leave/**
  • 系统管理接口:/api/system/**

系统架构

分层架构

  • 表现层:处理HTTP请求响应
  • 业务层:实现核心业务逻辑
  • 持久层:数据库访问操作
  • 通用层:工具类、配置类

项目结构

src
├── main
│   ├── java
│   │   └── com.example.attendance
│   │       ├── controller
│   │       ├── service
│   │       ├── mapper
│   │       ├── entity
│   │       ├── dto
│   │       ├── vo
│   │       ├── config
│   │       └── util
│   └── resources
│       ├── mapper
│       ├── static
│       └── application.yml

系统特点

高性能

  • 采用Redis缓存提升系统响应速度
  • MyBatis-Plus提供高效的数据库操作
  • 前端组件懒加载优化页面加载速度

高可用

  • 统一异常处理机制
  • 操作日志记录
  • 数据备份恢复机制

安全性

  • Spring Security权限控制
  • JWT令牌认证
  • 密码加密存储
  • SQL注入防护

部署方案

开发环境

  • JDK 1.8+
  • Maven 3.6+
  • Node.js 14+
  • MySQL 8.0
  • Redis 6.0+

生产环境

  • Nginx:反向代理服务器
  • Docker:容器化部署
  • Jenkins:持续集成部署

项目亮点

技术创新

  • 采用最新的Spring Boot 2.6.x版本
  • 使用Vue 3 Composition API
  • 引入Element Plus最新组件库

业务创新

  • 智能考勤提醒
  • 请假审批自动化
  • 考勤数据可视化分析

总结展望

本项目采用主流的Java全栈技术栈,实现了一个功能完整、性能优秀的学生考勤请假系统。系统具有良好的可扩展性和维护性,为学校管理提供了现代化的解决方案。

未来计划持续优化系统性能,增加移动端适配,引入人脸识别等新技术,提供更智能化的考勤解决方案。

一、用户管理模块的实现

数据库设计

用户表(sys_user)

CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    password VARCHAR(100) NOT NULL COMMENT '密码',
    real_name VARCHAR(50) COMMENT '真实姓名',
    email VARCHAR(100) COMMENT '邮箱',
    phone VARCHAR(20) COMMENT '手机号',
    avatar VARCHAR(200) COMMENT '头像URL',
    status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:正常)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE INDEX idx_username(username)
) COMMENT '用户表';

角色表(sys_role)

CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID',
    role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
    role_code VARCHAR(50) NOT NULL COMMENT '角色编码',
    description VARCHAR(200) COMMENT '角色描述',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_role_code(role_code)
) COMMENT '角色表';

用户角色关联表(sys_user_role)

CREATE TABLE sys_user_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    role_id BIGINT NOT NULL COMMENT '角色ID',
    UNIQUE INDEX idx_user_role(user_id, role_id)
) COMMENT '用户角色关联表';

权限表(sys_permission)

CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
    parent_id BIGINT COMMENT '父权限ID',
    name VARCHAR(50) NOT NULL COMMENT '权限名称',
    permission_code VARCHAR(50) NOT NULL COMMENT '权限编码',
    path VARCHAR(200) COMMENT '路由地址',
    component VARCHAR(200) COMMENT '组件路径',
    type TINYINT COMMENT '类型(1:菜单,2:按钮)',
    icon VARCHAR(50) COMMENT '图标',
    sort_order INT DEFAULT 0 COMMENT '排序',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_permission_code(permission_code)
) COMMENT '权限表';

角色权限关联表(sys_role_permission)

CREATE TABLE sys_role_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_id BIGINT NOT NULL COMMENT '角色ID',
    permission_id BIGINT NOT NULL COMMENT '权限ID',
    UNIQUE INDEX idx_role_permission(role_id, permission_id)
) COMMENT '角色权限关联表';

后端实现

实体类

@Data
@TableName("sys_user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    private String realName;
    private String email;
    private String phone;
    private String avatar;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

DTO类

@Data
public class LoginDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    private String password;
}

@Data
public class UserDTO {
    private Long id;
    private String username;
    private String realName;
    private String email;
    private String phone;
    private List<String> roleIds;
}

服务接口

public interface UserService extends IService<User> {
    UserDTO login(LoginDTO loginDTO);
    void register(UserDTO userDTO);
    void updatePassword(Long userId, String oldPassword, String newPassword);
    List<PermissionVO> getUserPermissions(Long userId);
}

服务实现

@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Override
    public UserDTO login(LoginDTO loginDTO) {
        User user = baseMapper.selectOne(new QueryWrapper<User>()
            .eq("username", loginDTO.getUsername()));
            
        if (user == null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
            throw new BusinessException("用户名或密码错误");
        }
        
        if (user.getStatus() == 0) {
            throw new BusinessException("账号已被禁用");
        }
        
        // 生成token
        String token = jwtTokenUtil.generateToken(user);
        
        // 转换并返回UserDTO
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        userDTO.setToken(token);
        return userDTO;
    }
    
    // 其他方法实现...
}

控制器

@RestController
@RequestMapping("/api/user")
@Validated
public class UserController {

    @Autowired
    private UserService userService;
    
    @PostMapping("/login")
    public Result<UserDTO> login(@RequestBody @Valid LoginDTO loginDTO) {
        return Result.success(userService.login(loginDTO));
    }
    
    @PostMapping("/register")
    public Result<?> register(@RequestBody @Valid UserDTO userDTO) {
        userService.register(userDTO);
        return Result.success();
    }
    
    @PutMapping("/password")
    @RequiresAuthentication
    public Result<?> updatePassword(@RequestBody PasswordUpdateDTO dto) {
        userService.updatePassword(SecurityUtils.getCurrentUserId(), 
            dto.getOldPassword(), dto.getNewPassword());
        return Result.success();
    }
}

前端实现

API封装

// src/api/user.ts
import request from '@/utils/request'

export function login(data: LoginData) {
  return request({
    url: '/api/user/login',
    method: 'post',
    data
  })
}

export function register(data: UserData) {
  return request({
    url: '/api/user/register',
    method: 'post',
    data
  })
}

export function updatePassword(data: PasswordData) {
  return request({
    url: '/api/user/password',
    method: 'put',
    data
  })
}

登录组件

<!-- src/views/login/index.vue -->
<template>
  <div class="login-container">
    <el-form
      ref="loginForm"
      :model="loginForm"
      :rules="loginRules"
      class="login-form"
    >
      <h3 class="title">学生考勤系统登录</h3>
      
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          placeholder="用户名"
          type="text"
        />
      </el-form-item>
      
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          placeholder="密码"
          type="password"
        />
      </el-form-item>
      
      <el-form-item>
        <el-button
          :loading="loading"
          type="primary"
          style="width:100%"
          @click.prevent="handleLogin"
        >
          登录
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import type { FormInstance } from 'element-plus'

const router = useRouter()
const userStore = useUserStore()

const loginForm = reactive({
  username: '',
  password: ''
})

const loginRules = {
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}

const loading = ref(false)
const loginFormRef = ref<FormInstance>()

const handleLogin = async () => {
  if (!loginFormRef.value) return
  
  await loginFormRef.value.validate()
  
  try {
    loading.value = true
    await userStore.login(loginForm)
    router.push('/')
  } catch (error) {
    console.error('登录失败:', error)
  } finally {
    loading.value = false
  }
}
</script>

Vuex状态管理

// src/stores/user.ts
import { defineStore } from 'pinia'
import { login, getUserInfo } from '@/api/user'
import { setToken, removeToken } from '@/utils/auth'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null,
    permissions: []
  }),
  
  actions: {
    async login(loginForm: LoginForm) {
      try {
        const { data } = await login(loginForm)
        this.token = data.token
        setToken(data.token)
      } catch (error) {
        removeToken()
        throw error
      }
    },
    
    async getUserInfo() {
      try {
        const { data } = await getUserInfo()
        this.userInfo = data
        this.permissions = data.permissions
        return data
      } catch (error) {
        removeToken()
        throw error
      }
    },
    
    logout() {
      this.token = ''
      this.userInfo = null
      this.permissions = []
      removeToken()
    }
  }
})

路由守卫

// src/router/permission.ts
import router from './index'
import { useUserStore } from '@/stores/user'
import NProgress from 'nprogress'

router.beforeEach(async (to, from, next) => {
  NProgress.start()
  
  const userStore = useUserStore()
  const hasToken = userStore.token
  
  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      const hasUserInfo = userStore.userInfo
      
      if (hasUserInfo) {
        next()
      } else {
        try {
          await userStore.getUserInfo()
          next()
        } catch (error) {
          userStore.logout()
          next(`/login?redirect=${to.path}`)
        }
      }
    }
  } else {
    if (to.meta.requiresAuth === false) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

以上就是用户管理模块的主要实现代码。这个模块实现了:

  1. 基于RBAC的权限控制
  2. 用户认证和授权
  3. JWT token的生成和验证
  4. 密码加密存储
  5. 前端路由权限控制
  6. 用户状态管理

模块的特点是:

  1. 安全性高:使用Spring Security + JWT
  2. 可扩展性好:基于RBAC模型
  3. 维护性强:模块化设计
  4. 用户体验好:前端交互流畅

二、考勤管理模块的实现

数据库设计

考勤记录表(attendance_record)

CREATE TABLE attendance_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '考勤记录ID',
    class_id BIGINT NOT NULL COMMENT '班级ID',
    course_id BIGINT NOT NULL COMMENT '课程ID',
    teacher_id BIGINT NOT NULL COMMENT '教师ID',
    start_time DATETIME NOT NULL COMMENT '开始时间',
    end_time DATETIME NOT NULL COMMENT '结束时间',
    status TINYINT DEFAULT 1 COMMENT '状态(0:未开始,1:进行中,2:已结束)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (class_id) REFERENCES sys_class(id),
    FOREIGN KEY (course_id) REFERENCES sys_course(id),
    FOREIGN KEY (teacher_id) REFERENCES sys_user(id),
    INDEX idx_class_course(class_id, course_id)
) COMMENT '考勤记录表';

考勤明细表(attendance_detail)

CREATE TABLE attendance_detail (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '考勤明细ID',
    record_id BIGINT NOT NULL COMMENT '考勤记录ID',
    student_id BIGINT NOT NULL COMMENT '学生ID',
    status TINYINT NOT NULL COMMENT '考勤状态(0:未签到,1:正常,2:迟到,3:早退,4:旷课)',
    sign_time DATETIME COMMENT '签到时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (record_id) REFERENCES attendance_record(id),
    FOREIGN KEY (student_id) REFERENCES sys_user(id),
    INDEX idx_record_student(record_id, student_id)
) COMMENT '考勤明细表';

后端实现

实体类

@Data
@TableName("attendance_record")
public class AttendanceRecord {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long classId;
    private Long courseId;
    private Long teacherId;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

@Data
@TableName("attendance_detail")
public class AttendanceDetail {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long recordId;
    private Long studentId;
    private Integer status;
    private LocalDateTime signTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

DTO和VO类

@Data
public class AttendanceCreateDTO {
    @NotNull(message = "班级ID不能为空")
    private Long classId;
    @NotNull(message = "课程ID不能为空")
    private Long courseId;
    @NotNull(message = "开始时间不能为空")
    private LocalDateTime startTime;
    @NotNull(message = "结束时间不能为空")
    private LocalDateTime endTime;
}

@Data
public class AttendanceStatisticsVO {
    private Long studentId;
    private String studentName;
    private Integer normalCount;
    private Integer lateCount;
    private Integer earlyLeaveCount;
    private Integer absentCount;
}

服务接口

public interface AttendanceService {
    // 教师发起签到
    Long createAttendance(AttendanceCreateDTO dto);
    
    // 学生签到
    void signIn(Long recordId, Long studentId);
    
    // 统计考勤
    List<AttendanceStatisticsVO> getStatistics(Long classId, LocalDateTime startDate, 
        LocalDateTime endDate);
    
    // 获取异常考勤记录
    List<AttendanceDetail> getAbnormalRecords(Long classId, LocalDateTime date);
}

服务实现

@Service
@Transactional(rollbackFor = Exception.class)
public class AttendanceServiceImpl implements AttendanceService {
    
    @Autowired
    private AttendanceRecordMapper recordMapper;
    
    @Autowired
    private AttendanceDetailMapper detailMapper;
    
    @Override
    public Long createAttendance(AttendanceCreateDTO dto) {
        // 验证课程和班级信息
        validateCourseAndClass(dto.getCourseId(), dto.getClassId());
        
        // 创建考勤记录
        AttendanceRecord record = new AttendanceRecord();
        BeanUtils.copyProperties(dto, record);
        record.setTeacherId(SecurityUtils.getCurrentUserId());
        record.setStatus(1);
        recordMapper.insert(record);
        
        // 初始化学生考勤明细
        List<User> students = getClassStudents(dto.getClassId());
        List<AttendanceDetail> details = students.stream()
            .map(student -> createInitialDetail(record.getId(), student.getId()))
            .collect(Collectors.toList());
        detailMapper.insertBatch(details);
        
        return record.getId();
    }
    
    @Override
    public void signIn(Long recordId, Long studentId) {
        AttendanceRecord record = recordMapper.selectById(recordId);
        if (record == null || record.getStatus() != 1) {
            throw new BusinessException("考勤未开始或已结束");
        }
        
        // 判断考勤状态
        LocalDateTime now = LocalDateTime.now();
        int status = calculateAttendanceStatus(now, record.getStartTime(), 
            record.getEndTime());
        
        // 更新考勤明细
        AttendanceDetail detail = detailMapper.selectOne(new QueryWrapper<AttendanceDetail>()
            .eq("record_id", recordId)
            .eq("student_id", studentId));
            
        detail.setStatus(status);
        detail.setSignTime(now);
        detailMapper.updateById(detail);
    }
    
    @Override
    public List<AttendanceStatisticsVO> getStatistics(Long classId, 
        LocalDateTime startDate, LocalDateTime endDate) {
        return detailMapper.selectStatistics(classId, startDate, endDate);
    }
    
    private int calculateAttendanceStatus(LocalDateTime signTime, 
        LocalDateTime startTime, LocalDateTime endTime) {
        if (signTime.isBefore(startTime.plusMinutes(10))) {
            return 1; // 正常
        } else if (signTime.isBefore(endTime)) {
            return 2; // 迟到
        } else {
            return 4; // 旷课
        }
    }
}

控制器

@RestController
@RequestMapping("/api/attendance")
public class AttendanceController {
    
    @Autowired
    private AttendanceService attendanceService;
    
    @PostMapping("/create")
    @RequiresRoles("TEACHER")
    public Result<Long> createAttendance(@RequestBody @Valid AttendanceCreateDTO dto) {
        return Result.success(attendanceService.createAttendance(dto));
    }
    
    @PostMapping("/sign/{recordId}")
    @RequiresRoles("STUDENT")
    public Result<?> signIn(@PathVariable Long recordId) {
        attendanceService.signIn(recordId, SecurityUtils.getCurrentUserId());
        return Result.success();
    }
    
    @GetMapping("/statistics")
    public Result<List<AttendanceStatisticsVO>> getStatistics(
        @RequestParam Long classId,
        @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
        @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate
    ) {
        return Result.success(attendanceService.getStatistics(classId, 
            startDate.atStartOfDay(), endDate.atTime(23, 59, 59)));
    }
}

前端实现

API封装

// src/api/attendance.ts
import request from '@/utils/request'

export function createAttendance(data: AttendanceCreateData) {
  return request({
    url: '/api/attendance/create',
    method: 'post',
    data
  })
}

export function signIn(recordId: number) {
  return request({
    url: `/api/attendance/sign/${recordId}`,
    method: 'post'
  })
}

export function getStatistics(params: StatisticsQuery) {
  return request({
    url: '/api/attendance/statistics',
    method: 'get',
    params
  })
}

教师发起签到组件

<!-- src/views/attendance/create.vue -->
<template>
  <div class="attendance-create">
    <el-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="100px"
    >
      <el-form-item label="班级" prop="classId">
        <el-select v-model="form.classId" placeholder="请选择班级">
          <el-option
            v-for="item in classList"
            :key="item.id"
            :label="item.className"
            :value="item.id"
          />
        </el-select>
      </el-form-item>
      
      <el-form-item label="课程" prop="courseId">
        <el-select v-model="form.courseId" placeholder="请选择课程">
          <el-option
            v-for="item in courseList"
            :key="item.id"
            :label="item.courseName"
            :value="item.id"
          />
        </el-select>
      </el-form-item>
      
      <el-form-item label="考勤时间" required>
        <el-col :span="11">
          <el-form-item prop="startTime">
            <el-date-picker
              v-model="form.startTime"
              type="datetime"
              placeholder="开始时间"
            />
          </el-form-item>
        </el-col>
        <el-col :span="2" class="text-center">-</el-col>
        <el-col :span="11">
          <el-form-item prop="endTime">
            <el-date-picker
              v-model="form.endTime"
              type="datetime"
              placeholder="结束时间"
            />
          </el-form-item>
        </el-col>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="handleSubmit">发起签到</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { createAttendance } from '@/api/attendance'
import { getClassList } from '@/api/class'
import { getCourseList } from '@/api/course'
import type { FormInstance } from 'element-plus'

const formRef = ref<FormInstance>()
const form = reactive({
  classId: '',
  courseId: '',
  startTime: '',
  endTime: ''
})

const classList = ref([])
const courseList = ref([])

onMounted(async () => {
  classList.value = await getClassList()
  courseList.value = await getCourseList()
})

const handleSubmit = async () => {
  if (!formRef.value) return
  
  await formRef.value.validate()
  await createAttendance(form)
  ElMessage.success('考勤发起成功')
}
</script>

考勤统计组件

<!-- src/views/attendance/statistics.vue -->
<template>
  <div class="attendance-statistics">
    <div class="search-bar">
      <el-form :inline="true" :model="queryForm">
        <el-form-item label="班级">
          <el-select v-model="queryForm.classId" placeholder="请选择班级">
            <el-option
              v-for="item in classList"
              :key="item.id"
              :label="item.className"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="queryForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
          />
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
        </el-form-item>
      </el-form>
    </div>
    
    <el-table :data="statisticsList" border>
      <el-table-column prop="studentName" label="学生姓名" />
      <el-table-column prop="normalCount" label="正常出勤" />
      <el-table-column prop="lateCount" label="迟到" />
      <el-table-column prop="earlyLeaveCount" label="早退" />
      <el-table-column prop="absentCount" label="旷课" />
      <el-table-column label="出勤率">
        <template #default="{ row }">
          {{ calculateAttendanceRate(row) }}%
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { getStatistics } from '@/api/attendance'

const queryForm = reactive({
  classId: '',
  dateRange: []
})

const statisticsList = ref([])

const handleQuery = async () => {
  const [startDate, endDate] = queryForm.dateRange
  const params = {
    classId: queryForm.classId,
    startDate: startDate?.format('YYYY-MM-DD'),
    endDate: endDate?.format('YYYY-MM-DD')
  }
  
  statisticsList.value = await getStatistics(params)
}

const calculateAttendanceRate = (row) => {
  const total = row.normalCount + row.lateCount + 
    row.earlyLeaveCount + row.absentCount
  return total === 0 ? 0 : 
    ((row.normalCount / total) * 100).toFixed(2)
}
</script>

这个考勤管理模块实现了以下功能:

  1. 教师发起签到功能
  2. 学生在线签到
  3. 自动判断考勤状态(正常、迟到、早退、旷课)
  4. 考勤统计分析
  5. 异常考勤记录查询

系统特点:

  1. 实时性:支持实时签到和状态更新
  2. 准确性:自动判断考勤状态
  3. 可追溯:详细记录签到时间和状态
  4. 数据分析:支持多维度统计
  5. 权限控制:基于角色的功能访问控制

三、请假管理模块的实现

数据库设计

请假申请表(leave_application)

CREATE TABLE leave_application (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '请假ID',
    student_id BIGINT NOT NULL COMMENT '学生ID',
    class_id BIGINT NOT NULL COMMENT '班级ID',
    type TINYINT NOT NULL COMMENT '请假类型(1:事假,2:病假,3:其他)',
    start_time DATETIME NOT NULL COMMENT '开始时间',
    end_time DATETIME NOT NULL COMMENT '结束时间',
    reason VARCHAR(500) NOT NULL COMMENT '请假原因',
    attachment_url VARCHAR(500) COMMENT '附件URL',
    status TINYINT DEFAULT 0 COMMENT '状态(0:待审批,1:已批准,2:已驳回,3:已取消)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (student_id) REFERENCES sys_user(id),
    FOREIGN KEY (class_id) REFERENCES sys_class(id),
    INDEX idx_student(student_id),
    INDEX idx_class(class_id)
) COMMENT '请假申请表';

请假审批记录表(leave_approval)

CREATE TABLE leave_approval (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '审批ID',
    leave_id BIGINT NOT NULL COMMENT '请假ID',
    approver_id BIGINT NOT NULL COMMENT '审批人ID',
    approver_role TINYINT NOT NULL COMMENT '审批人角色(1:教师,2:管理员)',
    status TINYINT NOT NULL COMMENT '审批状态(1:同意,2:驳回)',
    comment VARCHAR(200) COMMENT '审批意见',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (leave_id) REFERENCES leave_application(id),
    FOREIGN KEY (approver_id) REFERENCES sys_user(id),
    INDEX idx_leave(leave_id)
) COMMENT '请假审批记录表';

后端实现

实体类

@Data
@TableName("leave_application")
public class LeaveApplication {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long studentId;
    private Long classId;
    private Integer type;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private String reason;
    private String attachmentUrl;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

@Data
@TableName("leave_approval")
public class LeaveApproval {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long leaveId;
    private Long approverId;
    private Integer approverRole;
    private Integer status;
    private String comment;
    private LocalDateTime createTime;
}

DTO类

@Data
public class LeaveApplicationDTO {
    @NotNull(message = "请假类型不能为空")
    private Integer type;
    
    @NotNull(message = "开始时间不能为空")
    private LocalDateTime startTime;
    
    @NotNull(message = "结束时间不能为空")
    private LocalDateTime endTime;
    
    @NotBlank(message = "请假原因不能为空")
    private String reason;
    
    private String attachmentUrl;
}

@Data
public class LeaveApprovalDTO {
    @NotNull(message = "请假ID不能为空")
    private Long leaveId;
    
    @NotNull(message = "审批状态不能为空")
    private Integer status;
    
    private String comment;
}

服务接口

public interface LeaveService {
    // 提交请假申请
    Long submitLeave(LeaveApplicationDTO dto);
    
    // 审批请假申请
    void approveLeave(LeaveApprovalDTO dto);
    
    // 取消请假申请
    void cancelLeave(Long leaveId);
    
    // 获取请假记录
    IPage<LeaveVO> getLeaveRecords(LeaveQueryDTO query);
}

服务实现

@Service
@Transactional(rollbackFor = Exception.class)
public class LeaveServiceImpl implements LeaveService {
    
    @Autowired
    private LeaveApplicationMapper leaveMapper;
    
    @Autowired
    private LeaveApprovalMapper approvalMapper;
    
    @Override
    public Long submitLeave(LeaveApplicationDTO dto) {
        // 获取当前学生信息
        User student = SecurityUtils.getCurrentUser();
        
        // 校验请假时间
        validateLeaveTime(dto.getStartTime(), dto.getEndTime());
        
        // 创建请假申请
        LeaveApplication leave = new LeaveApplication();
        BeanUtils.copyProperties(dto, leave);
        leave.setStudentId(student.getId());
        leave.setClassId(student.getClassId());
        leave.setStatus(0);
        leaveMapper.insert(leave);
        
        return leave.getId();
    }
    
    @Override
    public void approveLeave(LeaveApprovalDTO dto) {
        // 获取请假申请
        LeaveApplication leave = leaveMapper.selectById(dto.getLeaveId());
        if (leave == null) {
            throw new BusinessException("请假申请不存在");
        }
        
        // 校验状态
        if (leave.getStatus() != 0) {
            throw new BusinessException("请假申请已处理");
        }
        
        // 创建审批记录
        User approver = SecurityUtils.getCurrentUser();
        LeaveApproval approval = new LeaveApproval();
        approval.setLeaveId(dto.getLeaveId());
        approval.setApproverId(approver.getId());
        approval.setApproverRole(approver.getRoleId());
        approval.setStatus(dto.getStatus());
        approval.setComment(dto.getComment());
        approvalMapper.insert(approval);
        
        // 更新请假状态
        leave.setStatus(dto.getStatus());
        leaveMapper.updateById(leave);
    }
    
    @Override
    public void cancelLeave(Long leaveId) {
        LeaveApplication leave = leaveMapper.selectById(leaveId);
        if (leave == null) {
            throw new BusinessException("请假申请不存在");
        }
        
        // 只能取消待审批的申请
        if (leave.getStatus() != 0) {
            throw new BusinessException("请假申请已处理,无法取消");
        }
        
        // 只能取消自己的申请
        if (!leave.getStudentId().equals(SecurityUtils.getCurrentUserId())) {
            throw new BusinessException("无权操作");
        }
        
        leave.setStatus(3);
        leaveMapper.updateById(leave);
    }
}

控制器

@RestController
@RequestMapping("/api/leave")
public class LeaveController {
    
    @Autowired
    private LeaveService leaveService;
    
    @PostMapping("/submit")
    @RequiresRoles("STUDENT")
    public Result<Long> submitLeave(@RequestBody @Valid LeaveApplicationDTO dto) {
        return Result.success(leaveService.submitLeave(dto));
    }
    
    @PostMapping("/approve")
    @RequiresRoles({"TEACHER", "ADMIN"})
    public Result<?> approveLeave(@RequestBody @Valid LeaveApprovalDTO dto) {
        leaveService.approveLeave(dto);
        return Result.success();
    }
    
    @PostMapping("/cancel/{leaveId}")
    @RequiresRoles("STUDENT")
    public Result<?> cancelLeave(@PathVariable Long leaveId) {
        leaveService.cancelLeave(leaveId);
        return Result.success();
    }
    
    @GetMapping("/list")
    public Result<IPage<LeaveVO>> getLeaveRecords(LeaveQueryDTO query) {
        return Result.success(leaveService.getLeaveRecords(query));
    }
}

前端实现

API封装

// src/api/leave.ts
import request from '@/utils/request'

export function submitLeave(data: LeaveSubmitData) {
  return request({
    url: '/api/leave/submit',
    method: 'post',
    data
  })
}

export function approveLeave(data: LeaveApprovalData) {
  return request({
    url: '/api/leave/approve',
    method: 'post',
    data
  })
}

export function getLeaveRecords(params: LeaveQueryParams) {
  return request({
    url: '/api/leave/list',
    method: 'get',
    params
  })
}

请假申请表单组件

<!-- src/views/leave/submit.vue -->
<template>
  <div class="leave-submit">
    <el-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="100px"
    >
      <el-form-item label="请假类型" prop="type">
        <el-select v-model="form.type">
          <el-option label="事假" :value="1" />
          <el-option label="病假" :value="2" />
          <el-option label="其他" :value="3" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="请假时间" required>
        <el-col :span="11">
          <el-form-item prop="startTime">
            <el-date-picker
              v-model="form.startTime"
              type="datetime"
              placeholder="开始时间"
            />
          </el-form-item>
        </el-col>
        <el-col :span="2" class="text-center">-</el-col>
        <el-col :span="11">
          <el-form-item prop="endTime">
            <el-date-picker
              v-model="form.endTime"
              type="datetime"
              placeholder="结束时间"
            />
          </el-form-item>
        </el-col>
      </el-form-item>
      
      <el-form-item label="请假原因" prop="reason">
        <el-input
          v-model="form.reason"
          type="textarea"
          :rows="4"
          placeholder="请输入请假原因"
        />
      </el-form-item>
      
      <el-form-item label="附件">
        <el-upload
          action="/api/file/upload"
          :on-success="handleUploadSuccess"
        >
          <el-button type="primary">上传附件</el-button>
        </el-upload>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="handleSubmit">提交申请</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { submitLeave } from '@/api/leave'
import type { FormInstance } from 'element-plus'

const formRef = ref<FormInstance>()
const form = reactive({
  type: undefined,
  startTime: '',
  endTime: '',
  reason: '',
  attachmentUrl: ''
})

const handleUploadSuccess = (response) => {
  form.attachmentUrl = response.data
}

const handleSubmit = async () => {
  if (!formRef.value) return
  
  await formRef.value.validate()
  await submitLeave(form)
  ElMessage.success('请假申请提交成功')
}
</script>

请假审批组件

<!-- src/views/leave/approval.vue -->
<template>
  <div class="leave-approval">
    <el-table :data="leaveList" border>
      <el-table-column prop="studentName" label="学生姓名" />
      <el-table-column prop="className" label="班级" />
      <el-table-column prop="type" label="请假类型">
        <template #default="{ row }">
          {{ getLeaveType(row.type) }}
        </template>
      </el-table-column>
      <el-table-column prop="startTime" label="开始时间" />
      <el-table-column prop="endTime" label="结束时间" />
      <el-table-column prop="reason" label="请假原因" show-overflow-tooltip />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button
            v-if="row.status === 0"
            type="primary"
            size="small"
            @click="handleApprove(row)"
          >
            审批
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 审批对话框 -->
    <el-dialog
      v-model="dialogVisible"
      title="请假审批"
      width="500px"
    >
      <el-form ref="approvalFormRef" :model="approvalForm" :rules="rules">
        <el-form-item label="审批结果" prop="status">
          <el-radio-group v-model="approvalForm.status">
            <el-radio :label="1">同意</el-radio>
            <el-radio :label="2">驳回</el-radio>
          </el-radio-group>
        </el-form-item>
        
        <el-form-item label="审批意见" prop="comment">
          <el-input
            v-model="approvalForm.comment"
            type="textarea"
            :rows="3"
          />
        </el-form-item>
      </el-form>
      
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitApproval">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { getLeaveRecords, approveLeave } from '@/api/leave'

const leaveList = ref([])
const dialogVisible = ref(false)
const currentLeave = ref(null)

const approvalForm = reactive({
  status: null,
  comment: ''
})

onMounted(() => {
  loadLeaveRecords()
})

const loadLeaveRecords = async () => {
  const res = await getLeaveRecords({ status: 0 })
  leaveList.value = res.data
}

const handleApprove = (row) => {
  currentLeave.value = row
  dialogVisible.value = true
}

const submitApproval = async () => {
  await approveLeave({
    leaveId: currentLeave.value.id,
    ...approvalForm
  })
  
  ElMessage.success('审批成功')
  dialogVisible.value = false
  loadLeaveRecords()
}
</script>

请假历史记录查看组件

<!-- src/views/leave/history.vue -->
<template>
  <div class="leave-history">
    <!-- 搜索条件 -->
    <div class="search-bar">
      <el-form :inline="true" :model="queryForm">
        <el-form-item label="请假类型">
          <el-select v-model="queryForm.type" clearable>
            <el-option label="事假" :value="1" />
            <el-option label="病假" :value="2" />
            <el-option label="其他" :value="3" />
          </el-select>
        </el-form-item>
        
        <el-form-item label="请假状态">
          <el-select v-model="queryForm.status" clearable>
            <el-option label="待审批" :value="0" />
            <el-option label="已批准" :value="1" />
            <el-option label="已驳回" :value="2" />
            <el-option label="已取消" :value="3" />
          </el-select>
        </el-form-item>
        
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="queryForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
          />
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <!-- 请假记录表格 -->
    <el-table :data="leaveList" border>
      <el-table-column prop="createTime" label="申请时间" width="180" />
      <el-table-column prop="type" label="请假类型" width="100">
        <template #default="{ row }">
          {{ getLeaveTypeText(row.type) }}
        </template>
      </el-table-column>
      <el-table-column prop="startTime" label="开始时间" width="180" />
      <el-table-column prop="endTime" label="结束时间" width="180" />
      <el-table-column prop="reason" label="请假原因" show-overflow-tooltip />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="{ row }">
          <el-tag :type="getStatusType(row.status)">
            {{ getStatusText(row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180" fixed="right">
        <template #default="{ row }">
          <el-button 
            v-if="row.status === 0"
            type="primary" 
            link
            @click="handleCancel(row.id)"
          >
            取消申请
          </el-button>
          <el-button 
            type="primary" 
            link
            @click="handleDetail(row)"
          >
            查看详情
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页器 -->
    <div class="pagination-container">
      <el-pagination
        v-model:current-page="queryForm.pageNum"
        v-model:page-size="queryForm.pageSize"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>

    <!-- 详情对话框 -->
    <el-dialog
      v-model="dialogVisible"
      title="请假详情"
      width="600px"
    >
      <div class="leave-detail">
        <div class="detail-item">
          <span class="label">申请时间:</span>
          <span>{{ currentLeave?.createTime }}</span>
        </div>
        <div class="detail-item">
          <span class="label">请假类型:</span>
          <span>{{ getLeaveTypeText(currentLeave?.type) }}</span>
        </div>
        <div class="detail-item">
          <span class="label">请假时间:</span>
          <span>{{ currentLeave?.startTime }} 至 {{ currentLeave?.endTime }}</span>
        </div>
        <div class="detail-item">
          <span class="label">请假原因:</span>
          <span>{{ currentLeave?.reason }}</span>
        </div>
        <div class="detail-item">
          <span class="label">附件:</span>
          <el-link 
            v-if="currentLeave?.attachmentUrl"
            :href="currentLeave.attachmentUrl" 
            target="_blank"
          >
            查看附件
          </el-link>
          <span v-else>无</span>
        </div>
        <div class="detail-item">
          <span class="label">审批状态:</span>
          <el-tag :type="getStatusType(currentLeave?.status)">
            {{ getStatusText(currentLeave?.status) }}
          </el-tag>
        </div>
        <div class="approval-history" v-if="currentLeave?.approvals?.length">
          <div class="title">审批记录</div>
          <div 
            v-for="item in currentLeave.approvals" 
            :key="item.id" 
            class="approval-item"
          >
            <div class="approver">
              {{ item.approverName }}
              <span class="role">({{ getApproverRole(item.approverRole) }})</span>
            </div>
            <div class="result">
              {{ item.status === 1 ? '同意' : '驳回' }}
              <span class="time">{{ item.createTime }}</span>
            </div>
            <div class="comment" v-if="item.comment">
              审批意见:{{ item.comment }}
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { getLeaveRecords, cancelLeave } from '@/api/leave'
import { ElMessage, ElMessageBox } from 'element-plus'

const queryForm = reactive({
  type: undefined,
  status: undefined,
  dateRange: [],
  pageNum: 1,
  pageSize: 10
})

const leaveList = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const currentLeave = ref(null)

// 查询请假记录
const handleQuery = async () => {
  const [startDate, endDate] = queryForm.dateRange || []
  const params = {
    ...queryForm,
    startDate: startDate?.format('YYYY-MM-DD'),
    endDate: endDate?.format('YYYY-MM-DD')
  }
  
  const { data } = await getLeaveRecords(params)
  leaveList.value = data.records
  total.value = data.total
}

// 取消请假申请
const handleCancel = async (id: number) => {
  try {
    await ElMessageBox.confirm('确定要取消该请假申请吗?')
    await cancelLeave(id)
    ElMessage.success('取消成功')
    handleQuery()
  } catch (error) {
    // 用户取消操作
  }
}

// 查看详情
const handleDetail = (row) => {
  currentLeave.value = row
  dialogVisible.value = true
}

// 状态标签样式
const getStatusType = (status) => {
  const statusMap = {
    0: 'warning',
    1: 'success',
    2: 'danger',
    3: 'info'
  }
  return statusMap[status]
}

// 工具函数
const getLeaveTypeText = (type) => {
  const typeMap = {
    1: '事假',
    2: '病假',
    3: '其他'
  }
  return typeMap[type]
}

const getStatusText = (status) => {
  const statusMap = {
    0: '待审批',
    1: '已批准',
    2: '已驳回',
    3: '已取消'
  }
  return statusMap[status]
}

const getApproverRole = (role) => {
  return role === 1 ? '教师' : '管理员'
}
</script>

<style lang="scss" scoped>
.leave-history {
  .search-bar {
    margin-bottom: 20px;
  }
  
  .pagination-container {
    margin-top: 20px;
    text-align: right;
  }
  
  .leave-detail {
    .detail-item {
      margin-bottom: 15px;
      
      .label {
        display: inline-block;
        width: 100px;
        color: #606266;
      }
    }
    
    .approval-history {
      margin-top: 20px;
      border-top: 1px solid #EBEEF5;
      padding-top: 20px;
      
      .title {
        font-weight: bold;
        margin-bottom: 15px;
      }
      
      .approval-item {
        margin-bottom: 15px;
        padding: 10px;
        background: #F5F7FA;
        border-radius: 4px;
        
        .approver {
          .role {
            color: #909399;
            margin-left: 5px;
          }
        }
        
        .result {
          margin: 5px 0;
          
          .time {
            color: #909399;
            margin-left: 10px;
            font-size: 13px;
          }
        }
        
        .comment {
          color: #606266;
        }
      }
    }
  }
}
</style>

请假统计分析服务

@Service
public class LeaveStatisticsService {
    
    @Autowired
    private LeaveApplicationMapper leaveMapper;
    
    // 获取请假统计数据
    public LeaveStatisticsVO getLeaveStatistics(LeaveStatisticsQueryDTO query) {
        LeaveStatisticsVO statistics = new LeaveStatisticsVO();
        
        // 请假类型分布
        statistics.setTypeDistribution(leaveMapper.selectTypeDistribution(query));
        
        // 请假时长统计
        statistics.setDurationStatistics(leaveMapper.selectDurationStatistics(query));
        
        // 请假趋势分析
        statistics.setTrendAnalysis(leaveMapper.selectTrendAnalysis(query));
        
        return statistics;
    }
    
    // 导出请假记录
    public void exportLeaveRecords(LeaveQueryDTO query, HttpServletResponse response) {
        List<LeaveExportVO> records = leaveMapper.selectExportList(query);
        
        ExcelWriter writer = ExcelUtil.getWriter();
        writer.addHeaderAlias("studentName", "学生姓名");
        writer.addHeaderAlias("className", "班级");
        writer.addHeaderAlias("leaveType", "请假类型");
        writer.addHeaderAlias("startTime", "开始时间");
        writer.addHeaderAlias("endTime", "结束时间");
        writer.addHeaderAlias("duration", "请假时长(天)");
        writer.addHeaderAlias("reason", "请假原因");
        writer.addHeaderAlias("status", "状态");
        
        writer.write(records, true);
        
        response.setContentType("application/vnd.ms-excel;charset=utf-8");
        response.setHeader("Content-Disposition", "attachment;filename=leave_records.xls");
        
        writer.flush(response.getOutputStream());
    }
}

请假审批流程优化

  1. 添加审批流程配置表:
CREATE TABLE leave_approval_flow (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    class_id BIGINT COMMENT '班级ID',
    min_days INT COMMENT '最小天数',
    max_days INT COMMENT '最大天数',
    approver_roles VARCHAR(100) COMMENT '审批角色列表',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP
) COMMENT '请假审批流程配置表';
  1. 审批服务优化:
@Service
public class LeaveApprovalService {
    
    @Autowired
    private LeaveApprovalFlowMapper flowMapper;
    
    // 获取审批流程
    public List<Integer> getApprovalFlow(Long classId, int leaveDays) {
        LeaveApprovalFlow flow = flowMapper.selectOne(new QueryWrapper<LeaveApprovalFlow>()
            .eq("class_id", classId)
            .le("min_days", leaveDays)
            .ge("max_days", leaveDays));
            
        if (flow == null) {
            return Collections.singletonList(1); // 默认只需要教师审批
        }
        
        return Arrays.stream(flow.getApproverRoles().split(","))
            .map(Integer::parseInt)
            .collect(Collectors.toList());
    }
    
    // 处理审批
    public void handleApproval(LeaveApprovalDTO dto) {
        LeaveApplication leave = leaveMapper.selectById(dto.getLeaveId());
        List<Integer> flowRoles = getApprovalFlow(leave.getClassId(), 
            calculateLeaveDays(leave));
            
        // 创建审批记录
        createApprovalRecord(dto);
        
        // 判断是否需要继续审批
        if (isLastApproval(dto.getLeaveId(), flowRoles)) {
            // 更新请假状态为最终状态
            updateLeaveStatus(dto.getLeaveId(), dto.getStatus());
            
            // 发送通知
            sendNotification(leave, dto.getStatus());
        }
    }
    
    // 发送审批通知
    private void sendNotification(LeaveApplication leave, Integer status) {
        // 获取学生信息
        User student = userMapper.selectById(leave.getStudentId());
        
        // 发送站内通知
        NotificationMessage message = new NotificationMessage();
        message.setUserId(student.getId());
        message.setTitle("请假审批通知");
        message.setContent(String.format("您的请假申请已%s", 
            status == 1 ? "批准" : "驳回"));
        notificationService.sendNotification(message);
        
        // 发送邮件通知
        if (StringUtils.isNotBlank(student.getEmail())) {
            emailService.sendSimpleMail(student.getEmail(),
                "请假审批通知",
                message.getContent());
        }
    }
}

四、系统管理模块的实现

数据库设计

班级表(sys_class)

CREATE TABLE sys_class (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID',
    class_name VARCHAR(50) NOT NULL COMMENT '班级名称',
    grade VARCHAR(20) NOT NULL COMMENT '年级',
    department VARCHAR(50) COMMENT '院系',
    major VARCHAR(50) COMMENT '专业',
    head_teacher_id BIGINT COMMENT '班主任ID',
    student_count INT DEFAULT 0 COMMENT '学生人数',
    status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:正常)',
    remark VARCHAR(200) COMMENT '备注',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (head_teacher_id) REFERENCES sys_user(id),
    UNIQUE INDEX idx_class_name(class_name)
) COMMENT '班级表';

课程表(sys_course)

CREATE TABLE sys_course (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID',
    course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
    course_code VARCHAR(50) NOT NULL COMMENT '课程代码',
    credit DECIMAL(4,1) NOT NULL COMMENT '学分',
    period INT NOT NULL COMMENT '课时',
    type TINYINT COMMENT '课程类型(1:必修,2:选修)',
    department VARCHAR(50) COMMENT '开课院系',
    status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:正常)',
    description TEXT COMMENT '课程描述',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_course_code(course_code)
) COMMENT '课程表';

教师课程关联表(sys_teacher_course)

CREATE TABLE sys_teacher_course (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    teacher_id BIGINT NOT NULL COMMENT '教师ID',
    course_id BIGINT NOT NULL COMMENT '课程ID',
    semester VARCHAR(20) NOT NULL COMMENT '学期',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (teacher_id) REFERENCES sys_user(id),
    FOREIGN KEY (course_id) REFERENCES sys_course(id),
    UNIQUE INDEX idx_teacher_course(teacher_id, course_id, semester)
) COMMENT '教师课程关联表';

班级课程关联表(sys_class_course)

CREATE TABLE sys_class_course (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    class_id BIGINT NOT NULL COMMENT '班级ID',
    course_id BIGINT NOT NULL COMMENT '课程ID',
    teacher_id BIGINT NOT NULL COMMENT '任课教师ID',
    semester VARCHAR(20) NOT NULL COMMENT '学期',
    schedule VARCHAR(200) COMMENT '上课时间',
    classroom VARCHAR(50) COMMENT '教室',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (class_id) REFERENCES sys_class(id),
    FOREIGN KEY (course_id) REFERENCES sys_course(id),
    FOREIGN KEY (teacher_id) REFERENCES sys_user(id),
    UNIQUE INDEX idx_class_course(class_id, course_id, semester)
) COMMENT '班级课程关联表';

后端实现

实体类

@Data
@TableName("sys_class")
public class Class {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String className;
    private String grade;
    private String department;
    private String major;
    private Long headTeacherId;
    private Integer studentCount;
    private Integer status;
    private String remark;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    
    @TableField(exist = false)
    private String headTeacherName;
}

@Data
@TableName("sys_course")
public class Course {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String courseName;
    private String courseCode;
    private BigDecimal credit;
    private Integer period;
    private Integer type;
    private String department;
    private Integer status;
    private String description;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

Service层实现

@Service
@Transactional(rollbackFor = Exception.class)
public class ClassServiceImpl extends ServiceImpl<ClassMapper, Class> implements ClassService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public IPage<ClassVO> listClasses(ClassQueryDTO query) {
        return baseMapper.selectClassList(new Page<>(query.getPageNum(), query.getPageSize()), query);
    }
    
    @Override
    public void addClass(ClassDTO dto) {
        // 验证班级名称唯一性
        validateClassName(dto.getClassName());
        
        Class entity = new Class();
        BeanUtils.copyProperties(dto, entity);
        baseMapper.insert(entity);
    }
    
    @Override
    public void updateClass(Long id, ClassDTO dto) {
        Class entity = baseMapper.selectById(id);
        if (entity == null) {
            throw new BusinessException("班级不存在");
        }
        
        if (!entity.getClassName().equals(dto.getClassName())) {
            validateClassName(dto.getClassName());
        }
        
        BeanUtils.copyProperties(dto, entity);
        baseMapper.updateById(entity);
    }
    
    @Override
    public void deleteClass(Long id) {
        // 检查是否有关联学生
        Long studentCount = userMapper.selectCount(new QueryWrapper<User>()
            .eq("class_id", id));
        if (studentCount > 0) {
            throw new BusinessException("班级下存在学生,无法删除");
        }
        
        baseMapper.deleteById(id);
    }
}

@Service
@Transactional(rollbackFor = Exception.class)
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {
    
    @Override
    public IPage<CourseVO> listCourses(CourseQueryDTO query) {
        return baseMapper.selectCourseList(new Page<>(query.getPageNum(), query.getPageSize()), query);
    }
    
    @Override
    public void addCourse(CourseDTO dto) {
        // 验证课程代码唯一性
        validateCourseCode(dto.getCourseCode());
        
        Course entity = new Course();
        BeanUtils.copyProperties(dto, entity);
        baseMapper.insert(entity);
    }
    
    @Override
    public void assignTeacher(CourseTeacherDTO dto) {
        // 验证教师和课程是否存在
        validateTeacherAndCourse(dto.getTeacherId(), dto.getCourseId());
        
        TeacherCourse relation = new TeacherCourse();
        BeanUtils.copyProperties(dto, relation);
        teacherCourseMapper.insert(relation);
    }
}

Controller层实现

@RestController
@RequestMapping("/api/system/class")
public class ClassController {
    
    @Autowired
    private ClassService classService;
    
    @GetMapping("/list")
    public Result<IPage<ClassVO>> listClasses(ClassQueryDTO query) {
        return Result.success(classService.listClasses(query));
    }
    
    @PostMapping
    @RequiresPermissions("system:class:add")
    public Result<?> addClass(@RequestBody @Valid ClassDTO dto) {
        classService.addClass(dto);
        return Result.success();
    }
    
    @PutMapping("/{id}")
    @RequiresPermissions("system:class:edit")
    public Result<?> updateClass(@PathVariable Long id, @RequestBody @Valid ClassDTO dto) {
        classService.updateClass(id, dto);
        return Result.success();
    }
    
    @DeleteMapping("/{id}")
    @RequiresPermissions("system:class:delete")
    public Result<?> deleteClass(@PathVariable Long id) {
        classService.deleteClass(id);
        return Result.success();
    }
}

@RestController
@RequestMapping("/api/system/course")
public class CourseController {
    
    @Autowired
    private CourseService courseService;
    
    @GetMapping("/list")
    public Result<IPage<CourseVO>> listCourses(CourseQueryDTO query) {
        return Result.success(courseService.listCourses(query));
    }
    
    @PostMapping
    @RequiresPermissions("system:course:add")
    public Result<?> addCourse(@RequestBody @Valid CourseDTO dto) {
        courseService.addCourse(dto);
        return Result.success();
    }
    
    @PostMapping("/assign-teacher")
    @RequiresPermissions("system:course:assign")
    public Result<?> assignTeacher(@RequestBody @Valid CourseTeacherDTO dto) {
        courseService.assignTeacher(dto);
        return Result.success();
    }
}

前端实现

班级管理组件

<!-- src/views/system/class/index.vue -->
<template>
  <div class="class-manage">
    <!-- 搜索工具栏 -->
    <div class="search-bar">
      <el-form :inline="true" :model="queryParams">
        <el-form-item label="班级名称">
          <el-input
            v-model="queryParams.className"
            placeholder="请输入班级名称"
            clearable
          />
        </el-form-item>
        <el-form-item label="年级">
          <el-select v-model="queryParams.grade" clearable>
            <el-option
              v-for="item in gradeOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <!-- 操作按钮区 -->
    <div class="toolbar">
      <el-button
        type="primary"
        @click="handleAdd"
        v-hasPermi="['system:class:add']"
      >
        新增班级
      </el-button>
    </div>

    <!-- 班级列表 -->
    <el-table :data="classList" border>
      <el-table-column prop="className" label="班级名称" />
      <el-table-column prop="grade" label="年级" />
      <el-table-column prop="department" label="院系" />
      <el-table-column prop="major" label="专业" />
      <el-table-column prop="headTeacherName" label="班主任" />
      <el-table-column prop="studentCount" label="学生人数" />
      <el-table-column prop="status" label="状态">
        <template #default="{ row }">
          <el-tag :type="row.status === 1 ? 'success' : 'danger'">
            {{ row.status === 1 ? '正常' : '禁用' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200" fixed="right">
        <template #default="{ row }">
          <el-button
            type="primary"
            link
            @click="handleEdit(row)"
            v-hasPermi="['system:class:edit']"
          >
            编辑
          </el-button>
          <el-button
            type="primary"
            link
            @click="handleDelete(row)"
            v-hasPermi="['system:class:delete']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页器 -->
    <el-pagination
      v-model:current-page="queryParams.pageNum"
      v-model:page-size="queryParams.pageSize"
      :total="total"
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 编辑对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="500px"
      append-to-body
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="班级名称" prop="className">
          <el-input v-model="form.className" placeholder="请输入班级名称" />
        </el-form-item>
        <el-form-item label="年级" prop="grade">
          <el-select v-model="form.grade">
            <el-option
              v-for="item in gradeOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="院系" prop="department">
          <el-input v-model="form.department" placeholder="请输入院系" />
        </el-form-item>
        <el-form-item label="专业" prop="major">
          <el-input v-model="form.major" placeholder="请输入专业" />
        </el-form-item>
        <el-form-item label="班主任" prop="headTeacherId">
          <el-select
            v-model="form.headTeacherId"
            placeholder="请选择班主任"
            filterable
          >
            <el-option
              v-for="item in teacherOptions"
              :key="item.id"
              :label="item.realName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio :label="1">正常</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="form.remark"
            type="textarea"
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { 
  listClass, 
  addClass, 
  updateClass, 
  deleteClass 
} from '@/api/system/class'
import { listTeachers } from '@/api/system/user'
import { ElMessage, ElMessageBox } from 'element-plus'

// 查询参数
const queryParams = reactive({
  pageNum: 1,
  pageSize: 10,
  className: '',
  grade: undefined
})

// 班级列表数据
const classList = ref([])
const total = ref(0)

// 表单数据
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const form = reactive({
  id: undefined,
  className: '',
  grade: '',
  department: '',
  major: '',
  headTeacherId: undefined,
  status: 1,
  remark: ''
})

// 教师选项
const teacherOptions = ref([])

// 获取班级列表
const getList = async () => {
  const { data } = await listClass(queryParams)
  classList.value = data.records
  total.value = data.total
}

// 获取教师列表
const getTeachers = async () => {
  const { data } = await listTeachers({ roleCode: 'TEACHER' })
  teacherOptions.value = data
}

// 新增班级
const handleAdd = () => {
  resetForm()
  dialogTitle.value = '新增班级'
  dialogVisible.value = true
}

// 编辑班级
const handleEdit = (row) => {
  Object.assign(form, row)
  dialogTitle.value = '编辑班级'
  dialogVisible.value = true
}

// 删除班级
const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm('确认要删除该班级吗?')
    await deleteClass(row.id)
    ElMessage.success('删除成功')
    getList()
  } catch (error) {
    // 用户取消操作
  }
}

// 提交表单
const handleSubmit = async () => {
  if (!formRef.value) return
  
  await formRef.value.validate()
  
  if (form.id) {
    await updateClass(form.id, form)
  } else {
    await addClass(form)
  }
  
  ElMessage.success('操作成功')
  dialogVisible.value = false
  getList()
}

// 重置表单
const resetForm = () => {
  if (formRef.value) {
    formRef.value.resetFields()
  }
  Object.assign(form, {
    id: undefined,
    className: '',
    grade: '',
    department: '',
    major: '',
    headTeacherId: undefined,
    status: 1,
    remark: ''
  })
}

onMounted(() => {
  getList()
  getTeachers()
})
</script>

<style lang="scss" scoped>
.class-manage {
  .search-bar {
    margin-bottom: 20px;
  }
  
  .toolbar {
    margin-bottom: 20px;
  }
}
</style>

课程管理组件

<!-- src/views/system/course/index.vue -->
<template>
  <div class="course-manage">
    <!-- 搜索工具栏 -->
    <div class="search-bar">
      <el-form :inline="true" :model="queryParams">
        <el-form-item label="课程名称">
          <el-input
            v-model="queryParams.courseName"
            placeholder="请输入课程名称"
            clearable
          />
        </el-form-item>
        <el-form-item label="课程代码">
          <el-input
            v-model="queryParams.courseCode"
            placeholder="请输入课程代码"
            clearable
          />
        </el-form-item>
        <el-form-item label="课程类型">
          <el-select v-model="queryParams.type" clearable>
            <el-option label="必修" :value="1" />
            <el-option label="选修" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <!-- 操作按钮区 -->
    <div class="toolbar">
      <el-button
        type="primary"
        @click="handleAdd"
        v-hasPermi="['system:course:add']"
      >
        新增课程
      </el-button>
      <el-button
        type="success"
        @click="handleAssign"
        v-hasPermi="['system:course:assign']"
      >
        课程分配
      </el-button>
    </div>

    <!-- 课程列表 -->
    <el-table :data="courseList" border>
      <el-table-column prop="courseName" label="课程名称" />
      <el-table-column prop="courseCode" label="课程代码" />
      <el-table-column prop="credit" label="学分" />
      <el-table-column prop="period" label="课时" />
      <el-table-column prop="type" label="类型">
        <template #default="{ row }">
          {{ row.type === 1 ? '必修' : '选修' }}
        </template>
      </el-table-column>
      <el-table-column prop="department" label="开课院系" />
      <el-table-column label="操作" width="200" fixed="right">
        <template #default="{ row }">
          <el-button
            type="primary"
            link
            @click="handleEdit(row)"
          >
            编辑
          </el-button>
          <el-button
            type="primary"
            link
            @click="handleDelete(row)"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页器 -->
    <el-pagination
      v-model:current-page="queryParams.pageNum"
      v-model:page-size="queryParams.pageSize"
      :total="total"
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 课程表单对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="500px"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="课程名称" prop="courseName">
          <el-input v-model="form.courseName" />
        </el-form-item>
        <el-form-item label="课程代码" prop="courseCode">
          <el-input v-model="form.courseCode" />
        </el-form-item>
        <el-form-item label="学分" prop="credit">
          <el-input-number v-model="form.credit" :precision="1" :step="0.5" />
        </el-form-item>
        <el-form-item label="课时" prop="period">
          <el-input-number v-model="form.period" :min="1" />
        </el-form-item>
        <el-form-item label="课程类型" prop="type">
          <el-select v-model="form.type">
            <el-option label="必修" :value="1" />
            <el-option label="选修" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="开课院系" prop="department">
          <el-input v-model="form.department" />
        </el-form-item>
        <el-form-item label="课程描述" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="3"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>

    <!-- 课程分配对话框 -->
    <el-dialog
      title="课程分配"
      v-model="assignDialogVisible"
      width="600px"
    >
      <el-form
        ref="assignFormRef"
        :model="assignForm"
        :rules="assignRules"
        label-width="100px"
      >
        <el-form-item label="课程" prop="courseId">
          <el-select
            v-model="assignForm.courseId"
            filterable
            placeholder="请选择课程"
          >
            <el-option
              v-for="item in courseList"
              :key="item.id"
              :label="item.courseName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="教师" prop="teacherId">
          <el-select
            v-model="assignForm.teacherId"
            filterable
            placeholder="请选择教师"
          >
            <el-option
              v-for="item in teacherOptions"
              :key="item.id"
              :label="item.realName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="学期" prop="semester">
          <el-select v-model="assignForm.semester">
            <el-option
              v-for="item in semesterOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="assignDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleAssignSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script lang="ts" setup>
// ... 组件逻辑实现与班级管理类似,此处省略
</script>

管理员功能

  1. 系统参数配置:
@Data
@TableName("sys_config")
public class SysConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String configKey;
    private String configValue;
    private String remark;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

@Service
public class ConfigService {
    
    @Autowired
    private SysConfigMapper configMapper;
    @Autowired
    private RedisCache redisCache;
    
    public String getConfigValue(String key) {
        String cacheKey = getCacheKey(key);
        String value = redisCache.getCacheObject(cacheKey);
        if (StringUtils.isNotEmpty(value)) {
            return value;
        }
        
        SysConfig config = configMapper.selectOne(new QueryWrapper<SysConfig>()
            .eq("config_key", key));
            
        if (config != null) {
            redisCache.setCacheObject(cacheKey, config.getConfigValue());
            return config.getConfigValue();
        }
        
        return null;
    }
    
    public void updateConfig(String key, String value) {
        SysConfig config = configMapper.selectOne(new QueryWrapper<SysConfig>()
            .eq("config_key", key));
            
        if (config != null) {
            config.setConfigValue(value);
            configMapper.updateById(config);
        } else {
            config = new SysConfig();
            config.setConfigKey(key);
            config.setConfigValue(value);
            configMapper.insert(config);
        }
        
        redisCache.deleteObject(getCacheKey(key));
    }
}
  1. 数据备份功能:
@Service
public class BackupService {
    
    @Value("${backup.path}")
    private String backupPath;
    
    public void backup() {
        String fileName = String.format("backup_%s.sql", 
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
            
        String filePath = backupPath + File.separator + fileName;
        
        // 执行数据库备份命令
        String command = String.format(
            "mysqldump -u%s -p%s %s > %s",
            username, password, database, filePath);
            
        Process process = Runtime.getRuntime().exec(command);
        process.waitFor();
        
        // 记录备份日志
        BackupLog log = new BackupLog();
        log.setFileName(fileName);
        log.setFilePath(filePath);
        log.setFileSize(new File(filePath).length());
        backupLogMapper.insert(log);
    }
    
    public void restore(String fileName) {
        String filePath = backupPath + File.separator + fileName;
        
        // 执行数据库恢复命令
        String command = String.format(
            "mysql -u%s -p%s %s < %s",
            username, password, database, filePath);
            
        Process process = Runtime.getRuntime().exec(command);
        process.waitFor();
        
        // 记录恢复日志
        RestoreLog log = new RestoreLog();
        log.setFileName(fileName);
        log.setFilePath(filePath);
        restoreLogMapper.insert(log);
    }
}
  1. 操作日志功能:
@Data
@TableName("sys_operation_log")
public class OperationLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String operationModule;
    private String operationType;
    private String operationDesc;
    private String requestMethod;
    private String requestParams;
    private String requestIp;
    private Long operationUser;
    private LocalDateTime operationTime;
    private Integer status;
    private String errorMsg;
}

@Aspect
@Component
public class OperationLogAspect {
    
    @Autowired
    private OperationLogMapper logMapper;
    
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint point, OperationLog operationLog) {
        OperationLog log = new OperationLog();
        
        try {
            // 记录请求信息
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            
            log.setRequestMethod(request.getMethod());
            log.setRequestIp(IpUtils.getIpAddr(request));
            log.setRequestParams(JsonUtils.toJsonString(point.getArgs()));
            
            // 记录操作信息
            log.setOperationModule(operationLog.operationModule());
            log.setOperationType(operationLog.operationType());
            log.setOperationDesc(operationLog.operationDesc());
            log.setOperationUser(SecurityUtils.getCurrentUserId());
            log.setOperationTime(LocalDateTime.now());
            
            // 执行目标方法
            Object result = point.proceed();
            log.setStatus(1);
            
            return result;
        } catch (Throwable e) {
            log.setStatus(0);
            log.setErrorMsg(e.getMessage());
            throw new RuntimeException(e);
        } finally {
            logMapper.insert(log);
        }
    }
}

// 操作日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    String operationModule() default ""; // 操作模块
    String operationType() default "";    // 操作类型
    String operationDesc() default "";    // 操作描述
}

系统监控功能

@Data
public class ServerInfo {
    private CpuInfo cpu;
    private MemInfo mem;
    private JvmInfo jvm;
    private DiskInfo disk;
    
    @Data
    public static class CpuInfo {
        private int cpuNum;
        private double total;
        private double sys;
        private double used;
        private double wait;
        private double free;
    }
    
    @Data
    public static class MemInfo {
        private double total;
        private double used;
        private double free;
        private double usage;
    }
    
    // 其他内部类...
}

@Service
public class MonitorService {
    
    public ServerInfo getServerInfo() {
        ServerInfo info = new ServerInfo();
        
        // 获取CPU信息
        SystemInfo si = new SystemInfo();
        HardwareAbstractionLayer hal = si.getHardware();
        CentralProcessor processor = hal.getProcessor();
        
        CpuInfo cpu = new CpuInfo();
        cpu.setCpuNum(processor.getLogicalProcessorCount());
        // 设置其他CPU信息...
        info.setCpu(cpu);
        
        // 获取内存信息
        GlobalMemory memory = hal.getMemory();
        MemInfo mem = new MemInfo();
        mem.setTotal(memory.getTotal());
        mem.setUsed(memory.getTotal() - memory.getAvailable());
        mem.setFree(memory.getAvailable());
        mem.setUsage(mem.getUsed() / mem.getTotal() * 100);
        info.setMem(mem);
        
        // 获取其他系统信息...
        
        return info;
    }
}

用户在线管理

@Data
public class UserOnline {
    private String tokenId;
    private Long userId;
    private String username;
    private String ipaddr;
    private String loginTime;
    private String browser;
    private String os;
}

@Service
public class UserOnlineService {
    
    @Autowired
    private RedisCache redisCache;
    
    public List<UserOnline> getOnlineUsers() {
        Collection<String> keys = redisCache.keys(Constants.LOGIN_TOKEN_KEY + "*");
        List<UserOnline> userOnlineList = new ArrayList<>();
        
        for (String key : keys) {
            LoginUser user = redisCache.getCacheObject(key);
            if (user != null) {
                UserOnline online = new UserOnline();
                online.setTokenId(key);
                online.setUserId(user.getUserId());
                online.setUsername(user.getUsername());
                online.setIpaddr(user.getIpaddr());
                online.setLoginTime(DateUtils.formatDateTime(user.getLoginTime()));
                online.setBrowser(user.getBrowser());
                online.setOs(user.getOs());
                userOnlineList.add(online);
            }
        }
        
        return userOnlineList;
    }
    
    public void forceLogout(String tokenId) {
        redisCache.deleteObject(tokenId);
    }
}

定时任务管理

@Data
@TableName("sys_job")
public class SysJob {
    @TableId(type = IdType.AUTO)
    private Long jobId;
    private String jobName;
    private String jobGroup;
    private String invokeTarget;
    private String cronExpression;
    private String misfirePolicy;
    private String concurrent;
    private String status;
    private String remark;
}

@Service
public class JobService {
    
    @Autowired
    private Scheduler scheduler;
    
    public void addJob(SysJob job) throws SchedulerException {
        Class<? extends Job> jobClass = getQuartzJobClass(job);
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
            .withIdentity(job.getJobName(), job.getJobGroup())
            .build();
            
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        CronTrigger trigger = TriggerBuilder.newTrigger()
            .withIdentity(job.getJobName(), job.getJobGroup())
            .withSchedule(cronScheduleBuilder)
            .build();
            
        scheduler.scheduleJob(jobDetail, trigger);
    }
    
    public void pauseJob(Long jobId) throws SchedulerException {
        SysJob job = jobMapper.selectById(jobId);
        scheduler.pauseJob(JobKey.jobKey(job.getJobName(), job.getJobGroup()));
    }
    
    public void resumeJob(Long jobId) throws SchedulerException {
        SysJob job = jobMapper.selectById(jobId);
        scheduler.resumeJob(JobKey.jobKey(job.getJobName(), job.getJobGroup()));
    }
}

数据字典管理

@Data
@TableName("sys_dict_type")
public class DictType {
    @TableId(type = IdType.AUTO)
    private Long dictId;
    private String dictName;
    private String dictType;
    private String status;
    private String remark;
}

@Data
@TableName("sys_dict_data")
public class DictData {
    @TableId(type = IdType.AUTO)
    private Long dictCode;
    private Long dictSort;
    private String dictLabel;
    private String dictValue;
    private String dictType;
    private String status;
    private String remark;
}

@Service
public class DictService {
    
    @Autowired
    private DictTypeMapper dictTypeMapper;
    
    @Autowired
    private DictDataMapper dictDataMapper;
    
    public List<DictData> getDictData(String dictType) {
        return dictDataMapper.selectList(new QueryWrapper<DictData>()
            .eq("dict_type", dictType)
            .eq("status", "0")
            .orderByAsc("dict_sort"));
    }
    
    @CacheEvict(value = "dict", key = "#dictType")
    public void clearDictCache(String dictType) {
        // 清除缓存
    }
}

这些实现包括:

  1. 完整的操作日志记录功能
  2. 服务器监控功能
  3. 在线用户管理
  4. 定时任务管理
  5. 数据字典管理

主要特点:

  1. 模块化设计:各个功能模块独立
  2. 缓存支持:使用Redis缓存提升性能
  3. 异步处理:日志记录采用异步方式
  4. 安全性:操作记录和权限控制
  5. 可扩展性:便于添加新的管理功能

系统管理模块为整个应用提供了完整的后台管理支持,方便管理员进行系统维护和监控。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值