ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+SpringCloud微服务实战:构建民生政务举报平台

SpringBoot+Vue+SpringCloud微服务实战:构建民生政务举报平台 “市民之家”这类项目我一直觉得是政务数字化里最考验开发基本功的业务链路长、角色多、还要扛住突发流量。去年我用 SpringBoot Vue SpringCloud 微服务分布式这套组合完整落地了一个民生政务举报交流平台从市民提交举报、调度分派、部门办理到结果反馈和交流互动整条链路都跑了很久。这篇文章我会把服务划分、核心流程、关键代码、分布式事务与锁的取舍以及几个印象深刻的线上问题排查过程都摊开讲给正在做政务中后台或微服务项目的朋友一些可落地的参考。1. 平台整体设计与微服务拆分思路1.1 业务模块与核心流程民生政务举报平台本质上是一套“举报—受理—办理—反馈—交流”的闭环系统。我先把业务模块拆清楚再来谈技术。核心模块有这六块用户服务市民注册、登录、实名认证、历史记录、举报服务举报单创建、附件上传、查重、受理分派服务接单、按区域或类别分派、转办催办、部门办理服务办理结果提交、延期申请、交流互动服务追问、评价、留言板、消息通知服务站内信、微信公众号模板消息推送。市民端的操作路径其实很短填举报信息、传照片或视频、提交剩下就是在详情页看进度、等回复、做追问。但后台这条链路就长了举报单要经历“待受理—已分派—办理中—已办结”这几个节点中间还有退回、转办、延期各种分支。最容易被忽略的环节是去重。政务平台上重复举报极其常见一个人反复上报同一个井盖问题或者几十个人同时举报同一个占道经营点后台如果没有处理调度员会被大量无效工单淹没。我在 Redis 里做了短时去重同一个用户、同一个 geohash 区域、同一类别5 分钟内不允许重复提交。1.2 为什么坚持用微服务而不是单体说实话这个业务量级用单体架构是完全能跑的。市一级的“市民之家”举报量一天几千条顶天了一个性能还不错的单体应用撑住完全没问题。但我最终还是选了 SpringCloud 微服务原因不是性能是治理。第一是组织边界。政务场景里每个部门是独立责任主体。举报、分派、办理如果揉在一个应用里一个模块发版要牵动全部出了问题也很难说清楚是哪一块的责任。拆成独立服务后举报归举报、分派归分派、办理归办理边界非常清晰。第二是流量不可控。举报平台很容易受舆情影响某个事件被媒体放大后短时间可能涌进大量请求。微服务的价值在于可以单独把举报服务、消息服务扩容而不是把整个单体复制好几份。第三是团队协作。多个后端并行开发时独立仓库、独立部署能少很多合并冲突。当然代价也很明显分布式事务、跨服务调试、排查链路都变复杂了。所以这里要强调一个原则不为微服务而微服务。低频的统计报表模块、纯管理端的字典模块我先放在服务里不拆出去等数据量真的大了再独立。1.3 服务拆分边界与粒度拆服务最怕拆成“分布式单体”服务数量很多但彼此强耦合调用链绕成一团。我判断一个服务该不该拆就三条标准能不能独立部署、能不能独立演进、能不能独立故障。如果三个都满足拆满足不了说明边界没划对先合回去。最终的服务划分是这样的服务名职责数据库归属user-service市民/部门账号、角色权限用户相关表report-service举报单、附件元数据举报相关表assign-service受理分派、转办、催办工单流转表handle-service部门办理、延期申请办理记录表interaction-service追问、评价、交流留言互动表message-service站内信、公众号消息推送消息记录表gateway统一入口、鉴权、限流无每个服务独立数据库服务之间禁止跨库查询。跨服务的数据一致性靠事务消息、幂等补偿和最终一致来解决。这个红线一定得守住不然分分钟变成分布式泥潭。关于粒度我不建议把受理和办理拆得过细。受理、分派、转办本质上都是工单状态流转放一个 assign-service 里统一管状态机维护成本最低。最早我拆过“受理服务分派服务转办服务”结果一次转办要串四个服务每层都加事务排查问题到头大。后来合并成一个代码清爽很多。2. 后端技术骨架SpringBoot 与 SpringCloud 落地细节2.1 版本组合与项目结构我用的版本组合是 Spring Boot 2.7.x Spring Cloud 2021.0.x Spring Cloud Alibaba 2021.0.5.0。这套组合很成熟遇到问题网上资料多好排查。Spring Boot 3.x 和 Spring Cloud 2023 虽然新但部分 Alibaba 组件的适配还比较折腾政务项目更看重稳定。父 pom 统一管理依赖子模块按服务划分。核心依赖大致这些spring-cloud-starter-gateway网关路由、跨域、过滤器spring-cloud-starter-alibaba-nacos-discovery注册中心spring-cloud-starter-alibaba-nacos-config配置中心spring-cloud-starter-alibaba-sentinel限流熔断spring-cloud-starter-openfeign服务间调用spring-boot-starter-data-redis缓存、分布式锁、去重mybatis-plus-boot-starterORM 和分页复杂 SQL 走 XMLminio Java SDK附件对象存储每个服务内部的包结构我习惯固定为controller、service、mapper、entity、dto、enums、config、feign。feign 包专门放调用其他服务的 Client 接口。这样看代码时扫一眼目录就知道这个服务依赖了谁、被谁调用。2.2 Nacos 配置中心和网关鉴权注册中心和配置中心统一用 Nacos而不是 Eureka 加 Config Server 的组合。原因是 Nacos 一个组件同时干两件事内网部署少一套服务运维压力小很多。配置管理上每个服务建一个 dataId公共配置放 shared-configs环境区分用 profile。网关我用的 Spring Cloud Gateway所有请求先进网关做统一鉴权。我在网关里写了一个全局过滤器前端登录拿到 JWT存在 Authorization 头里。网关过滤器校验 JWT解析出 userId 和角色填充到 X-User-Id、X-User-Roles 请求头。下游服务不依赖 session只认这两个请求头。这个设计的核心价值是服务间调用不受登录态约束。Feign 调用时自己决定要不要带用户上下文灵活性高很多。网关环节最容易忽略的是白名单。登录、验证码、举报详情公开查询这些接口不能拦截否则前端调试一直 401。白名单统一维护在 Nacos 配置里调整不用发版。2.3 分布式锁与分布式事务的取舍这是微服务绕不开的两件事。分布式锁我用 Redis Redisson。场景就是举报单状态流转比如部门办理完成提交结果时要防止两个操作员同时点提交导致状态覆盖或者定时任务在多实例部署下重复扫表。RLock lock redissonClient.getLock(report:handle: reportId); boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { throw new BizException(系统繁忙请稍后重试); } try { // 办理业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }注意 tryLock 第一个参数是等待时间第二个是锁自动释放时间。业务处理时间不稳定时锁时间要留够否则锁提前释放其他线程就进来了。有些人不加锁也能跑但线上总会出一次“两个办理员同时提交结果被后提交的人覆盖了”的事故。分布式事务我第一版用了 Seata后来换成了更务实的方案本地消息表 定时任务补偿。流程是这样的report-service 在本地事务里写入举报单和一条“待分派”消息记录定时任务扫描待分派消息通过 Feign 调用 assign-service 创建分派任务assign-service 处理成功后回调幂等标识report-service 更新消息状态如果调用失败定时任务重试超时未处理转人工。为什么不用 Seata分派动作允许延迟几秒乃至几十秒业务上完全能接受。Seata 的全局锁在高峰期容易把举报服务拖垮。不是 Seata 不好是这种弱一致场景用本地消息表更简单、更可靠。2.4 文件上传的 XSS 攻击防护政务平台最怕安全问题。除了常规的验证码、限流、操作日志我重点做了 XSS 和文件上传校验。这个坑来自一次真实的踩踏。平台交流区允许上传 PDF 附件结果有人传了一个内嵌恶意脚本的 PDF管理员在后台预览时差点中招。后来我把文件安全链路做了三层文件上传统一走 file-service校验扩展名白名单同时校验文件头字节防止改个后缀传 exe。PDF 上传后用 PDFBox 解析纯文本做敏感词和脚本特征检测检测到直接拒绝。所有文本字段入库前做 HTML 转义前端展示用 Vue 插值语法天然转义避免 v-html 直出用户内容。同时写了一个全局过滤器对 POST 和 PUT 请求的参数做统一清洗把、、javascript:、onerror这类 payload 编码掉。Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } }这里有个特别容易踩的细节只重写 getParameter 远远不够SpringMVC 解析 JSON 用的是输入流必须重写 getInputStream把流里的内容读出来清洗再放回去。我一开始漏了这步JSON 请求的 XSS 完全没拦住白写了一层过滤。3. 前端 Vue 体系与多角色页面设计3.1 项目结构与动态路由设计前端我用的 Vue 2 Element UI开发环境是 Vue CLI。选 Vue 2 不是因为它好而是项目里既有组件、部门同事的技术栈都基于 Vue 2迁移成本不划算。新项目可以考虑 Vue 3 Vite Pinia这是后话。项目目录保持清晰的分层src/ api/ assets/ components/ # 公共组件上传、地图选点、时间轴 router/ store/ views/ citizen/ # 市民端举报提交、详情跟踪 dispatcher/ # 调度端待分派列表、分派操作 department/ # 办理端待办列表、办理结果提交 admin/ # 监管端统计看板、督办管理 utils/路由设计上最关键的一点按角色动态生成菜单。用户登录后从后端拉取菜单树再动态 addRoute。不要在路由表里写死所有页面否则市民端用户直接在地址栏敲部门办理的 URL 就能打开页面虽然接口会鉴权但体验上是漏洞。核心路由大概这几个/report/add 举报提交页/report/detail/:id 举报详情页/dispatch/list 调度分派列表/handle/todo 部门待办/interaction/:reportId 交流区。举报详情页是市民最看重的页面。市民只关心“我举报了到底有没有人管”所以详情页要用一条清晰的时间轴展示状态流转已提交、已受理、办理中、已办结、满意度评价。这个组件我用 Element UI 的 el-timeline 实现数据由后端按时间倒序返回。3.2 API 封装与 Token 刷新机制API 封装我做了两层。第一层是 axios 实例配置 baseURL、超时时间、请求头。第二层按业务模块拆分成多个 api 文件。请求拦截器做两件事从 localStorage 读 token 加到 Authorization 头以及记录当前请求时间用于日志排查。响应拦截器统一处理返回值。比较值得说的是 401 的处理逻辑。政务系统用户经常一个页面挂半天token 过期后用户还没反应过来提交举报时就报 401体验很差。我的做法是在拦截器里遇到 401 先不踢出而是尝试用 refreshToken 换新 token换成功再把原请求重放。service.interceptors.response.use( response { const res response.data if (res.code 401 !config._retry) { config._retry true return refreshToken().then(() { config.headers.Authorization Bearer getToken() return service(config) }) } return res }, error { /* 错误统一处理 */ } )这里有个并发坑页面同时发多个请求全部 401 后如果不加控制会同时触发多次 refreshToken刷新接口被刷爆。要加一个 isRefreshing 标记刷新期间把后续请求放进队列刷新完统一重放。3.3 举报表单、地图选点与附件上传举报提交页是使用频率最高的页面表单字段一定要克制。我最终只保留了问题类型下拉、问题位置地图选点加地址文本、问题描述、图片附件最多 9 张、视频附件单个不超过 100MB、联系方式默认带出。地图选点用高德地图 JS API选点后把经纬度和逆地理编码的地址一起提交。服务端保存经纬度后续分派时按区域边界做自动匹配这是自动分派的关键依赖。附件上传看起来简单实际有讲究。我用 el-upload 上传但上传走的是 file-service 独立接口不经过业务服务的 RequestBody。这样大文件不会把业务服务的内存吃满。上传完成后拿到的是文件 ID提交举报单时只传文件 ID 列表不传二进制。这个设计让业务服务和文件服务彻底解耦后续文件量大了上 CDN 也方便。交流区是一个类似帖子楼的页面按照时间线展示市民的追问和部门的答复。所有内容必须走后端 XSS 过滤图片用白名单域名链接统一加 relnoopener noreferrer。我在别的项目里见过交流区因为没过滤富文本管理员打开就被执行恶意脚本的案例这个口子必须堵死。4. 核心流程的完整实现链路4.1 从举报提交到分派的链路细节完整链路大概是这样的市民在举报页填表先调用 file-service 上传附件拿到 fileIds。前端调用 report-service 提交举报单参数包含 fileIds、经纬度、类别、描述。report-service 做参数校验然后走 Redis 去重逻辑校验通过后写入本地事务。本地事务写入举报单和一条待分派消息举报单状态为“待受理”。事务提交后定时任务扫描待分派消息调用 assign-service 创建分派任务。这里防重复提交一定不能省。我在 Redis 里去重的 key 设计成report:dup:{userId}:{category}:{geoHash}geoHash 是经纬度编码后的短字符串表示一个区域范围。同一人在同一区域对同一问题短时间重复提交会被直接拒绝提示“该问题已有处理中的举报单”。分派模块是核心中的核心。我把它设计成状态机待受理 - 已分派 - 办理中 - 待确认 - 已办结 - 已延期 - 办理中 - 已退回 - 待受理分派有三种方式自动分派按经纬度和问题类别匹配责任部门匹配不到就走人工人工分派由调度员手动选部门转办是部门认为自己无管辖权时转给其他部门。状态机实现我没有用重量级框架一个枚举加一个统一迁移方法就够了关键是守卫条件。public enum ReportStatus { PENDING(0, 待受理), ASSIGNED(1, 已分派), PROCESSING(2, 办理中), DELAYED(3, 已延期), AWAIT_CONFIRM(4, 待确认), COMPLETED(5, 已办结), REJECTED(6, 已驳回); public boolean canTransitionTo(ReportStatus target) { // 每个状态只允许迁移到规定的目标状态 } }这里一定要挡住异常迁移比如已办结不允许随便改回办理中除非走监管端的特殊审批通道。状态机是业务规则不是代码装饰。4.2 消息通知链路与微信模板消息消息通知做了站内信和微信公众号模板消息两路。微信公众号用测试号对接时要先配好模板 ID发送时通过 access_token 调接口。access_token 要缓存到 Redis有效期两小时不要每次重新获取否则接口会被限流。分派成功后会调用 message-service 给办理部门发通知部门登录看到待办。部门提交办理结果后message-service 给市民发通知市民看到进度更新。这个调用链比较长report-service 到 assign-service 再到 message-service。线上排查问题时要能串起来所以我在每个关键节点都打日志日志必须带 reportId。做微服务的人一定不要省日志不然出问题查起来非常痛苦。建议再加一个 traceId 贯穿全链路网关入口生成所有服务透传。4.3 日志打点与全链路追踪这个值得单独说一下。微服务排障最大的难点是跨服务调用链不清楚。我后来给每个服务接入了 Sleuth Zipkin网关生成的 traceId 会自动传给下游服务通过 Zipkin 能看到整条调用链路每个服务的耗时。公共日志格式则是时间、traceId、spanId、服务名、reportId、操作人、请求路径、状态码。这样在 ELK 里只要搜 traceId就能把一次举报提交涉及的所有服务日志拉出来从前端请求到数据库操作一目了然。5. 分布式环境下踩过的坑与排查实录5.1 跨服务调用超时导致工单状态不一致第一次联调就出问题举报单分派成功但状态一直停在“待受理”。排查日志发现 assign-service 调用 message-service 发通知时超时assign-service 里的事务回滚了但 message-service 那边可能已经写入了消息记录。这就是分布式调用的经典场景两个服务没有共享事务调用方回滚了被调方可能已经提交。我的解决思路是把分派和通知彻底解耦。assign-service 只负责写分派结果通知通过 MQ 异步发。如果团队不想引入 MQ就用本地消息表加定时任务补偿。同时在分派方法上加了幂等校验重复调用不会创建两条分派记录。经验总结微服务调用一定要明确哪些是强一致、哪些是最终一致。通知、统计这类弱一致场景直接异步化绝对不要塞进主事务。5.2 网关限流配置误伤正常请求上线当天就有市民反馈举报详情页打不开。排查发现是 Sentinel 网关限流规则写太死详情页的查询 QPS 阈值设成了 5。而详情页每次进页面有多个接口并发请求加上用户一刷新很快就触发限流。这是一次深刻的教训。限流阈值不是拍脑袋定的要看真实流量和接口耗时。我当时用压测工具跑了一遍详情页接口正常单个用户一次要发 3 个请求峰值 50 个用户同时刷就 150 QPS阈值 5 等于完全不能用。调整思路是分接口类型写接口提交举报、提交办理限流从严因为是防刷防暴读接口详情查询限流放宽保证正常访问。另外加了针对单一 IP 的限流防的是某个 IP 疯狂刷接口而不是误伤正常用户。5.3 前端跨域与 Cookie 携带问题开发阶段前端跑在 8080后端网关在 8088跨域问题是肯定有的。我在网关层统一配置了跨域。spring: cloud: gateway: globalcors: corsConfigurations: [/**]: allowedOriginPatterns: * allowedMethods: * allowedHeaders: * allowCredentials: true注意 allowCredentials 为 true 时allowedOrigin 不能直接用 *要用 allowedOriginPatterns否则浏览器会直接拦截。这个细节不特别注意开发半天发现请求都发不出去。另外登录态我全程用 token 放请求头不依赖 Cookie。政务环境经常前后端不同域名Cookie 方案要处理 SameSite、Secure 等一堆问题很容易翻车。能用 token 尽量用 token前端每次请求加 Authorization 头最省心。5.4 常见问题速查表现象排查方向解决方案服务间调用 401Feign 调用没带用户上下文Feign 拦截器统一设置请求头举报单状态不一致跨服务事务边界不清弱一致场景用本地消息表加定时补偿文件上传后打不开文件服务内外网地址没区分区分内网存储地址和外网预览地址页面白屏Vue 打包后资源路径错误publicPath 配置为相对路径或 CDN 绝对路径定时任务重复执行多实例部署没有分布式锁用 Redisson 保证单实例执行接口偶发超时Feign 默认超时时间太短根据接口耗时设置合理超时时间和重试次数这张表建议政务项目团队人手一份很多问题都是可以类比的照着排查效率能高很多。6. 部署运维与内网环境注意事项6.1 基础设施选型与启动顺序部署环境是内网服务器我用到的组件清单Nacos 至少 2 节点做注册和配置中心高可用、MySQL 8.0 每个服务独立库主从部署、Redis 6.0 做缓存和分布式锁、MinIO 做附件存储。MinIO 这里多说一句。正常用的是 MinIO 做对象存储替代传统的 FastDFS。MinIO 部署简单有现成 Java SDK支持分片上传和断点续传政务系统的 PDF、照片、短视频完全够用。传统 FastDFS 配置麻烦Nginx 插件配合也折腾实属没必要。服务启动顺序也重要先 MySQL、Redis、Nacos 这些基础设施再启动业务服务最后启动网关。否则服务注册时找不到 Nacos 会反复报错刷日志。6.2 容器化部署与发版流程实际部署我用了 Docker Compose 编排。每个服务一个镜像基础设施也用 Compose 一起编排。服务镜像打的是基础 JDK 镜像加 jar 包tag 用构建时间。构建命令大概是mvn clean package -DskipTests docker build -t registry.local/civic/report-service:${BUILD_NUMBER} . docker push registry.local/civic/report-service:${BUILD_NUMBER}发布时把新镜像 tag 更新到 compose 文件然后 docker compose up -d。健康检查就探每个服务的 actuator/health 接口。这个流程虽然不算完整 CI/CD但对政务内网环境够用而且稳。上不上 K8s 看团队运维能力小团队硬上 K8s 反而增加负担。有一个运维层面的心得内网服务器没有外网Maven 依赖在构建机上拉好打镜像时要保证镜像仓库是内网的。最好构建机上放一个完整的本地 Maven 仓库避免每台服务器去外网拉依赖失败。6.3 监控与报警配置日志统一输出到文件按服务名分目录用 filebeat 收集到 ELK。日志定期清理避免磁盘被打满。报警只配了三条最关键的服务存活探针失败立即报警、网关 5xx 比例超过阈值报警、举报提交接口成功率下降报警。不要一上来就配一堆监控大盘先把“服务挂了、接口写失败了”这两个最基本的问题盯住比什么都强。后续再慢慢丰富指标。最后说一点个人的体会。做政务类平台技术选型反而不是最难的部分最难的是想清楚业务流程和各方职责。市民、调度员、部门办理人员、监管领导每个角色对“举报”这个词的理解都不一样。代码写错了能改流程错了上线后被用户投诉就麻烦了。这里想提醒一下动手之前一定先和业务方把流程图画清楚反复确认状态节点和操作权限再拆服务写代码。如果让我再做一遍这个平台我会把交流互动模块做得更重让处理过程和反馈都能公开透明减少重复投诉和电话咨询的压力。技术上的东西都可以迭代业务边界一开始理清楚了项目才能真正顺起来。你在做类似政务微服务项目的时候遇到什么比较刁钻的坑也欢迎多交流。
返回列表