尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java智慧乡村管理系统后端设计:村级数据隔离与权限控制

Java智慧乡村管理系统后端设计:村级数据隔离与权限控制 简介一套基于Java与HTML构建的智慧乡村管理系统后端设计源码面向Java后端开发者、智慧乡村项目研究者及需要快速搭建乡村管理原型的技术人员可应用于课程设计、毕业设计或实际项目参考帮助解决乡村管理中资源统筹、流程优化与信息公开等痛点。资源包共45个文件压缩包仅61KB包含40个Java源文件、1个YAML配置文件、1个XML配置文件、1个HTML文件以及Git忽略文件和说明文档Java源文件承载用户管理、数据处理、接口定义等核心业务逻辑配置类文件用于环境与参数设置HTML文件可作前端展示预览目录结构清晰便于按模块学习。目前已有385人学习/下载具备一定社区参考价值。通过源码可系统学习MVC架构下的模块划分、JDBC/JPA数据库CRUD操作、Servlet网络通信、安全认证及异常日志处理等Java Web关键知识点对理解智慧乡村系统的后端搭建和后续二次开发均有直接帮助。1. 智慧乡村管理系统后端设计的真门槛是村级数据隔离一个挂着“基于Java和HTML的智慧乡村管理系统后端设计源码”名头的压缩包真正需要花时间的不在登录页也不在HTML首页那几个卡片图而在后端控制数据范围的那一层。同样的Java代码能做成村操作员只看到本村的户主、土地和补贴记录和把所有数据一锅端给所有账号完全是两个工作量。这套设计适合三类人来读要用Java后端做智慧乡村课程设计或毕设的学生、接手村务管理系统准备二次开发的维护人员以及会Spring Boot单表增删改查、但还没处理过“同一个数据库里多个村庄数据隔离”的开发者。2. 智慧乡村管理系统后端分层先从镇村两级业务角色说起拿到这套源码第一件事不是去数Controller写了多少行而是先回答“登录后有哪几种角色每种角色对应哪几个村”。角色模型不成立后面的村域数据隔离全是空谈。我一般按三层角色来拆镇级管理员维护全镇字典和汇总报表村操作员维护本村的人、地、钱、事村民账号只查自己户内信息。这三个角色对同一张household表的要求完全不同落库时最简单的方式不是建三张表而是给每张业务表冗余一个village_code字段。2.1 先按镇、村操作员、村民分角色再写Java实体和权限village_code从哪里来常见做法是在账号表sys_user里冗余一个village_code字段登录成功后把这个值放进LoginUser对象塞进会话和ThreadLocal。查询的时候不需要再去join机构表少一次多表关联也减少“查错村”的概率。权限表达式可以很轻普通接口只校验“已登录”重操作校验“当前用户是否是这个村的人”。不要在Service里到处写if (user.villageCode ...)而要把这个判断下沉到数据层或拦截器。com.rural.village ├── config // 配置Security、MyBatisPlus、WebMvc │ ├── SecurityConfig.java │ └── MybatisPlusConfig.java ├── common // 通用返回、异常、工具类 │ ├── Result.java │ └── BizException.java ├── security // 登录用户、会话上下文、数据权限 │ ├── LoginUser.java │ └── UserContext.java ├── module // 按业务模块分包不按三层结构堆 │ ├── village │ │ ├── controller/VillageController.java │ │ ├── service/VillageService.java │ │ ├── service/impl/VillageServiceImpl.java │ │ └── mapper/VillageMapper.java │ ├── household │ ├── land │ └── subsidy └── VillageApplication.java这个包结构里LoginUser的核心字段是userId、userName、roleType、villageCode。roleType决定数据粒度镇级管理员的villageCode为空村操作员的villageCode为具体编号。UserContext负责把当前登录人放到ThreadLocal里Service层通过UserContext.getVillageCode()取数而不是在Controller里层层传参。很多后端会把controller、service、mapper各建一个顶层包业务多了以后打开目录全是几十个同名文件。按module分包后新加一个“补贴管理”模块只复制household这一整块公共包不动删除模块也直接删目录。对源码维护来说这种结构的查找成本比按层分包低得多。2.2 按业务模块分包别把controller/service堆成顶层大包智慧乡村这种系统一般部署在乡镇机房或内网维护者可能就是村委里懂点电脑的人所以工程选型要尽量少依赖外部容器。Spring Boot内嵌Tomcat打包成jar后一条命令就能启动模板放在src/main/resources/templates里前端直接改HTML文件不用编译。技术栈上我不推荐JSP原因很现实JSP最终会被编译成Servlet报错信息对前端开发者不友好。用Thymeleaf的话模板后缀还是.html浏览器直接打开能看到完整结构只有带th:前缀的指令需要后端渲染。前端开发者学习后端Java知识时最先能上手的也是这一层。业务模块后端核心对象使用角色村务档案VillageController、VillageService镇级/村级人口档案HouseholdController、HouseholdService村操作员土地档案LandParcelController、LandParcelService村操作员补贴资金SubsidyController、SubsidyService镇级审核事项公告NoticeController、NoticeService村操作员/村民后台账号SysUserController、SysUserService镇级管理员上表里“使用角色”不是给权限配置看的而是给每个模块定数据范围。补贴资金如果由镇级做终审那么列表接口要以镇级维度查询全部村不能沿用村级拦截器所以每个模块必须在入口说明自己的数据范围类型。做后端设计时我会把数据范围先写成枚举再挂到Controller注解上避免同一个查询方法里出现“有时候加村条件、有时候不加”的薛定谔逻辑。2.3 智慧乡村页面用服务端渲染HTML比前后端分离更省事为什么不拆成Vue智慧乡村的页面绝大多数是表格、表单、详情页交互强度不高部署环境又偏向内网。Vue打包后要额外配静态服务器出问题时还要看Node构建链路的日志。服务端渲染的HTML由Spring Boot直接输出浏览器打开就是完整页面低版本浏览器也不会白屏。如果已经决定做前后端分离Java后端只出JSON也没问题但那就不算标题里的HTML直出方案了。前后端分离项目实战里常见的跨域、token刷新、路由守卫问题在服务端渲染里都不存在。需要注意的只有一个点静态资源目录src/main/resources/static只放CSS、JS、图片业务页面必须放templates否则Thymeleaf不会做模板解析。3. 后端设计里的数据库模型人、地、钱、事怎么落成MySQL智慧乡村和普通管理系统的差别在于数据对象非常固定一个村有哪几户每户有哪几个人名下有哪几块地享受过哪几笔补贴。只要“人、地、钱、事”四类对象能建清楚后面的大屏统计、报表导出都只是查询手段。我在设计数据库模型时不会一上来就做大宽表而是先把每个业务对象单独落表再用village_code把村级维度串起来。3.1 以“人、地、钱、事”四类对象建表先出乡村数据模型建表顺序一般从“户”开始户是人口和土地的自然载体。随后是土地档案、补贴记录、事项走访。每张表都带一个create_by和create_time方便排查数据是谁在什么时候录的。对于智慧乡村系统巡查走访记录比较特殊一条走访可能关联多个农户所以走访表不直接放household_id而是放household_no再用关联表把一次走访和多个户关联起来。表名业务对象关键字段village_info行政村档案village_code、village_name、province、city、areavillage_household农村家庭户village_code、household_no、contact_namevillage_villager户籍人口household_id、id_card、name、genderland_parcel土地地块village_code、household_no、land_type、area_acresubsidy_record补贴资金village_code、household_no、subsidy_type、amountvisit_task走访任务village_code、household_no、visit_result、visit_timenotice_info村务公告village_code、title、content、publish_time这里面的id_card一定要做脱敏展示后端返回给页面时只保留前六后四完整身份证号只允许导出接口使用。土地面积统一用“亩”作为单位所有前端展示都走后端的格式化工具否则同一个字段有的页面显示平方米、有的显示亩报表永远对不上。3.2 每张业务表带village_code并建立唯一约束村级维度落库最关键的约束是保证“同一个村里户编号唯一不同村之间可以重号”。如果把household_no单独设成唯一索引会在合村并镇或数据迁移时大量撞车。正确做法是建立联合唯一约束把village_code和household_no绑在一起。CREATE TABLE village_household ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, village_code VARCHAR(12) NOT NULL COMMENT 村编码, household_no VARCHAR(20) NOT NULL COMMENT 户编码, village_name VARCHAR(50) NOT NULL COMMENT 村名称, contact_name VARCHAR(30) NOT NULL COMMENT 户主姓名, contact_phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, member_count TINYINT DEFAULT 0 COMMENT 户籍人口数, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0注销, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_household (village_code, household_no), KEY idx_village_code (village_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT农村家庭户;uk_household是这章的重点以后不管是手工导数据还是对接上级系统只要village_code household_no重复数据库直接报错不会产生两条看起来很像的数据。idx_village_code是为了支撑按村统计的查询全部村级查询都走这个索引而不是全表扫描。3.3 用MyBatis-Plus的Page查询按村级维度取数ORM层使用MyBatis-Plus核心原因只有一个单表CRUD不需要写XML。乡村系统的字段变更频繁用MQB自带的LambdaQueryWrapper改查询条件时改动集中在Service层不容易出现SQL字符串拼接错误。PageHousehold page new Page(pageNum, pageSize); LambdaQueryWrapperHousehold wrapper Wrappers.lambdaQuery(); wrapper.eq(Household::getVillageCode, UserContext.getVillageCode()) .eq(Household::getStatus, 1) .orderByDesc(Household::getCreateTime); this.baseMapper.selectPage(page, wrapper);这里Page构造函数的第一个参数是页码从1开始第二个参数是每页条数页面直接传给Service。eq(Household::getVillageCode, ...)是 MyBatis-Plus 的 Lambda 写法避免把列名写成字符串后SQL里大小写不一致或改了实体字段没改查询。注意村操作员的villageCode从上下文取而不是从前端参数收前端传进来的villageCode一律忽略这样即使有人改请求参数也查不出其他村的数据。3.4 首页统计不要一次JOIN出30个字段首页大屏或者数据面板经常要统计总户数、总人口、土地面积、本月补贴金额。最容易翻车的写法是一条SQL JOIN五张表再用SUM和CASE WHEN算出所有指标。这种SQL在数据量小时很快到了年底数据膨胀后连查询都难以优化。常见做法是拆成四条独立统计然后用VillageStatsVO聚合。即便一次请求要多次访问数据库也只是走主键或village_code索引远比一条大JOIN稳定。如果SQL真的必须关联也尽量只联两张表并且把过滤条件放在子查询里去消耗。4. Java后端接口与HTML模板渲染在一台机器上的协作方式服务端渲染的智慧乡村系统页面由Controller返回视图名称来驱动业务数据则通过AJAX拿JSON。很多接手源码的开发者会混淆这两条链路把JSON接口直接return成页面或者把模板当静态文件访问。要理清这件事得从一次典型请求的完整生命周期说起。4.1 一个“人口档案”请求从URL到HTML的完整生命周期浏览器请求/village/household/listSpring Boot先走拦截器检查登录会话把当前用户的LoginUser放进UserContext然后进入Controller方法。Controller接收pageNum和pageSize调用Service拿到分页数据并把数据塞进Model最后返回视图名village/household-list。Thymeleaf根据这个名字找到templates/village/household-list.html把Model里的字段渲染成HTML再返回给浏览器。GetMapping(/village/household/list) public String householdList(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, Model model) { PageHousehold page householdService.queryPage(pageNum, pageSize); model.addAttribute(page, page); model.addAttribute(menu, household); return village/household-list; }model.addAttribute(menu, household)是给侧边栏高亮用的。HTML里通过th:classappend判断当前菜单是否active这样前端切页面时不用在每一份HTML里写死高亮。返回的字符串必须以templates目录为根且不要带/结尾否则Thymeleaf解析路径会多一层目录。4.2 HTML模板用th:fragment抽公共后台框架后台页面最讨厌的是每个页面都复制一份侧边栏、顶栏、底部版权。Thymeleaf的th:fragment可以解决这个问题把公共框架抽成一个layout.html业务页面只写内容区。aside classmain-sidebar th:fragmentsidebar ul classsidebar-menu li th:classappend${menu household} ? active : a th:href{/village/household/list}人口档案/a /li li th:classappend${menu land} ? active : a th:href{/village/land/list}土地地块/a /li /ul /asideth:fragmentsidebar定义了一个可以被其他页面引用的片段。业务页面里只需要在侧边栏位置写div th:replace~{layout :: sidebar}/divThymeleaf会把上面这一整块替换进去。th:href{...}会自动拼接应用部署根路径即使这个系统部署在/rural子目录下链接也不会断。前端改样式时只需要打开公共模板不用每个页面重复改。4.3 AJAX局部刷新时接口分两类整页URL与JSON API列表页的搜索、分页、删除如果每次都用整页刷新体验会很差。常见做法是列表区域单独做一个局部模板AJAX拿到数据后只刷新表格容器。这要求后端把接口约定清楚返回HTML的路径和返回JSON的路径分开。方式路径返回类型使用场景整页渲染/village/household/listtext/html菜单跳转、首次进入JSON接口/api/village/household/listapplication/json搜索、分页、删除JSON接口/api/village/household/detailapplication/json弹窗详情JSON接口统一使用Result结构{ code: 200, message: ok, data: ... }。前端AJAX只判断code不判断HTTP状态码因为很多错误是业务错误而不是网络错误。接口返回JS数组时data里放分页对象包括records、total、current、size四个字段前端表格直接取records渲染。$.ajax({ url: /api/village/household/list, type: GET, data: { page: 1, limit: 10, keyword: $(#keyword).val() }, success: function (res) { if (res.code 200) { renderTable(res.data.records); renderPager(res.data.total, res.data.current); } else { toastr.error(res.message); } } });这里的keyword是唯一允许从前端传进Service的查询条件。page和limit控制分页village_code不参与传参由后端从UserContext里取。前后端对接时容易错的是字段名后端Status序列化后是status前端不能再传statusType过去。5. 智慧乡村后端设计最容易翻车的三个点导出、定时任务、缓存单村的CRUD跑通后系统会逐步接上数据导出、定时汇总、缓存热点数据。这三个功能恰恰是村级数据隔离最容易失效的地方导出查了全镇数据、定时任务没有登录用户上下文、缓存Key不包含村编码都会让隔离形同虚设。下面把这几个坑拆开说清楚。5.1 村级数据隔离要用统一追加不能交给前端村操作员登录后页面上所有下拉框、搜索条件都由后端渲染。但不能只靠前端隐藏“村选择框”来保证安全因为接口可以被直接调用。正确做法是把数据隔离做成后端统一行为查询单表时自动追加village_code条件更新和删除时先校验记录归属。一个简单粗笨的做法是所有Service方法里手动写eq(village_code, ...)但难免有方法忘记。更好的办法是用MyBatis-Plus的多租户拦截器把village_code当成租户ID处理。拦截器会在所有不带别名的单表SQL后面自动拼上village_code ?开发者不再需要每段代码都记得这件事。5.2 用MyBatis-Plus多租户拦截器实现村编码隔离public class VillageTenantLineHandler implements TenantLineHandler { Override public Expression getTenantId() { return new StringValue(UserContext.getVillageCode()); } Override public String getTenantIdColumn() { return village_code; } Override public boolean ignoreTable(String tableName) { return tableName.startsWith(sys_) || tableName.equals(village_info); } }配置到MybatisPlusInterceptor时需要把TenantLineInnerInterceptor加进去并设置TenantLineHandler为上面这个实现。getTenantId()返回当前登录人的村编码镇级管理员可以在这里返回空值并跳过拦截。ignoreTable里放字典表、账号表、村档案表这类表要么全局共享要么由镇级统一维护。提示这个能力只在MyBatis-Plus 3.5.x里比较稳定。低版本如果发现SQL拼接异常直接升级版本不要尝试手写JSqlParser解析。5.3 导出Excel、定时任务、Redis缓存为什么会串村数据隔离拦截器面对“导出”这个场景会有问题因为导出通常由系统管理员全量执行而查询列表时又要按村过滤。如果继续启用拦截器导出接口只能导出当前登录人的村镇级管理员可能还好但村操作员就无法导出汇总Excel了。场景串村原因处理方式Excel导出拦截器自动加village_code条件导出接口单独标记忽略拦截器手动静默传村编码定时任务没有登录用户UserContext为空定时任务启动时显式设置上下文Redis缓存Key只有householdId不同村同ID缓存Key强制拼接village_code处理导出的常见做法是给Mapper方法加InterceptorIgnore(tenantLine true)然后在Mapper XML里手写village_code #{villageCode}条件。这样导出SQL自带村级过滤既绕过自动拦截器也不存在漏条件的问题。定时任务更隐蔽比如凌晨统计各村人口增长率任务本身拿不到登录用户必须在执行前把任务对应的village_code填进UserContext执行完在finally里清理掉。Redis的坑则很简单但很致命。如果你的redisKey写的是village:household:detail:{id}两个村都有ID为100的户后写入的村会覆盖先写入的村。改成village:household:detail:{villageCode}:{id}并带上业务版本号这种问题就不会出现。智慧乡村系统里数据量不大缓存主要用于首页统计和字典表宁可失效频繁一点也不要让缓存脏数据在村与村之间串。6. 源码跑起来后先给后端异常返回结构对齐一遍拿到源码后第一步不是急着加功能而是把异常处理统一掉。很多源码里的Controller还是各自返回String或Map报错时有的页面弹500有的弹{message: error}前端根本没法统一处理。6.1 统一Result和ControllerAdvice前端不用猜报错格式RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BizException.class) public Result? handleBiz(BizException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(ValidationException.class) public Result? handleValidation(ValidationException e) { return Result.error(400, e.getMessage()); } }BizException是业务层手动抛出的异常比如“户编号已存在”“该补贴记录已审核不能删除”。ValidationException由Java Bean Validation自动抛出参数校验失败时不走Controller内部逻辑直接进入这个handler。统一之后前端所有AJAX回调里只需要处理Result一种对象不再需要判断responseText是HTML还是JSON。6.2 前端AJAX只认一个错误对象$.ajaxSetup({ statusCode: { 401: function () { location.href /login; }, 403: function () { toastr.error(当前账号没有操作该村的权限); } } });这一步把最终的登录过期和越权行为收口到全局JS文件里。后端如果返回401或403浏览器层面的AJAX框架先拦截业务层的success回调根本不会触发。在此基础上正常的code: 400错误再走toastr提示。按这个顺序调一遍智慧乡村源码里的列表搜索、添加弹窗、导入导出功能报错格式就能完全一致。6.3 用登录心跳提示“账号被顶掉”别让操作员重复填表乡镇使用场景里同一个账号经常被多个人共用。账号被另一台设备登录后本地页面还开着操作员填了半天的表单提交时才发现被顶掉。后端可以在登录时生成一个sessionId并放进Redis每次心跳带上userId服务端比较Redis里的sessionId是否和当前请求一致不一致就返回newLogintrue。前端公共JS里只在newLogin true时触发退出并清空本地表单暂存用户下次登录后还能继续工作。本文还有配套的精品资源点击获取
返回列表