二月底接了个活儿,老同学在乡镇工作,说申家沟村准备上村务管理系统,问我能不能用Spring Boot做一版。说实话我一开始以为就是个常规的增删改查项目,等把村委会真实工作流程摸了一遍才发现,这个系统的难点不在技术框架,而是在于怎么把村干部手里那堆纸质台账、Excel表、微信群通知,整理成一套真正有人愿意用的数据系统。这篇文章就把申家沟村务管理系统的设计实现过程完整拆一遍:从哪里入手做需求、为什么选Spring Boot这套技术栈、数据库怎么设计、核心模块怎么写、部署到村里之后踩过哪些坑。给准备做村务、政务、基层管理类Spring Boot项目的朋友当个参考模板。
1. 申家沟村的管理困境:先从四件头疼事说起
1.1 纸质台账和微信群撑不起的日常
申家沟村户籍人口接近两千,分了六个村民小组。在系统上线之前,村里的人口底数靠什么管?几本纸质登记表,加上村会计电脑里一个用了七八年的Excel。每年镇里要常住人口、流出人口数据,村干部就翻箱倒柜找台账,再挨个打电话核实。嫁入的媳妇户口没迁、外出打工的人联系不上、老人去世了信息没更新,这类问题每年都要折腾一遍,一个人口统计少说耗上半个月。
村务公开的情况也差不多。村里财务收支、惠农补贴名单、项目建设进度,制度要求定期公示,实际操作就是在村务公开栏贴一张A4纸,风吹日晒一个月就烂了。后来大家用微信群拍照发公告,照片在聊天记录里一刷就找不到了,真到村民追问某笔钱去向的时候,村干部得翻半天聊天记录。
最费劲的是事项审批。村民要开个证明、办个宅基地审批、申请临时救助,流程全靠人跑腿。先找村民小组长签字,再找村主任,有时候还要等村干部开会。审批走到哪一步,村民完全不知道,只能一趟一趟跑村委会问。还有一个容易被忽略的问题——通知触达。防汛提醒、医保缴费、疫苗通知这类消息,群里接龙一片"收到",实际上很多老人根本没看到。
1.2 系统的边界:我先决定不做什么
搞清楚痛点之后,我做的第一件事不是画界面,而是定边界。村里有人建议我做个大而全的"数字乡村平台",把财务做账、农业生产、摄像头监控全包进去。我没接这个话,定下的原则很简单:这个系统只做四件事——管好人口底数、做好公开公示、打通申请审批、保证通知触达。财务做账交给现有的专业财务软件,农业生产和物联网设备跟这个系统没关系,跟上级部门重复的功能一律不做,数据能导出 Excel 交给镇里就行。
这个边界非常重要。基层系统的失败,一半不是功能太少,而是功能太多、流程太重。申家沟村的情况是:电脑老旧、网络一般、村干部平均年龄偏大,一个太复杂的系统根本没人用。先把最高频的四个痛点解决掉,系统才有机会活下去。后面的事实也证明,这个克制救了项目。
2. 为什么选Spring Boot:一套能维护五年以上的技术栈
2.1 Spring Boot版本的取舍:2.7.18 + JDK8
技术选型这块,我几乎没有纠结。团队就两个人,我负责后端,一个朋友负责前端。Spring Boot在这个场景下就是最优解——生态成熟、社区资料多、遇到问题一搜就有答案,而且整个Spring生态里的Spring Security、Spring Validation、Spring Data都现成,不用自己造轮子。
版本上我选的是Spring Boot 2.7.18 + JDK8,没用Spring Boot 3.x。原因有三个:第一,3.x 强制要求 JDK17,对2核4G的轻量服务器来说,JDK17的内存占用和GC行为不如JDK8省心;第二,3.x发布后一些基础组件(比如某些国产数据库驱动、老版本的中间件客户端)适配有坑,这个项目没有必须要用3.x新特性的场景;第三是求稳,基层政务类项目长期维护,稳定比追新重要。你可能会问JDK8是不是太老了,我的判断是:对一个要跑五年以上的村务系统,2.7.18加JDK8是成本最低、最不容易出幺蛾子的组合。
2.2 周边组件的搭配与理由
整体技术栈是这样的:
| 组件 | 选型 | 为什么这么选 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.18 | 生态成熟,稳定优先 |
| ORM | MyBatis-Plus 3.5.x | SQL可控、LambdaQueryWrapper写起来快、分页插件现成 |
| 数据库 | MySQL 8.0 | 免费、生态好、村级数据量毫无压力 |
| 缓存 | Redis | 验证码、登录Token缓存、热点数据 |
| 认证授权 | Spring Security + JWT | 前后端分离的标配,接口以后还能给小程序用 |
| 前端 | Vue 3 + Element Plus + Vite | 组件完善、后台管理界面开发效率高 |
| 部署 | Docker + docker-compose + Nginx | 环境隔离、迁移方便、前端静态文件由Nginx托管 |
这里有一个取舍可以说透。我见过很多同类系统用Thymeleaf做全栈,不分离,部署就一个jar包,省事。但我选了前后端分离,原因是申家沟村后续大概率要扩展微信小程序或者App,接口直接复用后端就行。代价是前端多了一套Node构建环境,对村里的技术员来说学习成本高一点,所以我写了一份很详细的前端部署文档,把npm构建步骤固定好。详细原因在第六章再说。
3. 需求分析:村干部的表格堆里藏着真正的功能清单
3.1 四类角色,权限完全不一样
需求分析做了两周,走访了村委会和几个村民小组。第一件事是把角色理清楚。系统里一共有四类角色,权限边界完全不同:
- 系统管理员:乡镇或村里的信息化负责人,负责用户管理、系统配置、数据备份。
- 村委会干部:日常使用主群体。其中会计侧重财务公开模块,村主任能看全村所有数据,普通村干部只能操作自己分管的那部分。
- 村民小组长:相当于网格员,负责本组村民信息维护、代办村民申请事项、完成村里指派的核查任务。
- 普通村民:账号由村干部批量开通,默认只能看公开信息和与自己相关的补贴、审批进度。
普通村民的账号是个敏感点。一开始有人建议不给村民开账号,说他们不会用,但我坚持要开,因为村务公开的"公开"必须有对象。村民端做得很克制,首页就三件事:看公开、查申请进度、留言反馈,没有多余功能。
3.2 把痛点翻译成功能清单
需求调研结束后,功能清单基本就浮出水面了。这里直接给当时整理的表格:
| 功能模块 | 还原哪个痛点 | 优先级 |
|---|---|---|
| 村民信息台账 | 人口底数不清、人口统计耗时 | 高 |
| 户信息管理 | 以家庭为单位管理,匹配宅基地和补贴场景 | 高 |
| 通知公告 | 微信群触达不精准、重要通知无法确认 | 高 |
| 村务公开 | 公开栏纸质公示不可查、无留痕 | 高 |
| 事项审批 | 村民跑腿多、审批进度不透明 | 中 |
| 资产管理 | 集体资产底数不清 | 中 |
| 意见反馈 | 村民意见收集渠道单一 | 低 |
| 统计报表 | 给镇里报表重复填写 | 高 |
注意,我把统计报表列为高优先级。事实证明这个判断非常正确——村干部对一个系统最直观的体感,不是界面多漂亮,而是月底填报表能不能省半天时间。后面我会详细说这个模块的实现,因为它是系统上线后被使用最多的功能之一。
3.3 数据权限:看到什么比能不能登录更重要
权限设计上做了两个维度。第一个维度是菜单权限,控制"能不能看到这个功能入口";第二个维度是数据权限,控制"能看谁的数据"。比如村民小组长登录后,只能看到本组村民的信息;会计账号可以看到财务公开管理菜单,但不能看全量村民台账;普通村民登录后,连管理后台的入口都不出现,直接进入村民端页面。
数据权限这块用MyBatis-Plus的数据权限插件实现。做法是在核心查询上通过自定义注解注入数据范围条件,比如"household.group_id = 当前用户所属组",避免每个Mapper手动拼SQL。这个设计后来被证明很值:系统上线一个月后,镇上要求各村数据互相隔离,我只需要改一个注解参数,不用动业务代码。
4. 数据库设计:17张表与四个不能省的设计细节
4.1 核心业务表的划分
整个系统一共17张表,可以分成几类:权限类5张(用户、角色、菜单、用户角色关联、角色菜单关联)、人口台账类3张(户表、村民表、家庭成员关联)、公开公示类2张(通知公告表、村务公开信息表)、审批类2张(审批申请表、审批流转记录表)、其他4张(资产、反馈、操作日志、数据字典)。另外还有一张用户扩展表,存村民的额外信息标识。
人口台账是核心中的核心,设计时采用了"户+人"两层结构。户表存户主、户籍地址、现居地址、联系电话、家庭类别(一般农户、低保户、独居老人户等),村民表存个人信息和与户的关系,比如户主、配偶、子女。为什么要拆两层?因为宅基地审批、低保申请、人口统计全都是以户为单位发起或核算的,而具体到补贴发放和疫苗接种记录又精确到个人。拆开之后,两种维度的查询都不别扭。
4.2 几个容易忽视的字段设计细节
第一,主键没有用数据库自增,用的MyBatis-Plus的ASSIGN_ID雪花算法。原因是乡镇以后很可能做数据交换或者多村合并,自增ID一旦撞上就是大麻烦。雪花ID还能从ID里读出大概的生成时间,排查数据问题时多一个维度。第二,所有表都加了deleted字段做逻辑删除。村务数据按制度要求要留痕,误删了也要能找回来,所以物理DELETE一律禁止。第三,没有用timestamp,统一用datetime存储时间。这里有个容易踩的坑:timestamp会受数据库时区影响,服务器时区设置错了会导致时间偏移8小时,而村务公开涉及公示时间,差8小时就可能出合规问题。用datetime存字面时间,插入时由Java统一用系统当前时间写入。
第四,也是最容易忽略的一点:身份证号和手机号不是明文存储,而是用AES加密后存进varchar(64)字段。村民身份证属于敏感个人信息,明文存库一旦泄露就是安全事故。查询展示时统一脱敏:身份证显示前6位和后4位,手机号显示前3位和后4位。这么做会带来一点麻烦——没法直接用身份证号模糊查询,只能通过脱敏逻辑做精确匹配或加解密工具类。我为此写了一个统一的CryptoUtil,限制所有入口都必须走这个工具。安全这块没有商量余地。
再补一个字段:村民表里预留了region_code区域编码字段。这个字段当时没用上,但等镇上要对接上级平台做数据同步时,你就知道它有多省事了。基层系统一定要有这种前瞻字段,不然以后对接就是噩梦。
5. 三个核心模块的实现:登录认证、事项审批、村务公开
5.1 登录认证:Spring Security与JWT的正确打开方式
登录这块用的是Spring Security + JWT,无状态会话,前端每次请求在Authorization头里带Token。密码加密用BCrypt,登录成功后签发JWT,然后通过一个自定义过滤器解析Token、把用户信息放进SecurityContext。核心配置如下:
@Configuration @EnableWebSecurity public class SecurityConfig { @Resource private JwtAuthenticationFilter jwtFilter; @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .requestMatchers("/api/auth/login", "/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }JWT的过期时间设的是8小时,村民端和村干部端都一样。8小时这个值不是随手定的:太短,村干部一上午要重新登录好几次,烦;太长,存在Token泄露的风险窗口。还有一个细节是登录接口做了验证码,用的Redis存储验证码,5分钟有效,防止暴力破解。系统上线后我加了一个小功能:连续输错5次密码,账号锁定15分钟。这个功能在基层很管用,因为大家习惯用"123456"这类弱密码,没这个限制心里不踏实。
5.2 事项审批:一个小型状态机胜过重量级工作流
审批模块一开始有人提议接个工作流引擎,我直接否了。村委会的审批流程是固定的:村民提交申请 -> 小组长初审 -> 村委复审 -> 办结归档,会签、分支、多级流转这些复杂流程根本不存在。用Activiti、Flowable纯属杀鸡用牛刀,维护还麻烦。最后设计了一个简单的状态机加一张流转记录表,状态流转是:
0草稿 -> 1待初审 -> 2待复审 -> 3已通过 -> 4已驳回,另外支持5已撤回。村民提交后可以撤回,小组长初审驳回的直接退回到草稿,村委复审驳回的就结束流程。
核心表结构分两张:审批申请表存业务主数据,审批流转记录表存每一步的操作痕迹。这个设计对基层特别重要——审计的时候,每一步谁操作、什么时候操作的、填了什么意见,全都能查。
审批状态流转的服务层代码大概是这样的:
@Transactional public void approve(Long approvalId, String operatorId, String action, String comment) { Approval approval = approvalMapper.selectById(approvalId); if (!checkTransition(approval.getStatus(), action)) { throw new BizException("当前状态不允许执行该操作"); } // 更新审批主表状态 approval.setStatus(nextStatus(approval.getStatus(), action)); approval.setCurrentNode(nextNode(approval.getCurrentNode(), action)); approvalMapper.updateById(approval); // 写入流转记录,留痕 ApprovalRecord record = new ApprovalRecord(); record.setApprovalId(approvalId); record.setOperatorId(operatorId); record.setAction(action); record.setComment(comment); approvalRecordMapper.insert(record); }状态机的关键就是一个checkTransition方法,它维护了一张"当前状态+动作 -> 下一步状态"的映射表。为什么不用多个if else?因为状态一旦多起来,if else会把自己绕晕,映射表一看就清楚。
业务流程上,村民提交申请后系统会推送通知给所属小组长,小组长手机上就能处理,不用专门跑村委会。审批过程中,村民在村民端可以实时看到进度:到哪一步了、卡在谁手里。就这一个简单的进度透明化功能,上线后村委会跑腿询问量直接少了一大半。
5.3 村务公开:内容留痕与隐私脱敏
村务公开模块看起来就是个"富文本+附件发布",但有两个设计点值得展开。
第一是公开内容一旦发布就不允许修改。为什么?财务收支和补贴名单这类信息,如果发布之后还能静默修改,一旦有村民质疑前后不一致,村委会说不清楚。所以我的实现是:发布之后在库里只允许下架(修改状态),不允许修改正文,真要更正只能重新发布一条,并在标题里注明"更正"。这是基层审查最容易查出的问题,一开始就堵上这个口子。
第二是隐私脱敏在展示层做。村民端列表页和详情页展示补贴名单时,身份证和手机号必须打码,只有具备管理权限的账号在后台才能看到完整数据。另外村务公开内容会设置一个expireTime,比如一则公示默认挂30天,到期后定时任务自动下架,避免过期信息长期挂在首页造成误解。
附件的处理上,PDF和图片上传到服务器上的一个upload目录,Nginx直接托管,数据库里只存相对路径。数据量不用担心,村一级的高频公示一个月也就十几条,几张表够用很多年。顺便提一句:村民端的搜索框一定要做,没有搜索的公开栏就是摆设。按类别、按时间筛选,加上评论留言的跳转,村民才会真的用起来。
5.4 统计报表:村干部最离不开的模块
统计报表这个模块,是上线后被使用频率最高的,没有之一。村里每个月要向镇上报表,口径包括户籍人口、常住人口、流出人口、低保户数、独居老人数、宅基地审批数等等。以前村干部靠Excel人工筛选,数据对不上就一个一个核对。现在系统里做了几个固定报表页面,数据全部实时聚合。
实现上,复杂统计没有用MyBatis-Plus的QueryWrapper硬拼,而是直接在Mapper XML里写聚合SQL。比如计算年龄结构:
SELECT CASE WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 18 THEN '0-17' WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) BETWEEN 18 AND 60 THEN '18-60' ELSE '60+' END AS age_group, COUNT(*) AS cnt FROM villager WHERE deleted = 0 GROUP BY age_group报表导出用Apache POI,一键导出Excel,格式和镇里发的模板保持一致。你要知道,在基层系统里,"一键生成报表"比任何大屏展示都更能打动使用者。村主任最常干的事情就是打开系统,点两下,导出Excel,发给镇里。以前这要花半天,现在五分钟搞定。
6. 部署和上线:在村里真实跑起来才知道的坑
6.1 部署环境与资源控制
服务器用的2核4G轻量云服务器,数据库、缓存、后端、前端全在这台机器上。部署用Docker编排,一套docker-compose文件全部搞定:
services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD} TZ: Asia/Shanghai volumes: - ./mysql-data:/var/lib/mysql command: --innodb_buffer_pool_size=512M --max_connections=200 redis: image: redis:6 restart: always command: redis-server --requirepass ${REDIS_PASSWORD} backend: build: ./backend restart: always depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod TZ: Asia/Shanghai nginx: image: nginx:1.24 restart: always ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./dist:/usr/share/nginx/html - ./upload:/usr/share/nginx/upload depends_on: - backend4G内存其实有点紧张。MySQL、Redis、后端jar包都挤在这一台机器上,不做限制很容易OOM。我的做法是:后端JVM参数强制设-Xmx512m、-Xms256m,MySQL的innodb_buffer_pool_size限制在512M,再加一个定时任务定时清理日志。数据库和上传文件目录一定要挂载到宿主机,项目里我吃过大亏——Docker重建容器的时候没挂载数据卷,数据差点全丢,从那次之后,凡是有状态的数据一律挂载。
6.2 老旧电脑、弱网络和手滑重复提交
村里真实环境给开发上带来的教训,比任何教科书都多。
第一是浏览器兼容。村委会还有几台老电脑,系统装的是老版本Chrome,Element Plus的部分组件在老浏览器上样式会错乱。排查了半天,最后方案是:在村委会的几台电脑上统一装了Chromium内核的浏览器,锁定不升级,前端尽量不用太新的CSS特性。这事情看着小,不处理的话村干部第一印象就是"这系统有问题"。
第二是网络不稳定。村民提交申请的时候网络卡顿,手一抖点了两次提交按钮,结果生成了两条重复申请。前端按钮要做loading防抖,后端service层还要有幂等校验:同一个村民、同一类申请、同一天,只能有一条待处理记录。一句话的校验就能避免数据垃圾。
第三是打印适配。村委会开证明、盖章的时候要打印,这是刚需。一开始前端没做打印样式,window.print打出来页面错乱。后来专门做了一个打印模板,用@media print隐藏掉无关元素,纸型设成A4,村干部打印一键搞定。这个功能别看小,村干部对系统的认可度,就是靠这些细节攒出来的。
第四是Excel数据迁移。老台账Excel脏数据多得一塌糊涂:出生日期格式有的是1990.1.1、有的是1990年1月、还有的是文本格式,身份证15位和18位混用。我写了一个Python清洗脚本先处理一遍,清洗结果逐条展示给村会计确认,确认后再导入。数据迁移这个阶段急不得,台账是村里的家底,导入错了后面全乱套。
6.3 数据迁移与推广期的过渡策略
推广上最重要的一个决策是并行期制度:系统上线后跟纸质台账并行运行三个月,每个月底核对一次数据,确认系统数据准确无误后,纸质流程才正式退出。这在基层数字化项目里是最稳妥的做法,不要指望一步切换,人都是有习惯的,要给适应期。
账号安全这块也吃过一个教训。系统上线初期,几个村干部嫌麻烦,共用一个账号登录,结果后来村委会内部要查某项操作是谁做的,根本查不出来。我在系统里加了一条规则:同一账号不允许同时多地登录,操作日志强制记录精确到秒。另外,村干部有离职或调整的时候,账号要及时停用,这事情我写成了一条运维约定。基层的数据安全,靠技术只是一半,另一半靠规则。
给村里培训的时候,我还搞了个"代办员"制度:村里选了两个年纪轻、会用手机的年轻人当代办员,年纪大的村民要办业务不用自己操作手机,找代办员就行。系统上线前两周,代办员几乎是手把手教每个村干部怎么录数据、怎么批申请。我写的操作手册只有一页A4纸,截图加箭头,不整那些花里胡哨的大部头。
最后分享一点我个人的体会。做申家沟村务管理系统这个过程,让我重新理解了"技术落地"这件事。Spring Boot、MyBatis-Plus这些都是成熟得不能再成熟的技术,真正的难点全在业务理解和使用习惯上:村干部要的是什么?是少填一次表;村民要的是什么?是办事少跑一趟。技术方案再漂亮,使用者不买账就是零。
另外还有个小建议:做这类基层系统,一定要把操作留痕和日志审计放在跟业务功能同等重要的位置。越是基层系统,越要经得起检查。申家沟这个项目上线三个月,被调用最多的接口点开一看不是登录也不是村民查询,而是"报表导出"。这让我特别感慨——基层数字化真正打动人心的,从来不是炫酷的技术,而是让人省力的细节。项目做完之后,这套系统的数据库设计和审批状态机还被隔壁两个村借去参考了,这大概就是它最大的价值了。