基于SSM的实验室计算机故障报修系统设计与实现全记录

📅 发布时间:2026/9/28 23:51:42
基于SSM的实验室计算机故障报修系统设计与实现全记录
实验室报修单还在靠吼用SSM搭一套计算机故障报修系统从需求到上线全记录作为一个常年跟高校实验室和机房打交道的人我最清楚设备报修这件事有多磨人。学生上课发现电脑坏了先找助教助教再找管理员管理员登记完找维修人员修完再层层反馈中间但凡有个环节拖一下这台机器就可能半个月没人管。所以我一直觉得实验室最缺的不是新电脑而是一套能把“报修”这件事跑通的流程工具。这次我用 Java 的 SSM 组合Spring SpringMVC MyBatis完整开发了一个实验室计算机故障报修系统。这套框架虽然现在听起来不算新潮但放在学校课程设计、毕业设计、甚至小规模内部管理系统里依然是性价比极高的选择。本文就把整个项目的完整落地过程写出来从需求拆解到数据库设计从核心代码实现到后期排查全是我实际开发中一步一步踩出来的经验希望能给正在做同类项目的同学一点参考。1. 项目整体设计与思路拆解1.1 实验室报修场景到底解决了什么问题先把这个业务场景拆透了再动代码。实验室报修系统要服务的角色至少有四类报修人学生或上课老师、实验室管理员、维修人员、系统管理员。这四类人对系统的核心诉求完全不同。报修人只关心一件事电脑坏了以后能不能快速提交故障信息并且之后能查到维修进度。实验室管理员关心的是哪些设备坏了、坏在哪、报修是否及时得到响应、有没有长期积压未处理的单子。维修人员关心的是任务分配给我没有、故障描述够不够清楚、修完之后怎么反馈。系统管理员关心的则是账号权限和基础数据维护。这个诉求拆开以后项目的核心就不是“写代码”而是把一条报修流程的状态流转理清楚。我设计的流程是提交报修单 - 管理员审核并派单 - 维修人员接单 - 维修中 - 维修完成 - 报修人确认 - 归档。这中间要考虑的边界情况非常多比如报了单没人处理怎么办、维修人员临时不在怎么办、机器修完又复发怎么办。这些在设计阶段就得想清楚不然后期全是在还债。1.2 为什么选 SSM 而不是 Spring Boot很多新手上来就问我为什么不直接用 Spring Boot我的回答很简单项目目标决定技术选型。如果这是企业内部的生产项目我肯定用 Spring Boot MyBatis Plus省去大量 XML 配置开发效率高得多。但这是一个典型的课堂设计/毕设/内部小系统场景选 SSM 有几层考量第一SSM 是 Java 课程体系中覆盖面最广的组合。Spring 的 IOC 和 AOP、SpringMVC 的请求处理流程、MyBatis 的持久层映射这三块恰恰是 Java 面试里最高频的考点。用 SSM 做一遍完整项目比刷一百道面试题都管用整个请求从 URL 到 Controller 再到 Service 最后到 Mapper 的链路会在脑子里形成肌肉记忆。第二SSM 涉及的配置项足够多踩过的坑也足够深。比如 Spring 容器和 SpringMVC 容器怎么分离、MyBatis 的 mapper 扫描路径怎么配、事务管理器管哪些方法、web.xml 里 DispatcherServlet 的 url-pattern 怎么设置这些细节在 Spring Boot 中被自动配置掩盖了但在 SSM 里每一步都是自己亲手配的理解深度完全不一样。第三代码兼容性和部署环境。很多学校机房的老机器还在用 JDK 1.7 或 1.8Tomcat 还是 8.0 甚至 7.0SSM 在这些老环境下跑得非常稳。Spring Boot 2.x 要求 JDK 8某些高版本连 Tomcat 都内嵌了这些在旧环境上反而容易出问题。当然如果是自己练手或做毕设且没有框架限制选 Spring Boot 也是完全可以的但本文全程按 SSM 来讲解因为按这套逻辑跑通以后切到 Spring Boot 只是把配置换成注解和 starter 的事没有本质障碍。1.3 项目技术栈与模块划分整个项目我用的版本组合是JDK 1.8、Spring 5.1.8、SpringMVC 5.1.8、MyBatis 3.5.3、MyBatis-Spring 2.0.3、MySQL 5.7、Maven 3.6。前端用的是 JSP Bootstrap jQuery管理端模板用的是 AdminLTE这些技术虽然老但好在资料多、坑都被人踩平了非常适合这个体量的项目。模块划分为五个部分用户认证模块、报修工单模块、设备信息模块、统计报表模块、系统管理模块。用户认证模块负责四种角色的登录、注册仅报修人、权限拦截。报修工单模块是核心负责工单的创建、审核、派单、接单、处理、完成、确认、归档的完整生命周期。设备信息模块维护实验室所有计算机的基本信息包括编号、位置、配置、状态等。统计报表模块用于展示各实验室报修数量、维修时长、故障类型分布等数据。系统管理模块负责账号管理、角色分配、基础数据字典维护。这里我想强调一个设计原则不要把数据库直接暴露给前端页面所有页面只能通过后端接口拿数据这是分层设计最基本的底线。有些同学做项目时喜欢在 JSP 里直接拼 JDBC 代码页面里写一堆 % % 脚本看起来开发快后期维护就是灾难。2. 数据库设计与核心细节解析2.1 核心表结构设计思路数据库设计是这个项目的地基。我的总体设计是六张核心业务表 两张辅助表不要贪多也不要刻意追求“表的数量多显得项目大”而是让每一张表都有明确的业务含义。核心表如下用户表 user字段包括 id、username、password、real_name、role枚举0-报修人1-实验室管理员2-维修人员3-系统管理员、phone、email、lab_id关联实验室、status、create_time。密码存储用 MD5 加盐处理虽然现在更推荐 BCrypt但教学项目中 MD5 加盐已经足够演示安全思路。实验室表 labid、name、location、manager_id、description。用于维护实验实信息报修人提交工单时需要选择自己的实验室这样系统才能自动计算出该实验室对应的管理员。设备表 deviceid、lab_id、device_code设备编号如 JSK-09-012 表示 9 号实验室第 12 号机器、device_name、device_type、status空闲/使用中/故障/维修中、purchase_date。设备信息是报修工单的“主语”没有设备信息的工单就是一团浆糊。报修工单表 repair_order这是核心表字段如下字段名类型说明idbigint主键order_novarchar工单编号如 BX20230715001device_idbigint故障设备 IDreporter_idbigint报修人 IDfault_desctext故障描述fault_typeint故障类型0-硬件故障1-软件故障2-网络故障3-其他fault_imagesvarchar故障图片路径逗号分隔statusint状态0-待审核1-待派单2-待接单3-维修中4-待确认5-已完成6-已取消assignee_idbigint维修人员 IDlab_idbigint实验室 IDcreate_timedatetime提交时间audit_timedatetime审核时间assign_timedatetime派单时间accept_timedatetime接单时间finish_timedatetime维修完成时间confirm_timedatetime报修人确认时间audit_remarkvarchar审核备注repair_resulttext维修结果描述除了上面四张主表还有两张辅助表维修记录表 repair_log 用于记录每次维修的操作明细操作日志表 operation_log 用于记录所有关键操作的日志方便追溯。2.2 工单状态机的设计心得工单状态是整个系统最核心的领域逻辑因为所有角色的操作本质上都是在做状态迁移。这个设计我前后改过两版第一版只有“待处理/处理中/已完成”三个状态一直跑不通原因就是业务流程中间信息断层。比如管理员审核通过但还没维修人员接单时这个单子算什么状态维修人员修完但报修人还没确认时又算什么状态所以我最终把状态扩成了七个节点待审核 - 待派单 - 待接单 - 维修中 - 待确认 - 已完成外加一个已取消。每个状态对应一个角色可以做的操作保证任何时刻系统里都有一名明确的“责任人”。实际开发中我是这样分配状态权限的报修人能提交新单、能在待审核时取消、能在待确认时确认或退回管理员能审核通过或驳回、能派单维修人员能接单、能填报维修结果并标记完成。这里有个非常容易踩的坑用户同时具备多个角色的情况。比如一位老师既是报修人又是实验室管理员其在页面上的操作按钮会因角色不同而异。我采用的方案是后端接口单独做权限校验不能只靠页面隐藏按钮否则把按钮通过浏览器调试一看就能越权操作。我的做法是在拦截器里对每个 URL 做角色匹配同时 Service 层做二次校验确认当前登录用户的角色和操作对象归属。另外数据库表设计时status字段我用了 int 而不是 varchar因为状态机逻辑中频繁用到比较和流转判断数字效率更高同时配合 Java 端的枚举类做状态解析代码可读性比冗长的 if-else 强得多。2.3 故障类型与设备关联的补充设计故障类型字段我没有做成纯数字字典而是在代码里用枚举类管理同时数据库存 int 值。这样既方便前端下拉框渲染又避免数据库里存中文导致后期统计口径不一致。比如故障类型明细为硬件故障内存条、硬盘、电源、主板、显示器、软件故障系统崩溃、蓝屏、软件安装失败、网络故障无法联网、IP 冲突、其他故障。设备与实验室的关联我用了冗余设计。因为一次报修单提交时报修人选择实验室、再选设备此时工单里存了 lab_id 和 device_id。从规范化的角度lab_id 可以不存因为通过 device 表就能查到 lab_id但冗余存一份有两个好处一是统计报表时按实验室维度筛工单不用 join 两张表二是防止设备信息后续调整后历史工单的归属跟随变化导致历史数据失实。这就是“宁可冗余不可失真的思路”。3. 核心功能实现与实操过程记录3.1 SSM 三大框架整合的配置清单SSM 整合配置是整个项目让新手最头疼的地方因为配置项太多任何一个标签写错都能让项目启动失败。我把最核心的配置文件和要点整理一遍。第一步是 pom.xml 依赖。需要注意 Spring 各模块的版本必须保持一致比如 spring-webmvc 和 spring-jdbc 都是 5.1.8我现在见过太多人 spring-webmvc 用 5.1.x 而 spring-context 用了 4.3.x直接导致 NoSuchMethodError。MyBatis 和 mybatis-spring 的版本也要配套我用的组合是前者 3.5.3、后者 2.0.3这套组合在互联网上有海量的踩坑记录可查。第二步是 web.xml。核心是配置 DispatcherServlet 和 CharacterEncodingFilter。一个关键的细节是 DispatcherServlet 的 url-pattern 我用了/而不是*.do这样 REST 风格路径更干净。但是用了/以后静态资源就会被拦截住我对应的 SpringMVC 配置里加了mvc:resources映射把/static/**、/upload/**路径放出去。第三步是 spring-context.xmlSpring 根容器。在这里配置数据源、SqlSessionFactoryBean、MapperScannerConfigurer、事务管理器。数据源我用的是阿里 Druid配置了初始连接数、最大连接数、连接超时时间等参数。事务管理器配置成 DataSourceTransactionManager并且用tx:annotation-driven开启了注解事务这样 Service 层一个Transactional就能覆盖绝大多数场景。第四步是 spring-mvc.xml。配置组件扫描只扫com.lab.controller包避免把 Service 扫进 SpringMVC 容器造成重复代理。配置视图解析器 InternalResourceViewResolver设置 JSP 文件放在 /WEB-INF/views 下这样页面就不能被浏览器直接访问必须经过 Controller。整合过程中我遇到过两个很经典的坑。第一个是 Spring 和 SpringMVC 容器重复扫描同一个 Service会导致事务失效因为产生了两个代理对象事务注解只在一个代理上生效。第二个是 MyBatis 的 mapper 接口和 XML 文件放在不同目录时maven 默认不会把 XML 打进最终项目需要在 pom.xml 里配置资源过滤把classpath:mapper/*.xml包含进去。3.2 报修流程闭环从提交工单到确认完成整个系统最核心的一条链路是报修流程我从前端页面的按钮到后端的状态流转把每个环节的实现细节都给你拆开讲。报修人提交工单时前端是一个表单用户选择实验室、选择设备号、填报故障类型、填写故障描述、可选上传图片。设备列表通过 AJAX 联动加载先选实验室再异步请求该实验室下所有设备。这里有一个关键处理就是设备状态筛选——只有“使用中”和“空闲”的设备才能报修已经处于“维修中”和“故障”状态的设备应该直接隐藏。这个逻辑容易被人忽略导致同一台设备被反复提交多张工单。工单创建的后端逻辑放在 RepairOrderServiceImpl 中我用代码片段来演示核心部分Override Transactional(rollbackFor Exception.class) public Long createOrder(RepairOrderVO vo, Long reporterId) { // 1. 校验设备是否存在且状态合法 Device device deviceMapper.selectById(vo.getDeviceId()); if (device null || device.getStatus() DeviceStatus.REPAIRING.getCode()) { throw new BusinessException(设备不存在或正在维修中); } // 2. 生成工单号 String orderNo generateOrderNo(); // 3. 组装实体 RepairOrder order new RepairOrder(); order.setOrderNo(orderNo); order.setDeviceId(vo.getDeviceId()); order.setReporterId(reporterId); order.setFaultDesc(vo.getFaultDesc()); order.setFaultType(vo.getFaultType()); order.setLabId(device.getLabId()); order.setStatus(OrderStatus.PENDING_AUDIT.getCode()); order.setCreateTime(new Date()); // 4. 插入工单并将设备状态改为故障 repairOrderMapper.insert(order); deviceMapper.updateStatus(device.getId(), DeviceStatus.FAULT.getCode()); // 5. 记录操作日志 operationLogMapper.insert(LogUtil.buildLog(create_order, 用户 reporterId 提交报修工单 orderNo)); return order.getId(); }注意我用了事务注解如果设备状态更新失败工单也不会落库保证数据一致性。generateOrderNo 方法用时间戳加随机数生成形如 BX20230715 三位随机数的编号因为工单号要短、要可读、要尽量无规则规律这样别人没法通过遍历编号批量抓数据。管理员审核派单的流程同样清晰管理员打开工单列表筛选状态为待审核逐个查看故障描述和设备位置点击通过后选择维修人员系统把工单状态改成待接单。维修人员在自己的工作台看到待接单的工单点击接单后状态变为维修中维修人员填写维修方案和结果点击完成状态变为待确认最后报修人对维修结果确认无误状态变为已完成整个闭环就结束了。3.3 权限拦截与角色工作台的实现细节权限问题我单独拿出来讲因为这是面试官最爱问、也是项目中防御最薄弱的一环。我的拦截器实现基于 SpringMVC 的 HandlerInterceptor请求进入 Controller 之前先判断当前 Session 中是否有已登录用户再判断请求路径是否匹配该用户的角色权限列表。我定义了一个权限映射关系把每个 URL 和允许访问的角色绑定比如/reporter/**只允许报修人访问/admin/**只允许管理员访问。在拦截器的 preHandle 方法里先从数据库把当前用户的角色查出来再比对路径前缀。这套方案简单有效但要注意一个细节用路径前缀做权限控制必须保持 URL 设计规范不要出现模糊匹配或路径穿越的情况。我试过在一个接口里用/order/list同时给三种人复用后来又加了/order/list/self和/order/list/all区分最后发现这种“一个接口多功能”的方式导致权限校验非常痛苦后来干脆按角色拆成了三个不同的接口各查各的列表代码反而更清爽。角色工作台的设计我遵循“用户看到的面板完全取决于角色”的原则。报修人登录后看到“我提交的工单”和“待我确认的工单”两个 Tab实验室管理员看到的是“本实验室全部工单”和“统计概览”维修人员看到的是“指派给我的任务”系统管理员看到系统设置和设备管理。实际操作上登录成功后跳转路径是/home?rolexxx首页侧边栏菜单根据 role 参数做条件渲染后端再次对菜单对应接口做鉴权双重保险。3.4 分页查询与统计报表的 SQL 写法分页查询是所有管理系统的刚需。我的方案是手写 LIMIT 分页配合 PageHelper 插件。PageHelper 虽然好用但有个隐性的坑它基于 MyBatis 拦截器在 SQL 执行前自动改写 SQL如果你动态拼接 SQL 时用了if标签且子语句里有嵌套子查询PageHelper 在计算 count 时不一定会生成正确语句导致总数不对。后来我在复杂查询中干脆手写LIMIT #{offset}, #{pageSize}再用一个独立 SQL 查 count。虽然代码多一点但是完全可控。统计报表模块我用了三层 SQL 聚合思路。第一层按实验室维度统计报修数量包括本月、本季度、本年分别一列SQL 里用SUM(CASE WHEN ... THEN 1 ELSE 0 END)做条件聚合。第二层按故障类型统计占比以fault_type分组算 count。第三层算平均维修时长即从assign_time到finish_time的平均小时数。这三组 SQL 写好后Java 后端只需要把它们分别查到 List 中传给前端 ECharts 渲染柱状图和饼图。这块我要重点强调一个 MyBatis 细节动态 SQL 的where标签能自动去掉第一个多余的 AND 或 OR但有条件的过滤字段很多时尽量把status这种高频条件放在最前面方便走索引。另外 MySQL 的 datetime 比较要用BETWEEN或 不要用函数包裹字段比如DATE_FORMAT(create_time, %Y-%m)因为这样会导致索引失效全表扫描。实测五万条数据下不规范写法查询要一两秒规范写法毫秒级返回。4. 开发中的高频问题与排查技巧实录4.1 SSM 整合期最容易翻车的配置问题第一个高频问题是项目启动直接报ClassNotFoundException: org.springframework.web.context.ContextLoaderListener。原因通常是 web.xml 中加载 Spring 根容器的监听器依赖缺失或者 maven 依赖没有完整打进 lib 目录。排查方法很简单在 IDEA 部署时点开 Artifacts 设置查看 Output Layout 里 lib 是否包含所有 maven 依赖。我见过不少人是在这里设置漏掉了lib目录导致本地编译没问题项目一启动就报类找不到。第二个高频问题是点击页面报 404 但 Controller 确实写了。这步要分两头排查先看控制台是否打印了 RequestMapping 的映射记录再看 JSP 路径是否与视图解析器拼接后的地址一致。很多时候 404 是因为 JSP 放在了/WEB-INF/page/而 Controller 返回的视图名写成了index拼接后是/WEB-INF/page/index.jsp如果你 JSP 实际叫index.jsp大小写不对都会 404。第三个高频问题是 MyBatis 的Invalid bound statement (not found)。这种问题九成是 mapper 接口和 XML 的 namespace 不一致或者方法 id 对不上。新手最爱犯的错是 XML 文件里mapper namespacecom.lab.mapper.UserMapper写成了别的名字或者接口方法叫selectByNameXML 里叫selectByUsername。还有一个隐蔽场景多个 mapper.xml 文件中有重复的 id比如两个 XML 里都有findListMyBatis 初始化阶段大概率不报错但运行时结果错乱很难查。4.2 报修流程中的逻辑漏洞与数据一致性这里分享一个我实际遇到过的问题报修人取消工单时设备状态没有改回正常。场景是这样的用户提交了一张报修单管理员还没审核时用户取消此时工单状态变成已取消但设备状态还停留在“故障”。于是这台设备在其他所有页面全部不可选择因为状态是故障但系统里又没有任何一个正在处理的工单。这就是典型的“状态流转不同步”问题。我当时的修复方案是在取消逻辑中加入设备状态恢复。实际上从这次事故之后我就梳理出一个规则任何改变工单状态为“终止态”的操作都必须伴随设备状态联动更新。终止态包括已取消和已完成已完成时设备状态变为使用中已取消时恢复为之前的状态。这个规则写成一个独立的方法放在 Service 层比如changeOrderStatusWithDevice()所有状态变更都走这个方法保证不遗漏。另外还有一类问题需要小心并发提交。两个学生同时填报同一台设备的故障如果页面没有做控制就能提交两张工单。虽然我在创建接口里查了设备状态但两个请求并发时先查的都是“空闲”然后都通过校验都会执行 update 和 insert。解决办法有两个层级数据库层面给设备表增加状态变更的乐观锁版本号或应用层做 Redis 锁。对于这个规模的项目我建议增加一个唯一索引来解决在 repair_order 表中对device_id建一个“状态为待审核、待派单、待接单、维修中时”的唯一索引。MySQL 不支持部分索引所以我的做法是用冗余字段active_flag置为 1 表示当前存在未完成工单建(device_id, active_flag)唯一索引新单插入前强制active_flag1结束后改 0这样数据库层面就兜住了并发。4.3 前端联调过程中常见的 AJAX 和 JSON 问题前端我用的是 jQuery 的 AJAX但和 SSM 后端的 JSON 交互有几个非常容易踩坑的地方。第一个坑是日期格式。Date类型序列化到 JSON 时SpringMVC 默认行为需要你配置。如果不配置前端拿到的是数字时间戳显示在页面上是一串 13 位数字非常反人类。我的解决方案是配置 Jackson 的 ObjectMapper统一输出yyyy-MM-dd HH:mm:ss格式在 spring-mvc.xml 中声明一个MappingJackson2HttpMessageConverter或者在实体类的日期字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。推荐注解方式粒度可控。第二个坑是 Long 类型精度丢失。数据库严格递增主键是 bigintJava 端对应 Long 类型。当 JSON 序列化后前端 JavaScript 在解析超过 2^53 的 Long 数值时会出现精度丢失页面里变了一个尾数不对的数字。项目里我传递工单 id、设备 id 时统一转成 String 类型传给前端具体做法是在字段上加JsonSerialize(using ToStringSerializer.class)。第三个坑是跨域问题。如果你把前端页放到 8080 端口、后端部署在另一台服务器 8081 端口浏览器会因为 CORS 拦截 AJAX 请求。统开发过程中我用的是统一部署在同一 Tomcat 下所以没遇到但如果前后端分离就要在 Java 后端加CrossOrigin或编写 CorsFilter。这个场景在 SSM 项目中不一定能用上但面试经常被问到。4.4 文件上传与路径处理故障图片上传我用的 SpringMVC 的 MultipartResolver。配置时需要声明CommonsMultipartResolver并指定上传编码和大小限制。我踩过的最隐晦的坑是MultipartResolver Bean 的 id 必须叫multipartResolver否则 DispatcherServlet 无法识别。换成其他名字后文件上传功能会直接失效页面报 500 或空指针。图片存储路径我最终采用了虚拟目录映射到本机磁盘的方式。实际的项目部署环境中把图片放在项目目录内不是一个好习惯因为 Tomcat 重新部署或 clean 会清空资源图片全丢。我把上传路径配置成项目外部的绝对路径比如/data/lab-repair/upload/然后通过 Tomcat 的虚拟主机映射或 SpringMVC 的资源映射暴露出来。具体做法是在 spring-mvc.xml 里配置mvc:resources mapping/upload/** locationfile:/data/lab-repair/upload/ /这样浏览器可以直接通过/upload/xxx.jpg访问图片而不需要 Controller 做转发。处理上传时还要注意的是文件名校验和类型白名单我限制了只能传 jpg、png、gif、bmp超过 5MB 直接拒绝文件名用 UUID 重命名防止中文路径或特殊字符导致的各种诡异问题。4.5 性能优化与数据量增长后的应对实验室报修系统运行一段时间后业务数据增长带来的性能问题会逐渐暴露。我最先遇到的是工单列表查询变慢。定位方式是 MySQL 执行计划看慢日志加了工时status字段的区分度其实不高取值只有 6 个但查询条件里又确实要筛它。此时的最佳实践是建联合索引比如(status, create_time)是高频查询列因为列表页总是按状态过滤并按时间倒序排列。同理device_id字段在工单表里也是高频查询建普通索引就够了。我还在列表页默认只查近三个月的工单通过时间筛选减少扫描范围。统计报表页因为涉及复杂聚合如果数据量继续上升更合理的选择是做一张物化统计表每天定时跑批汇总。这个优化我只在文档里做了设计实际项目数据量还没到那一步但对毕设答辩和有后续计划的同学来说这可以作为性能优化章节的一个亮点去补充。5. 写在最后的个人体会这个项目做下来我最深的体会就是SSM 虽然老但用它把一套完整业务跑通的收获远比“会用 Spring Boot 自动装配”大得多。你会在配置文件的堆叠中理解框架运行的本质会在状态机的梳理中学会抽象业务流程会在权限拦截的调整中体会到系统安全的细节。如果非要说有什么建议我会建议后来者做类似系统时不要在代码量上追求大而全而是在业务闭环上做深一层。比如工单状态、消息通知、操作日志这些流程度的东西像毛细血管一样覆盖整个系统把它们做得足够细系统才真的可用。再分享一个小经验项目开发过程中每完成一个功能点就随手记录一下碰到的问题和解决方案。这个习惯在最终写文档、准备答辩、甚至复盘面试时帮了我大忙。很多当时觉得“不记也行”的小问题最后都成了展示项目深度时的谈资。做管理系统的本质不是会了某套框架而是能把一个真实的业务场景拆明白、跑得通。祝你们也能从这套系统中收获相同的经验。