先聊点背景。前阵子接了个活儿,某乡镇要做一套政务办公系统,把村里那套纸质流转、电话通知、微信群传文件的路子换成线上办理,我当时定的技术组合就是项目标题里这套:Java SpringBoot 做后端服务,Vue3 负责前端界面,MyBatis 负责数据库操作,MySQL 存数据,整体前后端分离部署。这个组合有多常见就不多说了,但真正干下来你会发现,光会搭框架没用,乡村政务这种场景里藏着很多和普通CRUD系统不一样的门道,比如审批流怎么设计、表单怎么留痕、权限怎么切分、数据怎么归档,这些才是这套系统真正的价值所在。
这篇博文就围绕这套"SpringBoot+Vue3+MyBatis 乡村政务办公系统"展开,说说我从需求分析、数据库设计、接口开发到部署交付的完整流程,重点讲操作细节和踩过的坑,给正在做类似系统或者打算拿这套技术栈练手的朋友一些能直接落地的参考。
1. 先把需求掰开揉碎:这套系统到底在解决什么问题
1.1 乡村政务办公的典型痛点
做任何系统之前,一定要先搞清楚一个问题:业务方为什么需要你,他现在的状态到底痛在哪里。我当时去某乡镇的信息站蹲了两天,发现他们的日常办公状态非常有代表性。
第一,是材料流转效率低。村民办一个宅基地申请,要从村里开证明,再到乡镇自然资源所提交材料,中间还要经过村委会盖章、乡镇分管领导签字,一个流程跑下来少则一周,多则一个月。材料本身还是纸质的,某一步不小心弄丢了,整个流程重新来。第二,是信息不透明。村民把材料交上去之后,根本不知道流程走到哪一步了,只能反复跑窗口问,工作人员也得翻台账才能回答。第三,是数据统计费劲。上级要一份近半年的各类办件量报表,办公室几个人要手动数台账,数一上午还不一定准。第四,是归档和查找困难。纸质档案一多,仓库就堆满了,想调三年前某个申请记录的审批附件,得翻半天。
这套系统的核心目标就是把这四条线全部线上化、结构化、可追溯,让村民办事少跑腿,让工作人员审批有迹可循,让领导看数据一键导出。
1.2 为什么是 SpringBoot + Vue3 + MyBatis 这套组合
技术选型这件事,很多时候不是新不新的问题,是稳不稳、有没有人接得住的问题。我当时选这套组合主要基于三个考虑。
第一,SpringBoot 的生态成熟度确实无可争议。政务类项目对稳定性要求极高,你不能选一个小众框架赌它的社区活跃度。SpringBoot 的自动配置、内置容器、依赖管理,让我在最短时间内把项目骨架拉起来,后续加功能、加中间件都非常顺畅。第二,Vue3 组合式 API 的引入让前端复杂页面的状态管理更加清晰。我这个系统有审批流程、有用户管理、有多文件上传,页面之间需要共享很多状态,Vue3 的 reactive 和 Pinia 配合起来非常顺手。第三,MyBatis 的灵活 SQL 处理复杂政务查询有天然优势。政务系统的查询条件极其多变,比如"查一下某村所有已经办结的、2023年3月之后提交的、家里人口超过3人的宅基地申请",这种动态条件如果用 JPA 写,会非常痛苦,但在 MyBatis 里用动态 SQL 就是家常便饭。
当然,这套组合也有一些要注意的成本,比如没有强约束的 SQL 需要自己控制好规范,事务管理要格外小心,这些我在后面部署和生产环节会专门展开说。
2. 模块划分:从业务流反推系统边界
2.1 两条主线:村民办事线 + 工作人员审批线
系统规划阶段,我画了一张最原始的思维导图,顺着"村民要办事"和"政府要审批"两条主线把系统切开。
村民侧的核心诉求是"怎么快速交材料、怎么查进度"。所以对应要做的模块是:服务事项列表(列出乡镇能办的所有事项,比如低保申请、宅基地审批、老年优待证办理)、在线填表与材料上传、办件进度查询、公示公告查看。工作侧的核心诉求是"怎么高效处理待办、怎么统计"。对应要做的模块是:待办工作台、审批工具(查看材料、填写审批意见、选择通过或驳回)、历史办件管理、台账导出、用户与权限管理。
这两条线一划清楚,系统边界就非常明确了。我特别想强调一句,做政务系统最忌讳的就是把功能堆在一起,比如后台管理页面里又嵌套村民查询页面,权限逻辑会乱成一锅粥。前后端分离项目,模块边界在需求阶段就要定死。
2.2 角色权限的三层结构
角色权限这块我一开始想得很简单,就设管理员、工作人员、村民三种角色,但被现实教育了。乡镇办公室里坐在同一个办公室的人,分工也完全不同,有的管民政,有的管宅基地,有的管退役军人事务。如果给所有人都开全部审批权限,不出一个月数据就会被改乱。所以最终权限模型比想象中复杂一点,总共分为三层:
- 第一层是系统级角色,即管理员,负责用户开通、菜单配置、数据字典维护
- 第二层是业务角色,比如民政专员、土地专员、综合窗口受理员,每个角色绑定特定的审批流程节点
- 第三层是自然人,也就是村民,只拥有提交办件和查看自己办件的权限
这个结构的实现方式是利用 Spring Security 的 RBAC 模型,一个用户多个角色,一个角色多个菜单权限点,前端通过 Vue Router 的动态路由注册控制可见页面,后端通过注解做接口级拦截。两层锁下来,效果还是很扎实的。
3. 数据库设计:核心表结构和字段取舍
3.1 从审批流反推核心表设计
政务系统里最核心的流程是"申请-受理-审批-办结",所以数据库设计一定围绕一张主表和若干张从表展开。我拆出来的核心表有以下几张,字段设计可以给做类似系统的朋友直接参考。
第一张是事项类型表,字段包括事项编码、事项名称、所属业务分类、承诺办结天数、办理流程JSON(描述审批节点路径)。这张表的作用是定义"能办什么"。第二张是办件主表,字段包括办件编号、申请人ID、事项ID、当前节点、办理状态、提交时间、办结时间。这张表是系统的主动脉。第三张是审批记录表,每经过一个审批节点就写入一条记录,字段有办件ID、审批人ID、审批节点名、审批动作(通过/驳回/退回补正)、审批意见、审批时间。这张表用于留痕和追溯。第四张是材料表,字段有办件ID、材料名称、文件存储路径(本地或OSS)、上传时间、材料类型。
注意:办件编号一定要有业务含义,不能直接用自增ID。我采用的是"年+月+四位流水号"的格式,比如BS202310001,这样村民报出编号,工作人员一眼就能判断是哪年哪月的件,归档也和纸质台账对应得上。
3.2 为什么每个表都要有审计字段
做业务系统经验多的人都会习惯性在每个表里加 create_time、update_time、create_by、update_by 四个字段,但政务系统里这四个字段重要程度极高,因为牵涉到"谁在什么时间改过这条数据"的合规问题。我在代码里用 MyBatis 的公共字段自动填充功能,配置一个 MetaObjectHandler 实现类,在 insert 和 update 的时候自动填充这四个字段,前端传参和业务代码都完全不用显式关心。
还有一点必须提,就是要加逻辑删除标记。政务服务数据讲究留痕,你删掉一条办件记录,那这条数据就永远找不回来了,万一后续审计要追溯,你就说不清楚。所以每张业务表我都加了 deleted 字段,代码里所有查询自动拼接 deleted = 0 条件,真正物理删除只保留给管理员做数据清理时使用。MyBatis 里可以利用拦截器做全局逻辑删除处理,但更稳妥的方式是每张表的业务 SQL 中显式写条件,防止有漏网查询把已删除数据捞出来。
3.3 一张实际表字段的完整示例
我直接把办件主表的核心字段列出来,提供给大家参考设计思路:
CREATE TABLE t_work_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', work_no VARCHAR(32) NOT NULL COMMENT '办件编号', applicant_id BIGINT NOT NULL COMMENT '申请人ID', item_type_id BIGINT NOT NULL COMMENT '事项类型ID', current_node varchar(64) NOT NULL COMMENT '当前审批节点编码', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待受理 1审批中 2已办结 3已驳回 4已撤销', submit_time DATETIME NOT NULL COMMENT '提交时间', finish_time DATETIME DEFAULT NULL COMMENT '办结时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记', create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, create_by BIGINT COMMENT '创建人', update_by BIGINT COMMENT '更新人', KEY idx_status (status), KEY idx_applicant (applicant_id), KEY idx_item_type (item_type_id) ) COMMENT='办件主表';这个表设计的核心思路是所有办件统一流水,通过 item_type_id 关联不同的事项类型,而不是每个事项单独建一张办件表。这样做的好处是列表查询、统计报表非常统一,一句 SQL 就能跨事项类型统计办件量,缺点是要接受一定程度的信息冗余,比如不同事项的附加数据不一致时,需要通过扩展表或 JSON 字段解决。我最终选了 JSON 字段存附加信息,因为乡村政务的事项附加信息差异性极大,强行拆列反而会新增大量空字段,影响维护体验。
4. 前后端分离实操要点:从 JSON 约定到鉴权设计
4.1 接口层统一返回结构
前后端分离项目,接口约定一定是第一个要定死的东西。我和前端约定了一个统一返回格式:
{ "code": 200, "message": "success", "data": {} }code 为 200 表示成功,其他为业务异常码,message 返回给前端提示文案,data 放业务数据。分页查询再多封装一层:
{ "code": 200, "message": "success", "data": { "list": [], "total": 100, "pageNum": 1, "pageSize": 10 } }为什么不用 HTTP 状态码直接表达业务成功失败?因为跨端联调时 HTTP 状态码语义有限,而且某些代理环境下非 2xx 状态会吞报文。用业务码更灵活,比如"材料不齐"返回 4001,"重复提交"返回 4002,前端拿到之后可以精确跳到对应的业务提示。这一层约定在后端实现时写了一个全局响应体类,再用 @RestControllerAdvice 统一处理异常,确保任何一条接口路径返回的结构都一致,前端从来不需要为了某种特殊情况写特殊判断。
4.2 JWT 鉴权如何在 Vue3 和 SpringBoot 之间落地
政务系统对数据安全的敏感度不低,登录态管理我用的是 JWT + Redis 方案。具体流程是:用户登录成功后,SpringBoot 生成一个 JWT token,同时往 Redis 里存一份该用户的权限列表,token 有效期设为 4 小时,前端每次请求在 Authorization 头携带 token。后端用一个拦截器统一校验 token 合法性,如果 Redis 里权限列表存在则放行,不存在则认为登录过期,返回 401 提示重新登录。
选择 JWT 而不是传统 Session 方案,主要原因是前后端完全分离后,部署环境可能不止一台服务器,Session 同步做起来很琐碎。JWT 优点是无状态,后端不保存会话信息,缺点是一旦签发没法主动失效。所以我配合了 Redis 做了"服务端登出"的能力,用户点击退出登录时删除 Redis 中的 key,再配合 token 过期时间,既控制了会话生命周期,也保留了集群能力,目前实测下来足够稳。
前端这边,Vue3 里我用 Pinia 管理用户状态,用户信息、token、菜单权限存在 store 中,路由守卫里做全局判断:没有 token 一律跳转到登录页,有 token 但是角色权限列表未加载时,先去调获取用户信息接口,拿到权限后再动态注册路由。这个逻辑不复杂,但一定注意不要一次性把所有菜单都注册上去,要按用户角色过滤后再加路由,防止低权限用户通过手动输入 URL 访问高权限页面。虽然后端还有接口级拦截兜底,但前端做好体验会好很多。
4.3 文件上传:附件材料的存储与回显
政务办件最离不开的就是材料上传,身份证照片、户口本扫描、申请表盖章件,一个办件少则两三个文件,多则十来个。上传这块我遇到的问题还是蛮有代表性的。
第一个坑是跨域问题。前端服务跑在 5173 端口,后端跑在 8080,上传接口如果不在网关层该跨域会反过来咬你。我在后端写了一个全局 CORS 配置,允许指定来源(实际生产环境就是 Nginx 的域名),允许 GET/POST/PUT/DELETE 方法和 Authorization 请求头。
第二个问题是文件校验。不能让用户传 2G 的视频上来。我在前后端都做了限制,前端限制单文件不超过 10M,后端再校验一次,超过则报业务异常。文件类型白名单包括常用的 pdf、jpg、png、docx、xlsx。
第三个问题是存储路径。小系统没必要上对象存储,我用了本地磁盘存储,但路径做好分类:按"年/月/办件编号/原文件名"存储,这样存储目录本身就是索引。数据库材料表里只存相对路径,不存绝对路径,这样系统迁移服务器时不会出现找回不来文件的情况。
5. MyBatis 动态 SQL:把复杂查询和数据映射调到顺手
5.1 一个典型的动态条件查询
政务系统的查询页面非常考验 MyBatis 功底。比如"办件查询"页面,筛选项有办件编号、申请村民姓名、事项类型、办理状态、提交时间段,这几个条件任意组合,后台就得拼对应 SQL。MyBatis 的 标签结合 是一个标准做法,我贴一段实际写过的 mapper 片段:
<select id="selectWorkItemPage" resultType="com.example.vo.WorkItemVO"> SELECT w.id, w.work_no, w.status, w.submit_time, t.item_name, a.real_name AS applicant_name FROM t_work_item w LEFT JOIN t_item_type t ON w.item_type_id = t.id LEFT JOIN t_applicant a ON w.applicant_id = a.id <where> w.deleted = 0 <if test="workNo != null and workNo != ''"> AND w.work_no LIKE CONCAT('%', #{workNo}, '%') </if> <if test="applicantName != null and applicantName != ''"> AND a.real_name LIKE CONCAT('%', #{applicantName}, '%') </if> <if test="itemTypeId != null"> AND w.item_type_id = #{itemTypeId} </if> <if test="status != null"> AND w.status = #{status} </if> <if test="startTime != null"> AND w.submit_time >= #{startTime} </if> <if test="endTime != null"> AND w.submit_time <= #{endTime} </if> </where> ORDER BY w.create_time DESC </select>这段 SQL 的写法有几个细节要注意。LIKE 查询一定要手动拼接百分号,不要在 mapper 接口参数里写死,否则索引失效;时间范围用 >= 和 <= 是因为 XML 里 < > 符号会被解析成标签,必须用转义字符;LEFT JOIN 关联的字段一定要有索引,否则列表页一多 JOIN 数据量直接拖垮性能。
5.2 一对多映射:办件详情与审批记录的组装
列表页拿到办件数据后,点进来要看详情,详情里包含这单办件的所有材料列表和所有审批意见。这时候就遇到实体关系映射问题。我的做法是查两次:先查主表,再根据主表 ID 批量查询材料表和审批记录表,在 Service 层手动组装。为什么不用 MyBatis 的 collection 嵌套映射?我在性能测试中发现,当审批记录较多时,collection 嵌入会导致 SQL 返回大量冗余行数据,Mapper 泛型类型复杂化,调试成本高。手动组装代码虽然多写十几行,但逻辑清楚,每个查询都是独立的、可缓存的,适合中小系统的维护节奏。
如果你确实想用嵌套映射,也有一个折中方案:在主结果映射上搭配分布查询,实现主查询一次,子查询多次,避免笛卡尔积膨胀。但要注意 N+1 问题的风险,所以我更推荐手动组装。
5.3 下划线转驼峰和 SQL 注入两个隐藏雷区
在配置文件里设置 mapUnderscoreToCamelCase=true,这个一定要开。因为数据库字段名是 submit_time 的格式,Java 实体属性是 submitTime,不开这个映射,所有查询的结果都会变成 null,而且这种问题还不容易发现,控制台只显示查询成功了,但前端拿到的字段全是空值。
SQL 注入方面,MyBatis 的 #{} 参数是预编译占位符,安全性没有问题,但动态排序字段或者动态查询条件如果用了 ${} 拼接,一定要白名单校验。比如排序需求,前端传过来的 orderField 必须和预先设定好的可排序字段列表比对,不在列表里直接报错,绝不允许原样拼进 SQL。
6. 工作流设计:不做重流程引擎,用状态机就够
6.1 为什么不上 Activiti
我最早想过是不是引入 Activiti 或 Flowable 这样的工作流引擎,毕竟政务系统审批流看着挺复杂的。但后来仔细梳理了当前乡镇的业务,发现大部分事项的审批路径都是"受理 -> 部门初审 -> 分管领导复审 -> 办结",少数简单事项只有"受理 -> 办结",流程模型非常固定,并不需要复杂的并行网关、条件网关、会签签批。引入重型工作流引擎意味着要额外维护流程部署表、任务表、历史表,增加一套学习成本,对乡镇信息员来说维护门槛直接抬高。
所以这套系统最终采用的是轻量级状态机设计。每个事项类型的"办理流程JSON"字段里定义节点序列,Redis 存一个状态机的流转映射,代码里用一个简单的流程节点推进类处理:当前节点 = 下一个节点,流转到终态自动触发办结时间和统计更新。这使得审批逻辑非常直观,维护成本极低。
6.2 状态机的核心代码逻辑
流程节点的推进实现基本就是查字典 + 更新状态:
public WorkItemVO advanceNode(WorkItem workItem, String action) { List<String> nodeSeq = JsonUtil.parseList(workItem.getFlowJson()); int currentIndex = nodeSeq.indexOf(workItem.getCurrentNode()); if (currentIndex < 0 || currentIndex >= nodeSeq.size() - 1) { throw new BusinessException("当前流程节点已不允许推进"); } if ("REJECT".equals(action)) { workItem.setStatus(WorkItemStatus.REJECTED); } else { workItem.setCurrentNode(nodeSeq.get(currentIndex + 1)); if (currentIndex + 1 == nodeSeq.size() - 1) { workItem.setStatus(WorkItemStatus.FINISHED); workItem.setFinishTime(new Date()); } } workItemMapper.updateById(workItem); return assembleVO(workItem); }这段代码虽然简单,但它在逻辑上保证了一个关键点:审批路径永远只能按流程 JSON 定义的顺序推进,不会出现跳节点或者原地卡死的局面。对于并发审批的场景,我在 update 语句里加了乐观锁条件 update_time = 旧值 的校验,防止两个审批人同时点了通过导致状态错乱。
6.3 驳回与撤回:政务场景特有的边界情况
审批被人驳回之后,村民需要能修改材料重新提交,这就是"再次提交"。我在设计上专门加了一个动作:驳回状态下的办件,村民端可以发起"重新提交",提交后系统自动把状态从已驳回流转回"审批中",审批节点回到驳回发生时的那个节点。这套逻辑非常贴近线下纸质业务的"退件补正"。
另外一个边界情况是"撤回"。如果村民发现自己填错了信息,可以在工作人员尚未处理时撤回申请。我实现时限制了撤回操作的窗口期:只有状态为待受理且工作人员未点击"受理"时才允许撤回,一旦进入审批中,村民就不能自己撤了,只能电话联系工作人员打回。这个限制不是技术限制,是业务规则限制,做系统的人一定要把这类规则问清楚再动手写代码。
7. 前后端编码过程中掉过的那些坑
7.1 时区问题导致办件时间差八小时
这是我在本地测试时碰到的,当时数据库里存的时间,存进去的是一条,查出来就少了八小时。排查半天发现是 MySQL 的时区连接参数没设置。MySQL 8.x 默认使用数据库服务器时区,而我们的服务器用的是 UTC 时区,Tomcat 运行时用的是东八区,两边一错位,时间就全乱了。
解决的方案是在 JDBC 连接串上显式声明时区:
jdbc:mysql://localhost:3306/rural_gov?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai这个坑很多人会踩一次,但踩完之后再也不会忘记。分布式系统里时间问题最麻烦,建议所有时间字段统一用 DATETIME 存储,所有服务统一走东八区,不要在代码里手动加减时区偏移量。
7.2 JSON 字段在 MySQL 里查询慢
之前提到办件主表用 JSON 存附加信息,查询列表时偶尔需要按 JSON 里的字段过滤,比如"查所有家里人口是4人以上的申请"。MySQL 的 JSON_EXTRACT 函数可以做到,但它的索引非常难伺候。乡村政务的办件量不算大,最坏情况一年也就几万条,反正全表扫描性能也扛得住,所以这个坑影响不严重起。但如果你预判业务量会增长到几十万条,还是老老实实把常用筛选字段单独拆列出来加索引,JSON 只做纯存档用途。
7.3 前端表格大数据量渲染卡顿
Vue3 的列表页一次性渲染几百条数据其实没太大压力,但政务查询页面经常要把当前季度所有办件全部展示出来,几千条数据一次性渲染,浏览器会出现明显卡顿。我的处理方案是后端强制分页,前端配合虚拟滚动组件,每页 20-50 条,用户通过翻页或者滚动加载获取更多数据。后台管理端完全可以接受这个交互模式,真正要导出的数据量走"导出Excel"接口,异步生成文件,不经过前端表格渲染。
7.4 数据库连接池连接不够用
上线初期,村民集中尝试在线提交材料的时候,应用短暂出现过连接池打满的告警。HikariCP 默认配置 maximumPoolSize=10,一旦并发提交办件的人数增多,连接池就排队了。后来我把最大连接数调到了 50,同时给查询和更新配置了合理的超时时间,问题就不再出现。这里有个经验,数据库连接池的大小不是越大越好,连接数过大反而会让数据库服务器因为上下文切换变慢。数据库和连接池指标要一起监控,最稳妥的做法是在压测环境模拟高峰期流量,调整到最优值。
8. 部署交付:从开发环境到真实服务器
8.1 Docker Compose 编排一套依赖
这个系统部署结构不复杂,核心是三个部分:MySQL 数据库、后端 Java 应用、前端静态资源,外加一个 Nginx 做代理。为了方便交付,我准备了一个 docker-compose.yml,把 MySQL、后端、Nginx 编排起来,前端打包成 dist 目录后挂载到 Nginx 容器内。
Nginx 的配置要点是:静态资源请求直接走本地文件,/api 前缀的请求反向代理到后端服务,同时配置 Gzip 压缩、缓存策略,这样前端页面加载速度会比直接打 Tomcat 快很多。部署时要注意 IPv4 和 IPv6 的监听,Nginx 默认监听 IPv6 可能导致容器内访问404,这个问题花了我一些时间才排查出来。
后端服务的 Dockerfile 我采用多阶段构建,第一阶段用 Maven 镜像编译,第二阶段用 JRE 镜像运行,打包出来的镜像体积很小,几乎不包含多余内容。这样每次发版传输镜像也简单非常多。
8.2 数据库备份策略
对于政务系统,数据就是资产,备份是不可妥协的底线。我的方案是每天凌晨 2 点通过 crontab 执行 mysqldump 全量备份,备份文件压缩后保留最近 15 天,异地备份则通过 rsync 同步到另一台内网机器。测试环境单独起一个恢复任务,每个月自动把某一天的备份恢复到测试库,验证备份文件不是坏的。
注意:MySQL 备份时一定考虑锁表问题。小数据量 mysqldump 默认操作很快,但如果是大表,要评估是否需要开启 --single-transaction 参数做热备份,避免备份期间业务写入被长时间阻塞。
8.3 系统上线前的那次模拟演练
我特别想分享这个经验。当时系统开发完,我并没有立刻让乡镇切换线上,而是先用导出的真实脱敏数据在测试环境做了一个完整的模拟运行,找了几个人扮演村民、窗口受理员、审批领导,把主要的二十个事项类型全部走了一遍流程。结果真的发现了一堆测试时没暴露的问题,比如某个审批节点的按钮在达到终态后没有禁用,村民重复点击提交按钮会生成两条相同办件,特殊字符在 Excel 导出时乱码。这些问题如果上了生产环境,每一件都是真实投诉。
所以强烈建议,政务类系统交付前一定要做一个"业务演练日",把真实用户数据集导入测试环境,让业务方安排不同角色的人实际操作一遍,发现问题记录成清单,逐个修复后再正式割接上线。这个步骤的意义比写多少行代码都重要。
9. 常见问题速查:从开发到运维的高频故障清单
最后把我在这个项目中遇到的高频问题整理成一个速查表,供做同类系统的朋友参考:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 查询结果中对象字段为null | 数据库字段下划线未转驼峰 | 开启 MyBatis mapUnderscoreToCamelCase |
| 跨域请求被拦截 | 缺少 CORS 配置或配置了错误来源 | 后端 @CrossOrigin 或全局 WebMvcConfigurer 配置 |
| 前端页面刷新后404 | 前端路由模式是 history,Nginx 未做 try_files 回退 | Nginx location / 配置 try_files $uri $uri/ /index.html |
| 上传大文件超时 | Nginx 层默认上传限制太小 | http 块配置 client_max_body_size 20m |
| JWT 获取不到当前用户 | token 未放进 Header 或拦截器放行路径漏配 | 检查 axios 拦截器是否统一设置 Authorization |
| 审批状态错乱 | 并发发起两个请求 | update 时加乐观锁或分布式锁 |
| MySQL 数据库连接失败 | 连接串未指定时区或密码不匹配 | 检查 serverTimezone 参数及账号授权 |
| 列表页加载极慢 | LEFT JOIN 字段没索引 | 给关联字段添加联合索引 |
这些情况基本是我在这个项目里真实碰到过的,有的容易排查,有的差点把人逼疯。配置类问题占大多数,代码逻辑类问题反而是少部分,所以如果你的系统出问题,优先检查配置文件。
10. 写在最后
这套乡村政务办公系统做完,我最大的感触是,技术框架本身并不难,难的是把业务流程理解透,并且在每一层代码里把业务的严谨性体现出来。SpringBoot 和 Vue3 都只是工具,真正让系统跑起来的,是设计阶段对事项流转、角色权限、数据留痕这些细节的较真。如果你也打算做一个类似的系统,建议第一步不要急着写代码,先花时间把业务流程图反复画几遍,把每个节点上的操作人和操作条件都标清楚再动手。
再分享一个小经验:开发期间可以刻意把日志打印做得完整一些。我在所有审批操作里都加了详细的业务日志,包括操作人、操作节点、提交参数、耗时,这样后期排查问题特别方便,很多找不出来的 bug 都是靠日志一秒定位的。这个习惯我一直保留到现在,你觉得用不上,等系统上线跑一个月再回头看,你会发现它帮了你大忙。
以上就是这套系统从设计到交付再到运维的真实回顾,希望里面的表结构、代码片段和踩坑记录能给正在做同类项目的你节省一些时间。有问题的话可以按文中的思路先排查一轮,大部分情况都能自己解决。