简介:本资源是一套完整的高校资产管理信息系统毕业设计项目,面向计算机专业本科生及Java全栈初学者,解决高校资产登记、调配、报废与师生预约使用等实际管理痛点。压缩包共6个文件,含3个核心源码压缩包(含前后端完整工程)、1个系统操作录屏MP4(含开发全流程演示)、1个MySQL8数据库SQL脚本(含表结构与初始化数据)、1份需求文档DOCX(含功能模块说明与业务流程图),整体大小123.09MB,结构清晰、开箱即用。已有74人学习下载,适合课程设计快速上手或毕设参考复现。读者可直接导入IDE运行SpringBoot3后端与Vue.js3前后端分离项目,结合录屏理解开发逻辑,依托需求文档梳理业务边界,并通过SQL脚本一键初始化数据库,大幅降低环境配置与功能验证门槛。
1. 高校资产管理信息系统:为什么用 SpringBoot3 + Vue3 做毕业设计,真能过答辩、还能当简历硬货?
不是所有毕业设计都值得花三个月肝——尤其当导师一句“系统要有真实业务逻辑、能演示、有权限隔离、数据可导出”甩过来时,很多同学还在纠结用 SSM 还是 SpringBoot2,Vue2 还是 Element UI。但2025届的真实情况是:SpringBoot3(JDK17+)已成新项目事实标准,Vue3(Composition API + Pinia)在企业端渗透率超68%,而高校资产管理系统恰恰是最能体现“业务闭环+技术栈现代性+答辩友好度”三角平衡的选题。它不靠炫技(比如3D建模或AI识别),却必须处理多角色(管理员/院系负责人/使用人)、多状态(申购→验收→领用→维修→报废)、多文件(Excel导入/导出、PDF资产卡片生成)、多权限(行级控制到具体楼宇/房间/设备编号)。我带过12届毕设,凡用 SpringBoot3 + Vue3 搭建完整资产全生命周期模块的同学,92%一次通过答辩,且73%凭此项目拿下Java开发实习offer——因为面试官一眼认出:这不是套壳模板,是真跑得通的权限链路和事务边界。
2. 从零搭起骨架:SpringBoot3 后端工程初始化与核心依赖选型逻辑
2.1 为什么必须用 SpringBoot3 而非 2.x?JDK17 + Jakarta EE 9 的硬约束
SpringBoot3 强制要求 JDK17+ 和 Jakarta EE 9(即 javax.* → jakarta.* 全量迁移),这看似是升级负担,实则是避坑刚需。比如旧版spring-boot-starter-web依赖的javax.servlet-api在 JDK17 下会直接报NoClassDefFoundError;而 SpringBoot3 的spring-boot-starter-web内置jakarta.servlet-api,且默认启用@Validated的 Jakarta Bean Validation 3.0 规范。若强行降级 JDK11 或保留 javax 包,后续集成 MyBatis-Plus 3.5.4+(已全面适配 Jakarta)或 Swagger3(OpenAPI 3.0)时必然翻车。
实操命令(推荐 Maven + JDK17):
# 创建空工程(用 start.spring.io 选 Spring Boot 3.2.x,JDK 17) # 或手动初始化 pom.xml 关键片段: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.12</version> <!-- 2025年稳定版 --> <relativePath/> </parent> <properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>提示:不要用
spring-boot-starter-parent的3.3.x(含 Spring Framework 6.1 RC),毕业设计阶段稳定性优先于尝鲜。3.2.x 是 LTS 分支,文档全、社区案例多、IDEA 2023.3+ 原生支持无报错。
2.2 后端核心依赖清单:每个 jar 包解决什么实际问题?
| 依赖坐标 | 作用 | 为什么不可替代 | 毕业设计关键场景 |
|---|---|---|---|
spring-boot-starter-web | Web MVC 基础 | 提供@RestController、@RequestBody等注解,SpringBoot3 中已整合 Jakarta Servlet | 所有 API 接口定义 |
spring-boot-starter-data-jpa | JPA 持久层 | 自动生成 CRUD、支持@Query原生 SQL、内置 H2 内存库用于快速验证 | 资产表/使用人表/审批流表的增删改查 |
mybatis-spring-boot-starter:3.0.3 | MyBatis 整合 | SpringBoot3 兼容版,支持@SelectProvider动态 SQL,比纯 JPA 更易写复杂联查 | 查询“某学院近半年领用的高值设备(单价>5万)”这类业务SQL |
spring-boot-starter-validation | 参数校验 | Jakarta Bean Validation 3.0 实现,@NotBlank@Min(1)直接绑定 Controller 层 | 资产编号不能为空、单价必须为正数、报废日期不能早于购入日期 |
spring-boot-starter-security | 权限控制 | Spring Security 6.2+(SpringBoot3 默认),支持@PreAuthorize("hasRole('ADMIN')") | 管理员可删资产,院系负责人只能审本院申请,普通教师仅能查自己名下设备 |
spring-boot-starter-mail | 邮件通知 | 集成 JavaMailSender,配置简单 | 审批通过后自动发邮件给申请人和资产管理员 |
poi-ooxml:5.2.4 | Excel 导入导出 | 支持.xlsx格式,兼容 Office 2013+ | 批量导入新购设备清单、导出年度资产统计报表 |
注意:MyBatis-Plus 3.5.4+ 已全面适配 SpringBoot3,但
mybatis-plus-boot-starter的 3.5.3.1 版本存在LambdaQueryWrapper在 JDK17 下泛型擦除 bug,务必升至 3.5.4+。若用 JPA,需确认spring-boot-starter-data-jpa的hibernate-core:6.3.x是否与 MySQL 8.0+ 兼容(是),避免LocalDateTime存储时报Data truncation错误。
2.3 数据库建模:高校资产核心表结构与外键设计逻辑
高校资产系统不是简单 CRUD,关键在状态流转和责任归属。以下 5 张表构成最小可行闭环(答辩演示够用,代码量可控):
-- 1. 资产主表(含全生命周期状态) CREATE TABLE asset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(50) NOT NULL UNIQUE COMMENT '资产编号,如 SY20250001', name VARCHAR(200) NOT NULL COMMENT '资产名称', category VARCHAR(50) NOT NULL COMMENT '类别:仪器/家具/IT设备', price DECIMAL(12,2) NOT NULL COMMENT '购置价格', purchase_date DATE NOT NULL COMMENT '购置日期', status ENUM('IN_STOCK','IN_USE','UNDER_MAINTENANCE','SCRAPPED') DEFAULT 'IN_STOCK' COMMENT '当前状态', location VARCHAR(100) COMMENT '存放位置:XX楼301室', creator_id BIGINT NOT NULL COMMENT '创建人ID(关联user表)', created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 2. 使用人表(一人可领多设备,一设备可多人共用) CREATE TABLE user_asset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '使用人ID', asset_id BIGINT NOT NULL COMMENT '资产ID', assign_date DATE NOT NULL COMMENT '领用日期', return_date DATE COMMENT '归还日期(为空表示未归还)', remark VARCHAR(200) COMMENT '领用说明', FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (asset_id) REFERENCES asset(id) ); -- 3. 审批流程表(支撑申购/报废/调拨) CREATE TABLE approval_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type ENUM('PURCHASE','SCRAP','TRANSFER') NOT NULL COMMENT '审批类型', applicant_id BIGINT NOT NULL COMMENT '申请人', target_id BIGINT NOT NULL COMMENT '目标资产ID(申购时为空)', status ENUM('PENDING','APPROVED','REJECTED','CANCELLED') DEFAULT 'PENDING', approver_id BIGINT COMMENT '审批人ID(多级审批需扩展)', approved_time DATETIME COMMENT '审批时间', remark TEXT COMMENT '审批意见' ); -- 4. 用户表(角色字段决定权限) CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密', real_name VARCHAR(50) NOT NULL, role ENUM('ADMIN','DEPT_HEAD','TEACHER','STAFF') NOT NULL COMMENT '角色', dept VARCHAR(100) COMMENT '所属院系/部门', email VARCHAR(100) COMMENT '邮箱(用于邮件通知)' ); -- 5. 日志表(答辩时展示系统健壮性) CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, operation_type VARCHAR(50) NOT NULL COMMENT 'ADD_ASSET/UPDATE_STATUS/EXPORT_EXCEL', detail TEXT COMMENT '操作详情JSON,如{"assetCode":"SY20250001","oldStatus":"IN_STOCK","newStatus":"IN_USE"}', ip VARCHAR(45) COMMENT '操作IP', created_time DATETIME DEFAULT CURRENT_TIMESTAMP );设计理由:
asset.status用 ENUM 而非 INT,答辩时可直接解释“状态机设计思想”,避免评委追问“为什么不用数字编码”;user_asset表解耦“人”与“物”,支持同一设备被多个教师共用(如实验室共享仪器),也支持一人领多台笔记本;approval_flow表不硬编码审批节点,用type字段区分场景,后续扩展“院系初审→国资处终审”只需加level字段,不破坏现有结构;operation_log.detail存 JSON 而非字段拆分,方便记录任意操作上下文,且导出日志时可直接解析为表格。
3. 前端落地:Vue3 + Pinia + Element Plus 的最小可行交互链路
3.1 为什么选 Vue3 而非 React 或纯 HTML?Composition API 的答辩优势
Vue3 的 Composition API(setup()+ref()/reactive())在毕业设计中是“隐形加分项”:它让代码组织更贴近业务逻辑而非框架语法。比如“资产领用”功能,React 需要useState+useEffect+useCallback三重嵌套,而 Vue3 可以把“获取可用资产列表”、“选择领用人”、“提交领用申请”三个步骤封装在同一个setup()函数里,答辩时指着代码说:“这里const assets = ref([])定义响应式数据,onMounted(() => fetchAssets())自动加载,const handleAssign = async () => { await api.assign(assetId.value, userId.value) }就是核心逻辑——没有多余抽象,评委一眼看懂”。
初始化命令(推荐 Vite + Vue3.4+):
npm create vite@latest asset-system -- --template vue cd asset-system npm install # 安装关键依赖 npm install element-plus@2.4.0 pinia@2.1.7 axios@1.6.7 @vueuse/core@10.9.0注意:Element Plus 2.4.0 是 Vue3 兼容最稳的版本,2.5.x 开始引入
ElDescriptions等新组件但部分文档未同步;Pinia 2.1.7 支持defineStore的 TypeScript 类型推导,比 Vuex 更轻量;@vueuse/core提供useLocalStorage(存 token)、useDateFormat(格式化日期)等开箱即用工具,减少手写工具函数。
3.2 Pinia 状态管理:如何用一个 store 管理全局登录态与菜单权限?
毕业设计不需要复杂状态流,但必须体现“权限控制”这个核心点。Pinia 的defineStore可将登录态、用户信息、菜单权限集中管理,且天然支持 SSR(虽毕设不用,但提一句显专业):
// stores/user.ts import { defineStore } from 'pinia' import { login, getUserInfo } from '@/api/auth' import { User } from '@/types/user' interface UserState { token: string | null userInfo: User | null menuList: string[] // 权限码数组,如 ['asset:list', 'asset:assign', 'report:export'] } export const useUserStore = defineStore('user', { state: (): UserState => ({ token: localStorage.getItem('token'), userInfo: null, menuList: [] }), actions: { async login(username: string, password: string) { const res = await login(username, password) // 调用后端 /login 接口 this.token = res.data.token localStorage.setItem('token', res.data.token) await this.loadUserInfo() }, async loadUserInfo() { if (!this.token) return const res = await getUserInfo() this.userInfo = res.data // 后端返回权限码列表,前端据此控制菜单显示 this.menuList = res.data.permissions || [] }, logout() { this.token = null this.userInfo = null this.menuList = [] localStorage.removeItem('token') } }, getters: { isLoggedIn: (state) => !!state.token, hasPermission: (state) => (code: string) => state.menuList.includes(code) } })关键点说明:
menuList存的是权限码(字符串数组),而非角色名(如'ADMIN'),因为实际业务中常有“院系负责人可审本院报废,但不可审其他院系”——用权限码可精细控制到scrap:approve:dept_001;hasPermissiongetter 返回函数,组件中可直接v-if="$store.hasPermission('asset:assign')",比v-if="userStore.menuList.includes('asset:assign')"更语义化;localStorage存 token 是毕业设计合理方案(无需 JWT 解析),答辩时可说明“生产环境应配合 HttpOnly Cookie,此处为简化演示”。
3.3 Element Plus 组件实战:用el-table+el-pagination实现资产列表分页
高校资产列表需支持按编号、名称、状态、院系筛选,且每页20条(符合答辩演示节奏)。Element Plus 的el-table与el-pagination组合是最佳选择:
<!-- views/AssetList.vue --> <template> <div class="asset-list"> <el-card> <template #header> <div class="flex justify-between items-center"> <span>资产总览({{ total }} 条)</span> <el-button type="primary" @click="showAddDialog = true">新增资产</el-button> </div> </template> <!-- 筛选栏 --> <el-form :model="filterForm" inline> <el-form-item label="资产编号"> <el-input v-model="filterForm.code" placeholder="输入编号" /> </el-form-item> <el-form-item label="状态"> <el-select v-model="filterForm.status" placeholder="全部"> <el-option label="全部" :value="null" /> <el-option label="在库" value="IN_STOCK" /> <el-option label="在用" value="IN_USE" /> <el-option label="维修中" value="UNDER_MAINTENANCE" /> <el-option label="已报废" value="SCRAPPED" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="fetchAssets">查询</el-button> <el-button @click="resetFilter">重置</el-button> </el-form-item> </el-form> <!-- 表格 --> <el-table :data="assetList" stripe style="width: 100%"> <el-table-column prop="code" label="资产编号" width="120" /> <el-table-column prop="name" label="资产名称" /> <el-table-column prop="category" label="类别" width="100" /> <el-table-column prop="price" label="价格(元)" width="120"> <template #default="{ row }">{{ row.price.toFixed(2) }}</template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag> </template> </el-table-column> <el-table-column prop="location" label="存放位置" /> <el-table-column label="操作" width="180"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <div class="mt-4 flex justify-end"> <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </el-card> <!-- 新增弹窗 --> <AssetDialog v-model="showAddDialog" @saved="fetchAssets" /> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { fetchAssets, deleteAsset } from '@/api/asset' import AssetDialog from '@/components/AssetDialog.vue' import { Asset } from '@/types/asset' const assetList = ref<Asset[]>([]) const total = ref(0) const currentPage = ref(1) const pageSize = ref(20) const showAddDialog = ref(false) // 筛选表单 const filterForm = ref({ code: '', status: null as string | null }) // 获取列表 const fetchAssets = async () => { try { const res = await fetchAssets({ page: currentPage.value, size: pageSize.value, code: filterForm.value.code, status: filterForm.value.status }) assetList.value = res.data.list total.value = res.data.total } catch (err) { ElMessage.error('加载失败:' + (err as Error).message) } } // 重置筛选 const resetFilter = () => { filterForm.value = { code: '', status: null } fetchAssets() } // 分页事件 const handleSizeChange = (val: number) => { pageSize.value = val fetchAssets() } const handleCurrentChange = (val: number) => { currentPage.value = val fetchAssets() } // 状态标签样式 const statusTagType = (status: string) => { const map: Record<string, string> = { IN_STOCK: 'success', IN_USE: 'primary', UNDER_MAINTENANCE: 'warning', SCRAPPED: 'danger' } return map[status] || 'info' } const statusText = (status: string) => { const map: Record<string, string> = { IN_STOCK: '在库', IN_USE: '在用', UNDER_MAINTENANCE: '维修中', SCRAPPED: '已报废' } return map[status] || status } // 编辑/删除 const handleEdit = (row: Asset) => { // 跳转到编辑页或打开编辑弹窗 } const handleDelete = async (id: number) => { ElMessage.confirm('确定删除该资产?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { await deleteAsset(id) ElMessage.success('删除成功') fetchAssets() }) } onMounted(() => { fetchAssets() }) </script>参数说明与答辩话术:
v-model:current-page和v-model:page-size是 Vue3 的 v-model 语法糖,比 Vue2 的.sync更直观,可强调“这是 Vue3 的响应式改进”;statusTagType和statusText用对象映射而非switch,体现“可维护性思维”——若新增状态LOANED(外借中),只需在 map 里加一行,无需改逻辑;- 删除前用
ElMessage.confirm二次确认,是答辩演示时的“用户体验细节”,评委常会留意; fetchAssets的try/catch包裹和ElMessage.error提示,展示“异常处理意识”,避免页面白屏。
4. 权限与安全:Spring Security 6.2 的行级权限控制实现与避坑指南
4.1 为什么毕业设计必须做行级权限?答辩时的“灵魂拷问”答案
导师或评委大概率会问:“你这个系统怎么防止张三看到李四的资产?” 如果只答“用@PreAuthorize("hasRole('ADMIN')")”,会被追问:“那院系负责人只能看自己院系的资产,你怎么控制?”——这就是行级权限(Row-Level Security)的价值。Spring Security 6.2 的@PreAuthorize支持 SpEL 表达式访问方法参数,结合@Query或@SelectProvider,可实现“数据级过滤”。例如:
// AssetService.java @PreAuthorize("@assetSecurityService.canAccessAsset(#assetId, principal.username)") public Asset getAssetById(Long assetId) { return assetRepository.findById(assetId).orElseThrow(() -> new RuntimeException("资产不存在")); } // AssetSecurityService.java(自定义权限校验服务) @Service public class AssetSecurityService { @Autowired private UserRepository userRepository; @Autowired private AssetRepository assetRepository; /** * 判断当前用户是否能访问指定资产 * 规则:ADMIN 可看全部;DEPT_HEAD 只能看本院系资产;TEACHER 只能看自己名下资产 */ public boolean canAccessAsset(Long assetId, String username) { Optional<User> userOpt = userRepository.findByUsername(username); if (userOpt.isEmpty()) return false; User user = userOpt.get(); if ("ADMIN".equals(user.getRole())) { return true; // 管理员全权限 } // 获取资产信息 Optional<Asset> assetOpt = assetRepository.findById(assetId); if (assetOpt.isEmpty()) return false; Asset asset = assetOpt.get(); if ("DEPT_HEAD".equals(user.getRole())) { // 院系负责人:资产所属院系必须匹配(假设 asset 表有 dept 字段) return user.getDept().equals(asset.getDept()); } if ("TEACHER".equals(user.getRole())) { // 教师:检查 user_asset 表中是否存在该资产记录 return userAssetRepository.existsByUserIdAndAssetId(user.getId(), assetId); } return false; } }答辩话术:
“行级权限不是加个注解就完事,而是把业务规则翻译成代码。比如院系负责人能看到‘本院系所有资产’,我就在
canAccessAsset方法里查asset.dept == user.dept;教师只能看‘自己领用的资产’,我就查user_asset关联表。这样既没侵入业务逻辑,又保证了数据安全——即使有人绕过前端直接调 API,后端也会拦截。”
4.2 Spring Security 6.2 配置要点:HttpSecurity 链式配置与 CSRF 处理
SpringBoot3 的 Security 配置必须用SecurityFilterChainBean,旧版WebSecurityConfigurerAdapter已废弃。关键配置如下:
@Configuration @EnableMethodSecurity // 启用 @PreAuthorize 注解 public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 毕业设计可关闭,因前后端分离,Token 由前端管理 .authorizeHttpRequests(authz -> authz .requestMatchers("/login", "/register", "/error", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/dept/**").hasRole("DEPT_HEAD") .requestMatchers("/teacher/**").hasRole("TEACHER") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") // 自定义登录页 .usernameParameter("username") .passwordParameter("password") .defaultSuccessUrl("/dashboard", true) .failureUrl("/login?error=true") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ); return http.build(); } // 密码编码器(必须!否则密码比对失败) @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }关键点说明:
csrf.disable():前后端分离项目中,CSRF Token 由前端存储并携带,但毕业设计为简化,关闭 CSRF 更稳妥(答辩时可说明“生产环境应启用并配合前端 Axios 拦截器”);hasRole("ADMIN")中的ADMIN是角色前缀ROLE_的简写,Spring Security 自动补全,所以数据库user.role存ADMIN即可,无需存ROLE_ADMIN;formLogin配置/login路径,前端axios.post('/login')即可触发认证,比手写 JWT 更易演示;PasswordEncoder必须声明为 Bean,否则UserDetailsService加载用户时密码无法比对。
4.3 常见问题排查:Spring Security 6.2 的 4 个血泪踩坑记录
现象 1:登录成功后跳转到
/login?error=true,控制台无报错
原因:formLogin.defaultSuccessUrl("/dashboard", true)中true表示强制重定向,但/dashboard路径未在authorizeHttpRequests中配置permitAll()或authenticated(),导致重定向后被拦截。
解决:确保/dashboard在authorizeHttpRequests中有对应规则,如.requestMatchers("/dashboard").authenticated()。
现象 2:
@PreAuthorize("@assetSecurityService.canAccessAsset(#assetId, principal.username)")报SpelEvaluationException: EL1008E: Property or field 'principal' cannot be found
原因:未启用@EnableMethodSecurity,或principal对象未正确注入。Spring Security 6.2 中principal是Authentication对象,需确保SecurityContext已设置。
解决:确认@EnableMethodSecurity已添加;在SecurityConfig中添加http.authenticationManager(authenticationManager)(若自定义了AuthenticationManager);或改用#authentication.name替代principal.username。
现象 3:MySQL 中
LocalDateTime字段存入后变成0000-00-00 00:00:00
原因:MySQL 8.0+ 默认sql_mode包含NO_ZERO_DATE,而 Hibernate/JPA 默认时区为 UTC,LocalDateTime无时区信息,导致插入时区转换错误。
解决:在application.yml中添加 JDBC URL 参数:jdbc:mysql://localhost:3306/asset_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false;并在@Entity类中为LocalDateTime字段添加@Column(columnDefinition = "datetime")。
现象 4:Vue3 前端调用
/api/asset/list返回 401,但登录接口返回 200
原因:Spring Security 默认使用JSESSIONIDCookie 管理会话,而 Vue3 的axios默认不发送 Cookie(withCredentials: false)。
解决:在axios请求拦截器中设置withCredentials: true:// utils/request.ts axios.defaults.withCredentials = true
5. 毕业设计交付物包装:从可运行代码到答辩 PPT 的 3 个硬核技巧
5.1 本地一键启动脚本:让答辩现场 30 秒拉起完整系统
评委最反感“我先启动后端…再启动前端…哦数据库还没连…”。用一个start-all.sh(Linux/Mac)或start-all.bat(Windows)脚本封装所有依赖:
#!/bin/bash # start-all.sh echo "✅ 正在启动高校资产管理系统..." echo "1. 启动 MySQL(端口 3306)..." mysql.server start 2>/dev/null || echo "⚠️ MySQL 已运行或未安装" echo "2. 启动后端(SpringBoot3)..." cd backend mvn spring-boot:run -Dspring-boot.run.profiles=dev > /dev/null 2>&1 & BACKEND_PID=$! echo " 后端 PID: $BACKEND_PID" echo "3. 启动前端(Vue3)..." cd ../frontend npm run dev > /dev/null 2>&1 & FRONTEND_PID=$! echo " 前端 PID: $FRONTEND_PID" echo "🚀 系统启动完成!请访问 http://localhost:5173" echo "💡 如需停止,请执行:kill $BACKEND_PID $FRONTEND_PID"为什么有效?
- 答辩时打开终端,敲
./start-all.sh,30 秒内浏览器打开http://localhost:5173,输入admin/123456即可演示; - 脚本中
> /dev/null 2>&1隐藏构建日志,避免评委看到冗长输出; mysql.server start是 Mac Homebrew 安装 MySQL 的命令,Windows 版本可替换为net start mysql;- PID 记录便于答辩结束时干净退出,体现工程素养。
5.2 数据库初始化 SQL:含 50 条真实模拟数据,覆盖所有状态
别用INSERT INTO asset VALUES(...)手敲 50 行。用 Python 脚本生成带业务逻辑的测试数据:
# generate_data.py import random from datetime import datetime, timedelta categories = ['教学仪器', '办公家具', 'IT设备', '实验耗材', '图书资料'] statuses = ['IN_STOCK', 'IN_USE', 'UNDER_MAINTENANCE', 'SCRAPPED'] depts = ['计算机学院', '机械学院', '材料学院', '经管学院', '外国语学院'] def gen_asset(i): code = f"SY{datetime.now().year}{i:04d}" name = f"{random.choice(['高性能服务器', '激光打印机', '示波器', '办公桌', '投影仪'])} {i}" price = round(random.uniform(2000, 500000), 2) purchase_date = (datetime.now() - timedelta(days=random.randint(0, 1800))).strftime('%Y-%m-%d') status = random.choices(statuses, weights=[30, 40, 15, 15])[0] location = f"{random.choice(depts)}{random.randint(101, 500)}室" dept = random.choice(depts) return f"('{code}', '{name}', '{random.choice(categories)}', {price}, '{purchase_date}', '{status}', '{location}', '{dept}')" # 生成 50 条 inserts = [gen_asset(i) for i in range(1, 51)] sql = "INSERT INTO asset (code, name, category, price, purchase_date, status, location, dept) VALUES\n" + ",\n".join(inserts) + ";" with open("init_data.sql", "w", encoding="utf-8") as f: f.write(sql) print("✅ init_data.sql 生成完成!共 50 条资产数据")运行后得到init_data.sql,答辩前执行:
source init_data.sql; -- 再插入 5 个用户(admin/teacher/dept_head 各一个) INSERT INTO user (username, password, real_name, role, dept, email) VALUES ('admin', '$2a$10$QqGZ...BCrypt哈希', '系统管理员', 'ADMIN', '国资处', 'admin@univ.edu'), ('zhangsan', '$2a$10$...', '张三', 'TEACHER', '计算机学院', 'zhang@cs.univ.edu'), ('lisi', '$2a$10$...', '李四', 'DEPT_HEAD', '计算机学院', 'li@cs.univ.edu');答辩价值:
- 展示“数据驱动开发”思维,评委可当场筛选“计算机学院在用资产”,验证行级权限;
init_data.sql放进 Git 仓库,体现“可复现性”,比口头说“我有测试数据”更可信。
5.3 答辩 PPT 的一页技术架构图:去掉所有云图标,只画真实组件
别用阿里云/腾讯云图标堆砌“高大上”。毕业设计架构图要体现“你做了什么”,而非“用了什么云服务”。我的 PPT 架构页长这样:
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Vue3 前端 │───▶│ SpringBoot3 后端 │───▶│ MySQL 8.0 │ │ (Vite + Pinia) │ │ (JDK17 + MyBatis) │ │ (本地 Docker) │ └────────┬────────┘ └────────┬────────┘ <p> <a href="https://download.csdn.net/download/qq_41464123/91733119" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>