☰
SpringBoot+Vue+MySQL社区返乡管控系统实战:从设计到部署
2026/10/14 3:58:19 网站建设 项目流程

“返乡报备登记还在用纸质表?每天对着Excel统计重点人群,数据一多就卡成狗?上个月帮某社区做了套‘社区疫情返乡管控系统’,从数据库设计到前后端联调,踩了一路的坑,也总结了不少经验。这篇文章就从头到尾聊聊这套基于SpringBoot + Vue + MySQL + MyBatis的完整系统,从技术选型到核心代码,再到部署上线,把能直接落地的细节都掏出来说。

这套系统不是一个教学用的demo,而是真的能跑起来管理一个中型社区返乡业务的:居民在线填报返乡信息,社区网格员后台审核,每日健康打卡自动统计,数据用图表展示。对正在做毕业设计、或者想按真实项目标准搭一套JavaWeb管理系统的人来说,这文章能帮你少走一个月的弯路。

1. 项目整体设计与技术选型思路

1.1 业务需求拆解:社区返乡管控到底管什么

做技术之前,得先把业务痛点摸清楚。社区工作人员最崩溃的几件事,我列一下:

  • 返乡人员信息分散在各楼栋微信群、电话报备里,没有一个统一入口。
  • 手工登记后,要按“从哪里回来”“乘坐什么交通工具”“是否经过重点地区”等不同维度反复筛查。
  • 返乡后需要每天上报体温和健康状况,全靠人工打电话催,效率极低。
  • 上级要求定期汇总数据,Excel导来导去,格式一变就出错。

所以这套系统的核心目标,是把“返乡报备 - 信息审核 - 健康监测 - 数据统计”这条链路数字化。系统角色也不复杂:

  • 居民:填写返乡报备单,提交后查看审核状态,每天提交健康打卡。
  • 网格员(社区管理人员):审核报备单,处理异常打卡,导出统计报表。
  • 系统管理员:维护社区楼栋信息、管理人员账号、查看全局数据。

整个业务流是:居民提交报备 → 网格员审核(通过/驳回/要求补充材料) → 通过后进入健康监测期 → 居民每日打卡 → 系统生成统计报表。

1.2 技术栈选型:为什么是SpringBoot + Vue + MyBatis

这套组合现在几乎是JavaWeb项目的主流标配,原因很实在:

  • SpringBoot:简化了Spring的繁琐配置,内置Tomcat,打jar包直接跑,非常适合快速交付中小型管理系统。而且社区项目的开发周期通常只有一两个月,SpringBoot能省掉大量环境搭建时间。
  • Vue + Element UI:Vue的响应式机制和组件化开发,做动态表单、数据列表、状态切换非常顺手。Element UI提供现成的表格、表单、日期选择器、弹窗这些组件,不用自己写一堆繁琐的CSS。
  • MyBatis:比起JPA/Hibernate的自动SQL,MyBatis写SQL更直观,尤其适合这种报表多、统计多、条件组合动态变化的业务。后期调优SQL也方便。
  • MySQL:免费、成熟、社区资料多,对于社区级的数据量(几千条返乡记录、几万条打卡记录)完全够用。

选型时也有过纠结,比如前端要不要用Vue3?考虑到当前生态和文档成熟度,最终用了Vue2 + Element UI,毕竟大部分后台管理系统都不需要用到Vue3的Composition API的炫酷特性。后端也没有引入微服务那一套,单体应用加个Redis做缓存就已经能支撑很高的并发量了——一个社区系统,峰值也就百来个用户同时操作,没必要过度设计。

提示:如果你的项目是毕业设计,选型时用SpringBoot + Vue + MyBatis这套经典组合,答辩时理由更充足,因为每一层都能拆开讲清楚,而且市场上工作用的技术栈也恰好是这个方向。

1.3 系统架构与模块划分

系统采用前后端分离架构,后端提供RESTful接口,前端通过axios调用。整体结构:

前端Vue App(端口:8080) --> axios/HTTP --> 后端SpringBoot(端口:8088) --> MySQL(端口:3306)

前端项目内部按功能划分了视图和组件:

src/ ├── api/ # 封装axios请求 ├── router/ # 路由配置 ├── views/ │ ├── login/ # 登录页 │ ├── report/ # 居民返乡报备页 │ ├── audit/ # 网格员审核页 │ ├── health/ # 健康打卡页 │ ├── statistics/ # 数据统计页 │ └── admin/ # 系统管理页 ├── components/ # 复用组件(状态标签、表单等) └── utils/ # 工具函数

后端项目也是标准的分层结构:

com.example.community ├── controller/ # 接收请求、返回结果 ├── service/ # 业务逻辑 │ └── impl/ ├── mapper/ # MyBatis接口 ├── entity/ # 数据库实体 ├── dto/ # 请求/响应数据传输对象 ├── config/ # 跨域、拦截器、异常处理配置 ├── util/ # 通用工具类 └── common/ # 统一返回结果、状态码、常量

这样的分层带来的好处是:团队开发时各司其职,前端不用关心SQL,后端不用碰DOM。出问题时排查链路清楚——页面报错先看Network,接口报错看后端日志,SQL问题看控制台。

2. 数据库设计与核心表结构

2.1 核心实体关系

数据库设计是这套系统的地基,我画过的E-R关系大致是:

  • 用户表(sys_user):存放居民和网格员账号,通过角色字段区分。
  • 楼栋表(building):社区下的楼栋信息,居民绑定楼栋。
  • 返乡报备表(back_report):核心业务表,包含出发地、目的地、途径地区、交通工具、预计到达时间等。
  • 审核记录表(audit_record):记录每次审核的操作人、操作结果、意见。
  • 健康打卡表(health_check):每天一条体温、症状、位置信息。
  • 公告表(notice):社区管理员发布返乡政策通知。

其中返乡报备表和健康打卡表是数据量增长最快的,也是最需要索引支撑的。

2.2 建表SQL与设计要点

以返乡报备表为例,核心建表语句长这样:

CREATE TABLE `back_report` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `report_no` varchar(32) NOT NULL COMMENT '报备编号', `user_id` bigint(20) NOT NULL COMMENT '居民用户ID', `building_id` bigint(20) NOT NULL COMMENT '楼栋ID', `room_no` varchar(20) NOT NULL COMMENT '门牌号', `origin_province` varchar(20) NOT NULL COMMENT '出发地省份', `origin_city` varchar(20) NOT NULL COMMENT '出发地城市', `origin_district` varchar(20) DEFAULT NULL COMMENT '出发地区县', `via_cities` varchar(500) DEFAULT NULL COMMENT '途径城市(逗号分隔)', `transport_type` tinyint(1) NOT NULL COMMENT '1火车 2飞机 3长途汽车 4自驾 5其他', `train_no` varchar(20) DEFAULT NULL COMMENT '车次/航班号', `arrive_time` datetime NOT NULL COMMENT '预计到达时间', `health_code_status` tinyint(1) DEFAULT NULL COMMENT '健康码状态:1绿 2黄 3红', `nucleic_test` tinyint(1) NOT NULL COMMENT '48小时内核酸结果:1阴性 2阳性 3未做', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1待审核 2审核通过 3审核驳回 4已进入监测 5监测完成', `create_time` datetime NOT NULL COMMENT '提交时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_report_no` (`report_no`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_arrive_time` (`arrive_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='返乡报备表';

几个设计要点分享下:

  • 字段类型尽量贴近真实语义,比如“途经城市”用varchar存逗号分隔字符串,而不是建一张关联表。原因很简单,这个字段只做展示和模糊搜索,不做多表关联统计,用字符串更省事。
  • 状态字段用tinyint存数字,而不是varchar存汉字。好处是后续统计时按GROUP BY status效率高,而且避免了中文编码问题。页面上的状态文字由前端通过映射显示。
  • 必须带create_time和update_time。排查数据问题时,这两个字段能告诉你一条记录从提交到审核的真实时间线。
  • 索引不是越多越好。via_cities这种字段就算有索引也快不到哪去,因为查询基本是LIKE '%xx%',用不到索引,没必要加。而status字段区分度低,单独建索引收益不大,但考虑到列表页默认按状态过滤,配合上arrive_time做复合索引或单独索引也还能用。

2.3 数据字典与状态流转设计

返乡报备的状态流转,我用了最经典的状态机:

1 待审核 --> 2 审核通过 --> 4 已进入监测 --> 5 监测完成 --> 3 审核驳回(可重新提交)

审核驳回时,网格员会填写驳回原因。居民重新提交时,系统自动将状态改回“待审核”,同时保留历史审核记录。

这里有个细节:居民重新提交后,是应该新建一条报备记录,还是复用旧记录?我们的选择是复用旧记录,只更新内容并重置状态。这样能保证一个人只有一个“当前有效报备”,统计时不会重复。历史审核记录单独存表,随时可以追溯。

健康打卡表的设计,用user_id + check_date做唯一索引:

CREATE TABLE `health_check` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `report_id` bigint(20) NOT NULL, `check_date` date NOT NULL, `temperature` decimal(4,2) NOT NULL COMMENT '体温,如36.5', `cough` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否咳嗽:0否 1是', `fatigue` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否乏力', `is_normal` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否正常:1正常 0异常', `remark` varchar(200) DEFAULT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_date` (`user_id`, `check_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康打卡表';

唯一索引保证了一个人一天只能提交一条打卡记录,后端再配合“插入前先查询”的逻辑,就能有效防止重复打卡。

3. 后端核心功能实现与源码解析

3.1 SpringBoot项目搭建与分层

后端工程我习惯用Spring Initializr生成,pom.xml里核心依赖就那几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>

application.yml里重点配置这几项:

server: port: 8088 spring: datasource: url: jdbc:mysql://localhost:3306/community_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.community.entity configuration: map-underscore-to-camel-case: true

注意连接串里的serverTimezone=Asia/Shanghai,不加它,本地和服务器时区不一致时,datetime字段会差8小时,这是最容易踩的坑。

3.2 返乡报备接口实现:参数校验与防重复提交

居民提交返乡报备的接口,Controller层很简单:

@RestController @RequestMapping("/api/report") public class ReportController { @Autowired private ReportService reportService; @PostMapping("/submit") public Result submit(@RequestBody @Valid ReportSubmitDTO dto) { return reportService.submit(dto); } }

真正的逻辑在Service里。我重点说两个细节:参数校验和防重复提交。

参数校验用@Valid注解加DTO上的校验注解,比如:

public class ReportSubmitDTO { @NotNull(message = "楼栋ID不能为空") private Long buildingId; @NotBlank(message = "发货地省份不能为空") private String originProvince; @NotNull(message = "到时间不能为空") @Future(message = "到达时间必须在未来") private LocalDateTime arriveTime; // ... }

防重复提交,我用的方案是基于Redis的幂等校验。每次提交时,前端生成一个UUID放在请求头Idempotency-Key,后端拿到后先在Redis里判断这个key是否存在,如果不存在就设置key(有效期5分钟)并继续处理,否则直接返回“请勿重复提交”。

为什么不用数据库唯一键?因为报备表本身没有天然的业务唯一键,直接加唯一索引限制了居民只能提交一条,但实际业务允许驳回后修改重提,所以只能用Redis做短时锁。社区级并发量不高,Redis完全扛得住。

3.3 审核流程与状态机实现

审核接口接收的参数是:报备ID、审核结果(通过/驳回)、驳回原因。Service层代码逻辑:

@Transactional public void audit(Long reportId, Integer auditResult, String rejectReason) { BackReport report = backReportMapper.selectById(reportId); if (report == null) { throw new BusinessException("报备记录不存在"); } // 状态校验:当前状态必须是“待审核” if (report.getStatus() != 1) { throw new BusinessException("当前记录不可审核"); } // 更新报备状态 BackReport update = new BackReport(); update.setId(reportId); if (auditResult == 1) { update.setStatus(2); // 通过 } else { update.setStatus(3); // 驳回 } backReportMapper.updateById(update); // 写入审核记录 AuditRecord record = new AuditRecord(); record.setReportId(reportId); record.setAuditorId(SecurityUtils.getCurrentUserId()); record.setResult(auditResult); record.setRejectReason(rejectReason); auditRecordMapper.insert(record); }

这里必须加@Transactional,因为更新报备表和写入审核记录是两个写操作,任何一个失败都要回滚,否则会出现“状态改了但没记录”或者“记录写了但状态没改”的问题。

状态机约束我没有用复杂的StateMachine框架,而是在Service层里做判断。核心原则就是:当前状态不满足条件,绝不执行操作。代码量不大,但把规则写清楚了,出bug的概率也低。

3.4 健康打卡与数据统计实现

健康打卡的接口逻辑:

public void check(HealthCheckDTO dto) { // 1. 判断当前用户是否处于监测期(status=4) // 2. 判断当天是否已经打过卡 LocalDate today = LocalDate.now(); int count = healthCheckMapper.countByUserAndDate(dto.getUserId(), today); if (count > 0) { throw new BusinessException("今日已打卡,请勿重复提交"); } // 3. 根据体温和症状判断is_normal字段 // 4. 插入记录 }

统计模块是这套系统里SQL最复杂的部分。比如要统计“各楼栋每日返乡人数趋势”,我用了MySQL的日期函数:

<select id="countTrendByDay" resultType="map"> SELECT DATE_FORMAT(arrive_time, '%Y-%m-%d') AS day, COUNT(*) AS cnt FROM back_report WHERE status != 3 AND arrive_time BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(arrive_time, '%Y-%m-%d') ORDER BY day </select>

再比如统计“当前处于监测期的人数”,就是在status = 4的基础上,再排除已解除隔离的用户。如果数据量大,可以考虑定时任务把结果预计算到统计表里,但社区数据量真没必要。

实操心得:写统计SQL时,尽量把筛选条件放在WHERE里而不是HAVING里,这样能命中索引;GROUP BY字段能用date类型就不要用函数,否则索引可能失效。这里的DATE_FORMAT是不得已而为之,因为索引列上套了函数,如果数据量特大,建议改用BETWEEN分段查询后在Java里聚合。

4. 前端Vue页面设计与交互实现

4.1 项目初始化与路由配置

前端用Vue CLI创建,安装依赖:

vue create community-web cd community-web npm install element-ui axios vue-router vuex@3

路由配置里,关键点是区分“居民端”和“管理端”。我用了嵌套路由加访问控制:

const routes = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: Dashboard, meta: { title: '首页' } }, { path: 'report', component: ReportForm, meta: { title: '返乡报备', role: 'resident' } }, { path: 'audit', component: AuditList, meta: { title: '报备审核', role: 'admin' } }, { path: 'health', component: HealthCheck, meta: { title: '健康打卡' } }, { path: 'statistics', component: Statistics, meta: { title: '数据统计', role: 'admin' } } ] } ]

路由守卫里用Vuex存的用户信息做权限判断,如果当前用户身份是“居民”,访问/audit时直接跳转到首页,并提示“无权限”。

4.2 返乡报备表单设计

报备表单是居民最常用的功能,设计时要注意两点:填写体验和数据准确性。

我用Element UI的el-form实现,部分模板:

<el-form :model="form" :rules="rules" ref="reportForm" label-width="100px"> <el-form-item label="出发地" prop="originProvince"> <el-cascader :options="regionOptions" v-model="form.originRegion" placeholder="请选择省市/区县" clearable ></el-cascader> </el-form-item> <el-form-item label="到达时间" prop="arriveTime"> <el-date-picker v-model="form.arriveTime" type="datetime" placeholder="选择日期时间" :picker-options="pickOptions" value-format="yyyy-MM-dd HH:mm:ss" ></el-date-picker> </el-form-item> <el-form-item label="交通工具" prop="transportType"> <el-radio-group v-model="form.transportType"> <el-radio :label="1">火车</el-radio> <el-radio :label="2">飞机</el-radio> <el-radio :label="3">长途汽车</el-radio> <el-radio :label="4">自驾</el-radio> <el-radio :label="5">其他</el-radio> </el-radio-group> </el-form-item> <el-form-item label="核酸结果" prop="nucleicTest"> <el-radio-group v-model="form.nucleicTest"> <el-radio :label="1">48小时内阴性</el-radio> <el-radio :label="2">阳性</el-radio> <el-radio :label="3">未做</el-radio> </el-radio-group> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">提交报备</el-button> </el-form-item> </el-form>

这里的区域级联数据是从后端接口动态加载的,而不是写死在前端。因为不同时期社区关注的“重点地区”会变,后台上传最新数据,前端更新下拉框即可。

日期选择器限制了历史时间:

export default { data() { return { pickOptions: { disabledDate(time) { return time.getTime() < Date.now() - 8.64e7 } } } } }

4.3 管理后台列表与审核操作

网格员审核页的核心是数据表格加筛选条件。我用了el-table,启用stripe(斑马纹)和border,操作列放了“通过”“驳回”两个按钮,点击后弹窗确认。

关键代码:

<el-table :data="list" v-loading="loading" @selection-change="handleSelectionChange"> <el-table-column prop="reportNo" label="报备编号" width="180"></el-table-column> <el-table-column prop="userName" label="姓名" width="100"></el-table-column> <el-table-column prop="buildingName" label="楼栋" width="100"></el-table-column> <el-table-column prop="roomNo" label="门牌号" width="80"></el-table-column> <el-table-column prop="originAddr" label="出发地" width="180"></el-table-column> <el-table-column prop="transportType" label="交通方式" width="100"> <template slot-scope="scope"> <span>{{ transportMap[scope.row.transportType] }}</span> </template> </el-table-column> <el-table-column prop="arriveTime" label="到达时间" width="160"></el-table-column> <el-table-column prop="status" label="状态" width="100"> <template slot-scope="scope"> <el-tag :type="statusMap[scope.row.status].type">{{ statusMap[scope.row.status].text }}</el-tag> </template> </el-table-column> <el-table-column label="操作" width="180"> <template slot-scope="scope"> <el-button type="success" size="mini" @click="passAudit(scope.row)" v-if="scope.row.status === 1">通过</el-button> <el-button type="danger" size="mini" @click="showRejectDialog(scope.row)" v-if="scope.row.status === 1">驳回</el-button> <el-button type="info" size="mini" @click="viewDetail(scope.row)">详情</el-button> </template> </el-table-column> </el-table>

注意:状态标签使用el-tag时,颜色映射也要在统一的statusMap里维护,避免每个组件里各写各的,以后改状态文案只需改一处。

4.4 前端权限控制与登录逻辑

登录流程:用户输入账号密码,后端验证成功后返回一个JWT,前端把它存在localStorage里。每次axios请求前,请求拦截器带上Authorization: Bearer <token>。

// axios拦截器 service.interceptors.request.use( config => { if (store.state.token) { config.headers['Authorization'] = 'Bearer ' + store.state.token } return config }, error => Promise.reject(error) ) // 响应拦截器:统一处理401跳转登录 service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { store.dispatch('logout') router.push('/login') return Promise.reject(new Error('未登录或登录已过期')) } return res }, error => { Message.error(error.message || '系统异常') return Promise.reject(error) } )

注意:JWT存储位置有争议,有人用localStorage,有人为了安全用sessionStorage。我这个项目用的是localStorage,因为刷新页面后需要保持登录状态;如果对XSS攻击非常敏感,可以考虑放Cookie并设置HttpOnly,但那需要后端配合改成Cookie读取,维护成本稍微高一点。

5. 系统安全与性能优化实践

5.1 登录认证与密码加密

密码存储绝对不能明文,我用BCrypt加密。Spring Security里自带的BCryptPasswordEncoder可以直接用,但为了不引入整套Security框架(太重了),我单独引了jBCrypt库:

// 注册时加密 String hashed = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 登录校验 if (BCrypt.checkpw(inputPassword, user.getPassword())) { // 生成JWT }

BCrypt的好处是每次hash的盐是随机的,即使两个用户密码相同,存储的密文也不同。破解成本高,社区管理系统完全够用。

JWT生成我用java-jwt:

String token = JWT.create() .withSubject(userId.toString()) .withClaim("role", user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) // 2小时过期 .sign(Algorithm.HMAC256("your-secret-key"));

注意密钥不要写在代码里,放在application.yml中用@Value注入,或者用环境变量。如果部署到服务器,改起来不用重新打包。

5.2 防SQL注入与XSS开发规范

MyBatis里一定要用#{}而不是${}拼接SQL。#{}会被解析成PreparedStatement的参数占位符,能避免SQL注入;${}是直接拼接字符串,只有极少数情况(比如动态排序字段)才可能用,而且使用前必须做白名单校验。

例如:

<!-- 错误示范 --> <select id="query"> SELECT * FROM back_report WHERE origin_city = '${city}' </select> <!-- 正确示范 --> <select id="query"> SELECT * FROM back_report WHERE origin_city = #{city} </select>

前端方面,为了防止XSS,我做了两层防护:

  • 在表单提交前,用简单的正则过滤掉<script>、onerror等危险关键字符。
  • 在展示用户输入的字段时,Vue默认会转义文本插值{{ }},所以用{{ }}而不是v-html,乱用v-html才是XSS的元凶。

5.3 查询性能优化:索引、分页与缓存

社区管理系统数据量不大,但也有几个典型优化做到了实处:

  • 列表查询用MySQL分页,LIMIT offset, size,但offset过大会导致扫描慢,对于管理端的审核列表,可以用“上次查询的最后ID”做游标分页,效果更好。
  • 查询条件顺序调整:联合查询多表时,尽量让小表先关联,大表后关联,但这在现代优化器下作用不明显,重点还是给WHERE条件加了合适的单列索引。
  • Redis缓存登录信息和基础配置:比如楼栋列表、区域级联数据,这些信息不常变动,缓存到Redis后接口响应能从几十毫秒降到几毫秒。但缓存必须有失效策略,我的做法是后台修改楼栋后,调用删除缓存接口,保证数据一致性。

下面是分页查询的Mapper实现:

<select id="selectReportPage" resultType="com.example.community.dto.ReportVO"> SELECT r.*, u.real_name, b.building_name FROM back_report r LEFT JOIN sys_user u ON r.user_id = u.id LEFT JOIN building b ON r.building_id = b.id <where> <if test="status != null"> AND r.status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (u.real_name LIKE CONCAT('%', #{keyword}, '%') OR r.report_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="buildId != null"> AND r.building_id = #{buildId} </if> <if test="startTime != null"> AND r.arrive_time &gt;= #{startTime} </if> <if test="endTime != null"> AND r.arrive_time &lt;= #{endTime} </if> </where> ORDER BY r.create_time DESC LIMIT #{offset}, #{limit} </select>

注意:MyBatis中>、<在XML里必须转义为&gt;、&lt;,不然会报XML解析错误。

6. 部署上线与常见问题排查

6.1 环境配置与打包部署

后端打包:

mvn clean package -DskipTests

生成target/community-manage.jar,然后调用:

nohup java -jar community-manage.jar --spring.profiles.active=prod > app.log 2>&1 &

前端打包:

npm run build

生成的dist目录放到Nginx的html目录下。Nginx配置域名和反向代理,关键配置:

server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; # Vue路由history模式必须加 } location /api/ { proxy_pass http://127.0.0.1:8088/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

这里try_files是Vue项目部署时最容易漏的配置,不加它,刷新页面或直接访问子路由会报404。

6.2 典型坑点:跨域、时区、上传大小

跨域问题是前后端分离开发时绕不开的坎。我本地的解决方案是后端配置CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8080") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

生产环境因为用了Nginx反向代理,同源访问就不存在跨域了,但这个配置保留也无妨。

时区问题刚才说了,连接串加serverTimezone=Asia/Shanghai。另外,前端el-date-picker传给后端的日期字符串,后端用@DateTimeFormat或全局配置Jackson:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=Asia/Shanghai

上传报备附件时,SpringBoot默认限制1MB。如果想支持上传核酸报告截图,需要改配置:

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB

6.3 常见Bug与解决实录

Bug 1:列表查询慢

现象:审核列表首次加载需要3秒多。排查后发现SQL里有两个子查询,每条记录都要去打卡表查“最近一次打卡时间”,导致全表扫描。解决:把子查询改成LEFT JOIN打卡表聚合结果的一次性关联查询,时间降到200ms。

Bug 2:居民重复提交报备

现象:居民连点两次提交按钮,生成了两条报备记录。解决:双重防护,前端按钮在提交后立即loading,后端Redis幂等校验拦住重复请求。

Bug 3:中文乱码

现象:MySQL里查询出来的中文正常,但插入后变成???。原因:数据库连接串没加characterEncoding=utf8,而且建表时用了DEFAULT CHARSET=utf8mb4但表本身的排序规则不对。统一改成utf8mb4后解决。

Bug 4:Vue打包后访问页面空白

现象:npm run build后打开页面空白,控制台报找不到JS文件。原因:vue.config.js里没有配置publicPath。设置publicPath: './'使用相对路径后正常。

最后再分享几个我实践后的体会

这套系统从头到尾做完,我最深的感触是:业务管理系统能跑起来不难,难的是把细节做扎实。比如状态机设计、幂等处理、时区处理,这些平时写demo不会遇到的问题,在真实使用中都会暴露出来。我实际在某个社区试运行了一个月,居民填报率从原来的电话通知的六成提升到了九成以上,网格员从每天汇总表格变成只在后台看数据,效率提升非常明显。

如果你准备拿这个项目做毕业设计或者学习用,我建议不要只停留在照抄源码,试着按这些方向去扩展:

  • 加入短信通知模块,审核通过后自动给居民发短信。
  • 增加数据导出Excel功能,用EasyExcel一行代码就能生成。
  • 把统计图表换成ECharts,让大屏展示更直观。

有具体的功能实现细节或者代码调试问题,欢迎在评论区交流,我踩过的坑你大概率也会遇到,能帮一个是一个。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询