☰
SpringBoot+Vue科研验收管理系统实战:状态机、动态SQL与部署全解析
2026/10/15 4:47:47 网站建设 项目流程

这个项目的标题里有“管理系统”重复了两遍,我猜是发布时笔误,但这不影响它本身的定位:这是一套面向企业级科研项目的验收管理系统,技术栈是 SpringBoot + Vue + MyBatis + MySQL。说句实话,科研项目验收这个场景在管理系统里属于“看着简单、做起来绕”的类型,流程长、角色多、材料杂、审批链路容易乱。如果只是做增删改查那确实不难,难的是把验收状态机、材料版本、专家评审、审批记录这些串起来还能不出错。

我前前后后做过两套类似的系统,一套是某研究院的纵向课题验收,一套是某公司的内部研发项目结项。踩了不少坑,也总结了一些我认为比较稳妥的做法。这篇文章就把这套系统的架构拆解、数据库设计、后端接口、前端页面、部署排错完整过一遍,适合正在做类似管理系统、或者想拿这类项目练手的开发者参考。

1. 项目全貌与核心价值

1.1 科研验收管理到底管什么

科研项目验收这件事,在企业里往往比想象中复杂。表面看是“项目做完了,领导签字确认”,实际拆开有这几个环节:项目基本信息登记、任务书/合同上传、成果材料提交、专家评审、验收结论确认、文件归档、后续查询追溯。

最麻烦的不是某一个环节,而是环节之间的状态衔接。项目可能处于“材料补交中”“专家评审中”“结论待审批”“已驳回需整改”等状态,每个状态对应不同的操作权限。如果流程没设计好,就会出现材料传了一半丢了、专家评完分管领导没看到、项目驳回后状态没回退这类问题。

这套系统解决的正是这些问题。它的定位不是一个普通的档案管理工具,而是把科研项目从“申请验收”到“完成归档”的整个流程管起来,关键节点透明、操作留痕、状态可查,最终形成一份完整的验收档案。

1.2 技术选型为什么是这一套

SpringBoot + Vue + MyBatis + MySQL 这个组合,在这个场景下可以说是“稳”字当头,不是最炫,但一定是最适合业务落地的。

SpringBoot 负责的是后端接口和业务逻辑。验收系统里有很多事务性操作,比如项目提交时要同时更新项目状态、写入申请记录、创建待办事项,必须在一个事务里做。SpringBoot 的声明式事务一个小注解就能搞定,开发效率很高。它内置的定时任务还能处理评审超时提醒、验收到期通知这类需求。

Vue 负责前端交互。验收系统的用户包括科研人员、科研秘书、专家、分管领导,角色差异大,页面形态差异也大。Vue 的组件化开发让这些不同角色的页面可以拆分成独立组件复用,比如文件上传组件、审批意见组件、状态时间线组件,写一次到处用。

MyBatis 是这类业务系统的“老朋友”。验收管理里查询条件极其多变:按项目名称模糊查、按负责人查、按状态查、按时间范围查、按经费来源查,组合一起就是动态 SQL。MyBatis 的<where>、<if>标签写动态条件非常顺手,比 JPA 那种自动生成 SQL 的方式更容易控制执行计划,性能问题也好排查。

MySQL 不用多说,事务支持成熟、部署成本低、运维资料多,企业内网环境基本都能接受。这套系统的数据量不会大到需要分布式数据库的程度,单库单表的 MySQL 配合合理索引完全够用。

2. 数据库设计:从验收流程倒推表结构

2.1 核心表与字段设计思路

我做这类系统有个习惯,先画流程图再建表,不先从实体关系入手。因为验收系统的核心是流程,表结构就该围绕流程节点去铺。

基础表是用户表和角色表,用户表放账号、姓名、部门、手机号、邮箱,角色表放角色编码。角色和用户是多对多,中间表关联。表结构上大家都会写,重点是业务表。

科研项目表是业务起点,我习惯叫它 project_info。字段包含项目编号、项目名称、负责人、承担部门、项目来源、经费总额、计划开始时间、计划结束时间、当前状态、创建人、创建时间。项目编号建议用业务规则生成,比如年份+部门编码+流水号,方便后续检索。

验收申请是另一个核心表,acceptance_application。一项目只允许一条有效的验收申请记录,设计上要在项目编号字段上加唯一索引,避免并发提交时产生两条申请。字段上包括申请编号、项目主键、申请日期、申请说明、验收类型(结题验收/中期检查)、当前阶段、提交状态。

材料表是科研验收系统比较特殊的表。一份验收材料往往是多层级的,项目下有任务书、工作报告、技术报告、财务决算表,每一类下面又可能有多个附件文件。我建议把材料分类和附件分开:material_category 存分类信息,attachment_file 存文件元信息。attachment_file 的字段有原始文件名、存储路径、文件大小、文件类型、上传人、上传时间、属于哪个材料分类、版本号。附件表一定要存版本号,后面验收整改重新上传时,版本控制能帮你追溯“领导最终批的是哪一版材料”。

评审相关表是流程的核心。我拆了 review_task 和 review_score 两张表。review_task 记录“某一次评审活动”,字段有任务编号、关联验收申请、评审组长、评审截止时间、评审意见汇总;review_score 是评审组每个人的打分和意见,字段有关联评审任务、评审人、评分、是否同意通过、评审意见。拆两张表而不是合成一张,是因为一个评审任务有多位专家参与,每位专家的意见必须单独留痕。

审批记录表 approval_record 记录了整个验收过程中每一次节点流转。字段很简单:关联业务主键、操作人、动作(提交/初审通过/驳回/归档)、审批意见、操作时间。这张表是验收系统追溯的关键,领导问“这个项目怎么走到这步的”,查这张表一目了然。

2.2 状态机与节点流转设计

状态字段我统一用整数存储,每个状态在代码里定义常量或枚举,不要在数据库里直接用中文状态,否则后续写统计 SQL 非常痛苦。

这套系统的完整状态链路我设计成:

0 草稿 → 1 已提交 → 2 初审中 → 3 材料补交 → 4 专家评审中 → 5 待领导审批 → 6 已通过 → 7 已驳回 → 8 已归档

注意,这个状态机不是单纯的线性推进。已驳回状态会重新回到草稿或材料补交阶段,项目状态要支持“状态回退”。这就是为什么不能用简单的 update 方式,必须在代码里判断当前状态是否允许跳到目标状态。

我建议在代码里维护一个状态流转表,用一个 Map 或者数据库表记录“当前状态+操作 → 目标状态”。例如:

  • 草稿 + 提交操作 → 已提交
  • 已提交 + 初审通过 → 专家评审中
  • 已提交 + 初审驳回 → 材料补交
  • 材料补交 + 重新提交 → 已提交
  • 专家评审中 + 评审完成 → 待领导审批
  • 待领导审批 + 审批通过 → 已通过
  • 待领导审批 + 审批驳回 → 材料补交
  • 已通过 + 归档操作 → 已归档

这样做的好处是,新增状态时只需要在流转表里加一条规则,不会因为漏写 if 判断导致状态卡死。这是我第一次做流程系统时踩过的大坑,当时在多个 Service 方法里写 if else 判断状态,后面加需求改了七八处才改全,第二次直接切到状态机方式清爽很多。

还要注意并发问题。两个用户同时操作同一个项目状态,可能前一个提交了,后一个又把状态改了。我的处理方式是在状态更新 SQL 里带上旧状态条件,也就是乐观锁的思路。更新时 set status = 新状态 where id = ? and status = 旧状态,如果影响行数为 0,说明状态已被其他事务修改,直接抛异常提示用户刷新页面。

3. 后端核心模块落地:SpringBoot + MyBatis

3.1 工程结构与代码分层

后端工程我按功能模块分包,而不是按技术层次硬拆。简单说,不要把 controller、service、mapper 这种按技术类型建三个大包,而是把“项目模块”“验收模块”“评审模块”“系统管理模块”作为一级分包,每个模块内部再建 controller、service、mapper 子包。

这个习惯是从实际维护里学来的。按技术分层,时间长了代码都在 controller 里堆着,因为按技术分层时开发者不想跨多个目录改文件。按业务模块分包,改动一个功能基本只要进一个包,内聚性好很多。

基础包结构大致如下:

com.example.acceptance ├── common // 通用类:统一返回、全局异常、状态枚举、工具类 ├── config // 配置类:事务、拦截器、文件上传、跨域 ├── security // 登录认证、权限校验 ├── module │ ├── project // 项目管理:controller/service/mapper/entity │ ├── acceptance // 验收申请与材料 │ ├── review // 专家评审 │ ├── approval // 审批流程 │ └── system // 用户、角色、菜单 └── Quartz 定时任务

统一返回类是我强烈建议从一开始就设计的。所有接口返回统一格式{ code: 0, message: "success", data: {} },前端拿到后直接判断 code。这个习惯能省掉非常多前后端联调的扯皮。我之前接手的系统就是每个接口返回格式都不一样,有的直接返回对象,有的返回数组,有的返回带 message 的 Map,前端每个接口写单独判断逻辑,维护成本极高。

全局异常处理也一样,用 @RestControllerAdvice 兜底。业务异常、参数校验异常、数据库异常、未知异常分别处理,错误信息统一格式返回。数据库异常要特别处理,不要把原始 SQL 异常直接抛给前端,用户看到“Duplicate entry”这种报错完全不知道怎么办。

3.2 验收申报流程的接口实现

验收申报是系统使用频率最高的功能,接口设计要特别仔细。整体我用 POST 请求,一个创建接口、一个提交接口、一个材料上传接口。

创建接口/api/acceptance/create接收项目编号、申请说明、验收类型,Service 里做的事包括:校验项目是否处于可申请验收状态、校验是否已有进行中的验收申请、创建 acceptance_application 记录、把 project_info 状态改为草稿。这三步必须在一个事务里,任何一个失败都要整体回滚。

提交接口/api/acceptance/submit接收申请编号和材料分类信息。这里是整个系统最容易出问题的地方。提交时要做几件事:更新申请状态为已提交、更新项目状态为已提交、写入审批记录“提交验收申请”、给科研秘书生成待办消息。这些写入涉及四张表,事务必须覆盖。

我每次写这类多表写入时都会提醒自己:先查后写、写前再查。提交之前必须重新查一次当前状态,确认不是已提交状态才允许操作,防止用户双击提交按钮导致重复提交。前端按钮加了 loading 状态,后端也要加幂等处理。最稳妥的做法是在提交接口上用分布式锁或数据库乐观锁。对于这个量级的系统,分布式锁杀鸡用牛刀,数据库乐观锁完全够。

材料上传接口我单独拆一个/api/attachment/upload,不跟提交逻辑混在一起。这样用户可以分批次上传材料,每传完一个立即保存,避免一次性上传大文件超时导致整单提交失败。文件保存路径按业务维度组织,比如/data/acceptance/{申请编号}/{分类编码}/{文件名},后续归档打包时直接按目录整体拷贝,非常方便。

3.3 权限模型与角色控制

验收系统的角色我建议拆五个:系统管理员、科研人员、科研秘书、评审专家、单位领导。每个角色的操作范围要明确,不能靠前端隐藏按钮来控制权限,必须后端接口做权限校验。

这里有个细节容易被忽略:角色和动作不是简单的一对一关系。科研秘书在初审阶段是操作者,在归档阶段也是操作者,但在专家评审阶段不能看打分明细,因为专家打分需要保密。所以我的设计里,权限控制不仅仅校验“角色”,还校验“项目当前状态”。

实现方式不复杂,在 Service 层开头加一个权限校验方法,传入项目状态和当前用户角色,返回是否有权限执行当前操作。比如“查看专家评分明细”这个操作,科研秘书和科研人员都不允许,只有系统管理员和单位领导允许。“提交验收申请”这个操作,只有项目负责人(科研人员角色)允许,而且要求项目当前状态必须是草稿。

技术实现上我是用拦截器 + 自定义注解双保险。拦截器校验登录状态和 token,注解校验具体操作权限。登录认证用的 JWT,token 里不存敏感信息,只放用户 ID、角色编码、过期时间。JWT 无状态,适合前后端分离的架构,后端不需要维护会话,减少内存占用。

4. 前端实现要点:Vue 为核心的工程化实践

4.1 项目结构与权限路由

前端工程我用的 Vue CLI 搭建,配合 Vue Router 和状态管理。页面按角色维度组织,登录之后根据角色动态生成菜单,不同角色看到不同入口。

路由守卫是权限控制的第一道关卡。我在路由表里给每个页面配置 meta 信息,包含 allowedRoles 数组。用户登录后把角色信息存到状态管理里,每次路由跳转时判断当前用户角色是否在 allowedRoles 中,不在就重定向到首页或 403 页面。注意这只是前端体验优化,真正的安全校验必须靠后端接口完成。

组件拆分的思路要跟着业务走。验收系统的核心复用组件有三个:文件上传组件、审批记录时间线组件、状态标签组件。

文件上传组件封装了上传进度条、文件类型校验、文件大小校验、重传覆盖逻辑。这个组件做得合适,后续所有需要传文件的地方直接引用,不用每个页面单独写一遍。状态标签组件更小,就是根据状态值显示不同颜色的标签。别小看这个组件,验收系统到处都要显示项目状态,表格里、详情页里、审批记录里,统一封装后改样式只改一处。

4.2 核心业务页面设计与交互

项目列表页是科研人员最常看的页面。表格展示项目名称、项目编号、负责人、状态、最近更新时间,操作列根据状态显示不同按钮。草稿状态显示“编辑”“提交验收”,已通过状态显示“归档申请”,其他状态显示“查看详情”。按钮显隐逻辑用一个 computed 方法根据状态和角色计算,不要让前端页面到处写 if。

验收申请页面是流程的入口。页面分为三块:基本信息区(从项目信息带入,只读)、材料分类区(动态渲染各个材料分类,每个分类下点“上传文件”)、提交区(展示当前流程步骤,确认材料完整后点击提交)。

这里我特别想提一个交互细节:材料完整性校验不能只在前端做。前端校验用户看到的是“还需要上传材料”,但这个判断逻辑必须后端也有一套,因为用户可能绕过前端直接调接口提交。后端收到提交请求时,根据验收类型拿到必传材料分类清单,逐一检查 attachment_file 表中是否有对应分类下的文件,缺少任何一项直接驳回接口请求。前后端双校验,才能保证数据不脏。

专家评审页面要简洁。评分的项目信息、已上传的材料预览链接、打分项、整体意见,一屏放完,不要让专家来回滚动找内容。评审页还有一个隐藏需求:专家只能看到分配给自己的评审任务,不能看见其他专家的评分。数据隔离要在后端做好,查询接口带当前用户 ID 作为条件,不能只靠前端过滤。

4.3 表格、弹窗、消息提醒的实现细节

表格统一用分页组件,后端接口接收 pageNum、pageSize、排序字段、筛选条件,返回页数据加总条数。科研项目列表查询条件多,我建议把所有筛选条件放在一个对象里传到后端,后端根据非空字段拼动态 SQL。排序字段要注意白名单,不能直接拼接用户传入的字段名,否则有 SQL 注入风险。前端传的排序字段是固定字典值,后端映射到真实列名。

弹窗在审批场景用得最多。领导审批时弹窗展示项目概况、评审汇总意见、材料目录,领导只需要填写审批意见并选择通过或驳回。这里我建议把评审专家打分的明细默认收起,领导想看可以展开,避免一上来就信息过载。驳回时意见必填,防止领导随手一驳回,科研人员不知道改哪里。

消息提醒功能我是用定时任务实现的。每天早上扫描一次验收申请,超过预期评审截止时间还没有提交评审结论的任务,给评审专家和管理员推送站内消息。十点还没截止要评审的项目给项目负责人发推。消息表设计得简单点:接收人、消息内容、关联业务单号、是否已读、创建时间。前端在顶部导航栏放一个消息铃铛,轮询未读数量,有变化就刷新。

5. 关键流程链路与数据一致性

5.1 评审流程的状态推进规则

专家评审是整个验收链路中最难设计的环节,因为多个人参与,结果要汇总。我建议先提交个人评审结论,所有专家提交后再由秘书进行汇总确认,确认完成后状态才推进到“待领导审批”。

具体实现:评审任务创建时,根据项目领域从评审专家库中抽取专家,这里我建议规则可配。有的单位需要同领域三名专家,有的需要五名,系统的做法是在配置表中维护评审组人数。创建评审任务时给每位专家生成一条 review_task 关联记录,专家登录后能看到待评审列表。

评审状态判断用两个计数即可:评审任务关联的专家总数、已提交个人结论的专家数。相等时状态机自动把任务标记为“个人结论已齐”,页面提示秘书可以进行汇总。这里有个细节,一旦某位专家被判定不合格或申请回避,任务会重新抽取替补专家,总数会变,所以不要硬编码总数,每次查询时动态计算。

有个坑是专家提交个人结论后想修改。我在设计上允许在任务截止前撤回重提,但每次修改都要更新批注记录,留痕。这样后续领导问“这个结论怎么变过”,可以追溯修改历史。

5.2 多表事务与版本控制

验收系统里最复杂的一个事务发生在“审批通过并归档”这个动作。它要同时更新验收申请状态为已通过、项目状态为已归档、写入审批通过记录、生成归档编号、把附件从临时目录转移到归档目录、给所有相关附件打上归档标签。六七个操作分布在多张表,任何一步失败都会造成状态不一致。

事务只需在 Service 方法上打 @Transactional。要注意 rollbackFor 设置为 Exception.class,因为 Spring 默认只回滚 RuntimeException。如果归档过程中抛了个受检异常,不加这个参数会导致事务不生效,文件目录拷贝了一半,数据库状态却全变了,这问题很隐蔽。

文件版本控制这事我提过,再展开说。验收整改是常态,材料要反复修改。每次科研人员重新上传同一分类下的材料,我不建议直接覆盖旧文件,而是生成新版本。attachment_file 表里加 version 字段,同分类 + 同业务单号唯一,每次上传 version 加一。归档时选择最新版本,同时保留旧版文件路径,方便后续整改审计。

如果不用版本控制,会出现一种尴尬状况:项目已归档后,领导想查整改前的材料长什么样,结果旧文件被覆盖找不到了。这在上线后被审计单位质疑过,版本控制加完之后就再没人提了。

6. 数据库与接口性能优化实录

6.1 MyBatis 动态 SQL 与索引优化

验收系统最常见的性能瓶颈是列表查询。科研项目表动辄几万条数据,如果加上了复杂的条件查询子查询关联,性能就会走下坡路。我调优时发现的最典型问题是前端筛选“负责人”“部门”“来源”等条件时,后端 Java 代码里做字符串拼接,写出来的 SQL 是SELECT * FROM project_info WHERE 1=1这种低劣的方式。问题不大但很别扭。

MyBatis 的<where>配合<if>标签可以避免 1=1。更重要的是要关注索引设计。project_info 表我建了这些索引:project_name 前缀索引、responsible_person 普通索引、status 普通索引、update_time 普通索引。负责人和状态的组合查询很频繁,我给 responsible_person + status 建了联合索引。这里要提醒一下,单列索引跟联合索引的取舍不是绝对的,需要看实际执行计划,不要凭感觉建。

另一处性能隐患在附件表。查询一个项目当前某分类下的最新文件版本,SQL 写法是分组取 max(version)。数据量大了之后这条查询会比较慢,我给附件表的关联业务 ID + 分类编码建了联合索引,并且在业务上做了个小优化:项目归档时不频繁更新每条附件,而只是更新归档状态,减少 UPDATE 语句。

6.2 列表接口响应优化与数据分页

分页查询的 count 和 list 两条 SQL,工程上我建议用 MyBatis 分页插件处理,不要手写 limit 拼 SQL。插件能拦截执行计划,自动生成 count 语句,效率高也省代码。但是要留意,分页插件对复杂 SQL 的 count 生成偶尔会出错,比如多表 left join 时 count 出来的是关联后的行数,不是本表行数。我在系统里就遇到过统计数翻倍的问题,排查了半天才发现是 left join 产生的重复记录,解决办法是给分页查询的参数里加 distinct 标记,或者手动指定 count 的 SQL。

排序字段这个问题我说过。用户想要按项目更新时间排序、按经费排序、按状态排序,前端传orderBy=updateTime/orderBy=amount,后端做一个 white list 映射,不直接把字符串拼进 ORDER BY。这个习惯防的是 SQL 注入,防止用户在请求参数里拼接 hack 语句。

前端分页数据请求频率高,我还做了一层轻量缓存。项目基本信息在小范围内变化不大,列表页查询时如果请求参数和上一次完全一样,直接从缓存返回。用了一个本地 Map 加过期时间实现,没有引入 Redis。项目数据量更大、缓存要求更高时再上 Redis 也不迟。

7. 部署上线与避坑指南

7.1 环境配置与打包部署

前后端分离项目的部署,后端打 jar 包,前端打静态资源包,用 Nginx 做静态服务与反向代理。这是目前最标准的部署方式。

后端打包我在 pom.xml 里配置了 profile,区分开发、测试、生产三套配置。生产环境的数据库连接、文件存储路径、日志级别都抽到单独配置文件里,打包时通过-Pprod指定使用生产配置。这样做的好处是开发环境不会因为误用生产数据库而出事故。如果你不用 profile,所有配置写在一个 application.yml 里,上线之前容易漏改数据库地址。

前端打包时有个经典坑是接口地址。我用.env.production文件配置生产环境的接口地址,VUE_APP_API_BASE_URL=/api,Nginx 把 /api 前缀反向代理到后端服务。注意 baseURL 不要写死 IP 地址,否则后端换服务器前端又要重新打包。

Nginx 配置要点是解决前端路由 history 模式刷新 404 的问题。Vue Router 用 history 模式时,刷新/project/123这个路径,Nginx 找不到对应的静态文件就会返 404。配置里要加一个 try_files 规则,把所有路径回退到 index.html。这个配置当时搞了我不少时间,写完顺手记下来。

7.2 文件上传大小限制与跨域问题

后端默认的 Spring Boot 上传文件大小限制是 1MB,验收系统的材料文件经常是几十兆的 PDF。不修改配置的话,用户传大文件会收到 500 报错,且报错信息不明确。我改了两处:spring.servlet.multipart.max-file-size 和 max-request-size,分别设置单文件大小和单次请求总大小。建议单文件设 100MB,单次请求 200MB,根据实际材料体量调整。

如果项目里有很多超大视频或报告附件,100MB 也不太安全,建议做分片上传。前端把文件切成几 MB 的片段逐个请求,后端按片段顺序写入临时文件,全部传完后合并。分片上传实现复杂一些,但对科研项目里动辄几百兆的现场视频材料是必要的。这个系统受众是普通科研材料,我暂时保持 100MB 限制。

跨域问题也很常见。前端开发环境在 localhost:8080,后端在 localhost:8081,浏览器会拦截跨域请求。我用 SpringBoot 的 CorsFilter 配置跨域,允许来源为前端开发地址,允许携带凭证。生产环境走 Nginx 同源代理,不需要再开跨域。

7.3 数据库初始化和定时任务

第一次部署时数据库结构初始化,我建议直接用 SQL 脚本初始化,不要用框架的自动建表。开启 hibernate ddl-auto 自动建表看似方便,但表结构变更时容易丢数据。我在系统里维护了 db 目录,按版本号命名脚本,比如 v1.0_init.sql、v1.1_add_archive_record.sql。上线执行脚本时小心顺序,已经执行过的不要重复执行,最好手动记录版本。

定时任务的场景是消息提醒和超时检查,我用 Quartz 框架实现。两个任务:一个扫描超时未评审的项目,生成提醒消息;一个扫描接近验收期限的项目,提前发提醒。定时任务的执行时间建议错开,避免数据库并发扫描压力大,我配置的是每天上午九点和下午三点各执行一次。

Quartz 配置里还有个细节:生产环境要设置集群模式或者至少保证单实例运行。如果应用用多实例部署,定时任务每台都触发,消息会重复推送。我的做法是在配置文件里设置一个全局开关,只有主节点开启定时任务,备节点关闭。

8. 常见问题与排查心得

8.1 状态卡住不推进怎么办

我遇到过最多的是“状态没变化”的问题。用户点了提交,前端提示成功,但项目管理页面里状态还是“草稿”。排查路径一般是先去数据库查 acceptance_application 表,看申请记录的 status 字段,再去 project_info 表看 project_status 字段。很多情况下是两个字段其中一个没同步,导致页面显示的是另一个字段。

根本原因是状态更新分散在多处而不是统一做状态机管理。我后来把状态推进整合到一个类里面,提供统一的方法“推进状态(业务单号,当前状态,目标状态,操作人)”,所有节点流转都调用这个方法,同步更新申请状态和项目状态。这样做之后再没出现过单边更新的情况。

如果已经上线了且线上数据出了这类问题,补数据脚本也很好写:根据审批记录的最后一个动作和操作时间,推导应该处于什么状态,然后更新对应的表。

8.2 MyBatis 的 N+1 查询问题

列表页查询项目时显示负责人姓名,常规思路是查询项目表拿到 responsible_person_id,再在循环里逐个查用户表拿姓名,这就是典型的 N+1 问题。数据量小的时候感觉不到,项目到几百条后接口响应从几百毫秒飙升到十几秒。

解决方法是查询时直接联表,SQL 里 join sys_user 表查出负责人姓名,一条 SQL 返回所有数据。MyBatis 的 resultMap 配置关联映射,不要用嵌套 select 的方式,嵌套查询其实就是 N+1。这是我最初做 MyBatis 项目常犯的毛病,后来写 mapper 时严格检查有没有在循环里执行查询。

排查 N+1 的时候可以打开 MyBatis 的 SQL 日志,看到“查询项目列表”这条 SQL 之后跟着几十条“根据主键查用户”的 SQL,基本就可以确定是 N+1。开发环境开启 SQL 日志,生产环境用慢查询日志配合排查。

8.3 并发提交与重复数据

两个科研人员同时提交同一个项目的验收申请,后端都通过了项目状态校验,各创建一条申请记录,这种情况在并发下是有可能发生的。我在设计时用唯一索引解决,acceptance_application 表对 project_id 加了唯一约束,第二次插入直接抛异常,再由全局异常处理器统一转成“该项目已有进行中的验收申请”。

这里要强调,唯一索引是数据安全的最后一道防线,业务代码里的判断只是减少触发概率,不能作为唯一保障。尤其对创建审批、创建验收申请这类操作,我建议都找一找是否存在天然的唯一约束,有就加索引。数据库层面的约束永远比代码靠谱。

遇到过另一种重复是消息推送重复。一个项目状态变化可能同时推送消息给多个角色,如果定时任务和前端轮询重复触发,用户会收到两条一样的提醒。我的做法是在消息表加业务去重字段,存业务单号和消息类型,插入前先检查是否已存在,存在则跳过。

最后再补充一点

这套系统做完我最大的体会是:验收管理系统的难点不在某一块技术,而在把流程设计得严谨又不僵化。数据库设计时一定要先理清状态机,代码实现时一定要把权限控制放在后端,上线之后一定要保留完整的操作日志。这些做好了,系统就算功能不算多,用起来也会让人觉得“顺手”。如果你正在做类似的项目,建议先花时间把自己单位的验收流程完整走一遍,访谈一下科研秘书和项目负责人,把那些“流程外的习惯动作”纳入设计,你做的系统会真正解决用户的问题。

如果你准备上手这套源码,先把数据库脚本导入,按开发环境配置跑起来,重点看验收创建和状态推进这两个核心模块的代码,理解透了再根据实际需求调整。祝你的系统一次上线,少走弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询