基于SpringBoot+Vue的高校教研室活动管理系统开发全记录 每年到毕设季基于SpringBootVue的XX管理系统这类题目就会刷屏高校教研室活动管理系统是里面出场率特别高的一道题。听起来不过是个普通的CRUD项目但真的从需求分析做到上线部署你会发现它涉及了权限、审批流、文件、统计图表、前后端联调这些几乎每个企业级Web项目都会遇到的核心问题。这篇文章是我自己的完整落地记录把业务建模、表结构设计、后端接口与前端页面实现、以及联调和部署阶段遇到的那些坑都捋一遍适合正在做类似题目、或者准备接手一个小型前后端分离项目的同学参考。1. 先弄清楚业务教研室的活动管理不是只有增删改查很多同学拿到这个题目第一反应就是活动表建好写个接口页面列表展示完事。真这么干到了中期答辩很容易被问住你的活动是什么类型的活动从发起到归档经历了哪些阶段教研室主任在系统里到底要审什么这些问题如果不先想清楚后面写代码就是边写边改痛苦得很。1.1 这个系统的用户和权限到底怎么分高校教研室活动管理系统的用户角色我最终收敛成了三类系统管理员维护用户信息、重置密码、管理基础数据比如活动类型字典、教室场地信息一般不管具体业务审批。教研室主任负责审批本教研室的活动计划查看教研室的整体活动统计。普通教师可以发起活动、填写活动记录、上传附件、查看自己参与过的活动。权限矩阵大概是这样的功能模块管理员教研室主任普通教师活动发起否是是活动审批否是否活动记录填写否是是活动归档查看是是是仅本人参与用户管理是否否数据统计是本教研室否这里有一个很容易踩的误区一开始我把活动发起权限只给了普通教师但实际调研下来教研室主任自己也要组织活动比如学期初的教研计划会议所以主任同样需要能发起活动。权限不是简单按角色等级划分的要按业务动作划分这是我从这个项目里学到的第一课。1.2 活动全生命周期与状态流转教研室活动不是创建完就结束了它有一个完整的生命周期草稿 → 待审批 → 已通过 / 已驳回 → 已开展 → 已归档活动发起人先填一份活动计划包括活动主题、类型、参与人员、时间地点、经费预算这些信息存为草稿可以反复修改。确认无误后提交给教研室主任审批主任通过后才能正式开展。活动实际办完之后发起人再补充活动记录、现场照片、签到表等附件提交归档。归档后的活动这才算真正结束可以被统计、被查阅。这个状态机是整个系统业务复杂度最高的地方后面章节我会单独讲后端怎么实现。你只需要先记住一个结论活动表里必须有 status 字段而且状态流转必须在前端和后端双重校验尤其是后端不能信前端传过来的状态。1.3 需求裁剪别把毕设做成大杂烩我在网上看到很多类似题目的开题报告动不动就写用户管理、课程管理、科研管理、考勤管理、会议室预约、问卷调查……这其实是个大坑。毕设考察的是你把一个完整项目做扎实的能力而不是功能堆得越多越好。功能一多每块都做得稀碎答辩反而不讨好。我最后只保留了五个核心业务模块活动发起与审批、活动记录与归档、通知公告、统计报表、系统管理。这样既能覆盖业务主线又能展示技术深度——审批流程涉及状态流转统计报表涉及SQL聚合通知公告涉及站内信最近创建的数据系统管理涉及密码加密和角色权限。麻雀虽小五脏俱全。2. 技术选型与工程初始化SpringBootVue这套组合好在哪里这个题目限定了SpringBoot和Vue但具体用哪个版本、搭配什么ORM框架、用什么权限方案里面还是有很多门道。选对了后面一路顺畅选错了光环境问题就能折腾两三天。2.1 后端用SpringBoot的版本选择问题SpringBoot目前最常见的是2.7.x和3.x两个大系列。这里要特别提醒SpringBoot 3.x 要求JDK17及以上而很多同学本机装的还是JDK8如果选了SpringBoot 3环境就得先折腾一轮而且一些老版本的MyBatis-Plus、Knife4j跟SpringBoot 3不兼容需要升级对应版本。我最终选的是SpringBoot 2.7.18 JDK8这个组合最稳妥网上资料最多遇到问题随便一搜就有答案。选它的理由也很实在JDK8在高校的实验室机器、答辩机器上几乎都装了兼容性最好。MyBatis-Plus、JWT、EasyExcel这些常用库对SpringBoot 2.x的适配最成熟。内嵌Tomcat打包成jar直接java -jar就能跑部署成本低。如果你是交作业阶段用JDK8绝对不会出幺蛾子如果你有余力想秀一下新特性再考虑SpringBoot 3 JDK17不迟但不要拿毕设当新技术试验田。2.2 前端Vue2与Vue3的选择Vue这边我选的是Vue2 Element UI。虽然Vue3已经是当前的主流但毕设场景下Vue2的优势非常明显Element UI组件库稳定表格、表单、弹窗这些后台管理系统的标配组件开箱即用。网上基于Vue2 Element UI的后台管理系统模板多如牛毛想做权限路由、侧边栏、面包屑直接参考现成方案。Vue3对应的Element Plus同样不错但如果对组合式API不够熟写起来反而不如Vue2的选项式API顺手。如果你本来对Vue2已经很熟没必要为了用新版而花时间去适应Vue3的差异。毕设的核心是把系统做完不是比拼版本号。2.3 工程结构与项目初始化前后端分离的工程结构我建议直接分成两个目录teacher-research-activity/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/com/example/activity/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ │ ├── entity/ # 数据库实体类 │ │ ├── dto/ # 请求和响应对象 │ │ ├── config/ # 配置类跨域、MyBatisPlus分页等 │ │ ├── common/ # 统一返回体、异常处理、工具类 │ │ └── security/ # JWT认证相关的过滤器 │ └── src/main/resources/ │ ├── mapper/ # XML文件复杂SQL │ └── application.yml └── frontend/ # Vue前端 ├── src/ │ ├── api/ # axios请求封装 │ ├── components/ # 自定义组件 │ ├── views/ # 页面组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ └── utils/ # 工具函数token存取等 └── package.json建工程的时候有个小技巧后端用 Spring Initializr 生成最省事勾选Spring Web和MySQL Driver就行前端用vue create创建选上Router和Vuex。完事之后立刻提交一个git初始版本后面每个模块做完再提交一次回滚也方便。3. 数据库表设计与后端实现核心是活动表和审批记录表后端最核心的产出就是数据库表和这层接口。我画表结构的时候推翻过两版最后沉淀下来的方案是这样的直接说结论和理由。3.1 表结构设计一共六张表核心关联关系是用户-活动一对多、活动-审批记录一对多、活动-附件一对多。-- 用户表 CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 姓名, role tinyint(4) NOT NULL DEFAULT 3 COMMENT 角色 1管理员 2教研室主任 3教师, department_id bigint(20) DEFAULT NULL COMMENT 所属教研室, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 活动表 CREATE TABLE teaching_activity ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 活动标题, type tinyint(4) NOT NULL COMMENT 活动类型 1教学研讨 2集体备课 3听课评课 4学术讲座, content text COMMENT 活动内容/计划详情, location varchar(200) DEFAULT NULL COMMENT 活动地点, start_time datetime NOT NULL COMMENT 开始时间, end_time datetime NOT NULL COMMENT 结束时间, creator_id bigint(20) NOT NULL COMMENT 发起人, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态 0草稿 1待审批 2已通过 3已驳回 4已开展 5已归档, actual_summary text COMMENT 实际开展总结, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_creator_id (creator_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 审批记录表 CREATE TABLE activity_approval ( id bigint(20) NOT NULL AUTO_INCREMENT, activity_id bigint(20) NOT NULL COMMENT 活动ID, approver_id bigint(20) NOT NULL COMMENT 审批人, approval_result tinyint(4) NOT NULL COMMENT 审批结果 1通过 2驳回, comment varchar(500) DEFAULT NULL COMMENT 审批意见, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_activity_id (activity_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么审批要单独建表而不是在活动表里直接加几个字段这是很多同学容易忽略的地方。如果只记录审批结果和审批意见这两个字段确实够用但一旦活动被驳回后修改重新提交你就会发现问题审批意见覆盖了之前驳回的原因丢了完全没有审计痕迹。单独建审批记录表一条活动可以对应多条审批记录每次审批动作都有据可查哪怕下次被驳回也能对比前后两次的意见。这就是设计服务于业务演进。班级/教研室这种基础表比较简单我就用department表存学院下辖的教研室名称。附件表就是activity_attachment存文件名、存储路径、上传人、关联活动ID这里也不再赘述。3.2 后端接口与认证实现后端接口我走的是RESTful风格所有接口前缀/api统一返回体是{ code: 200, message: 操作成功, data: {} }统一返回体用泛型类实现类似下面这样Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }认证方案用的JWT。流程是登录成功后后端生成JWT返回给前端前端存到localStorage每次请求在请求头里带上Authorization: Bearer token后端写一个拦截器统一校验。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { // 解析token把用户ID放入request Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // token无效 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }JWT这个方案单独用足够安全吗对于答辩项目来说完全够了。但要注意token过期时间的设置我设的是12小时避免用户写一篇活动计划写到一半token过期体验很差。Base64、密钥这些不要写死在前端密钥放后端的配置文件里。3.3 审批状态流转的实现细节活动状态流转是业务里最容易被搞出bug的地方核心问题是并发情况下状态错乱。举例主任在审批页面点通过同时发起人把活动重新编辑提交了如果代码不加以限制就可能导致已通过的活动又变成待审批或者出现审批结果和活动状态不一致的情况。我的处理方案比较朴素但可靠更新活动状态时在SQL里带上前置状态条件比如update idupdateStatusWithLock UPDATE teaching_activity SET status #{newStatus} WHERE id #{activityId} AND status #{expectStatus} /update如果影响行数为0说明当前状态并不是你预期的那个状态直接抛出操作冲突请刷新页面后重试。这个方案相当于数据库层面的乐观锁不需要引入Redisson这种分布式锁对这个项目来说完全够用。后端具体审批逻辑伪代码如下Transactional(rollbackFor Exception.class) public void approve(Long activityId, Long approverId, Integer result, String comment) { TeachingActivity activity activityMapper.selectById(activityId); if (activity null) { throw new BusinessException(活动不存在); } if (activity.getStatus() ! TeachingActivity.STATUS_PENDING_APPROVAL) { throw new BusinessException(当前活动不在待审批状态); } // 状态流转通过 - 2驳回 - 3 int newStatus result 1 ? TeachingActivity.STATUS_APPROVED : TeachingActivity.STATUS_REJECTED; int affected activityMapper.updateStatusWithLock( activityId, newStatus, TeachingActivity.STATUS_PENDING_APPROVAL); if (affected 0) { throw new BusinessException(操作冲突请刷新后重试); } // 插入审批记录 ActivityApproval approval new ActivityApproval(); approval.setActivityId(activityId); approval.setApproverId(approverId); approval.setApprovalResult(result); approval.setComment(comment); approvalMapper.insert(approval); }Transactional一定要加审批记录插入和活动状态更新要么同时成功要么同时失败否则数据库里就会留下活动状态变了但审批记录没了这种脏数据。4. 前端页面从列表到日历Vue组件化的落地前端的核心工作是接接口、做交互、展示数据。这个系统里我总共写了9个页面最值得展开说的有三个活动列表页、审批页、统计报表页。4.1 登录与权限路由前端登录页调用后端/api/auth/login成功后把token存在localStorage同时把用户信息存到Vuex。路由守卫里统一判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })权限控制不能只靠路由守卫菜单本身也要做动态渲染。后端登录接口返回的role字段保存在Vuex里侧边栏菜单用v-if判断角色这样教师登录就看不到用户管理和审批中心这两个菜单项。菜单显示只是第一层后端的接口权限校验才是关键。我是在拦截器里加了一点扩展对/api/admin/**这种路径额外校验角色普通教师即使自己拼URL也调不通这些接口。前端隐藏菜单只是提升体验后端校验才是安全底线。4.2 活动列表页与筛选条件活动列表页是最常见的表格页Element UI的el-table配合分页el-pagination。但这里有个细节值得琢磨筛选条件区。我提供了四个筛选条件活动类型、活动状态、时间范围开始时间和结束时间、关键字搜索。前端把这些条件封装成一个queryParams对象传给后端后端用MyBatis-Plus的Page和LambdaQueryWrapper做条件分页查询。查询接口的写法public PageTeachingActivity queryActivityPage(ActivityQueryDTO dto) { PageTeachingActivity page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperTeachingActivity wrapper new LambdaQueryWrapper(); // 类型条件注意判空 wrapper.eq(dto.getType() ! null, TeachingActivity::getType, dto.getType()); // 状态条件 wrapper.eq(dto.getStatus() ! null, TeachingActivity::getStatus, dto.getStatus()); // 时间范围 wrapper.ge(dto.getStartTime() ! null, TeachingActivity::getStartTime, dto.getStartTime()); wrapper.le(dto.getEndTime() ! null, TeachingActivity::getEndTime, dto.getEndTime()); // 标题关键字模糊搜索 wrapper.like(StringUtils.hasText(dto.getKeyword()), TeachingActivity::getTitle, dto.getKeyword()); // 按创建时间倒序 wrapper.orderByDesc(TeachingActivity::getCreateTime); return activityMapper.selectPage(page, wrapper); }分页和自动填充这些基础配置MyBatis-Plus封得非常完善你只需要在启动类上配置分页插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }列表页看起来简单但它决定了用户对这个系统的第一印象筛选条件好不好用、分页是否准确、数据加载是否带骨架屏这些细节做扎实了答辩的时候都是加分项。4.3 活动日历视图与统计报表系统里我额外做了一个日历视图这是展示前端能力的一个亮点。用fullcalendar插件或Element UI的el-calendar都能实现。我采用的是el-calendar自定义日期单元格内容按活动类型显示不同的颜色标签点击某天能看到当天的活动列表。这个功能很多人以为很难其实原理很简单后端提供一个接口传一个月份参数返回当月所有已发布活动前端按日期字段分组展示到对应日期单元格里。真正麻烦的是时间跨天的活动如何展示——我当时的策略是按开始日期归类月视图里跨天活动只在开始那天显示进入详情页才看到完整时间避免一个活动占掉两个格子导致日历错乱。统计报表用ECharts这三个图最有代表性活动类型分布饼图展示本学期各类教研活动占比。每月活动数量柱状图直观看到教研活动的高峰期在几月。教研室活跃度排行榜横向条形图按教研室统计活动总数排个名。ECharts在Vue里的标准用法是// 组件里引入 import * as echarts from echarts // 图表初始化 mounted() { this.chart echarts.init(this.$refs.chartRef) this.loadChartData() } // 后端返回统计接口前端setOption this.chart.setOption({ series: [{ type: pie, data: this.chartData }] })一个比较容易被问到的点是统计是前端算还是后端算前端拿到全部数据再算会很慢而且如果数据量大接口会卡。所以我坚持让后端直接返回聚合后的数据用一条SQL搞定SELECT type, COUNT(*) AS count FROM teaching_activity WHERE status IN (2, 4, 5) AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY type前端只负责把后端聚合好的数据渲染成图这样页面加载快SQL的聚合逻辑也在后端调优空间更大。5. 联调阶段的高频问题跨域、日期、文件上传前后端分离之后联调阶段的问题比想象的要多。这三个坑我印象最深几乎每个做这个题目的同学都可能踩到。5.1 跨域配置前端跑在localhost:8080后端跑在localhost:8081前端用axios请求后端接口浏览器直接给你报跨域错误。解决方法就是在后端加一个配置类允许来源、方法、请求头Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意有JWT拦截器的情况下OPTIONS预检请求会被拦截到需要在拦截器里对OPTIONS请求直接放行否则前端一样报跨域。这个OPTIONS预检被拦截的坑我修了一个晚上后来查日志才发现是拦截器把预检请求拦了返回401浏览器就认为跨域失败。加上if (OPTIONS.equals(request.getMethod())) { return true; }这一行就全部解决。5.2 日期序列化与前端显示后端LocalDateTime返回给前端默认会序列化成2024-05-20T10:30:00这种带T的格式前端直接显示或者放进日期选择器都会出问题。统一解决方法是配置全局的JSON序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8time-zone: GMT8也很关键不配的话多地服务器部署时可能差8个小时。数据库连接串里也建议加上serverTimezoneAsia/Shanghai保证MySQL连接用的时区正确。前端再配合封装好的日期格式化工具把接口返回的字符串格式化成想要的格式日期这块就彻底理顺了。5.3 文件上传大小与类型限制活动记录往往要上传活动照片、签到表扫描件。SpringBoot默认的上传大小限制是1MB传几张照片就直接报MaxUploadSizeExceededException。在application.yml里调大限制spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB但只是调大限制还不够还要对文件类型做校验。最直接的方式是白名单校验扩展名和Content-Type而不仅仅信任前端传的文件名private static final SetString ALLOWED_EXTENSIONS Set.of(jpg, jpeg, png, pdf, doc, docx, xls, xlsx); public void validateFile(MultipartFile file) { String originalFilename file.getOriginalFilename(); String extension originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); if (!ALLOWED_EXTENSIONS.contains(extension)) { throw new BusinessException(不支持的文件类型 extension); } if (file.getSize() 10 * 1024 * 1024) { throw new BusinessException(文件大小不能超过10MB); } }文件存储路径我建议放在后端项目外的独立目录比如D:/activity-files/不要放在项目内resources/static下否则每次重新打包都会把历史文件覆盖掉。然后提供一个文件访问接口用静态资源映射的方式对外提供访问Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file:D:/activity-files/); } }上传成功后返回/files/xxx.jpg这个相对路径前端拼上后端地址就能访问这样即使以后换服务器只改这一处配置就行。6. 打包部署与验收前的自查清单项目做完之后就是打包部署和验收。这一部分虽然不花什么时间但有不少细节容易翻车。6.1 前端打包与Nginx配置前端打包npm run build执行完会在frontend/dist生成静态文件把整个dist目录丢到服务器上用Nginx托管。后端打包mvn clean package -DskipTests生成target/activity-server.jar直接扔服务器上java -jar activity-server.jar --spring.profiles.activeprodNginx的关键配置是这样的server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/activity-frontend; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 文件访问 location /files/ { proxy_pass http://127.0.0.1:8081; } }try_files $uri $uri/ /index.html;这行很重要Vue是单页应用前端路由用的是history模式不配这行的话刷新非首页的URL会404。6.2 数据库初始化数据库初始化建议准备一个init.sql脚本里面包含建库语句和初始化数据管理员账号、教研室数据。初始化管理员账号时密码一定要用BCrypt加密后的密文别直接明文存进去。我用的工具是 DataGrip但为了答辩演示方便建议把SQL脚本放到项目的doc/目录下评委问起来就直接打开来看比在数据库工具里翻半天显得专业得多。6.3 验收前应该检查的细节基于我的经验验收前花不到一小时做过一遍自测能避免90%的翻车。以下是我整理的自查清单[ ] 教师登录只能看到自己相关的活动不能看到别的老师创建的活动。[ ] 管理员能看全部活动但不能审批注意后端接口校验。[ ] 活动从草稿提交后状态变为待审批此时不能再编辑。[ ] 活动被驳回后重新编辑提交审批记录表能保留两条不同的记录。[ ] 活动归档时没有填写实际总结的不允许提交。[ ] 上传大文件能正常提示文件过大而不是页面卡死或报500。[ ] 日历视图跨月显示正常跨天活动只出现在开始日期。[ ] 统计报表在12月31日跨年附近的数据也能正确聚合。[ ] 刷新页面不会跳回登录页Token在localStorage里还存在。[ ] 管理员重置用户密码后老Token立即失效。最后这条我当时差点踩雷用户被重置密码后原本的JWT还在有效期理论上还能继续用。后来在拦截器里加了一个判断从数据库里查用户的密码更新时间和token签发时间对比token签发时间早于密码更新时间就判定过期。这个细节其实体现的是你对认证体系理解得透不透答辩时是一个亮点。7. 几个可以继续扩展的方向如果做完上述内容还有余力或者想在答辩时展示更强的设计能力可以考虑下面这几个方向消息通知活动审批结果通过后站内消息或邮件通知发起人。我用的是WebSocket做实时通知配合前端右上角红点提示效果挺直观。活动签到二维码签到老师扫一下二维码就能完成签到签到记录存表。这个功能适合展示你对移动端交互的理解。Excel导入导出用EasyExcel做教研活动数据的批量导入和统计报表导出在答辩演示时把几十行活动数据一键导出Excel比截图PPT有说服力。多级审批目前是一级审批如果加上学院分管领导做二级审批状态机复杂度会提升但技术亮点也会增加适合对状态模式有自信的同学。我自己带过几个做类似题目的朋友最后的体会是这一类Web管理系统真正拉开差距的不是谁用的框架新而是谁对业务理解得透、谁的代码结构清晰、谁的异常处理完整、谁的细节做得扎实。一个大部分功能都正常、偶尔有边角bug但能讲清楚原因的项目远好过一个功能满满但哪里都打折扣的项目。做的时候多留个心眼每个模块做完都自己点一遍比最后集中测试省心得多。