实训室设备管理这事儿,看着简单,真做起来全是细节。实训室里几十台设备,借出去没登记、归还时发现损坏没人认、设备维修记录全在纸质本子上,学期末想统计个使用率还得人工翻表——这套系统的需求就是从这些真实场景里长出来的。基于SpringBoot+Vue的实训室设备管理系统,核心就是解决设备台账、借用审批、维护报废和统计报表这一整条链路的信息化问题。如果你是正在做课设、毕业设计的学生,或者是学校/培训机构里负责信息化建设的同事,这篇内容基本能覆盖从需求分析、技术选型、数据库设计、前后端实现到最后的文档PPT整理、环境部署的全过程。
下面内容我按自己实际做这类管理系统的思路来拆,重点讲清楚每一步为什么要这么做,以及哪些地方容易出问题。
1. 实训室设备管理系统的真实需求起点
1.1 没有系统时的混乱场景
实训室的设备管理痛点,我总结下来就四个字:账、借、修、报。
“账”是设备底数不清。设备买回来贴个标签就完事,存放位置靠人脑记忆,谁负责保管也说不清楚,到了盘点的时候只能一台台去找。“借”是借用流程随意,学生找老师签字、老师找管理员拿钥匙,登记本上字迹潦草,归还时间一拖再拖没人催。“修”是故障信息断裂,设备坏了口头报给管理员,修没修、修了多少钱、换过什么零件,没有一条完整记录。“报”是报废处置无据,设备老化到不能用了,想报废却拿不出维修历史和折旧数据。
这四个痛点对应到系统里,就是四个核心模块:设备台账管理、借用归还管理、维护维修管理、报废处置管理。再往后延伸,还需要用户管理、角色权限、操作日志、统计报表这些支撑性功能。
1.2 角色梳理:谁在用这个系统
我在设计权限模型之前,习惯先把用户角色画清楚。这套系统里典型的角色有三种,外加一个可选项:
- 系统管理员:管账号、管角色、管系统参数,一般不直接参与设备业务。
- 实训室管理员(设备管理员):核心角色,负责设备入库、维修派单、报废审批初审、借用审批。相当于设备全生命周期的“管家”。
- 教师/学生:发起借用申请、查看设备基本信息、查询自己的借用记录。教师还可能承担“设备责任人”的角色,对自己名下的设备负责。
- 可选:维修工(技术员):如果维修流程想做得更细,可以单独拆一个角色,只负责接单、填写维修结果。
角色一旦定下来,菜单权限就跟着定了。管理员看到的是完整的业务管理菜单,教师和学生看到的是申请、查询、个人中心。这个设计决策我建议放在写代码之前做,因为后端的接口过滤和前端路由都要依赖它。
1.3 核心功能清单:从需求到模块
把那四个痛点翻译成功能模块,具体是这样的:
| 模块 | 功能点 | 说明 |
|---|---|---|
| 设备台账 | 新增、编辑、删除、批量导入 | 记录设备编号、名称、型号、分类、存放位置、购置日期、原值、状态 |
| 借用归还 | 借用申请、审批、出库、归还、逾期提醒 | 支持按课时或按天借用,归还时登记设备状况 |
| 维护维修 | 故障上报、维修派单、维修结果登记 | 记录故障现象、处理方式、费用、维修日期 |
| 报废处置 | 报废申请、审批、处置登记 | 关联维修记录,作为报废依据 |
| 统计报表 | 设备分类统计、使用率、维修费用 | 用柱状图、饼图展示,支持导出 |
| 系统管理 | 用户、角色、菜单、日志 | 基于RBAC模型的常规设计 |
功能清单列完,再往后才是技术方案。很多人上来就写代码,结果做着做着发现少张表、少个状态,返工成本很高。先把模块和角色对应清楚,后面都是按表施工。
2. 技术选型:为什么是SpringBoot + Vue
2.1 后端骨架:SpringBoot加MyBatis-Plus的理由
SpringBoot做这类管理系统的后端,几乎是当前国内项目的主流选择,原因很实际:生态成熟、招人容易、教程多、遇到问题能搜到答案。对一个实训室设备管理系统来说,SpringBoot + MyBatis-Plus + MySQL这套组合完全够用。
具体到ORM框架,我推荐MyBatis-Plus而不是原生MyBatis。这个系统里有大量单表CRUD,比如设备列表分页查询、用户信息维护、借用记录的增删改查,MyBatis-Plus的BaseMapper和ServiceImpl能把这些重复代码砍掉一半以上。它自带的LambdaQueryWrapper让条件查询写起来非常直观:
// 设备列表分页 + 多条件筛选 Page<Equipment> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Equipment> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), Equipment::getName, name) .eq(categoryId != null, Equipment::getCategoryId, categoryId) .eq(StringUtils.isNotBlank(status), Equipment::getStatus, status) .orderByDesc(Equipment::getCreateTime); equipmentMapper.selectPage(page, wrapper);这段代码就是设备管理列表页最核心的查询逻辑。如果有复杂的多表关联统计,比如查每个分类的设备数量,再写一点自定义SQL搭配@Select注解或者XML就行。简单场景用MP,复杂场景写SQL,两者结合。
2.2 前端选型:Vue加组件库的配套
前端选Vue,主要是看中它的组件化开发效率。配合Element UI(Vue 2项目)或Element Plus(Vue 3项目),管理后台的表格、表单、弹窗、树形控件都能直接拿来用,开发速度非常快。
选Vue 2还是Vue 3,我的建议是:如果是自己新写的课设或毕设项目,直接上Vue 3 + Element Plus + Vite,一步到位。如果是要跑网上现成源码,那得先看清项目用的是Vue 2还是Vue 3,两个版本生态不通用,混着装依赖很容易出问题。
前端项目的关键依赖大概是这些:
vue / vue-router / pinia(或vuex) element-plus(或element-ui) axios echarts sass(可选,样式预处理)组件库选好后,页面搭建速度会快很多。设备列表页就是el-table加el-pagination,借用申请页就是el-form加各种表单项,首页看板用echarts画柱状图和饼图。真正需要动脑子的反而是业务逻辑,比如状态流转、权限控制。
2.3 前后端分离的接口契约与统一响应
前后端分离开发,最大的坑是“接口各说各话”。前端期望返回{code:200, data:{}, msg:'success'},后端返回一个裸对象,联调的时候就得反复改代码。我建议第一步就把统一的响应结构定下来。
@Data public class Result<T> { private Integer code; // 200 成功,401 未登录,403 无权限,500 异常 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } }所有Controller统一返回Result,配合一个全局异常处理器,把业务异常和系统异常包装成统一格式返回。前端在Axios拦截器里统一判断code,等于把错误处理收敛到一个地方,联调效率高得多。这个约定看起来简单,但很多半成品项目恰恰是死在“响应格式五花八门”。
3. 数据库设计:设备台账与借还状态机的核心
3.1 设备信息表和分类表怎么建模
设备台账是整个系统的数据地基。我见过不少人把分类直接当成一个字符串字段写在设备表里,结果统计的时候按分类聚合非常痛苦。正确的做法是单独建一张分类表,设备表通过category_id关联。
设备表的关键字段基本是这些:
CREATE TABLE equipment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, equipment_no VARCHAR(64) NOT NULL COMMENT '设备编号', name VARCHAR(128) NOT NULL COMMENT '设备名称', category_id BIGINT NOT NULL COMMENT '分类ID', model VARCHAR(128) COMMENT '规格型号', sn VARCHAR(128) COMMENT '序列号', location VARCHAR(128) COMMENT '存放位置', purchase_date DATE COMMENT '购置日期', price DECIMAL(10,2) COMMENT '购置原值', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1借用中 2维修中 3已报废', manager_id BIGINT COMMENT '责任人ID', description VARCHAR(512) COMMENT '设备描述', create_time DATETIME, update_time DATETIME );status字段是整个设备生命周期的主线,所有业务操作都在改这个状态。建议用TINYINT存数字而不是直接存中文,扩展性更好,也方便做状态颜色映射。这里值得注意的是一台设备上面可能有多个相同型号的同款设备,它们共用规格型号,但每一台都是独立的equipment记录,序列号或设备编号保证唯一,别把“型号”和“单台设备”混为一谈。
3.2 借用流程的状态流转设计与并发控制
借用记录表是业务量最大的表,设计好坏直接影响用户体验。我的建议是单独建一张borrow_record表,设备表和用户表都只通过ID引用,不冗余状态以外的业务信息。
CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, equipment_id BIGINT NOT NULL COMMENT '设备ID', user_id BIGINT NOT NULL COMMENT '申请人ID', borrow_type TINYINT NOT NULL COMMENT '0课堂借用 1课外借用', plan_start_time DATETIME COMMENT '计划开始时间', plan_return_time DATETIME COMMENT '计划归还时间', actual_return_time DATETIME COMMENT '实际归还时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝 3借用中 4已归还 5已逾期', audit_user_id BIGINT COMMENT '审核人ID', audit_remark VARCHAR(255) COMMENT '审核备注', return_remark VARCHAR(255) COMMENT '归还备注', create_time DATETIME );借用状态流转是个典型的“状态机”:待审核 → 已通过 → 借用中 → 已归还;任何一步都可以根据情况转成已拒绝或提前终止。写到代码里时,要注意两个操作点:
第一,审核通过时,要同时把设备状态改成“借用中”,并确保设备当前是“空闲”状态,否则就报“该设备当前不可借用”。第二,并发问题。两个人同时申请同一台空闲设备,如果都读到空闲都改成功,就会发生“一机两借”。解决方案是借用审核的SQL加上行级锁:
// 事务内先锁行查询 Equipment eq = equipmentMapper.selectForUpdate(equipmentId); if (!"0".equals(eq.getStatus())) { throw new BizException("设备当前状态不可借用"); } // 更新设备状态 equipmentMapper.updateStatus(equipmentId, "1");很多初学者忽略这一步,演示环境没并发问题,等真到使用场景就会暴露。写借还功能时心里始终绷着“状态一致性”这根弦,就不容易翻车。
3.3 维护记录与报废记录如何挂接
维护维修表相对简单,核心是记录设备“生病”和“治病”的过程。设计上要注意:设备状态为“维修中”时,不能出现在可借用列表里;维修完成后要把设备状态恢复成“空闲”,并在维修记录里登记结果。
维修记录表可以参考这样:
CREATE TABLE maintenance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, equipment_id BIGINT NOT NULL, report_user_id BIGINT COMMENT '报修人ID', fault_desc VARCHAR(1024) COMMENT '故障现象', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1维修中 2维修完成 3无法维修', assign_user_id BIGINT COMMENT '维修人ID', repair_result VARCHAR(1024) COMMENT '维修结果', cost DECIMAL(10,2) COMMENT '维修费用', repair_time DATETIME COMMENT '维修完成时间' );报废记录表可以单独建,也可以在设备表上加一个scrap_time字段,我倾向于建一张独立表。原因是报废审批在真实场景里是需要留痕的:谁申请的、谁审核的、报废原因是什么、有没有维修记录作为支撑材料。一张完整的scrap_record表比在设备表里加几个字段要严谨得多。
数据库设计这部分,我最后再强调一句:表和表之间的关联尽量用ID,不要在业务表里冗余大段文本。查询性能不够的时候再去考虑冗余字段,而不是一开始就堆冗余。
4. 后端关键功能实现与踩坑记录
4.1 登录鉴权与角色权限控制
登录鉴权我用的方案是SpringBoot + JWT + 拦截器,属于管理系统的标配。流程是:用户输入账号密码,后端校验通过后签发一个token,前端把token存在本地存储里,每次请求放在Authorization请求头里,后端拦截器统一校验。
Interceptor的核心逻辑大概长这样:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String path = request.getRequestURI(); if (path.contains("/login")) return true; String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BizException(401, "未登录"); } // 解析token,失败则抛401 Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }权限控制分两级。第一级是接口级,用拦截器检查角色是否允许访问某个路径;第二级是菜单级,前端根据用户角色动态渲染菜单。做课设的时候很多人只做了登录没做权限,答辩老师一问“学生和管理员看到的界面一样吗”就卡壳了。我的建议是最少要按“管理员”和“普通用户”两个角色把菜单和接口分开,这体现的是完整工程思维。
另外一个容易被忽略的地方是密码加密。直接用明文存数据库属于重大安全事故,哪怕课设项目也一样。用Spring Security自带的BCryptPasswordEncoder或者写一个简单的加盐MD5都可以,但别用MD5裸存。代码里加一行password = DigestUtils.md5DigestAsHex((password + salt).getBytes())就能挡住很多低级问题。
4.2 设备借用审批的核心接口逻辑
借用审批接口是业务流程里最复杂的一个,因为要同时操作两张表:借阅记录表和设备状态表。实现时要包在同一个事务里,任何一个步骤失败都要回滚。
@Transactional(rollbackFor = Exception.class) public void approveBorrow(Long recordId, Long auditorId, boolean pass, String remark) { // 1. 查询借用记录并锁定 BorrowRecord record = borrowRecordMapper.selectById(recordId); // 2. 校验当前状态必须是待审核 if (record.getStatus() != 0) { throw new BizException("该记录已被处理"); } if (pass) { // 3. 锁定设备行,防止并发 Equipment eq = equipmentMapper.selectForUpdate(record.getEquipmentId()); if (!"0".equals(eq.getStatus())) { throw new BizException("设备当前状态不可借用"); } // 4. 更新设备为借用中 equipmentMapper.updateStatus(eq.getId(), "1"); record.setStatus(3); // 借用中 } else { record.setStatus(2); // 已拒绝 } record.setAuditUserId(auditorId); record.setAuditRemark(remark); borrowRecordMapper.updateById(record); }这段代码的要点在于:第一步锁借用记录,第二步锁设备行,顺序不能乱。如果反过来先锁设备再锁记录,在高并发场景下可能死锁。归还接口就是反向操作:核对归还的设备、把设备状态重置为“空闲”、登记实际归还时间,同时检查是否有损坏,有损坏就挂一条维修记录。这个接口同样要事务包裹。
4.3 统计报表与数据导出
统计报表的原理就是把数据库里的明细数据按维度聚合。比如“设备数量按分类统计”,一个GROUP BY就出来了:
select c.name, count(e.id) total from equipment e left join equipment_category c on e.category_id = c.id group by c.name前端拿到这个结果后用ECharts画饼图;设备使用率可以按月统计借用记录数,画折线图或柱状图;维修费用统计按年汇总maintenance_record.cost。报表模块的坑主要在SQL的连表,建议先单独在数据库客户端里跑通SQL,再贴到Mapper里,能省不少调试时间。
数据导出我用的是EasyExcel。一行注解就能把实体类和表头列对应起来:
@ExcelProperty("设备编号") private String equipmentNo; @ExcelProperty("设备名称") private String name; @ExcelProperty("状态") private String status;导出的时候在Service里查出数据,转成对应的VO列表,直接写回HttpServletResponse就行。相比POI需要手写一堆样式,EasyExcel对报表导出这种场景友好得多,而且能边写边降低内存占用,避免一次性加载全量数据把服务器打崩。
5. 前端页面从零搭建的落地细节
5.1 路由与权限菜单的联动
前端的路由设计,我推荐在router/index.js里把基础路由和动态路由分开。基础路由只有登录页和404,业务页路由在用户登录后根据角色动态添加。这样能保证未登录用户即使在浏览器里乱输地址,也进不了业务页面。
// 登录成功后动态添加路由 function addDynamicRoutes(menus) { const routes = []; menus.forEach(menu => { if (menu.type === '1') { // 目录或菜单 routes.push({ path: menu.path, name: menu.name, component: () => import(`../views/${menu.component}`), meta: { title: menu.title } }); } }); menus.forEach(route => router.addRoute(route)); }这是“权限控到菜单”的常见做法。有些简单的课设为了省事,把组件都静态引入然后只隐藏导航栏,这能实现“看不见”,但做不到“进不去”,安全性差一截。用router.addRoute的方式加动态路由,才是真正的前端路由权限控制。
5.2 设备借用表单的校验与弹窗反馈
设备借用申请页,是用户接触最多的页面。表单用el-form的rules做校验,必填项、日期格式、归还时间不能早于开始时间这些规则都要写全。这里有一个非常容易被忽略的细节:回显数据时,设备编号和设备名称要让用户能看见但不能改。借用什么、借多久、借给谁,这三条是借用记录的核心,必须清晰。
交互上建议三步走:选中设备 → 填写借用时间 → 提交申请。提交成功后弹窗提示“申请已提交,等待实训室管理员审核”,同时把状态栏更新为“待审核”。前端的状态映射可以统一封装成一个方法,避免每个页面都写一遍中文判断:
const statusMap = { 0: { text: '待审核', type: 'info' }, 1: { text: '已通过', type: 'success' }, 2: { text: '已拒绝', type: 'danger' }, 3: { text: '借用中', type: 'warning' }, 4: { text: '已归还', type: 'primary' }, 5: { text: '已逾期', type: 'danger' } };el-table的列渲染里直接用statusMap[row.status].text,状态值永远是数字,展示永远是中文,后面做筛选、统计都方便。
5.3 首页看板的数据可视化实现
首页看板是给管理员看的“驾驶舱”,通常放四个数据:设备总数、今日借用次数、维修中设备数、本月维修费用,下面再配两个统计图。数据从后端报表接口拿,前端用ECharts渲染。
这里有个我自己趟过的坑:ECharts图表容器必须在渲染时已经有确定高度,否则图表画不出来。如果页面是v-if切换的,初始化图表前还要确认容器已经在DOM里,否则拿不到尺寸。稳妥做法是封装一个组件,在mounted里初始化图表,监听数据变化更新:
mounted() { this.chart = echarts.init(this.$refs.chartRef); }, watch: { data: { handler(val) { this.chart.setOption({ xAxis: { data: val.categories }, series: [{ data: val.values, type: 'bar' }] }); }, deep: true } }看板的数据接口要避免频繁刷数据库,可以考虑在Redis里缓存统计结果,比如每小时刷新一次。如果项目不引入Redis,至少在页面加载时只请求一次,别用每秒轮询的方式刷,数据库扛不住也没必要。
6. 文档、PPT与源码的交付规范
6.1 项目文档应该覆盖哪几章
这套项目标题里带了“文档+PPT+源码”,可见交付物本身就是项目的组成部分。文档写的质量,很多时候比代码还影响最终评价。我建议一份完整的项目文档按这个顺序来写:
- 引言:项目背景、建设目标、系统意义。
- 需求分析:功能需求(用功能模块列表或用例表)、非功能需求(性能、安全)、角色用例。
- 系统设计:整体架构图、技术选型说明、数据库设计(表结构清单加ER关系说明)、接口设计(关键接口路径和参数)。
- 系统详细设计:每个模块的功能描述、流程说明、核心代码逻辑。
- 测试:测试用例表格、测试环境、测试结果。
- 总结:项目完成情况、改进方向。
写文档最忌讳“抄概念”。答辩老师翻文档,第一眼看目录结构,第二眼就看数据库表设计能不能对上功能模块。我在文档里都会画一张数据库表关系说明,哪怕手画再截图,也比纯文字清楚得多。
6.2 答辩PPT的演示主线
PPT不用长,15到20页足够,但要有一条清晰的演示主线:背景 → 需求 → 技术架构 → 数据库设计 → 功能演示截图 → 总结。
其中功能演示截图是最有说服力的部分,我建议不要放一堆代码,而是截真实的页面:登录页、设备列表、借用审批流程、统计看板,每一页配一句说明文字。如果现场能直播演示,那就演示借用审批这个核心流程,从申请到审批到归还,五分钟讲透,比放10页截图都管用。答辩时如果被问到“系统有哪些改进空间”,可以提前准备两点:比如借用超时自动提醒、设备二维码扫码借用,这两个方向都说得通,也显得你有思考深度。
6.3 源码目录如何组织才加分
源码组织得好不好,内行人一眼就能看出来。后端我习惯这样分包:
com.example.lab ├── config # 配置类:跨域、拦截器、全局异常 ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 表实体 ├── dto # 请求入参对象 ├── vo # 响应出参对象 ├── utils # 工具类:JWT、Redis等 └── common # 公共类:Result、枚举、业务异常前端按页面和组件分:
src ├── api # 每个模块一个API文件 ├── views # 页面级组件 ├── components # 公共组件 ├── router # 路由配置 ├── store # 状态管理 └── utils # axios封装、公共方法统一响应、统一异常处理、注释覆盖核心逻辑,这几样做好,代码评分基本不会低。
7. 让项目在本机跑起来的全流程
7.1 环境准备清单
拿到一套前后端分离的源码,第一步不是急着npm install,而是先核对环境。我列一个清单:
| 环境项 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8以上,推荐1.8或11 | 对应SpringBoot 2.x |
| Maven | 3.6以上 | 后端依赖管理 |
| MySQL | 5.7或8.0 | 数据库 |
| Node.js | 14以上 | 前端构建 |
| npm/yarn | 随Node自带 | 前端包管理器 |
| IDE | IDEA + VSCode | 后端、前端分开 |
7.2 初始化数据库与后端配置
拿到源码后,通常你会看到项目根目录下有个sql文件夹,里面是数据库初始化脚本。执行步骤:
- 用Navicat或命令行创建数据库,字符集选
utf8mb4。 - 导入
xxx.sql文件。 - 打开后端
application.yml,核对数据库账号密码、端口、JWT密钥:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/lab_equipment?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password这里有一个高频报错:MySQL 8.0的驱动包和MySQL 5.7的驱动包不一样,pom.xml里的mysql-connector-java版本要和你本地数据库版本匹配。版本不匹配启动时报错或者连不上数据库,十次里有八次是这个问题。
7.3 启动前端联调
后端启动成功后,再开前端。前端项目目录下执行:
npm install npm run serve如果npm install速度很慢或者报错,可以换淘宝镜像源:
npm config set registry https://registry.npmmirror.com/启动成功后访问http://localhost:8081(端口在vue.config.js或Vite配置里看)。前端启动常见问题是依赖版本冲突,尤其是Element Plus和Vue版本不匹配,启动直接报错。解决办法是严格按package.json里锁定的版本安装,别乱升。
7.4 上线前要检查的几个隐患
项目跑通之后,我建议做一轮“上线前自检”,重点查这几项:
- 跨域配置:前后端端口不同,必须有跨域处理。后端配置
CorsFilter,或者前端用代理转发,二选一。 - Token过期处理:前端Axios拦截器要处理401响应,跳回登录页。
- 时区问题:数据库连接串里带上
serverTimezone=Asia/Shanghai,不然日期字段可能差8个小时。 - 默认密码:初始管理员账号密码是否改掉,别留着
admin/123456就这样公开演示。 - 日志输出:后端至少要有基本的日志,出问题才能定位,别只靠System.out.println。
我一直觉得设备管理这类系统,最大的价值不是技术有多花哨,而是把一条冗长琐碎的线下流程真正梳理清楚,让管理员能从“每天被人追着问设备在哪”里解放出来。如果你拿到的源码结构清晰、文档齐全,建议先完整跑通一遍流程,把每个模块的代码读一遍再动手改需求。改的时候优先从统计报表和权限这两块入手,一个能展示对业务的理解,一个能展示对系统架构的理解,都比单纯换个页面样式加分得多。
最后再分享一个小技巧:设备管理系统的测试数据一定要造得像样一点。把几十台设备、几十条借用记录、几条维修记录造进去,再用这些真实感强的数据演示,答辩和汇报的可信度完全不一样。数据造得好,等于项目成功了一半。