ARTICLE DETAIL

资讯详情

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

SpringBoot + Vue3 + MyBatis 隔离点人员管理系统实战开发

SpringBoot + Vue3 + MyBatis 隔离点人员管理系统实战开发 1. 项目背景与整体技术选型逻辑1.1 为什么做这样一个前后端分离系统前一阵接到一个实际需求给隔离点做一套人员管理和健康状态跟踪系统。隔离点的业务并不复杂核心就三件事——人员登记、日常健康上报、状态流转入点、隔离中、解除隔离。但真做起来坑并不少人员信息分散在表格里每日体温和核酸记录靠人工统计隔离时间算不明白异常情况无法及时预警。所以这套系统的目标很明确用一套可落地的Web应用把隔离点日常管理从“微信聊天Excel表”变成“系统录入自动提醒可追踪记录”。系统采用前后端分离架构SpringBoot Vue3 MyBatis MySQL后端提供REST接口前端用Vue3做独立SPA数据库统一存放业务数据。选择前后端分离不是跟风。隔离点这类内部系统往往需要在多个终端访问办公电脑、管理人员笔记本后期可能加移动端而分离架构天然适合这种多端接入场景——后端接口做好前端可以单独换壳不影响业务逻辑。想深入学SpringBoot、Vue3或者MyBatis的人也可以拿这个项目做一份综合性的练手范例。1.2 技术栈选择的取舍SpringBoot Vue3 MyBatis技术栈看似常见但每个选型都有它的原因。后端选SpringBoot不太需要解释。它解决了Spring大部分繁琐配置问题内置Tomcat打jar包直接跑部署门槛非常低。对于隔离管理这种业务迭代快、运维人员可能不固定的内部系统这个优势非常实在。ORM层选MyBatis而不是JPA/Hibernate也是有意为之。隔离管理涉及大量动态条件查询按姓名、按状态、按隔离点、按时间段筛选业务规则经常临时调整。MyBatis的XML映射文件写动态SQL非常直接一个where标签就能搞定多个可选条件的组合后期排查SQL问题就是打开XML看两句的事完全不需要去理解框架自动生成的SQL行为。前端选Vue3一方面因为Composition API对复杂页面逻辑的组合复用效果好另一方面Vite冷启动快、构建效率高开发体验明显比Vue2配Webpack时期流畅。配合Element Plus组件库表格、弹窗、表单这类管理端页面的开发速度非常快。数据库选MySQL理由更简单隔离点系统属于中小规模内部应用MySQL完全能扛住日常千级别并发访问而且运维资料多、排查方案好找对硬件要求也低。建好表索引之后查询性能基本不用操心。2. 后端核心实现SpringBoot MyBatis 从配置到接口2.1 工程结构与数据库驱动配置后端工程按照标准分层结构组织这也是隔离系统这种业务型项目最稳妥的布局com.isolation ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // MyBatis Mapper接口 ├── entity // 实体类 ├── dto // 入参、出参对象 ├── config // 跨域、拦截器配置 ├── common // 统一返回体、异常 └── util // 工具类日期、状态流转等在pom.xml中核心依赖主要是这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency这里特别说一下MyBatis版本选择。很多人直接引用mybatis-spring-boot-starter最新版如果SpringBoot用的是3.x要注意starter版本和SpringBoot的兼容性。我最开始本地一套SpringBoot 3.1配了一个老版本的starter启动直接报ClassNotFoundException。后来统一改成SpringBoot 2.7.x mybatis-spring-boot-starter 2.3.1全程稳定。你要是新建项目建议在写依赖之前先确认版本对应关系别到了启动才排查。application.yml中的核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/isolation_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.isolation.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置建议一定开启。数据库字段用下划线命名如isolate_start_date实体类属性用驼峰命名如isolateStartDate开启后自动映射省掉一大片resultMap手写代码。2.2 实体、Mapper 与 XML 映射以隔离人员核心实体为例实体类字段尽量和数据库对齐同时把状态、日期类型的Java格式设计好public class IsolationPerson { private Long id; private String name; private String idCard; private String phone; private Integer gender; // 1男 2女 private String isolateLocation; // 隔离点名称 private Integer status; // 1待隔离 2隔离中 3解除 4异常转出 private Date inTime; // 入点时间 private Date outTime; // 解除时间 private Integer expectedDays; // 预计隔离天数 private Date createTime; }Mapper接口public interface IsolationPersonMapper { int insert(IsolationPerson person); int updateStatus(Param(id) Long id, Param(status) Integer status); ListIsolationPerson selectByCondition(IsolationQueryDTO dto); IsolationPerson selectById(Long id); }XML里最核心的是动态查询这是MyBatis真正发挥优势的地方。比如按条件查询隔离人员可能组合筛选姓名、状态、隔离点select idselectByCondition resultTypecom.isolation.entity.IsolationPerson SELECT id, name, id_card, phone, gender, isolate_location, status, in_time, out_time, expected_days, create_time FROM isolation_person where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND status #{status} /if if testisolateLocation ! null and isolateLocation ! AND isolate_location #{isolateLocation} /if /where ORDER BY create_time DESC /select这里的where标签是我反复跟人推荐的写法。它会自动处理掉第一个条件前的AND不然自己写WHERE 11虽然能用但看着难受执行计划也不够干净。2.3 Service 层与业务规则设计Service层是业务规则集中的地方也是这套系统里最值得展开的部分。隔离管理的核心规则我用状态机的方式理了一遍待隔离1已登记但未到达隔离点可以取消或转为隔离中。隔离中2在点隔离每日提交健康记录。解除3隔离期满且无异常走解除流程。异常转出4出现发热、密接升级等情况转到定点医院或特殊处理流程。登记入点这个动作涉及事务。因为要同时写人员和生成一条出入记录任何一个失败都要回滚Service public class IsolationPersonServiceImpl implements IsolationPersonService { Autowired private IsolationPersonMapper personMapper; Autowired private TransferRecordMapper transferRecordMapper; Override Transactional(rollbackFor Exception.class) public void checkIn(IsolationCheckInDTO dto) { IsolationPerson person new IsolationPerson(); person.setName(dto.getName()); person.setIdCard(dto.getIdCard()); // 省略字段赋值 person.setStatus(2); person.setInTime(new Date()); personMapper.insert(person); TransferRecord record new TransferRecord(); record.setPersonId(person.getId()); record.setType(1); // 1入点 2出点 record.setOperateTime(new Date()); record.setRemark(隔离人员入点登记); transferRecordMapper.insert(record); } }一个容易忽视的细节Transactional只对RuntimeException和Error默认回滚如果业务代码里catch了异常再抛一个自定义Exception必须显式加上rollbackFor Exception.class否则事务不会回滚。我早期写事务踩过这个坑最后排查半天发现是异常被吞了。状态变更同理不能只改person表状态字段还要插入一条流转记录保证每一步操作有迹可循。隔离管理对审计要求高“谁在什么时候干了什么”必须清晰。2.4 Controller 与统一返回值/异常处理Controller层尽量保持薄。所有接口返回统一结构前端拿到后直接判断code即可public class ResultT { private Integer code; // 200成功 500失败 private String message; private T data; }接口示例RestController RequestMapping(/api/person) public class IsolationPersonController { Autowired private IsolationPersonService personService; PostMapping(/checkIn) public ResultVoid checkIn(RequestBody Valid IsolationCheckInDTO dto) { personService.checkIn(dto); return Result.success(); } GetMapping(/list) public ResultPageResultIsolationPerson list(IsolationQueryDTO dto) { return Result.success(personService.pageQuery(dto)); } }参数校验用Valid加注解直接写在DTO字段上省去一堆手写判断public class IsolationCheckInDTO { NotBlank(message 姓名不能为空) private String name; NotBlank(message 身份证号不能为空) Pattern(regexp ^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$, message 身份证格式错误) private String idCard; }全局异常处理用RestControllerAdvice包一层逻辑很直接遇到业务异常返回对应message其他异常统一返回“系统异常”并打日志。这层做好之后前端基本只需要处理code200和code!200两种分支不用到处写try-catch。3. 前端 Vue3 部分从搭建到业务页面3.1 Vite 搭建与工程组织前端使用Vite初始化项目。现在创建一个Vue3项目已经非常简单了npm create vitelatest isolation-web -- --template vue cd isolation-web npm install工程结构按视图和组件拆分src ├── api // 接口请求封装 ├── views // 页面级组件 │ ├── person // 隔离人员管理 │ ├── report // 健康上报 │ └── dashboard // 数据看板 ├── components // 通用组件 ├── router // 路由配置 ├── stores // Pinia状态 └── utils // 请求封装、日期处理路由配置用Vue Router注意做权限拦截——虽然内部系统权限要求不重但页面前置守卫还是得有避免跳过登录页直接访问后续页面router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })3.2 使用 Vue3 Composition API 做数据请求Vue3最核心的变化就是Composition API。以人员列表页为例把数据请求和交互逻辑按功能组织比Options API分散在各处要直观得多script setup import { ref, onMounted } from vue import { getPersonList } from /api/person const loading ref(false) const list ref([]) const queryParams ref({ name: , status: undefined, isolateLocation: }) const fetchData async () { loading.value true try { const res await getPersonList(queryParams.value) if (res.code 200) { list.value res.data.list } } finally { loading.value false } } const handleSearch () { queryParams.value.pageNum 1 fetchData() } onMounted(() { fetchData() }) /scriptscript setup语法是Vue3项目中绕不开的重点。它让代码看起来更像普通JavaScript函数组合调试的时候不需要在两个选项里来回跳。对于列表查询这种高频操作把loading和list放在一起即可整体心智负担明显降低了。axios请求封装核心是拦截器。请求拦截器统一加token响应拦截器把code!200的情况统一拎出来提示避免业务代码里每处都写错误弹窗import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(请求失败请检查网络) return Promise.reject(error) } )3.3 关键页面与组件拆分前端页面主要分三块人员管理页、健康上报页、数据概览页。人员管理页是典型的表格弹窗组合用Element Plus的el-table、el-dialog、el-form组件拼装代码量最大但套路固定。实际操作中我建议把表单抽成独立组件因为入点登记、编辑人员、解除确认都复用同一套字段只是提交接口不同。健康上报页移动端使用频率更高所以在布局上尽量用大按钮和清晰的状态色块实现。这里顺带说一个细节表格里日期时间显示不能直接拿时间戳裸奔要格式化成YYYY-MM-DD HH:mm:ss。我在utils里封装了一个格式函数export function formatDate(value) { if (!value) return const date new Date(value) const y date.getFullYear() const m String(date.getMonth() 1).padStart(2, 0) const d String(date.getDate()).padStart(2, 0) const hh String(date.getHours()).padStart(2, 0) const mm String(date.getMinutes()).padStart(2, 0) const ss String(date.getSeconds()).padStart(2, 0) return ${y}-${m}-${d} ${hh}:${mm}:${ss} }表单校验用Element Plus的rules即可但一定要在表单提交时手动调用validate()方法不然校验规则形同虚设const formRef ref(null) const handleSubmit async () { await formRef.value.validate() // 后续提交逻辑 }4. MySQL 数据库设计与隔离业务建模4.1 核心表结构人员、隔离点、健康上报、出入记录隔离管理系统的数据量并不大但业务关系清晰。我建了五张核心表isolation_person隔离人员主表存身份信息、隔离状态、入点出点时间。isolation_point隔离点表包括名称、地址、最大容量、当前容量。health_report每日健康上报记录含体温、症状、核酸记录等。transfer_record出入点/流转记录含类型、时间、操作人。sys_user系统账号用于登录和操作审计。建表时注意几个细节第一所有业务表都带create_time和update_time而且update_time建议设计为自动更新update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP第二状态字段用TINYINT存数字不要直接存中文。数据量小但查询条件多数字状态配合代码注释比字符串更省空间索引效率也高。第三身份证号建议加唯一索引。隔离人员入点登记不能重复这是业务刚需CREATE TABLE isolation_person ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(20), gender TINYINT COMMENT 1男 2女, isolate_location VARCHAR(100) COMMENT 隔离点名称, status TINYINT DEFAULT 1 COMMENT 1待隔离 2隔离中 3解除 4异常转出, in_time DATETIME, out_time DATETIME, expected_days INT DEFAULT 14, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status_location (status, isolate_location) );health_report表CREATE TABLE health_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, person_id BIGINT NOT NULL, temperature DECIMAL(4, 1) NOT NULL, symptom VARCHAR(255) COMMENT 症状描述, has_touch_history TINYINT DEFAULT 0 COMMENT 是否疑似接触, report_date DATE NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_person_date (person_id, report_date) );我给person_id report_date加了唯一索引这是防止重复上报的关键。前端可以做按钮状态控制但数据库约束才是最终底线。4.2 关键索引与字段约束隔离管理系统的典型查询场景按状态查人员、按隔离点查人员、按身份证精确查人、按日期范围查健康记录。索引设计就围绕这几个场景来表索引字段业务场景isolation_personstatus, isolate_location各隔离点各状态人数统计isolation_personid_card身份证精确查询查重health_reportperson_id, report_date人员每日上报唯一性health_reportreport_date每日汇总统计筛选索引不是越多越好。内部系统数据量撑死几万条三个主要索引完全够用。我见过有人给每个字段都加索引结果插入速度变慢磁盘占用增加查询优化器还经常选错索引纯属自找麻烦。字段约束方面建议在数据库层面限制枚举值CHECK (status IN (1, 2, 3, 4))MySQL 5.7及之前版本CHECK约束不生效但8.0之后是可以执行的。如果你用的MySQL 5.7这种约束就得靠代码和查询条件双重保证。4.3 关系映射在MyBatis中的落地对于联表查询MyBatis最简单的方式是直接写联表SQL然后映射到一个VO类。比如查健康上报记录时需要带出人员姓名public class HealthReportVO { private Long id; private String personName; private String phone; private BigDecimal temperature; private String symptom; private Date reportDate; }XMLselect idselectReportVOList resultTypecom.isolation.vo.HealthReportVO SELECT hr.id, p.name AS person_name, p.phone, hr.temperature, hr.symptom, hr.report_date FROM health_report hr LEFT JOIN isolation_person p ON hr.person_id p.id where if testpersonName ! null and personName ! AND p.name LIKE CONCAT(%, #{personName}, %) /if if testreportDate ! null AND hr.report_date #{reportDate} /if /where ORDER BY hr.report_date DESC /select联表查询加动态条件这就是MyBatis XML最舒服的场景。不需要复杂的级联映射也不需要Service层多次查库拼装一条SQL搞定还能直接用EXPLAIN看执行计划。5. 核心业务流程实操5.1 隔离人员登记与状态流转完整业务流是隔离点管理员登记入点人员登记时填姓名、身份证、电话、预计隔离天数。系统自动计算隔离结束日期——入点日期加预计天数。这个日期计算不能用毫秒数直接加要按天级处理可以用Java 8的LocalDateLocalDate inDate LocalDate.parse(dto.getInTime(), DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); LocalDate expireDate inDate.plusDays(dto.getExpectedDays());每天凌晨会有一个定时任务扫出“隔离期满且没有异常上报”的人员清单标记为待解除状态打印给管理人员核实。用SpringBoot的Scheduled实现Component public class IsolationScheduleTask { Autowired private IsolationPersonMapper personMapper; Scheduled(cron 0 30 1 * * ?) // 每天凌晨1点30分 public void checkIsolationExpire() { Date today new Date(); ListIsolationPerson expiredList personMapper.selectExpiredByDate(today); // 处理到期人员更新状态或生成待办提醒 } }定时任务这里要注意凌晨执行的任务有失败风险建议扫描后只更新状态另外再发一条提醒记录到提醒表不要直接改状态。万一任务挂了或数据有误后续人工排查时至少有迹可循。5.2 每日健康上报与异常预警隔离人员在隔离期间每天上报一次体温、症状、接触情况。这里用UNIQUE KEY uk_person_date保证一个人一天只能有一条上报记录。上报逻辑Service public class HealthReportServiceImpl implements HealthReportService { Transactional(rollbackFor Exception.class) public void report(HealthReportDTO dto) { HealthReport report new HealthReport(); report.setPersonId(dto.getPersonId()); report.setTemperature(dto.getTemperature()); report.setSymptom(dto.getSymptom()); report.setHasTouchHistory(dto.getHasTouchHistory()); report.setReportDate(new java.sql.Date(System.currentTimeMillis())); healthReportMapper.insert(report); // 判断是否异常 if (dto.getTemperature().compareTo(new BigDecimal(37.3)) 0 || StringUtils.hasText(dto.getSymptom()) || dto.getHasTouchHistory() 1) { alarmRecordMapper.insert(buildAlarmRecord(dto)); } } }异常判定阈值定在37.3这是行业通用标准。温度大于等于37.3、有症状描述、有接触史三者任一命中就会自动生成一条异常记录并且把人员的状态标记为待关注。前端数据概览页会有一个红点提醒管理人员点开就能看到异常清单。这里还有一个经验不要把所有判断逻辑堆在Service里体温阈值、症状关键词这些可以放在一个独立的RuleUtil里方便后期调整。我做的时候把阈值放在application.yml里这样改配置不用改代码isolation: threshold-temperature: 37.3 alarm-symptom-switch: true5.3 解除隔离与全流程数据闭环解除隔离不是简单点一下按钮需要两步确认第一步定时任务自动扫描隔离期满人员第二步人工核实健康上报记录无异常后确认解除。解除动作会更新人员状态为3解除同时在transfer_record写入出点记录记录出点时间。整个流程完成后人员的档案仍然保留在系统中支持后期查询历史记录和统计每日解除人数。统计看板用聚合SQL比如统计各隔离点当前隔离中的人数SELECT isolate_location, COUNT(*) AS cnt FROM isolation_person WHERE status 2 GROUP BY isolate_location;类似地每日新增入点人数、每日上报人数配比、异常人数环比这些数据用于隔离点容量监控和每日汇报。数据库层面都是简单聚合关键是前端把卡片式看板做出来数据一目了然。6. 部署、常见坑与排查经验6.1 前后端联调常见问题跨域与路径前后端分离开发模式下跨域是第一道坎。后端单独跑在8080前端Vite默认跑在5173直接请求就会跨域。实际开发中我用两种方案处理。方案一后端开启CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方案二前端用Vite的代理转发。这种方式更推荐因为上线后前后端同域部署时不需要改任何配置Vite的vite.config.jsserver: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }如果遇到登录状态丢失问题大概率是跨域情况下cookie或token处理不对。我的建议是直接用token头传递身份不走cookie可以省掉withCredentials相关的配置少踩很多坑。6.2 MyBatis 动态 SQL 常见坑动态SQL是MyBatis的最大优势但也是最多人踩坑的地方。第一个常见病是if test写错尤其是判断数字时if teststatus ! null and status ! AND status #{status} /if如果status是Integer类型status ! 这个条件基本等于恒真而且MySQL会把空字符串和0之间做隐式转换导致查询结果异常。数字类型判断只用status ! null就可以字符串类型才需要同时判断空串。第二个坑是#{}和${}乱用。排序字段、动态表名只能用${}但会有SQL注入风险所以外部传入的排序字段一定要做白名单校验。其他普通参数一律用#{}MyBatis会转成预编译参数既安全又高效。第三个坑是批量插入XML的foreach标签很多人第一次写会报SQL语法错误多半是没配对separator“,”导致SQL拼接出错。其实更好的方式是直接在Mapper接口上使用InsertProvider或者用MyBatis Plus的saveBatch但如果没有框架支持注意XML里的写法insert idbatchInsert INSERT INTO health_report (person_id, temperature, report_date) VALUES foreach collectionlist itemitem separator, (#{item.personId}, #{item.temperature}, #{item.reportDate}) /foreach /insert6.3 其他实操排查经验补充集成过程中还会遇到一些不致命但很磨人的问题数据库连接时区问题。serverTimezoneAsia/Shanghai这行配置必须加上否则MySQL 8.0及以上版本会报时区错误或者插入的日期时间相差8小时。MySQL SSL连接错误。如果连接字符串出现SSLHandshakeException在url里加useSSLfalse即可。内部系统走内网非敏感数据不做传输层加密问题不大。ID自增与唯一索引冲突。隔离人员重复登记身份证直接被DuplicateKeyException拦截全局异常里单独捕获这个异常提示前端“该人员已登记”。Vue3响应式丢失问题。如果直接把普通对象赋值给响应式数组可能出现页面不更新的情况。用ref包数组再整体赋值或者用reactive注意对象层级能避免大部分响应式异常。Element Plus表格排序参数。后端排序接口如果接收前端传的prop字段名不能直接拼进SQL否则引入注入风险。我用白名单映射比如前端传inTime后端映射成in_time再使用ORDER BY。个人实操心得这整套系统从骨架搭建到核心流程跑通前后用了不到四周大部分时间花在流程细节打磨上而不是写代码本身。我觉得这类内部管理系统最大的价值不在于技术多花哨而在于把隔离点日常管理的流程真正跑顺——谁入点、几点入的、每天上报了什么、是否异常、什么时候解除每一条都能在系统里被追溯。最后分享两个小技巧。一是数据库表结构设计阶段宁愿多花半天梳理业务字段关系也别急着写代码。我一开始把健康上报直接挂在人员表上做了冗余字段后来加每日上报记录时不得不拆表迁移费了很大功夫。二是接口设计尽量用同步简单模型不要为了以后扩展去做异步消息队列。隔离管理系统当前阶段完全用不上MQSpringBoot自带的同步调用就够了加中间件只会增加运维负担。如果你也想拿这套系统练手建议从人员登记和每日上报这两条核心链路入手先把主流程跑通再逐步补齐统计看板、权限管理、Excel导出这些外围功能。这个项目的价值不在代码量而在“完整闭环”四个字上。等你能把状态流转、事务回滚、动态查询、数据库约束联调跑顺一遍SpringBoot和Vue3的实战手感就基本到位了。
返回列表