资讯动态

基于Spring Boot + MyBatis的土地资源管理子系统开发实战

发布时间:2026/10/6 13:09:57 来源:尧图企业网站定制
做毕业设计或者接乡镇数字化的私活时土地资源管理子系统几乎是绕不开的一个典型业务场景。最近刚完整交付了一套基于Spring Boot MyBatis 的土地资源管理系统从需求梳理、数据库落表到权限控制、统计报表、前后端联调整套流程走下来我发现这个命题其实远不止增删改查这么简单。这篇文章就把我实际开发中踩过的坑、定过的方案、留下的代码结构一次性讲清楚末尾有源码获取方式需要完整工程的同学直接找我拿即可。先交代一下我这边项目的最终情况后端采用 Spring Boot 2.6.13 MyBatis MySQL 8.0前端用的是 Vue 2 Element UI鉴权走的是 JWT 拦截器方案部署在单台 2 核 4G 云服务上。整个系统分了系统管理、基础信息、地块管理、承包管理、流转管理、统计报表六大模块其中土地资源管理子系统是信息平台的核心业务端主要解决土地信息怎么录、变更怎么记、归属怎么查、统计怎么出这四件最基础也最头疼的事。这个项目我定位为乡村振兴数字化平台里的一个业务子系统所以没有做微服务那一套而是用模块化分包的方式保持边界。实际做下来我的体会是对于县、乡镇级别的土地管理场景单体应用绰绰有余硬拆微服务反而会让部署和运维成倍变复杂。下面我把这套系统从设计到落地的全过程拆开讲。1. 需求分析与系统定位1.1 土地资源管理到底在管什么很多同学把土地资源管理直接理解成村子地块的录入和查询这没错但太单薄了。我实际接触到乡镇工作人员后才发现他们每天的痛点集中在三个层面第一档案底数不清。全村到底多少块地、每块地权属是谁、面积多大、什么地类很多还停留在纸质台账阶段想查一块地的历史记录要翻半天档案。第二变更流程靠人记。土地承包、流转、征收、宅基地审批每一次变动如果没在系统里形成一条可追溯的记录后续很容易产生纠纷。第三统计口径千差万别。上级要报表村里报一个数乡镇核对又发现对不上原因就是各方手里的数据源不一致。所以我做需求分析时把子系统拆成了四个核心域基础档案域地块本身的属性、权属域承包信息、变动域流转和变更记录、分析域统计报表。这四块搞清楚了系统的主干就立住了。1.2 核心用户与角色权限划分这套系统我设计了四种角色权限层级非常清晰具体如下表角色主要职责核心权限范围系统管理员账号维护、字典管理、日志查看系统管理模块全部权限乡镇管理人员地块录入、变更审批、流转登记基础信息、地块、承包、流转的增删改查村级信息员地块信息采集录入、资料上传新增地块、修改待审核信息、查看本村数据普通浏览用户查阅公开地块信息、公告只读部分公开字段这里有个关键设计思路村级信息员录入的数据必须经过乡镇管理人员审核后才正式生效这是现实业务里最真实的需求——村里录错了不能直接污染全乡镇的数据。所以我在每张核心表上都加了status字段用状态机控制数据流转草稿 - 待审核 - 已生效 - 已归档这个设计审计人员一眼就能看懂后续扩展驳回退回也方便。1.3 为什么强调子系统标题里写的是信息平台建设——土地资源管理子系统这说明整个平台还会有村庄概况、农产品信息、村务公开、在线办事等其他子系统。我在项目里用 Maven 多模块结构来承载这个设计思路platform-common 公共工具、统一返回、异常处理 platform-system 系统管理用户、角色、菜单、字典 platform-land 土地资源管理子系统核心业务 platform-web 启动模块各模块之间通过 Service 接口解耦platform-land 不直接依赖 platform-system 的实现类只依赖接口。这样做的好处是如果后面平台新增一个宅基地管理子系统或者农业补贴子系统新模块可以直接复用系统管理和公共模块不需要改老代码。2. 技术选型Spring Boot 版本与配套组件2.1 Spring Boot 2.6.x 还是 2.3.x最开始我准备用 Spring Boot 3.x但考虑到大部分学校课程和老项目还在用 JDK 8而且第三方 Starter 兼容性在 3.x 下偶尔会有幺蛾子稳妥起见还是选了 2.6.13。这里我对比过 2.3.x 和 2.6.x核心差异在于Spring Boot 2.3.x 比较老虽然稳但一些新特性如spring-boot-starter-validation的独立版本管理、更好的配置处理支持不够好。Spring Boot 2.6.x 引入了路径匹配策略调整PathPatternParser成为默认这对 RESTful 风格接口很友好且生命周期覆盖到 2023 年之后用于毕业设计和中小项目完全够。2.4.x 之后的配置文件不再支持多文档spring.profiles写法改成了spring.config.activate.on-profile这个变化很多人升级时栽过跟头。最终我决定使用 Spring Boot 2.6.13 JDK 8 MyBatis 3.5.x。实际跑下来稳定性很好没有遇到明显问题。2.2 持久层方案MyBatis 还是 MyBatis-Plus热词里那个spring boot mybatis 的 java 开源多商户跨境商城源码说明很多人确实在招这种组合。我到今天还是倾向于原生 MyBatis 搭配通用 Mapper 工具类主要原因有两点土地资源管理业务的 SQL 复杂度比普通 CRUD 高不少涉及多表关联、按条件动态查询、地理属性筛选MyBatis 的动态 SQL 能力在这些场景下非常舒服。我的团队和读者里有大量习惯裸写 SQL 的开发者原生 MyBatis 让 SQL 可读、可控、可优化排查慢查询时直接捞 XML 出来执行不用经过一层抽象。我同时引入了 PageHelper 做分页。PageHelper 拦截器会改写 SQL需要注意在配置里指定合理的reasonable参数避免页码越界时查到脏数据。2.3 Spring Boot Admin 做监控我看到热搜里有spring boot admin这个词忍不住多说一嘴。在这个项目里我确实集成了 Spring Boot Admin 2.6.8用来自定义内存、线程池和接口响应耗时监控。它不需要引入太重的东西被监控端加一个spring-boot-starter-actuator服务端单独起一个轻量工程界面能看到实时的堆内存使用、HTTP 接口调用情况和在线实例列表对单体应用排查内存泄漏和慢接口非常有帮助。2.4 前端与后端数据交互格式前端的技术选型我没有花哨直接用 Vue 2 Element UI。或许是私活交付和毕设场景下最稳妥的组合组件丰富、中文文档齐全、后端同学容易上手。接口统一返回 Result 结构{ code: 200, msg: 操作成功, data: { } }返回码不用 HTTP 状态码作为业务成功与否的唯一标识而是用业务 code 区分成功和失败这样前端拦截器处理 401 跳登录、403 跳无权限、业务异常全局提示就非常清晰。3. 数据库设计表结构拆解与核心字段说明3.1 土地基础信息表land_info土地表是整个系统的地基字段设计尤其重要。我的核心字段如下字段名类型说明idbigint 主键自增主键land_codevarchar(32) 唯一索引地块编码规则乡镇编码村编码序号land_namevarchar(64)地块名称land_typetinyint地类1水田 2旱地 3林地 4宅基地 5其他area_mudecimal(10,2)面积单位亩location_descvarchar(255)位置描述longitude / latitudedecimal(10,6)经纬度坐标owner_idbigint权属人ID关联农户表statustinyint状态0草稿 1待审核 2已生效 3已归档create_by / create_time / update_by / update_time审计字段这里最要注意的是area_mu必须用 decimal不能用 float。土地面积涉及到补贴计算、流转费用浮点误差会导致金额对不上这是实际数据核对时最容易暴露的问题。另外经纬度字段不要用 varchar配合地图组件后续做可视化定位会很麻烦。3.2 承包信息表与流转记录表承包信息表存的是这块地归谁承包、承包期到什么时间流转记录表则保存每一次经营权流转的详细信息。流转表我额外加了一个contract_attachment字段用来存合同附件的文件路径。不要小看这个字段实际业务里签合同是家常便饭没地方传附件系统根本用不起来。文件我采用本地磁盘存储上传后的访问路径做了静态资源映射这样部署时只挂一块数据盘就够不需要引入 OSS省心也减少私活成本。4. 核心模块功能实现与实操代码4.1 JWT 登录认证与权限拦截器登录这块我用了 JJWT 0.9.1 Spring 拦截器实现Component public class JwtInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录或登录已过期); } Claims claims JwtUtil.parseToken(token.replace(Bearer , )); Long userId Long.valueOf(claims.get(userId).toString()); // 校验 token 是否存在于 Redis单设备登录 String redisToken redisTemplate.opsForValue().get(LOGIN_TOKEN_ userId); if (!token.replace(Bearer , ).equals(redisToken)) { throw new BusinessException(401, 账号在其他设备登录请重新登录); } request.setAttribute(userId, userId); return true; } }这个方案里我把 token 放在 Redis 里做单点登录控制用户在别处登录后旧 token 会失效。功能不算复杂但很实用避免了改完密码旧 token 还能用的尴尬。4.2 土地信息分页条件查询土地查询是使用频率最高的接口。支持按地块编码、地类、权属人、状态和面积区间五种条件组合查询。Mapper 里用了动态 SQLselect idselectLandPage resultTypecom.platform.land.entity.LandInfoVO SELECT li.*, ho.holder_name, ho.idcard_no FROM land_info li LEFT JOIN holder_info ho ON li.owner_id ho.id where if testlandCode ! null and landCode ! AND li.land_code LIKE CONCAT(%, #{landCode}, %) /if if testlandType ! null AND li.land_type #{landType} /if if testholderName ! null and holderName ! AND ho.holder_name LIKE CONCAT(%, #{holderName}, %) /if if teststatus ! null AND li.status #{status} /if if testminArea ! null AND li.area_mu gt; #{minArea} /if if testmaxArea ! null AND li.area_mu lt; #{maxArea} /if /where ORDER BY li.create_time DESC /select这里我踩过一个坑一开始LIKE CONCAT(%, #{landCode}, %)的写法在老版本 MyBatis 里没问题但如果用${landCode}直接拼字符串会有 SQL 注入风险所以必须用#{}参数绑定。4.3 Excel 批量导入导出乡镇工作人员手里往往有一堆旧 Excel 台账要迁入系统手打录入会崩溃。我引入了 EasyExcel 做批量导入导出。导入的过程不复杂前端上传 Excel - 后端解析成对象列表 - 逐条校验 - 合法数据入库 - 生成导入结果详情包括成功条数和每行的失败原因public ImportResult importLand(MultipartFile file) { ListLandExcelDTO list EasyExcel.read(file.getInputStream()) .head(LandExcelDTO.class) .sheet() .doReadSync(); ImportResult result new ImportResult(); int successCount 0; ListString errorMessages new ArrayList(); for (int i 0; i list.size(); i) { LandExcelDTO dto list.get(i); String validateMsg landService.validateLand(dto); if (validateMsg ! null) { errorMessages.add(第 (i 2) 行 validateMsg); continue; } landService.insertLand(dto); successCount; } result.setSuccessCount(successCount); result.setFailMessages(errorMessages); return result; }导出的逻辑同理把查询出的 List 直接EasyExcel.write(response.getOutputStream())。这套方案目前处理单次几千行数据毫无压力而且不需要让前端装任何依赖。4.4 统计报表与地图可视化统计模块我实现了三张报表按地类统计面积、按村统计确权数量、按年份统计流转趋势。这些报表的底层只是集 SQL 查询比如按地类统计SELECT land_type, SUM(area_mu) AS total_area, COUNT(*) AS land_count FROM land_info WHERE status 2 GROUP BY land_type ORDER BY total_area DESC前端用 ECharts 画饼图和柱状图。ECharts 很成熟但要注意折线图时间轴的数值类型转换后端返回的时间字段一定要格式化成字符串否则前端 new Date() 会解析失败。地图可视化我做了层聚合展示把每块地的经纬度查询出来在地图上打点点击弹窗显示地块详情。做的过程发现一个很实际的坑——如果某块地的经纬度是 0打点会全部挤在偏航位置所以查询时必须过滤掉longitude 0 OR latitude 0的记录。5. 接口设计与第三方对接的思考5.1 子系统接口应该怎么规划有很多人问过我Spring Boot 对外提供的接口给第三方应该放在哪里是单独的服务还是放在对应的业务模块里我的建议是如果接口是同一个团队内部使用比如前端页面调用直接放在对应的 Controller 里不加额外前缀如果确定要开放给第三方系统比如县级数据平台想要拉取全镇的耕地数据那就单独开一层 Api 模块/api/v1/land/info /api/v1/land/statistics /api/v1/land/change-record这层接口与页面接口分离统一加/api/v1前缀并且走独立的密钥鉴权AppId AppSecret 签名不跟用户登录态混在一起。这样做的理由是页面接口的鉴权和限流策略不适合第三方比如第三方不应该刷新页面 token、不应该依赖用户角色的数据隔离规则。第三方对接往往有固定版本/api/v1本身就是对外版本承诺升级时新增/api/v2不影响老客户。单独打包成模块后可以单独做流量监控和接口文档接入 Swagger/knife4j不会污染内部接口。我在实际项目里就遇到了县里农业大数据平台要求对接数据的需求因为提前做了接口分层两天就完成了对接任务省了很多麻烦。5.2 接口幂等与防重提土地流转模块有一个接口非常容易踩坑提交流转登记。如果前端网络抖动时用户重复点击或者消息重试就会产生两条重复的流转记录。我在流转提交接口上加了一个简单的幂等控制前端在提交时生成一个requestIdUUID 或者基于业务信息生成的 MD5后端在 Redis 里SETNX key requestId EX 60拿到锁才执行保存逻辑执行完删除 key。这样即使同一个请求重复发过来后端的幂等校验也能拦住。这个小功能我建议所有写提交类接口的同学都安排上成本极低收益立竿见影。6. 常见问题与排查技巧实录6.1 高频报错排查速查表问题现象原因解决方式启动报Invalid bound statement (not found)Mapper XML 没有扫描到检查MapperScan路径与 XML 的 namespace 是否完全匹配Whitelabel Error Page404请求路径没映射检查 Controller 类有没有加RestController以及请求方法是否匹配 GET/POST中文乱码前后端编码不一致后端接口方法加produces application/json;charsetUTF-8数据库连接串加characterEncodingutf8分页数据总数不对PageHelper 和嵌套查询冲突不要在PageHelper.startPage()后使用带LEFT JOIN的复杂 count 查询手动拆分或用SelectProvider跨域调不通端口不同配置 CorsFilter 时加上allowCredentials(true)且指定allowedOriginPatterns不能用*和 allowCredentials 同时内存持续上涨大 Excel 导入全部 readSync使用AnalysisEventListener分块解析不要一次性把几万行全 load 进内存前端上传文件报 413Nginx 请求体大小限制client_max_body_size 50m;6.2 开发过程中让我印象最深的三个坑第一个是 MyBatis 的 SQL 里! null做条件判断时如果传参是类型为 Integer 的包装类并且值为 nullMyBatis 不加if包裹直接比较! null是没问题的但如果用的是if testlandType neq null这种写法在与script语法混合时偶尔会解析报错。我后来统一规范成if testlandType ! null再没出过问题。第二个是 JWT 双人同时登录问题。系统一开始只验证 token 是否有效没有做唯一登录控制结果村信息员把账号密码告诉另一个人后两人互相挤掉线天天来骂系统。后来加了 Redis 单设备登录逻辑才算把这个烂摊子收拾干净。第三个是 Excel 导入的数据校验一定要放在事务外层逐条处理。我最初用Transactional包了整个导入方法结果中间某行数据非法导致全部回滚用户导入 3000 行只因为第 500 行有问题整批全失败。后来改成校验通过才入库失败行收集错误信息返回前端用户体验完全提升了一个档次。7. 源码结构与获取方式整套系统的源码我整理得非常干净适合学习或二次开发。主要目录结构如下platform-web/src/main/java/com/platform ├── common # 统一返回、全局异常、常量、工具类 ├── system # 用户、角色、菜单、字典管理 ├── land # 土地信息、承包、流转、变更记录、报表 │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ └── vo └── LandApplication.java源码里面包含了完整的数据库初始化 SQL、接口文档Swagger 注解、前端 Vue 工程含登录页、主框架、土地管理页面、统计图表页面、部署说明文档。你可以直接拿来改改用于自己的毕业设计或者小型乡镇数字平台。如果看到这里觉得这套土地资源管理子系统的思路和代码对你有帮助需要源码做参考或者二次开发可以直接在文末留下你的联系方式或者通过博客底部的按钮联系我。我发你完整工程包的时候会带上打包好的数据库脚本和部署文档按 README 一步步操作就能跑起来。最后再分享一点个人体会做这类管理系统新手往往沉迷于把代码写得更炫什么微服务、高并发、分布式缓存都想往上堆但实际真正让项目被客户认可的永远是数据可靠、操作流畅、权限清晰、报表准确。土地资源管理表面上是 CRUD本质上是一套信息治理的基础设施。把表设计吃透、把状态流转梳理清楚、把导出导入这类细节做到位比堆十个框架都有用。这套系统后续我还在规划接入地图遥感和宅基地管理模块到时候会再写文章分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑