消防政务服务平台App源码与开题:多角色权限和工单状态机 政务服务类的系统做过几个消防这个方向算是比较特殊的一个——它既要面向普通群众开放隐患上报、知识学习、办事预约又要支撑内部人员的日常业务流转检查记录、工单派发、整改复核角色一多权限和状态机就容易写乱。很多同学拿到“消防政务服务平台系统app源码开题”这个题目第一反应是堆页面结果做到一半发现工单状态对不上、不同角色看到的数据串了、开题报告里的技术路线和代码完全两张皮。这篇就把我从零跑通这套东西的完整思路拆开讲整体架构怎么定、数据库怎么设计才不返工、工单流转的状态机怎么落地、源码怎么跑起来、开题报告每一节该写什么不该写什么以及答辩现场最容易被追问的几个点。适合正在做类似政务/民生类毕设的同学也适合想快速摸清一套“多角色流程审批”系统骨架的开发者前端后端的基础不用太深跟着步骤走能复现。1. 项目整体设计与思路拆解1.1 这个题目为什么好做也容易做砸先说清楚这类系统的业务边界。消防政务服务平台的核心不是“消防”两个字有多专业而是“政务服务”这四个字带来的结构特征一件事从群众发起经过受理、派发、处理、复核最后归档。这条链路天然带着角色分工和状态流转正好是毕设评审最看重的东西——有业务闭环、有角色差异、有数据统计。我做过的几个类似项目里最容易出彩的模块其实是隐患上报群众拍照上传、填位置描述、提交后台受理后派给对应的检查人员检查人员现场核实、填写处理结果、上传整改照片最后复核归档。这一条线走通你的系统就有了“故事”演示的时候能从头点到尾不至于每个页面都是孤立的增删改查。但也容易做砸。最常见的翻车方式是把所有功能都做成“管理员一个人能操作的表格”群众端随便放两个静态页面凑数。这样代码量看着不少实际上没有一个完整的业务流程答辩老师问一句“这个工单从提交到关闭中间经历了哪些状态、谁能改”直接卡住。所以在动手之前我建议先把三件事定死角色清单群众、单位消防安全责任人、检查人员网格员、后台管理员。四个角色足够不要贪多。核心业务链路只做一条主线隐患上报→工单→整改→归档其余模块资讯、知识题库、预约作为辅助保证主线能完整跑通。数据可见范围群众只看自己的上报记录检查人员只看派给自己的工单管理员看全部。这一条决定了后面所有查询接口都要带数据范围过滤。把这三条写在纸上贴在显示器边上后面写代码的时候不容易跑偏。1.2 技术选型为什么我坚持“单体少依赖”选型这块我踩过坑说点实在的。很多同学一上来就想上微服务、上容器编排觉得显得高级。实际情况是答辩现场用的是自己的笔记本或者评审机房的机器配置一般网络还不一定通。你搞五六个服务加一个注册中心光是启动就要等两分钟中间任何一个起不来整个演示就废了。我的建议是单体后端 前后端分离 一套移动端具体组合层次选型选择理由后端框架Spring Boot 2.7 MyBatis-Plus起步快CRUD 代码量少注解式权限好写数据库MySQL 8.0环境普及图形化工具多答辩机器基本都有缓存可选 Redis只用来存登录态或字典缓存没有也不影响跑通管理端Vue 3 Element Plus Vite表格、表单、弹窗组件齐全后台页面能快速铺开移动端uni-appVue 语法一套代码能出 H5 和安卓包演示时用 H5 最稳文件存储本地磁盘目录不接对象存储减少外部依赖答辩断网也能传图这里解释两个关键取舍。为什么不用 Spring Security 全家桶完整的安全框架配置链路长对于只有四个角色的系统来说属于过度设计。我用的是“JWT 自定义拦截器 注解校验”的轻量方案核心逻辑两百行以内出了问题自己一眼能看出来答辩被问到也能讲清楚原理比背框架配置强得多。为什么移动端选 uni-app 而不是原生安卓原生安卓要配 Android Studio、Gradle、SDK 版本一个版本对不上就是半天。uni-app 用 HBuilderX 打开就能跑 H5需要出安装包的时候再云打包对毕设场景友好得多。而且 Vue 语法和管理端一致学习成本几乎为零。注意如果学校明确要求“必须是原生 Android 或必须是微信小程序”那就按学校要求来技术方案可以换但业务链路和数据结构不用变迁移成本不高。1.3 功能模块怎么切才不互相打架模块划分我一般按“谁在用”来切而不是按“技术上像一类”来切。按角色切的好处是权限边界天然清晰不会出现“这个接口到底该不该给群众调”的纠结。群众端移动端为主消防资讯浏览、隐患随手拍上报、我的上报记录与进度查询、消防知识学习与自测、办事预约申请。单位端移动端H5本单位隐患自查记录、整改任务接收与反馈、单位基础信息维护。检查人员端移动端待办工单列表、现场检查表单填写、拍照上传、整改复核。管理端PC 后台工单受理与派发、资讯发布、知识题库维护、预约审核、用户与角色管理、数据统计看板。这样切完之后你会发现工单是贯穿所有角色的那根线其他模块都是围绕它的配角。写代码的顺序也应该按这个来先把工单表和状态机定死再做各角色的列表页最后补资讯和题库这些“填空”模块。顺序反了后面改工单字段要牵动一堆页面。2. 核心细节解析与实操要点2.1 数据库设计几张表撑起整条业务线数据库这块我不追求表多追求的是“一张表能说清一件事”。核心八张表足够跑通全流程表名作用关键字段sys_user用户主表id、账号、密码哈希、真实姓名、手机号、单位id、状态sys_role / sys_user_role角色与关联角色编码、用户id、角色idhazard_report隐患上报表上报人、隐患类型、描述、经纬度、地址、图片、状态、创建时间work_order工单表关联上报id、工单号、当前处理人、状态、优先级、派发时间、办结时间order_log工单流转日志工单id、操作人、原状态、新状态、备注、时间inspection检查记录工单id、检查项结果、现场照片、检查结论article资讯/知识标题、分类、正文、封面、发布状态attachment附件索引业务类型、业务id、文件路径、上传人这里有个细节值得单说工单和上报表要分开。很多人图省事直接在 hazard_report 上加个“处理人”和“处理状态”字段结果一个隐患被退回重报、或者拆成两个工单的时候就没法处理了。分开之后上报是“事实记录”群众说了什么工单是“处理过程”谁来办、办到哪一步职责清晰日志也好挂。建表时的几个实操要点CREATE TABLE hazard_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reporter_id BIGINT NOT NULL COMMENT 上报人, hazard_type VARCHAR(32) NOT NULL COMMENT 隐患类型字典码, description VARCHAR(500) NOT NULL, address VARCHAR(200) NOT NULL, longitude DECIMAL(10,6) DEFAULT NULL, latitude DECIMAL(10,6) DEFAULT NULL, cover_url VARCHAR(255) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待受理 1已受理 2已派发 3处理中 4待复核 5已归档 9已驳回, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_reporter (reporter_id, create_time), KEY idx_status (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;状态用数字码而不是字符串索引效率高前端做映射也方便但要记得在代码里用枚举包一层别到处写魔法数字。经纬度用 DECIMAL(10,6)六位小数大概精确到 0.1 米够用了别用 float精度丢失会导致地图打点偏移。version 字段一开始就加上后面处理“两个人同时点受理”的并发问题时会感谢自己。deleted 做逻辑删除政务类数据不建议物理删除答辩被问到“数据能不能恢复”时有话说。索引按查询习惯建群众端查“我的上报”走idx_reporter管理端按状态筛选走idx_status。2.2 权限模型四个角色怎么隔得干净权限这块我的做法是**“接口级注解 数据级过滤”双层**。接口级解决“能不能调”数据级解决“能看到几条”。接口级用自定义注解配合拦截器实现Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); } // 拦截器里做判断 String[] allow handlerMethod.getMethodAnnotation(RequireRole.class).value(); if (!Arrays.asList(allow).contains(currentUser.getRoleCode())) { throw new BizException(403, 无权限访问); }数据级过滤放在 Service 层用一个统一的数据范围构建器// 根据当前登录角色拼接查询条件 public LambdaQueryWrapperWorkOrder buildScope(LoginUser user) { LambdaQueryWrapperWorkOrder qw new LambdaQueryWrapper(); if (RoleConst.INSPECTOR.equals(user.getRoleCode())) { qw.eq(WorkOrder::getHandlerId, user.getUserId()); // 检查员只看自己 } else if (RoleConst.UNIT.equals(user.getRoleCode())) { qw.eq(WorkOrder::getUnitId, user.getUnitId()); // 单位只看本单位 } // 管理员不加条件 return qw; }这么写的好处是任何新增的列表接口只要调一次buildScope数据隔离就自动生效不依赖开发者的记性。注意前端隐藏菜单只是体验优化绝对不能当权限用。我见过有人只在前端做了菜单控制接口裸奔用 Postman 一调什么都能看到这种在答辩现场被问到会很被动。所有鉴权必须在后端做前端做的是“不给用户添堵”。2.3 工单状态机把“谁能改状态”钉死这是整个系统里最值得花时间的一块。状态机不设计好后面会出现“已归档的工单还能被改”“检查员直接跳到归档”这类问题。我用的状态定义和流转规则当前状态允许操作操作角色目标状态待受理(0)受理管理员已受理(1)待受理(0)驳回管理员已驳回(9)已受理(1)派发管理员已派发(2)已派发(2)接单检查员处理中(3)处理中(3)提交整改检查员待复核(4)待复核(4)复核通过管理员已归档(5)待复核(4)退回整改管理员处理中(3)已驳回(9)重新提交上报人待受理(0)把这套规则写成代码里的一个 Map每次变更前先校验不合法直接抛异常private static final MapInteger, MapString, Integer FLOW new HashMap(); static { FLOW.put(0, Map.of(accept, 1, reject, 9)); FLOW.put(1, Map.of(dispatch, 2)); FLOW.put(2, Map.of(take, 3)); FLOW.put(3, Map.of(submit, 4)); FLOW.put(4, Map.of(pass, 5, back, 3)); FLOW.put(9, Map.of(resubmit, 0)); } public void changeStatus(Long orderId, String action, Long operatorId) { WorkOrder order orderMapper.selectById(orderId); MapString, Integer next FLOW.getOrDefault(order.getStatus(), Map.of()); Integer target next.get(action); if (target null) { throw new BizException(当前状态不允许该操作); } // 乐观锁更新防止并发重复操作 int rows orderMapper.updateStatus(orderId, order.getStatus(), target, order.getVersion(), operatorId); if (rows 0) { throw new BizException(操作冲突请刷新后重试); } orderLogMapper.insert(new OrderLog(orderId, order.getStatus(), target, operatorId)); }这里有两个经验性的设计值得展开说。第一状态变更必须写日志。order_log这张表看起来是“多余的”但演示的时候它是最好用的东西——点开一个工单下面一条时间线展示“谁在什么时候把状态从什么改成了什么”评审老师一眼就明白你的流程是真的跑通了不是摆样子。而且出了问题排查的时候日志能直接定位到哪一步错了。第二用乐观锁而不是悲观锁。政务系统的并发量其实很低但在答辩演示时老师很可能会问“如果两个人同时点受理怎么办”。这时候你回答“我在 update 语句里带了原状态和版本号做条件更新影响行数为 0 就提示用户刷新重试”比说“我加了 synchronized”专业得多而且分布式场景下也成立。2.4 前后端接口约定别让联调吃掉一半时间接口约定这件事我的原则是**“一个人定两个人守”**。定完之后写进一个文档前后端都按这个来改动必须同步通知。统一响应体{ code: 200, msg: success, data: {}, timestamp: 1710000000000 }分页参数统一用pageNum/pageSize返回统一{ total, records }。错误码就分四档200 成功、400 参数错误、401 未登录、403 无权限、500 服务异常。别搞几十个错误码自己都记不住。请求头统一带Authorization: Bearer token拦截器解析后放进 ThreadLocalController 里通过一个工具类取当前用户不用每个方法都传一遍。上传接口单独说一句图片上传返回相对路径如/upload/2024/05/xxx.jpg不要返回带域名的绝对路径。原因很简单——你本地是localhost:8080部署到服务器换了 IP数据库里存的绝对路径全废了。前端拼接的时候用配置的 baseURL一行代码的事省掉后期批量改数据。3. 实操过程与核心环节实现3.1 从零把源码跑起来环境准备清单拿到一套源码之后别急着点运行先对一遍环境。我整理了一份清单按这个顺序检查基本不会卡住组件版本要求验证命令JDK8 或 11java -versionMaven3.6mvn -vMySQL5.7 / 8.0mysql --versionNode.js16 / 18node -vHBuilderX最新版打开即用起步动作分四步走# 1. 建库并导入脚本脚本一般在 sql/ 目录下 mysql -uroot -p -e CREATE DATABASE fire_service DEFAULT CHARSET utf8mb4; mysql -uroot -p fire_service sql/fire_service.sql # 2. 改后端配置重点就三处数据库账号密码、文件上传目录、服务端口 # src/main/resources/application.yml # 3. 后端启动 mvn clean package -DskipTests java -jar target/fire-service-1.0.0.jar # 或者直接在 IDE 里跑启动类 # 4. 前端启动 cd web-admin npm install npm run devapplication.yml里几个必须改的地方server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/fire_service?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB file: upload-path: D:/upload/ # 换成你自己机器上存在的目录末尾要有斜杠注意serverTimezoneAsia/Shanghai这一项千万别漏。漏了之后你会发现列表里的时间比实际早八个小时很多人以为是前端格式化的问题其实是 JDBC 连接的时区没设白折腾半天。启动成功的判断标准后端控制台打出Started Application in x.x seconds浏览器访问http://localhost:8080/doc.html如果有接口文档能看到接口列表前端页面能打开登录页用脚本里预置的账号能登进去。脚本里的初始账号一般在sys_user表里密码是 MD5 或 BCrypt 加密的默认密码通常在 README 或者建表语句的注释里写着。3.2 隐患上报接口从参数校验到事务落库这是移动端最核心的一个写接口我把它完整拆开讲其他写接口都是同一个套路。RestController RequestMapping(/api/hazard) public class HazardController { PostMapping(/report) RequireRole({CITIZEN, UNIT}) public ResultLong report(RequestBody Valid HazardReportDTO dto) { Long userId UserContext.getUserId(); HazardReport entity new HazardReport(); BeanUtils.copyProperties(dto, entity); entity.setReporterId(userId); entity.setStatus(0); entity.setCoverUrl(dto.getImages().isEmpty() ? null : dto.getImages().get(0)); hazardService.saveReport(entity, dto.getImages()); return Result.ok(entity.getId()); } }DTO 上的校验注解public class HazardReportDTO { NotBlank(message 隐患类型不能为空) private String hazardType; NotBlank(message 请填写隐患描述) Size(max 500, message 描述不能超过500字) private String description; NotBlank(message 请填写详细地址) private String address; private BigDecimal longitude; private BigDecimal latitude; Size(max 6, message 最多上传6张照片) private ListString images new ArrayList(); }Service 里做两件事保存主记录、批量保存附件索引套在一个事务里Transactional(rollbackFor Exception.class) public void saveReport(HazardReport report, ListString images) { this.save(report); if (CollUtil.isNotEmpty(images)) { ListAttachment list images.stream().map(url - { Attachment a new Attachment(); a.setBizType(HAZARD); a.setBizId(report.getId()); a.setFilePath(url); return a; }).collect(Collectors.toList()); attachmentMapper.insertBatch(list); } }几个实操细节图片先上传再提交表单移动端用户选完照片后立即调上传接口拿到路径列表最后提交表单只传路径。这样提交速度快用户感知也顺。不要做成“提交时一次性传所有内容”大图会卡住。Valid必须配合NotBlank/Size才生效光加注解不加Valid等于没写。校验失败的异常在全局异常处理器里捕获MethodArgumentNotValidException返回 400 和第一条错误信息前端直接弹出来。覆盖图取第一张列表页只需要一张缩略图没必要每次都查附件表。冗余一个cover_url字段是合理的反范式别纠结。地址和经纬度都收有些场景用户手动输地址有些是定位自动带出来的两个都存后面地图打点用经纬度文本展示用地址。3.3 移动端页面与列表分页的实现uni-app 的页面结构很简单一个pages目录每个页面一个 vue 文件。隐患上报页我用的是“表单 图片上传 定位”的组合template view classform picker :rangehazardTypes range-keylabel changeonTypeChange view classitem隐患类型{{ form.hazardTypeName || 请选择 }}/view /picker textarea v-modelform.description placeholder请描述隐患情况 maxlength500 / view classuploader image v-for(img, i) in form.images :keyi :srcbaseUrl img modeaspectFill / view classadd-btn clickchooseImage/view /view button typeprimary :loadingsubmitting clicksubmit提交上报/button /view /template列表页的分页逻辑是个容易出问题的地方说三个必踩的坑坑一下拉刷新和上拉加载的状态没分清。很多人用一个loading变量控制两个动作结果下拉刷新的时候把上拉加载的锁也占住了。正确做法是两个独立状态data() { return { list: [], pageNum: 1, pageSize: 10, total: 0, refreshing: false, loadingMore: false, finished: false } }, methods: { async loadList(reset false) { if (reset) { this.pageNum 1; this.finished false; } if (this.finished) return; const res await api.getHazardPage({ pageNum: this.pageNum, pageSize: this.pageSize }); this.list reset ? res.data.records : this.list.concat(res.data.records); this.total res.data.total; this.finished this.list.length this.total; this.pageNum; } }坑二total用错。后端返回的total是总条数不是当前页条数。判断是否加载完毕要用list.length total不要用records.length pageSize因为最后一页刚好满页的时候会多请求一次空数据。坑三时间格式。后端返回的LocalDateTime序列化默认带个T前端展示很丑。统一在配置里加spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai serialization: write-dates-as-timestamps: false全局配一次所有接口都干净了比在每个实体字段上标JsonFormat省事。3.4 开题报告跟代码对得上的写法开题这块我说点实在的很多人卡在“写得太空”或者“写得跟代码没关系”。评审看开题核心就看两件事你打算解决什么问题你打算怎么解决。围绕这两点写就不会跑偏。选题背景与意义别一上来就宏大叙事从“群众发现消防通道被占用不知道该向谁反映、反映之后不知道处理到哪一步”这个具体场景切入然后说清平台在其中起的作用是把这条反馈链路固定下来。意义部分写两点就够对群众是反馈渠道更顺畅对管理方是处理过程可追溯、有据可查。不要写三条以上多了就散了。国内外研究现状这一节最容易抄成空话。我的建议是聚焦到“同类系统通常怎么做”上比如对比一下纯网页版、纯小程序版、还有带工作流引擎的版本各自适合什么场景然后引出你的方案选择。这样写既不会显得没做功课又自然过渡到你的技术路线。研究内容与目标直接抄你系统的模块表但要用“业务语言”而不是“技术语言”。比如不要写“实现工单表的增删改查”写“实现隐患从上报到归档的完整流转支持退回整改与流转记录追溯”。技术路线与方案这块跟第 1.2 节的内容直接对应。把架构图用文字描述或表格替代别画复杂图、技术选型表和选择的理由写清楚。老师最容易问的就是“为什么用这个不用那个”你提前写进开题答辩时就是照着念。进度安排给个真实可执行的表按周排阶段时间跨度主要工作交付物需求分析第1-2周角色梳理、业务流程梳理需求说明、用例图系统设计第3-4周架构设计、库表设计、接口定义设计文档、ER图编码实现第5-9周后端接口、管理端、移动端可运行系统测试联调第10-11周功能测试、流程验证测试记录论文撰写第12-14周文档整理、修改定稿论文初稿与定稿参考文献别列一堆没看过的。挑十篇左右相关的其中至少三篇是你真读过摘要的答辩问“你参考的哪篇对你有启发”的时候能接上话。注意开题里写的功能最终系统里必须都有。我见过开题写了“智能推荐消防知识”代码里连推荐算法影子都没有答辩直接被追问到哑口无言。宁可开题少写两个模块也别写了实现不了。4. 常见问题与排查技巧实录4.1 环境与启动类问题速查这类问题占了新手卡壳原因的八成整理成表方便对照现象大概率原因处理办法启动报端口占用8080 被占改server.port或结束占用进程连不上数据库账号密码错 / 服务没起先用命令行连一次确认页面能打开但接口 404前端代理没配对检查vite.config.js的 proxy 指向浏览器控制台报跨域后端没配 CORS加全局跨域配置别在 Nginx 上绕上传图片成功但显示 404静态资源映射没加配置addResourceHandlers把上传目录暴露出去中文显示成问号库或连接没设 utf8mb4建库、连接串、表三处都要设编译报找不到符号Maven 依赖没下全mvn clean install -U强制更新跨域配置给一段可直接用的Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这两个方法经常被漏掉后一个。上传成功但图片打不开九成是addResourceHandlers没配或者file:后面少了路径分隔符。4.2 业务逻辑层面的隐藏陷阱环境问题好查业务问题才磨人。下面这几个是我实际踩过的。工单状态“看起来对但日志对不上”。原因通常是代码里有两处地方在改状态一处在 Service 的状态机方法里另一处在某个业务的旁支逻辑里直接update status。表现是偶发地出现“工单显示已归档但日志停在待复核”。解决办法是把状态变更收敛成唯一入口其他任何地方都只能调那个方法代码评审的时候专门查一遍update语句里有没有status字段。提交按钮点两次生成两条数据。这是移动端弱网下的经典问题。前端加防抖只能缓解根本解决要在后端做幂等上报接口可以要求前端带一个客户端生成的requestId后端用唯一索引或者 Redissetnx拦住重复请求重复的直接返回第一次的结果。答辩如果被问到“网络不好重复提交怎么办”这就是加分项。分页查出来的总数和实际条数对不上。多表 join 的时候count写错了或者 MyBatis-Plus 的分页插件没配。配置很简单Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }漏了这段selectPage会静默地返回全部数据前端翻页功能看着正常但其实没生效。逻辑删除之后查询还在。加了deleted字段但实体上忘了标TableLogic或者查询用了手写 XML 没带deleted 0条件。这两处有一处漏了就会出现“删了还在”的诡异现象。4.3 答辩现场最容易被追问的四个问题这块我按被问到的频率排序提前准备好答案现场不至于慌。第一个你的系统创新点在哪。别硬编“技术创新”政务类系统本来就不是靠算法取胜的。我的回答思路是讲“业务闭环的完整性”——把隐患上报到归档的每一步都固化成状态流转每一步都有角色约束和操作日志出现问题可追溯到具体节点。这是流程设计上的完整度不是技术上的花哨。第二个数据安全怎么保证。分三层答传输层用 token 鉴权接口层做角色注解校验数据层做范围过滤另外密码是加盐哈希存储的不存明文。如果你做了操作日志顺便提一句“关键操作有留痕”。第三个为什么不用微服务。这个问题的正确答法不是“我不会”而是“当前业务体量和部署条件不匹配”。单体应用在这个规模下部署简单、调试方便、事务好控制等业务量和团队规模真的上来了再拆分也不迟。这个回答显得你有判断力而不是盲目追新技术。第四个并发量大怎么办。诚实回答当前没有做过压测但设计上留了余地状态变更用乐观锁保证一致性列表查询都走了索引图片走静态资源不占应用线程。如果真有高并发场景下一步会考虑加缓存和读写分离。承认边界比吹牛更让人信服这一点我深有体会。4.4 我踩过的几个坑你可以直接绕开最后分享几个不写在任何文档里、但确实影响交付效率的经验。坑一一开始就追求页面好看。我在第一个版本上花了三天调 UI 细节结果后面改业务逻辑的时候页面推倒重来白干。正确的节奏是先做能跑通的丑页面把数据链路打通最后两天统一调样式。功能对了样式再差也及格样式再好流程断了就是不及格。坑二把所有数据都塞进一张宽表。图省事把上报信息、处理信息、检查结果全放一张表字段堆到三十多个。做到一半发现“一个隐患被检查了两次”没法存只能加个inspection_json字段存 JSON 字符串查询和统计全废。表该拆就拆第三范式不是白来的。坑三不留演示数据。系统做完了数据库里三条测试数据演示的时候列表空空荡荡看不出效果。提前造三十条左右的仿真数据覆盖各种状态待受理、处理中、已归档、已驳回都要有演示时每个筛选条件点下去都有内容观感完全不一样。数据用脚本批量插别手动点。坑四只在自己电脑上跑通过。交付前一定要换一台干净的机器试一遍完整部署流程你会发现一堆“我本地有”的依赖问题——某个字体、某个本地路径、某个没写进 pom 的 jar。提前踩一遍比答辩前一晚发现强太多。这套东西做下来快的话两三周能出完整版慢的话一个半月也够了。真正耗时间的从来不是写代码而是想清楚“这件事到底应该由谁在哪一步做什么”。把那张状态流转表贴在显示器边上写代码的时候照着填返工率能降一大半。