ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Spring Boot的乡村务工人员管理系统开发实践

基于Spring Boot的乡村务工人员管理系统开发实践 做这类带“某村”“某镇”字样的管理系统开发最容易被误解成“就是一个增删改查”。实际动手之后你会发现真正花时间的不是写那几个接口而是把业务状态梳理清楚、把数据结构设计得能支撑统计上报、再把权限边界控制到位。这篇文章我会以一个基于Spring Boot的疫情期间乡村外出务工人员信息管理系统为例从需求痛点、技术选型、表结构设计、核心功能实现、权限控制到部署调试完整讲一遍我实际开发时的思路和踩坑经验。不管你是在做毕业设计还是工作中临时要接一个类似的基层管理后台这篇都能给你一套可以直接参考的落地方案。1. 这类系统背后的真实业务场景台账、动态摸排与上报压力1.1 纸质表和Excel台账的极限在哪里很多没在基层待过的人对“村级外出务工人员管理”的第一反应是这也要做个系统实际上在村里外出务工人员的管理一直是个很让人头疼的事。过去最常见的管理方式就是纸质登记表和Excel台账村干部定期入户摸排问清楚人在哪里、什么时候走、什么时候回。这套流程在人口少的时候勉强能用但存在几个绕不开的问题第一数据不唯一。同一个村民可能出现在好几张表里有时候叫“张三”有时候登记成了“张叁”电话字段格式五花八门村东头和村西头各记了一份等到要汇总的时候根本不知道哪份是对的。第二状态严重滞后。谁上周出门了、谁昨天刚回来靠的是村干部的记忆和零散的微信聊天记录。村里平时事情又多几天不更新台账就跟现实脱节了。第三上级要数据的时候非常被动。乡镇往村里要一个“外出务工人员名单及去向统计”通常只给几小时反应时间手动翻Excel再汇总加班加到半夜是常事。这套系统在特殊时期的意义就体现出来了需要快速搞清每一个外出人员的去向、行程节点和健康状态数据如果还停留在纸质和散装Excel的状态摸排和上报的效率就跟不上。1.2 系统真正要解决的是“底数、动态、上报、追溯”四个问题所以在设计阶段的第一个任务不是急着写代码而是把需求翻译成四个明确的目标底数清全村到底有多少外出务工人员每个人的基本信息、外出地点、务工单位、联系方式都要有唯一且完整的档案。动态明外出、返乡、中途变更去向这些状态变化要能被记录和查询不能只停留在“登记过就算完”。上报快乡镇需要的各类统计口径比如按省份分布、按返乡时间分布、按健康状态分布系统要能一键出数。可追溯每条关键记录要能看出是谁、在什么时间、通过什么方式登记的出现异常情况时能查清楚信息链路。这四点听起来很朴素但决定了整个系统的核心架构导向它不能只是一个简单的“通讯录管理后台”必须把“人员”和“动态行为”分开建模再通过关联关系把两类数据串起来。1.3 在毕业设计场景下这类项目的合理定位如果你是在做毕业设计这类题目的定级建议是“中等难度业务系统”不要把它想得太简单也不要过度设计。很多人一上来就微服务、Redis缓存、消息队列其实都是给自己挖坑。这个规模的系统单体应用加关系型数据库就是最优解。把精力花在业务逻辑的完整度、表结构设计的合理性、权限控制的严谨性上答辩的时候反而更有话可讲。我在实际开发中也验证过Spring Boot单体内实现这些功能完全够用维护起来还省心。2. 技术选型与项目架构为什么Spring Boot是这个场景的最优解2.1 相比传统SSH或原生Spring MVCSpring Boot带来了什么这个系统使用Spring Boot框架来构建在实际开发中的体感差异非常明显。传统Spring项目要配置一堆XML数据源、事务、视图解析器都得手动声明环境变量换一套就折腾半天。Spring Boot用自动配置和约定大于配置的思路把大量样板配置消灭掉了只用application.yml一个文件就能把数据源、JPA/MyBatis、模板引擎、日志、端口全部管好。这里顺便回应一个经常被问到的问题IntelliJ IDEA社区版能不能开发Spring Boot项目答案是可以而且体验并不差。社区版缺少的主要是Spring相关的专属辅助工具比如依赖提示和运行面板但Maven/Gradle插件、代码补全、调试器、Git这些核心能力都是完整的。你只需要自己手动配置一下运行配置Main Class指向启动类日常开发完全不受影响。所以不要因为用的IDE不是旗舰版就不敢选Spring Boot做毕业设计或商用项目。2.2 技术栈清单与版本搭配建议以我当时搭建的环境为例做个完整的组合参考组件选型说明核心框架Spring Boot 2.7.x稳定、资料多3.x需要JDK17且部分依赖有兼容门槛JDK1.8或11如果坚持用2.7.xJDK8也完全可行ORMMyBatis Plus 3.5.x单表CRUD基本不用写XML分页、条件构造器都好用数据库MySQL 8.0默认字符集utf8mb4防止emoji和生僻字乱码模板引擎Thymeleaf适合服务端渲染的普通管理后台权限框架Spring Security基于角色的URL权限控制够用且规范前端组件Layui或AdminLTE不依赖复杂构建工具原生HTML即可上手这套组合最大的优势是“每一层都有成熟方案不折腾”。MyBatis Plus特别适合这种以单表CRUD为主、少量多表统计为辅的业务场景写起来比手写大量XML要舒服得多。如果换成Spring Data JPA也可以但在统计查询、动态SQL这块个人感觉可控性不如MyBatis Plus。2.3 后端分层与包结构设计包结构这个问题很多教程讲得模模糊糊我直接给出我实际使用的划分方式com.example.townworker ├── config │ ├── SecurityConfig.java │ └── MybatisPlusConfig.java ├── controller │ ├── LoginController.java │ ├── WorkerController.java │ ├── TravelRecordController.java │ ├── HealthReportController.java │ └── StatsController.java ├── entity │ ├── SysUser.java │ ├── Worker.java │ ├── TravelRecord.java │ ├── HealthReport.java │ └── Notice.java ├── mapper │ ├── WorkerMapper.java │ └── StatsMapper.java ├── service │ ├── WorkerService.java │ ├── WorkerServiceImpl.java │ └── StatsService.java ├── common │ ├── Result.java │ └── PageResult.java ├── dto │ ├── TravelRecordDTO.java │ └── StatsDTO.java └── SpringbootVillageWorkerApplication.javaController只负责参数接收和结果封装Service层放业务规则和事务控制Mapper层专注数据访问。这个分层虽然看起来“老生常谈”但真能坚持做到位后期维护会轻松很多。我见过不少同学生成代码时图省事直接在Controller里写业务逻辑数据量大以后一个方法几百行调试起来非常痛苦。3. 核心表结构设计人员档案、出行记录、健康信息、通知公告3.1 人员档案表不是一张简单的通讯录人员表是这个系统的根基设计得好坏直接决定后面所有功能的复杂度。我当时建的worker表重点字段有CREATE TABLE tb_worker ( id BIGINT AUTO_INCREMENT PRIMARY KEY, worker_no VARCHAR(32) NOT NULL COMMENT 档案编号, name VARCHAR(64) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL COMMENT 身份证号, phone VARCHAR(20) COMMENT 联系电话, gender TINYINT COMMENT 性别 0-女 1-男, birthday DATE COMMENT 出生日期, province VARCHAR(64) COMMENT 目前所在省, city VARCHAR(64) COMMENT 目前所在市, work_unit VARCHAR(128) COMMENT 务工单位, work_type VARCHAR(64) COMMENT 工种/行业, go_out_date DATE COMMENT 外出日期, emergency_name VARCHAR(64) COMMENT 紧急联系人, emergency_phone VARCHAR(20) COMMENT 紧急联系电话, household_register VARCHAR(256) COMMENT 户籍地址, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态 0-注销 1-在册 2-已返乡, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_worker_no (worker_no), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT外出务工人员档案表;这里有两个容易被忽略的设计点身份证号设唯一索引这是保证“数据不重复”的底线。真实场景里身份证号录入错误率不低配合Web端输入校验18位、格式正则可以大大降低脏数据。status字段不只是“登记/注销”还承担了“已返乡”这个业务状态。为什么要放在档案表上而不完全依赖行程表因为在业务上一个村民只要有一次返乡记录没有再次外出他的整体状态就应该变为“已返乡”这在列表筛选和统计里非常常用单独算会很麻烦。3.2 出行记录表把“动态”和“静态”分开存储外出和返乡在业务上是两种不同方向的动作我统一放在一张travel_record表里用travel_type区分CREATE TABLE tb_travel_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, worker_id BIGINT NOT NULL COMMENT 关联人员ID, travel_type TINYINT NOT NULL COMMENT 1-外出 2-返乡 3-中转, depart_province VARCHAR(64) COMMENT 出发地省份, depart_city VARCHAR(64) COMMENT 出发地城市, arrive_province VARCHAR(64) COMMENT 到达地省份, arrive_city VARCHAR(64) COMMENT 到达地城市, travel_way VARCHAR(32) COMMENT 交通方式火车/飞机/自驾/大巴, flight_train_no VARCHAR(64) COMMENT 车次/航班号, depart_time DATETIME COMMENT 出发时间, arrive_time DATETIME COMMENT 到达时间, risk_level TINYINT COMMENT 风险等级 0-低 1-中 2-高, record_user_id BIGINT COMMENT 登记人ID, record_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255), KEY idx_worker_id (worker_id), KEY idx_arrive_time (arrive_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出行记录表;关键设计思路有两点。第一是不用两张表分别存“外出记录”和“返乡记录”宁愿加一个字段区分类型。原因是后期统计“某一时间段内有多少人次返回本村”时一条SQL就能搞定维护起来也简单。第二是“出发地/到达地”不要只存一个字符串而是拆成省市两级否则按省份统计时还得LIKE匹配既慢又不准。risk_level这个字段我要多说一句。它应该由登记人选填或系统按目的地风险等级自动带出不建议做成固定值因为风险等级本质上是会随时间变化的。系统里只要把最近一次行程的风险等级记录在案再配合本地规则修正就能满足大多数上报需求。3.3 健康信息表连续按日上报才有统计价值疫情管理的核心数据之一是健康上报记录。这个表我命名为tb_health_report设计如下CREATE TABLE tb_health_report ( id BIGINT AUTO_INCREMENT PRIMARY KEY, worker_id BIGINT NOT NULL, report_date DATE NOT NULL, body_temperature DECIMAL(4,1) COMMENT 体温, is_cough TINYINT COMMENT 是否有咳嗽 0-否 1-是, is_fatigue TINYINT COMMENT 是否乏力 0-否 1-是, is_contact TINYINT COMMENT 是否接触过确诊/疑似人员 0-否 1-是, vaccine_count TINYINT COMMENT 疫苗接种针次, test_result TINYINT COMMENT 检测结果 0-未检测 1-阴性 2-阳性, remark VARCHAR(255), UNIQUE KEY uk_worker_date (worker_id, report_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康上报记录表;这里一个容易踩的坑是重复上报。同一人同一天只能有一条记录所以给(worker_id, report_date)加了唯一索引Service层在新增前先查一次存在则改成更新。不加这个索引测试人员多点几次提交按钮库里就会灌满脏数据。3.4 通知公告表与用户表通知公告表比较简单字段就是title、content、publisher_id、publish_time、status草稿/已发布。这类表没什么技术难度但要提前考虑列表页的发布时间排序和分页查询所以在create_time上建索引。用户表我更建议用独立的sys_user表而不是直接复用人员的档案表。原因是两类对象的属性差别很大用户需要用户名、密码、角色、状态、最后登录时间而外出务工人员档案跟系统登录没有必然关系。一开始为了省事把两者合一后面做权限控制时反而会非常别扭。CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(64), role_code VARCHAR(20) NOT NULL COMMENT ADMIN / VILLAGE_ADMIN / VIEWER, village VARCHAR(128) COMMENT 负责村, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;我的权限模型是三级乡镇管理员ADMIN、村级管理员VILLAGE_ADMIN、只读查看员VIEWER。如果你们村下面还分片区可以在用户表加一个team字段做更细粒度的过滤。4. 后端核心功能实现状态流转、统计报表与接口设计4.1 档案管理批量导入比单条新增更重要写WorkerController的CRUD并不难难的是“好用的导入”。实际使用中村干部手里往往已经有一份Excel名单让他们一条条录到系统里基本是不可能完成的任务。所以我在系统里提供了一个基于EasyExcel的批量导入接口只需按照模板上传Excel后端解析后逐条校验错误行返回给前端下载错误报告。Override Transactional(rollbackFor Exception.class) public MapString, Object importWorkers(MultipartFile file) { ListWorkerImportRow rows EasyExcel.read(file.getInputStream()) .head(WorkerImportRow.class) .sheet() .doReadSync(); ListString errors new ArrayList(); int success 0; for (int i 0; i rows.size(); i) { WorkerImportRow row rows.get(i); if (StringUtils.isBlank(row.getIdCard()) || StringUtils.isBlank(row.getName())) { errors.add(第 (i 2) 行姓名和身份证号不能为空); continue; } boolean exists checkIdCardExists(row.getIdCard()); if (exists) { errors.add(第 (i 2) 行身份证号已存在跳过新增); continue; } // 组装实体并保存 Worker worker new Worker(); BeanUtils.copyProperties(row, worker); worker.setWorkerNo(generateWorkerNo()); worker.setStatus(1); save(worker); success; } MapString, Object result new HashMap(); result.put(successCount, success); result.put(errorCount, errors.size()); result.put(errors, errors); return result; }这段代码里有三个关键点事务注解保证批量操作的一致性部分失败时已成功的行不回滚因为是分批校验、跳过错误行全量读取数据后逐行校验而不是边读边写避免解析中途出错导致状态不一致generateWorkerNo()生成唯一档案编号格式比如“W202406001”这个编号后面在纸质台账上也会用到方便线上线下对应。4.2 返乡登记与状态联动一次操作维护两条数据返乡登记是本系统里业务逻辑最复杂的操作因为同时涉及行程记录和人员状态。Service public class TravelRecordServiceImpl implements TravelRecordService { Override Transactional(rollbackFor Exception.class) public boolean registerReturn(TravelRecordDTO dto) { // 1. 校验关联人员存在 Worker worker workerService.getById(dto.getWorkerId()); if (worker null) { throw new BizException(关联的务工人员不存在); } // 2. 新增一条返乡记录 TravelRecord record new TravelRecord(); record.setWorkerId(dto.getWorkerId()); record.setTravelType(2); // 返乡 record.setDepartProvince(dto.getDepartProvince()); record.setDepartCity(dto.getDepartCity()); record.setArriveProvince(dto.getArriveProvince()); record.setArriveCity(dto.getArriveCity()); record.setTravelWay(dto.getTravelWay()); record.setDepartTime(dto.getDepartTime()); record.setArriveTime(dto.getArriveTime()); record.setRiskLevel(dto.getRiskLevel()); record.setRecordUserId(currentUserId()); travelRecordMapper.insert(record); // 3. 同步更新人员状态为“已返乡” Worker update new Worker(); update.setId(worker.getId()); update.setStatus(2); workerService.updateById(update); return true; } }这里最值得学习的是“一个业务操作涉及多张表时事务必须覆盖整个操作”以及“Service层不要只做单表CRUD要承担状态流转逻辑”。很多初写代码的同学容易把状态更新逻辑丢给前端来处理前端调两个接口各改各的表一旦中间失败数据就处于不一致状态。使用Transactional(rollbackFor Exception.class)之后任何一步抛异常都会把整条链路回滚掉。还有一个细节登记返乡时虽然会把人员状态置为“已返乡”但如果此人几天后又再次外出新外出记录会再把状态改回“在册”。状态字段只是用于快速筛选完整过程永远以行程记录表为准这是一个必须向用户解释清楚的设计原则否则村干部会经常疑惑“为什么这个人状态又变了”。4.3 统计报表接口考虑的是上级口径而不是花哨图表统计功能我建议别一上来就搞ECharts大屏先想清楚两个问题谁在看这个统计他要什么在这个场景下使用者就是乡镇管理员或者需要往上报数据的村委他们要的不是炫酷的可视化而是几个明确数字外出总人数、分布在多少个省份、已返乡多少人、高风险地区返回多少、疫苗接种覆盖率是多少。我用一个统计聚合接口来满足需求GetMapping(/api/stats/overview) public ResultMapString, Object overview() { long totalWorker workerMapper.selectCount(new LambdaQueryWrapperWorker() .ne(Worker::getStatus, 0)); long nowOutside workerMapper.selectCount(new LambdaQueryWrapperWorker() .eq(Worker::getStatus, 1)); long returned workerMapper.selectCount(new LambdaQueryWrapperWorker() .eq(Worker::getStatus, 2)); // 按省份分组统计 ListMapString, Object provinceStats workerMapper.selectMaps( new QueryWrapperWorker() .select(province AS name, COUNT(*) AS value) .eq(status, 1) .groupBy(province)); // 返乡高峰按日期统计最近30天 ListMapString, Object returnStats travelRecordMapper.selectMaps( new QueryWrapperTravelRecord() .select(DATE(arrive_time) AS date, COUNT(*) AS count) .eq(travel_type, 2) .ge(arrive_time, LocalDate.now().minusDays(30)) .groupBy(DATE(arrive_time)) .orderByAsc(date)); MapString, Object result new HashMap(); result.put(totalWorker, totalWorker); result.put(nowOutside, nowOutside); result.put(returned, returned); result.put(provinceStats, provinceStats); result.put(returnStats, returnStats); return Result.ok(result); }统计接口看似简单有几点值得注意第一多条统计数据合并在一个接口里比前端请求N次要高效得多第二分组统计用MyBatis Plus的QueryWrapper完全够用但复杂JSON返回时建议用Map接收不要为了强行建VO把代码写复杂第三返回的日期字符串要注意时区问题否则前端会看到日期少一天的情况在jdbc连接串里加serverTimezoneAsia/Shanghai可避免此问题。4.4 接口给第三方用的位置问题独立校验方案有人在搜索里问“Spring Boot对外提供的接口应该放在哪里是单独服务还是同项目”对这个项目而言我的建议非常明确同项目、不同模块路径下放前缀用/api/open/区分。因为乡镇数据上报往往不是一次性的如果做成了单独服务部署成本和数据同步成本都会显著增加。正确做法是在外接接口的Controller里单独做API鉴权使用简单的Token校验即可RestController RequestMapping(/api/open) public class OpenApiController { Value(${open.api.token}) private String openApiToken; GetMapping(/export/outside-list) public Result? exportOutsideList(RequestHeader(X-Api-Token) String token) { if (!openApiToken.equals(token)) { return Result.fail(无访问权限); } // 返回到前一天的外出人员名单 ListWorkerVO list workerService.listOutsideWorkers(); return Result.ok(list); } }把open.api.token配置在yml里对方系统拿着token来调返回统一JSON结构。这样既不影响内部系统的Spring Security配置也不需要为了一个接口单独部署一个服务是性价比最高的方案。5. 权限控制与前端交互让村里的干部们真的愿意用5.1 基于Spring Security的角色权限配置用Spring Security做权限控制最怕的是配置繁琐、拦截粒度不好把控。我在这里用的是一个比较克制的方案基于角色的URL拦截而不是方法级细粒度权限。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers(/login, /css/**, /js/**, /api/open/**).permitAll() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/village/**).hasAnyRole(ADMIN, VILLAGE_ADMIN) .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) .defaultSuccessUrl(/index, true) .permitAll() .and() .logout().logoutSuccessUrl(/login).permitAll() .and() .exceptionHandling().accessDeniedPage(/403); return http.build(); } }这个配置的核心思路是不是每个接口都配权限而是按路径前缀来划分权限区。比如/admin/**下面放乡镇管理员专属功能用户管理、全乡镇数据汇总/village/**下面放村委功能本村人员管理、行程登记。这样即使以后新增Controller只要遵守路径命名规则权限自动生效。密码加密一定要用BCryptPasswordEncoder注册用户时不能存明文密码。这个知识点几乎必考同时也是真实项目的基本要求。生成一个BCrypt密码可以直接用Spring Boot里最常见的命令行方式也可以用一个简单的测试类来完成。5.2 页面级操作权限的“隐藏”比“拦截”更重要服务端权限拦截解决的是“未授权不能访问接口”但前端界面如果不做配合普通用户会看到一堆点不进去的菜单和按钮体验很差。我在模板引擎里通过Thymeleaf的sec标签库控制元素渲染div sec:authorizehasRole(ADMIN) a th:href{/admin/user/list} classbtn用户管理/a /div使用Thymeleaf的好处在这里充分体现服务端渲染时就可以根据当前用户角色生成不同的HTML不需要额外写前端权限判断逻辑。这个思路比前后端分离方案在基层管理系统场景下更实用因为不需要维护一套前端权限路由表开发速度也更快。5.3 前端设计要降低使用门槛基层系统的使用者大多不是专业IT用户所以前端设计的核心原则是大按钮、少输入、可扫视。我这边的页面结构围绕高频操作来组织首页Dashboard放“外出总人数”“已返乡人数”“近30天返乡趋势”几个大数字卡片。人员台账页支持关键字搜索、按状态/去向省份筛选表格密度适中关键字段高亮显示。返乡登记页一个表单搞定头部选人姓名/身份证联动查询下面填行程信息提交后同时联动人员状态。统计上报页把乡镇管理员需要的口径直接展示成表格旁边放“导出Excel”按钮。遇到的最大坑是表格数据量变大以后普通分页接口扛不住慢查询。排查后发现是关联人员表的查询没有用索引后来给Mapper加了一条优化列表查询时先过滤掉已注销状态再联结查询行程和健康数据速度从3秒降到200毫秒以内。对于这种以几十万条为上限的小型系统不需要上搜索引擎做好索引就是最实用的优化手段。前端表单校验也必须有后端兜底不能只依赖HTML5的required。因为有些村干部会复制粘贴Excel里的数据偶尔会带出不可见字符和换行符后端做trim和正则校验后再入库能避免大量脏数据问题。6. 部署调试与排错实录把本地能跑变成现场能用6.1 环境准备与数据库初始化我开发时用的IntelliJ IDEA社区版直接通过Maven的spring-boot:run插件启动项目不需要额外安装插件。数据库初始化用Spring Boot的spring.sql.init机制把建表SQL和基础数据放到schema.sql和data.sql中首次启动自动执行。但你需要注意生产环境重启用这个机制如果SQL不是幂等的可能会有重复执行问题所以我在实际部署时改为手动执行一次初始化脚本。application.yml里最关键的配置是数据源和日志级别spring: datasource: url: jdbc:mysql://localhost:3306/village_worker?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ****** driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl开发阶段把thymeleaf.cache设为false改页面模板后刷新就能生效不用重启MyBatis Plus的StdOutImpl日志可以直接看到每条SQL和参数排查问题非常高效。但上线时这两项都必须关掉否则会拖慢性能并泄露SQL细节。6.2 踩过的几个真实坑和排查思路开发过程中有几个问题印象特别深刻逐一列出来供参考数据库中文乱码。一开始MySQL用默认latin1字符集建库页面显示全是问号。解决方法是建库时显式指定utf8mb4同时检查jdbc连接串加了characterEncodingutf8。注意utf8mb4和utf8是有区别的带emoji的人名虽然少见只有前者能存住。日期选择器提交后报错“Failed to convert value of type java.lang.String to java.time.LocalDate”。原因是前端表单date input提交的字符串格式与LocalDate默认解析格式不一致。解决方法是配置一个全局的日期转换器或使用DateTimeFormat(pattern yyyy-MM-dd)注解。我推荐在Controller的DTO字段上加注解比全局配置更好定位问题。返乡登记接口在并发点击时会同时插入两条记录。前面提到的uk_worker_date唯一索引在这里发挥了兜底作用但还有一个干扰项同一个操作者快速点击两次第一次请求还没完成第二次请求携带的worker状态还是旧的。解决方法是前端提交按钮点击后立即置灰后端保留唯一索引兜底双保险。导出Excel文件时中文文件名在浏览器里变成乱码。不是因为字体而是HTTP响应头需要URL编码文件名。用URLEncoder.encode(fileName, UTF-8)处理后前端decodeURIComponent再接收问题解决。6.3 系统上线前的手工测试清单我每次交付前都会走一遍手工测试清单这里分享给你基本能覆盖这类系统80%的核心流程人员档案新增、编辑、删除、按姓名模糊搜索、按状态筛选。行程记录外出登记后人员状态是否变成在册返乡登记后状态是否变成已返乡一条行程删除后状态是否会错误更新这是难点。健康上报同一天重复上报是否被拦截体温超过阈值时是否有提示。权限用VIEWER账号访问ADMIN接口是否被拦截未登录时直接访问页面是否自动跳转登录页。统计造一批测试数据核对总数和各分组的COUNT和SUM与实际SQL结果是否一致。导出导出Excel后检查各列字段是否对应正确空值如何处理。其中“一条行程删除后状态是否会错误更新”是最容易翻车的地方。比如一个人先外出再返乡行程表里有两条记录如果删掉了返乡记录人员状态是否应该自动回退到“在册”我的方案是不做自动回退而是靠编辑操作时重新计算这样避免复杂的级联逻辑。你可以在这个设计上作取舍但一定要明确告诉使用者数据维护规则。7. 一点个人的实操体会这个系统从设计到落地前前后后花了大半个月最深的感受是业务理解比技术实现重要得多。一开始我也在想怎么把界面做得更高级、接口写得更“优雅”后来跑去村里坐了一下午看村干部怎么登记、怎么汇总、怎么向镇里汇报回来之后把设计推翻重来了一半。真正的需求其实非常朴素录入要快、查询要准、数据不能错、导出要方便。技术的价值就是把这些朴素的诉求用最稳妥的方式实现出来。如果你也要做类似的项目我建议先花一天时间梳理“角色-操作-数据”的对应关系把谁能在什么页面看到哪些数据写清楚再动工写代码。表结构定下来之后后面的CRUD都是体力活真正的分水岭就在需求梳理和状态流转设计上。希望这篇对有同样开发任务的朋友有帮助也欢迎你在评论区聊聊你在这个项目里遇到的坑。
返回列表