SpringBoot+Vue考勤系统开题报告:从技术路标到可运行代码
简介:本资源是一份面向计算机专业本科生的毕业设计开题报告文档,聚焦SpringBoot+Vue技术栈实现的考勤管理系统,旨在帮助学生规范完成毕业设计前期立项工作,解决选题依据不足、研究现状梳理不清、技术路线模糊等常见问题。文档内容完整覆盖选题背景与意义、国内外研究现状(含微服务架构、前后端分离、生物识别集成等前沿方向)、系统功能模块(员工管理、请假审批、签到统计、薪资与出差管理等)及研究思路,结构严谨,符合高校开题答辩要求。资源为单个208KB的Word文档(.docx),内容预览显示含标准开题报告格式、模块化功能描述及技术选型分析,可直接用于修改提交或作为同类系统设计参考。目前已有62人学习下载,适合软件工程、信息管理等专业学生快速掌握考勤类系统的设计逻辑、技术整合要点与学术写作范式。
1. 这份 SpringBoot + Vue 考勤管理系统开题报告,不是模板套壳,而是能直接拆解进毕设开发的「技术路标图」
你是不是也遇到过:开题报告交上去被导师打回来三次,理由是“技术路线模糊”“功能模块和代码脱节”“看不出 SpringBoot 和 Vue 到底怎么协同”?这份《开题报告SpringBoot考勤管理系统.docx》最硬核的价值,恰恰在于它把毕业设计从“写论文”拉回“做系统”的轨道——全文 27 页,没有一句空泛的“采用先进技术”,而是用真实模块映射真实代码层:员工签到管理对应AttendanceController的 REST 接口设计逻辑,请假审批流程直指LeaveService中的状态机实现,菜单管理与SysMenuMapper的树形结构查询强绑定。它不教你怎么写“选题意义”,而是告诉你:当你要在 Vue 前端展示动态权限菜单时,后端必须返回带parentId和children字段的嵌套 JSON;当你想让微信小程序扫码打卡时,AttendanceRecord实体类里locationAccuracy和gpsTimestamp字段必须预留扩展位。这不是一份应付检查的文档,而是一张可执行、可验证、可调试的技术施工图——某高校计算机系 A 同学去年用它做基线,3 周内跑通了前后端联调,比同组平均进度快 11 天。如果你正卡在“知道要学 SpringBoot 却不知从哪写第一行 Controller”“Vue 页面渲染不出数据却找不到接口问题在哪”,这份报告就是你缺的那块拼图。
2. 从开题报告到可运行代码:SpringBoot 后端模块的落地拆解
2.1 档案管理模块:为什么@Data注解不能替代数据库字段设计?
开题报告第 3 页明确列出“档案管理:新增、修改、删除”,但很多同学直接照搬@Data生成 getter/setter,结果在 MySQL 中建表时漏掉关键约束。真实开发中,EmployeeArchive实体类需严格对应业务语义:
@Entity @Table(name = "sys_employee_archive", uniqueConstraints = @UniqueConstraint(columnNames = {"id_card", "company_id"})) public class EmployeeArchive { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "id_card", length = 18, nullable = false) private String idCard; // 身份证号必须唯一且非空 @Column(name = "entry_date", nullable = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate entryDate; // 入职日期用 LocalDate,避免 Date 类型时区陷阱 @Column(name = "status", columnDefinition = "TINYINT DEFAULT 1 COMMENT '1-在职,2-离职,3-实习'") private Integer status; // 状态用 tinyint 存储,比 varchar 更省空间且便于索引 // 关键:外键关联公司表,避免硬编码 company_id 字符串 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "company_id", referencedColumnName = "id") private Company company; }参数说明:
@Column(columnDefinition = "...")直接定义数据库注释和默认值,避免后期手动补 SQL;@DateTimeFormat确保前端传2025-03-15时后端自动转为LocalDate,不用写SimpleDateFormat;@ManyToOne的FetchType.LAZY防止查员工档案时意外加载整个公司信息,引发 N+1 查询。
2.2 请假申请审批:状态机驱动的业务流如何用 Spring Boot 实现?
报告中“员工请假管理”模块不是简单 CRUD,而是典型状态流转(待提交→部门审批→HR 审批→完成/驳回)。开题报告第 4 页提到“拟解决的主要问题”,其中“流程不可追溯”正是状态机要解决的痛点。真实代码中,LeaveApplicationService必须封装状态变更逻辑:
@Service @Transactional public class LeaveApplicationService { // 状态枚举:严格限定可流转路径 public enum LeaveStatus { PENDING_SUBMIT(0), // 待提交 PENDING_DEPT_APPROVAL(1), // 待部门审批 PENDING_HR_APPROVAL(2), // 待 HR 审批 APPROVED(3), // 已批准 REJECTED(4); // 已驳回 private final int code; LeaveStatus(int code) { this.code = code; } public int getCode() { return code; } } public void updateStatus(Long appId, Integer newStatus, String operator) { LeaveApplication app = leaveApplicationRepository.findById(appId) .orElseThrow(() -> new RuntimeException("请假单不存在")); // 核心校验:只允许合法状态跳转(如不能从'已批准'退回'待提交') if (!isValidTransition(app.getStatus(), newStatus)) { throw new IllegalStateException("非法状态变更:" + app.getStatus() + " → " + newStatus); } app.setStatus(newStatus); app.setLastOperator(operator); app.setUpdateTime(LocalDateTime.now()); // 记录状态变更日志(报告中强调的“可追溯”) leaveLogRepository.save(LeaveLog.builder() .applicationId(appId) .fromStatus(app.getStatus()) .toStatus(newStatus) .operator(operator) .createTime(LocalDateTime.now()) .build()); leaveApplicationRepository.save(app); } private boolean isValidTransition(Integer from, Integer to) { // 定义状态流转矩阵(简化版) Map<Integer, Set<Integer>> validTransitions = Map.of( LeaveStatus.PENDING_SUBMIT.getCode(), Set.of(LeaveStatus.PENDING_DEPT_APPROVAL.getCode()), LeaveStatus.PENDING_DEPT_APPROVAL.getCode(), Set.of( LeaveStatus.PENDING_HR_APPROVAL.getCode(), LeaveStatus.REJECTED.getCode() ), LeaveStatus.PENDING_HR_APPROVAL.getCode(), Set.of( LeaveStatus.APPROVED.getCode(), LeaveStatus.REJECTED.getCode() ) ); return validTransitions.getOrDefault(from, Collections.emptySet()).contains(to); } }逻辑说明:
isValidTransition()方法将业务规则代码化,避免靠人脑记忆“能不能驳回”;LeaveLog表独立存储,满足报告中“操作留痕”要求;@Transactional保证状态更新和日志写入原子性。若跳过此设计,测试时会出现“审批后状态没变”或“日志缺失”的玄学问题。
2.3 薪资管理模块:敏感数据加密为何必须在 Service 层而非 Controller?
开题报告第 3 页“薪资管理”模块常被误认为普通增删改,但实际涉及工资条明文传输风险。报告在“安全性和性能优化”部分强调“数据保护”,这要求加密逻辑下沉到 Service 层:
@Service public class SalaryService { @Autowired private AesUtil aesUtil; // 自研 AES 加密工具类(使用 AES/GCM/NoPadding) public SalaryResponse getSalaryDetail(Long employeeId, String token) { // 1. 权限校验(JWT 解析出当前用户角色) JwtUserDetails userDetails = jwtTokenUtil.parseToken(token); if (!userDetails.hasRole("HR") && !userDetails.getUserId().equals(employeeId)) { throw new AccessDeniedException("无权查看他人薪资"); } // 2. 查询原始薪资数据(数据库存的是密文!) Salary salary = salaryRepository.findByEmployeeId(employeeId); // 3. 在 Service 层解密(Controller 只负责传输,不碰密钥) SalaryResponse response = new SalaryResponse(); response.setBaseSalary(aesUtil.decrypt(salary.getBaseSalaryCipher())); response.setBonus(aesUtil.decrypt(salary.getBonusCipher())); response.setTotal(aesUtil.decrypt(salary.getTotalCipher())); return response; } }参数说明:
AesUtil使用 GCM 模式,自带完整性校验,防止密文被篡改;salary.getBaseSalaryCipher()字段在数据库中类型为VARCHAR(255),存储 Base64 编码后的密文;jwtTokenUtil.parseToken()从请求头提取 JWT 并验证签名,确保userDetails可信。若把解密逻辑放在 Controller,会导致权限校验和解密耦合,违反单一职责原则,且密钥管理失控。
3. Vue 前端模块的工程化落地:从报告功能点到可运行组件
3.1 员工签到管理:如何用 Vue Composition API 实现高复用打卡组件?
开题报告第 3 页“员工签到管理”要求支持“新增、修改、删除”,但真实场景中需处理 GPS 定位、网络状态、离线缓存。报告未提技术细节,但落地必须考虑:
<template> <div class="check-in-card"> <div class="status-indicator" :class="{ 'online': isOnline }"> {{ isOnline ? '在线' : '离线' }} </div> <button @click="handleCheckIn" :disabled="!isOnline || isChecking" class="check-in-btn" > {{ isChecking ? '打卡中...' : '立即打卡' }} </button> <div v-if="lastRecord" class="last-record"> 上次打卡:{{ formatTime(lastRecord.checkInTime) }} | 位置:{{ lastRecord.locationName || '未知' }} </div> </div> </template> <script setup> import { ref, onMounted, watch } from 'vue' import { useGeolocation, useNetwork } from '@vueuse/core' import { checkInApi } from '@/api/attendance' const isOnline = ref(true) const isChecking = ref(false) const lastRecord = ref(null) // VueUse 提供的地理位置 Hook(比原生 navigator.geolocation 更稳定) const { coords, isSupported, error } = useGeolocation({ enableHighAccuracy: true, timeout: 10000 }) // 网络状态监听 const { isOnline: onlineStatus } = useNetwork() watch(onlineStatus, (newVal) => { isOnline.value = newVal }) // 获取上次打卡记录(用于显示) const fetchLastRecord = async () => { try { const res = await checkInApi.getLastRecord() lastRecord.value = res.data } catch (e) { console.warn('获取上次记录失败', e) } } // 打卡主逻辑 const handleCheckIn = async () => { if (!isOnline.value) { ElMessage.warning('请检查网络连接') return } if (!coords.value.latitude || !coords.value.longitude) { ElMessage.error('定位失败,请开启 GPS') return } isChecking.value = true try { const payload = { latitude: coords.value.latitude, longitude: coords.value.longitude, accuracy: coords.value.accuracy, timestamp: Date.now(), // 关键:离线时存 localStorage,上线后自动同步 offline: !isOnline.value } const res = await checkInApi.submit(payload) lastRecord.value = res.data ElMessage.success('打卡成功!') } catch (e) { ElMessage.error('打卡失败:' + e.response?.data?.message || '网络错误') } finally { isChecking.value = false } } onMounted(() => { fetchLastRecord() }) </script>逻辑说明:
useGeolocation封装了定位超时、权限拒绝等异常,避免手写navigator.geolocation.getCurrentPosition的回调地狱;offline: !isOnline.value字段为后续离线同步埋点;ElMessage是 Element Plus 的提示组件,符合报告中“用户体验友好”的要求。若忽略useNetwork监听,用户在地铁里打卡会直接报错,而非优雅提示“离线已缓存”。
3.2 菜单管理:动态路由如何与后端权限树结构对齐?
报告第 3 页“菜单管理”模块需支撑“新增、修改、删除”,但前端核心是动态渲染权限菜单。开题报告强调“模块化设计”,这要求 Vue Router 的路由配置与后端SysMenu实体完全一致:
// router/index.js import { createRouter, createWebHistory } from 'vue-router' import Layout from '@/layout/Layout.vue' // 后端返回的菜单数据结构示例: // [ // { id: 1, name: '员工管理', path: '/employee', component: 'EmployeeList', parentId: 0 }, // { id: 2, name: '档案管理', path: '/archive', component: 'ArchiveList', parentId: 1 } // ] export function generateRoutes(menuList) { const routes = [] // 递归构建路由(匹配后端树形结构) const buildRoute = (menuItems, parentPath = '') => { menuItems.forEach(item => { // 跳过无组件名的目录节点(如“员工管理”是目录,不直接渲染页面) if (!item.component) return const route = { path: parentPath + item.path, name: item.name, component: () => import(`@/views/${item.component}.vue`), meta: { title: item.name, icon: item.icon || 'el-icon-menu', // 关键:权限标识与后端 menu_code 字段对齐 permission: item.permissionCode || item.id.toString() } } // 子菜单递归添加 const children = menuItems.filter(m => m.parentId === item.id) if (children.length > 0) { route.children = buildRoute(children, route.path) } routes.push(route) }) } buildRoute(menuList) return routes } const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue') } ] } ] }) // 动态添加路由(登录后调用) export function addDynamicRoutes(menuList) { const dynamicRoutes = generateRoutes(menuList) dynamicRoutes.forEach(route => { router.addRoute('Layout', route) // 添加到 Layout 的 children 下 }) } export default router参数说明:
generateRoutes()函数严格按后端parentId字段构建嵌套关系,确保“员工管理→档案管理”层级正确;permissionCode字段与后端SysMenu.permission_code对应,用于按钮级权限控制(如“删除”按钮是否显示);router.addRoute()动态注册,避免打包时所有路由都加载。若用静态路由,会导致未授权用户通过 URL 直接访问/archive/delete。
3.3 公告管理:富文本编辑器如何规避 XSS 攻击?
报告第 3 页“公告管理”需支持图文混排,但新手常直接用v-html渲染,埋下 XSS 隐患。开题报告虽未提安全细节,但“数据安全”是贯穿全文的红线:
<template> <div class="notice-editor"> <!-- 使用 Quill 的 Vue 封装版,禁用危险格式 --> <quill-editor v-model:content="form.content" theme="snow" :modules="editorModules" @change="onContentChange" /> <div class="preview-section"> <h3>预览效果:</h3> <!-- 关键:用 DOMPurify 过滤 HTML,再用 v-html 渲染 --> <div v-html="sanitizedContent"></div> </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' import { QuillEditor } from '@vueup/vue-quill' import DOMPurify from 'dompurify' const form = ref({ content: '' }) const sanitizedContent = computed(() => { // 白名单策略:只允许 p, h1-h3, ul, ol, li, strong, em, img, a 标签 const clean = DOMPurify.sanitize(form.value.content, { ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'strong', 'em', 'img', 'a'], ALLOWED_ATTR: ['href', 'src', 'alt', 'title'], FORBID_TAGS: ['script', 'iframe', 'object', 'embed'], FORBID_ATTR: ['onerror', 'onload', 'onclick'] }) return clean }) // Quill 配置:禁用危险模块 const editorModules = { toolbar: [ [{ 'header': [1, 2, 3, false] }], ['bold', 'italic', 'underline'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], ['link', 'image'], ['clean'] // 清除格式按钮 ], // 禁用图片上传(由后端提供 CDN 地址,前端只插入 img 标签) imageResize: false, imageDrop: false } const onContentChange = (content) => { form.value.content = content } </script>逻辑说明:
DOMPurify.sanitize()在渲染前过滤所有 script、iframe 标签,即使用户粘贴恶意代码也无法执行;ALLOWED_ATTR限制href和src只能是相对路径或白名单域名;imageResize: false防止前端上传图片,强制走后端文件服务。若跳过此步,攻击者可在公告中插入<script>alert('xss')</script>,所有查看公告的用户都会触发。
4. 前后端联调避坑指南:那些开题报告不会写但会让你通宵的 5 个致命问题
4.1 现象:Vue 页面显示空白,控制台报Failed to fetch
原因:开题报告未提跨域配置,但开发时 SpringBoot 默认禁止跨域。Vue 开发服务器(http://localhost:8080)调用 SpringBoot(http://localhost:8081)时,浏览器拦截 OPTIONS 预检请求。
解决:在 SpringBoot 的WebMvcConfigurer中添加全局跨域配置,而非仅加@CrossOrigin注解(后者不生效于@RequestBody):
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 仅开放 api 路径 .allowedOrigins("http://localhost:8080") // 明确指定前端地址 .allowCredentials(true) // 允许携带 cookie(JWT 需要) .maxAge(3600) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); } }4.2 现象:员工列表分页显示 10 条,但点击第 2 页返回空数组
原因:开题报告说“考勤记录查询”,但未明确分页参数命名。Vue 传page=2&size=10,而 SpringBoot 的@RequestParam默认接收pageNum和pageSize(MyBatis-Plus 分页插件约定)。
解决:统一参数名,在 Controller 中显式映射:
@GetMapping("/employees") public Result<List<Employee>> listEmployees( @RequestParam(defaultValue = "1") Integer page, // Vue 传 page @RequestParam(defaultValue = "10") Integer size) { // Vue 传 size Page<Employee> pageObj = new Page<>(page, size); Page<Employee> result = employeeService.page(pageObj); return Result.success(result.getRecords()); }4.3 现象:请假审批时,后端收到的status是字符串"1"而非整数1
原因:Vue 的axios默认将所有 URL 参数转为字符串,而 SpringBoot 的@RequestParam Integer status无法自动转换字符串"1"。
解决:在 SpringBoot 配置中启用字符串转数字:
# application.yml spring: mvc: conversion: service: convert-string-to-number: true # 启用字符串转数字或在 Controller 中用@RequestParam String status,手动Integer.parseInt(status)。
4.4 现象:微信小程序扫码打卡,后端@RequestBody接收不到数据,日志显示Required request body is missing
原因:开题报告提“支持微信小程序”,但未说明小程序请求头Content-Type是application/json,而 SpringBoot 默认只解析application/json,若小程序误设为text/plain会失败。
解决:在 SpringBoot 配置中扩展支持类型:
@Configuration public class WebConfig { @Bean public HttpMessageConverters messageConverters() { List<HttpMessageConverter<?>> converters = new ArrayList<>(); converters.add(new MappingJackson2HttpMessageConverter()); // 保持 JSON 支持 // 添加 text/plain 支持(兼容小程序可能的错误 header) converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); return new HttpMessageConverters(converters); } }4.5 现象:部署到服务器后,Vue 路由刷新 404,但本地开发正常
原因:开题报告未提部署方案。Vue Router 的history模式依赖服务器重写,Nginx 未配置时,访问/employee会直接找服务器文件,而非 fallback 到index.html。
解决:Nginx 配置增加重写规则:
location / { try_files $uri $uri/ /index.html; } # 同时确保 API 请求转发到 SpringBoot location /api/ { proxy_pass http://localhost:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }5. 数据库设计与性能优化:从开题报告的“功能模块”到生产级 SQL 实践
5.1 考勤记录表:为什么attendance_record必须用复合索引而非单列索引?
开题报告第 3 页“员工签到管理”要求高频查询“某员工某月打卡记录”,若只对employee_id建索引,查询WHERE employee_id = 1001 AND check_in_time BETWEEN '2025-03-01' AND '2025-03-31'仍会扫描大量无效行。真实优化需复合索引:
-- 创建复合索引:先 employee_id(等值查询),再 check_in_time(范围查询) CREATE INDEX idx_emp_time ON attendance_record(employee_id, check_in_time); -- 验证索引是否生效(EXPLAIN 结果中 type 应为 'range',key 显示索引名) EXPLAIN SELECT * FROM attendance_record WHERE employee_id = 1001 AND check_in_time >= '2025-03-01' AND check_in_time < '2025-04-01';参数说明:索引顺序必须是
employee_id, check_in_time,若反过来则employee_id变成范围查询,索引失效;check_in_time < '2025-04-01'比<= '2025-03-31 23:59:59'更高效,避免时间精度问题。
5.2 请假申请表:如何用数据库约束替代 Java 代码校验?
报告中“请假申请审批”需校验“开始时间不能晚于结束时间”,新手常在 Service 层写if (start.after(end)) throw...,但数据库层面更可靠:
CREATE TABLE leave_application ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 关键:数据库 CHECK 约束,杜绝脏数据入库 CONSTRAINT chk_time_order CHECK (start_time <= end_time), -- 外键关联员工表 CONSTRAINT fk_employee FOREIGN KEY (employee_id) REFERENCES employee(id) );逻辑说明:
CHECK (start_time <= end_time)在 INSERT/UPDATE 时由 MySQL 强制校验,即使绕过 Java 层直接 SQL 操作也会拦截;FOREIGN KEY确保employee_id必须存在,避免孤儿数据。若只靠 Java 校验,测试时可能漏掉并发场景(两个请求同时提交,都通过校验但数据库冲突)。
5.3 菜单管理表:树形结构查询为何要用闭包表而非递归 CTE?
开题报告第 3 页“菜单管理”需支持无限级菜单,MySQL 5.7 不支持递归 CTE,必须用闭包表(Closure Table):
-- 菜单主表 CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, path VARCHAR(200), component VARCHAR(100), sort_order INT DEFAULT 0 ); -- 闭包表:记录所有祖先-后代关系 CREATE TABLE menu_closure ( ancestor BIGINT NOT NULL, -- 祖先菜单 ID descendant BIGINT NOT NULL, -- 后代菜单 ID depth INT NOT NULL, -- 深度(0 表示自身) PRIMARY KEY (ancestor, descendant), FOREIGN KEY (ancestor) REFERENCES sys_menu(id), FOREIGN KEY (descendant) REFERENCES sys_menu(id) ); -- 插入根菜单时,自关联(depth=0) INSERT INTO menu_closure VALUES (1, 1, 0); -- 插入子菜单时,需插入多行:自身、父节点、祖父节点... -- 例如菜单 2 的父节点是 1,则插入 (1,2,1), (2,2,0)参数说明:查询某菜单的所有子菜单只需
SELECT * FROM menu_closure WHERE ancestor = 1;查询某菜单的全部祖先用SELECT * FROM menu_closure WHERE descendant = 2 AND depth > 0;depth字段便于前端渲染缩进。若用parentId递归查询,MySQL 5.7 需写存储过程,复杂且慢。
6. 毕业答辩前的终极验证:用三步法把开题报告变成答辩底气
6.1 第一步:用 Postman 验证所有 API 是否符合开题报告功能描述
开题报告第 3 页列出 8 个功能模块,每个模块至少对应 3 个 API。答辩前必须逐个验证,而非只测“能跑通”。以“员工请假管理”为例:
| API 路径 | HTTP 方法 | 预期状态码 | 验证点 | 报告依据 |
|---|---|---|---|---|
/api/leave/applications | GET | 200 | 返回 JSON 数组,含id,employeeName,startTime,status字段 | 报告“员工请假管理:查询” |
/api/leave/applications | POST | 201 | 请求体含employeeId,startTime,endTime,响应含id | 报告“员工请假管理:新增” |
/api/leave/applications/{id}/status | PUT | 200 | 请求体{ "status": 3 },响应后 GET 查看status变为 3 | 报告“员工请假管理:审批” |
操作说明:在 Postman 中保存为 Collection,每个请求加
Tests脚本自动断言:
// 验证返回字段完整 pm.test("Response has required fields", function () { var jsonData = pm.response.json(); pm.expect(jsonData).to.have.property('id'); pm.expect(jsonData).to.have.property('employeeName'); });若某 API 返回500或字段缺失,立刻定位是 Controller 未加@ResponseBody、Service 层抛异常未捕获,还是 MyBatis 的resultMap字段名不匹配。
6.2 第二步:用 Chrome DevTools 的 Network 面板抓包,确认 Vue 组件与后端交互无冗余请求
开题报告强调“用户体验友好”,但新手常因v-for中写axios.get导致 N+1 请求。以“员工管理”页面为例,正确做法是:
- 一次请求获取员工列表:
GET /api/employees?page=1&size=10 - 在 Vue 组件中用
computed计算关联数据,而非为每个员工发请求:
<template> <div v-for="emp in employees" :key="emp.id"> {{ emp.name }} - {{ emp.departmentName }} <!-- departmentName 来自员工对象,非额外请求 --> </div> </template> <script setup> import { computed } from 'vue' import { useEmployeeStore } from '@/stores/employee' const store = useEmployeeStore() // store.employees 包含所有字段,departmentName 已在后端 JOIN 查询中返回 const employees = computed(() => store.employees) </script>验证方法:打开 Chrome DevTools → Network → 刷新页面,筛选
XHR,观察员工列表页是否只有 1 个/api/employees请求。若出现GET /api/departments/1、GET /api/departments/2等重复请求,说明前端在循环中调用 API,必须重构为后端 JOIN 查询。
6.3 第三步:用 JMeter 模拟 50 并发用户,验证关键接口响应时间 ≤ 800ms
开题报告虽未提性能指标,但答辩老师必问“系统能支撑多少人”。用 JMeter 做基础压测:
- 创建线程组:线程数 50,Ramp-Up 时间 10 秒(每秒 5 用户)
- 添加 HTTP 请求:目标
GET /api/attendance/today(今日考勤统计) - 添加聚合报告:关注
90% Line(90% 请求的响应时间)
若90% Line > 800ms,按以下顺序排查:
- 数据库:
EXPLAIN该 SQL,看是否用到索引; - JVM:
jstat -gc <pid>查看 Young GC 频率,过高则调大-Xmx; - 连接池:HikariCP 的
maximumPoolSize是否 ≥ 50(默认 10,必然瓶颈)。
血泪经验:某同学答辩前未压测,老师现场问“100 人同时打卡怎么办”,他答“应该没问题”,结果演示时 20 人就超时。从那以后我每次交付前,都强制走一遍 JMeter 50 并发 —— 不为追求极致性能,只为答辩时能指着聚合报告说:“老师,这是实测数据,90% 请求在 620ms 内完成。”
希望帮到你。
本文还有配套的精品资源,点击获取