做这个项目的起因很现实:要给某沿海开放园区的运营方搭一套覆盖企业入驻、事务审批、公告发布、服务申报的智慧服务平台系统。最初我以为它只是又一个 Spring Boot + Vue 的 CRUD 堆料项目,等真正从头梳理完需求、把前后端拆开、再把部署文档和配套说明整理成可交付的状态之后,我的想法变了。这类系统的难点根本不在增删改查,而在权限边界、审批状态机、文件存储和部署环境这些容易被表面“功能”盖住的地方。
如果你现在正准备做一个“基于 Spring Boot + Vue 的智慧服务平台系统”,或者你手里正好有一个带“源码 + lw + 部署文档 + 讲解”这种交付清单的项目,这篇内容会比较适合你。我会从需求分析、架构选型、核心模块实现、部署文档整理几个维度,把这些年在类似项目里踩过的坑和沉淀下来的做法一次说清楚。
1. 这个平台到底解决什么问题:从“做功能”到“理业务”
很多全栈项目的通病是一上来就建表、写接口、画页面,等联调阶段才发现业务逻辑是散的。智慧服务平台尤其容易犯这个错,因为“智慧”两个字覆盖的范围太宽,今天想加园区导览,明天想加企业服务,后天又想接入设备数据。如果不先把角色、流程和边界定清楚,代码写到最后只会变成一张到处缝补的网。
1.1 三类使用者,三种完全不同的诉求
我习惯把平台的使用者拆成三类人,再分别列诉求。
| 使用者 | 主要诉求 | 平台里对应的能力 |
|---|---|---|
| 园区运营人员 | 在企业入驻、服务事项审批、公告发布这些环节上减少人工沟通成本,最好所有待办能在一个后台里看到 | 审批工作台、公告管理、企业台账 |
| 园区企业人员 | 能在线提交入驻申请、申报服务需求、查看进度;入口要简单,不要装一堆客户端 | 企业自助注册、服务申报、进度查询 |
| 平台管理员 | 分配账号、配置菜单、查看整体运营数据;系统不能随便崩,权限不能乱 | 用户管理、角色管理、数据看板 |
这三类人对应到系统里,就是前端用户端、后台运营端和管理员端。注意,我这里的“前端”不是指 Vue 项目里的页面,而是指“对外服务的前台系统”。很多同学在做这类项目时会把用户端和管理后台全塞在一个前端工程里,然后靠路由权限硬切。短期能用,到了权限细化和部署的时候,很容易被复杂的菜单权限逻辑绕进去。
我的做法是分开两套前端界面,但共享同一个后端服务。用户端用 Vue + Element UI 偏简洁的布局,运营端用偏表格和表单的后台布局,后端同一套 Spring Boot 服务,通过 JWT 里的角色信息控制接口访问范围。这样既不会让代码翻倍,又能保证两类页面互不干扰。
1.2 “源码 + lw + 部署文档 + 讲解”这四个交付物,分别对应什么能力
项目标题里常见的“源码 + lw + 部署文档 + 讲解”看起来像电商平台的商品描述,其实它恰好对应一个完整项目交付的四个层次。
- 源码是第一层,证明项目能跑。但光有源码远远不够,别人拿过去往往不知道怎么起环境,更不知道怎么改。
- lw,也就是配套论文或设计文档,解决的是“为什么这么做”。这一层需要把需求分析、系统设计、数据库设计、接口设计讲清楚,是答辩和评审的重点。
- 部署文档解决“怎么跑起来”。环境变量、数据库初始化、后端启动参数、前端打包、Nginx 反向代理,每一处都要写到别人照着敲命令就能复现。
- 讲解解决“怎么讲明白”。录视频也好,写讲解稿也好,核心目的是把代码和业务串成一条线,让别人能听懂你做了什么、怎么做、为什么这么做。
这四个交付物不是四个任务,而是同一套系统的四种表达方式。源码回答“是什么”,论文回答“为什么”,部署文档回答“怎么用”,讲解回答“怎么理解”。
1.3 需求收敛:哪些功能必须做,哪些可以先不做
做智慧服务平台最容易失控的地方就是把“智慧”理解得太满。我在项目里用了一个收敛原则:凡是需要线下盖章或专人面签的流程,第一版只做到“线上提交 + 审批留痕”,不做复杂的电子签章;凡是涉及硬件设备对接的功能,第一版只留数据接口,不直接接硬件。先把核心业务流程跑通,再谈智能化。
这个平台最终确定下来的核心功能是五个:企业入驻审批、服务事项发布与申报、公告通知、企业台账管理、数据看板。后来加的投诉建议和用户反馈模块,也是在这个骨架之上自然长出来的。先把这五个主流程做稳,整个系统就能真正被业务方用起来,而不是做一个放在服务器上吃灰的演示系统。
2. 技术选型与整体架构:为什么是 Spring Boot + Vue,而不是别的组合
很多人在选型时会有一种误区,觉得必须用微服务、必须用最新版本、必须搞容器化编排才算高级。但智慧服务平台本质上是典型的中后台管理系统,它的核心评价标准是稳定、可维护、好交接。Spring Boot + Vue 的组合在这个领域几乎是默认答案,不是因为它最新,而是因为它最不容易翻车。
2.1 前后端分离的取舍
用 Spring Boot 做后端,Vue 做前端,是这套系统的骨架。后端提供纯 RESTful API,前端通过 Axios 调用,数据格式统一用 JSON。
后端我选的是 Java 8 + Spring Boot 2.x,而不是 Java 17 + Spring Boot 3.x。原因很直接:当前大多数第三方依赖、教程和服务器环境对 2.x 的支持更成熟,部署文档写起来也更省事。如果你不是有特殊需求,不需要为了追新版本给自己挖坑。
前端我选的是 Vue 2 生态。虽然 Vue 3 已经很成熟,但那一年项目里的组件库兼容性、团队熟悉度、现有代码片段,都是 Vue 2 更稳。做交付型项目,稳定压倒一切。页面组件用 Element UI,表格、表单、弹窗、分页这些现成组件能省掉大量手写样式的成本。
数据库用 MySQL 8.0,缓存用 Redis。Redis 在项目里最主要的作用是存登录 Token、缓存热点数据,比如首页的公告列表和统计数据,避免每次刷新都去查数据库。
2.2 后端项目结构怎么分
后端我一直遵循一种按业务模块分包的方式,而不是简单按 controller、service、mapper 三层切成三个大包。按技术分层写小项目没问题,但这种平台类系统的业务模块比较多,企业入驻、服务申报、公告、台账、系统管理,如果全部塞在三个包里,后期找文件会非常痛苦。
我用的包结构大致是这样的:
com.smartpark ├── common // 通用类:返回结果、异常处理、工具类 ├── config // 全局配置:跨域、MyBatis Plus、Redis ├── security // JWT 认证、权限拦截 ├── modules │ ├── system // 用户、角色、菜单 │ ├── enterprise // 企业入驻、企业台账 │ ├── service // 服务事项、申报订单 │ ├── notice // 公告管理 │ └── dashboard // 统计看板这种结构的最大好处是,每一个业务模块内部的 controller、service、mapper 都放在一起,改企业入驻功能时只进一个包,不会误碰其他模块的代码。
2.3 数据库设计必须想清楚的几张表
这类系统并不缺表,缺的是表之间的关系设计。最核心的几张表我列一下,你在设计时也有个参考。
- 用户表:包含账号、密码、手机号、状态、角色标识、关联的企业 ID。不要把所有角色塞进一张表后再用 type 字段到处判断,除非你的系统真的小到不需要扩展。
- 企业表:包含企业名称、统一信用代码、法人、联系人、入驻状态、入驻时间。企业入驻审批里的所有关键字段都在这张表里。
- 服务事项表:服务名称、分类、办理时限、所需材料说明、上架状态。
- 申报单表:关联服务事项、关联企业、状态、提交材料路径、审批意见、审批时间。
- 公告表:标题、内容、发布状态、发布时间。
- 审批记录表:关联业务表单类型和业务 ID,记录每一步操作人、操作动作、时间、备注。这张表很重要,它让整个审批过程可追溯。
我在企业表和服务事项表上都加了逻辑删除字段。线上系统都不建议用物理删除,因为一旦业务方误删,恢复起来很麻烦。使用逻辑删除出问题时,只需要把 delete_flag 改回来。
2.4 权限模型:RBAC 是底线,不要再发明轮子
权限我直接用的 RBAC,用户关联角色,角色关联菜单和按钮权限。菜单权限决定侧边栏能显示什么,按钮权限决定页面上的操作按钮是否可见。
不过这里要提醒一点:前后端分离项目里,前端不能只用路由守卫来判断权限,后端接口必须二次校验。前端隐藏一个按钮只是体验层面的设计,真正的安全边界永远在后端。我在后端写了基于拦截器的接口权限校验,比如只有运营角色才能调用审批接口,只有管理员才能调用用户管理接口。用注解挂在 Controller 方法上,简洁明确。
3. 关键实现细节:审批流、文件上传、数据看板与权限拦截
接下来要讲的这四个部分是整个系统里最容易出问题的点,也是最容易在验收时被追问的点。我会把实现思路和实际踩过的坑一起说。
3.1 登录认证:JWT + Redis 双重校验
用户登录成功后,后端生成一个 JWT Token 返回给前端。前端在请求拦截器里把 Token 放在请求头 Authorization 字段里,后端通过过滤器校验 Token。
JWT 本身是无状态的,但它有一个问题:无法主动让某个 Token 失效。用户修改密码或管理员封禁账号后,旧的 Token 在到期之前仍然有效。我的处理方式是登录成功时把 Token 的键存进 Redis,过期时间跟 Token 保持一致;每次请求校验 Token 通过之后,再查一下 Redis 里是否存在,如果不存在就判定未登录。
// 核心思路:拦截器里同时校验 JWT 和 Redis 中的 Token String token = request.getHeader("Authorization"); Claims claims = JwtUtils.parseToken(token); if (claims == null) { throw new BusinessException("登录已过期"); } String redisKey = "login:token:" + claims.get("userId"); if (!redisTemplate.hasKey(redisKey)) { throw new BusinessException("账号已在其他设备退出"); }这样做还有个额外好处,以后如果做“同一账号只允许单点登录”的需求,只需要在登录时覆盖 Redis 里的旧 Token 就可以了,不需要改认证流程。
3.2 企业入驻审批状态机:别用一堆 if/else 堆流程
企业入驻审批是这类平台最核心的流程之一,也最容易写乱。很多初学者会直接在 Controller 里写一个 updateStatus 方法,前端传一个状态值进来,后端就改成这个值。这样做看着简单,实际上等于让任何人都能跳过审批环节。
我用的是状态机思路,先把入驻审批状态定义清楚:
| 状态 | 含义 | 可流转到 |
|---|---|---|
| 0 | 草稿/待提交 | 1 |
| 1 | 待初审 | 2 或 5 |
| 2 | 初审通过/待终审 | 3 或 5 |
| 3 | 终审通过/待签约 | 4 或 5 |
| 4 | 已入驻 | 5 |
| 5 | 已驳回/已终止 | 1 |
后端不接收任意状态值,而是接收“动作”。比如 submit、approve、reject、terminate 分别对应提交、通过、驳回、终止。每个动作只能在特定状态上执行,否则直接抛出异常。这样既保证了审批流程的完整性,写出来的论文也更有亮点,答辩时你可以很清楚地讲出“状态与动作分离”的设计思想。
// 示例:通过动作 public void approve(Long enterpriseId, String operatorName) { Enterprise enterprise = getById(enterpriseId); if (enterprise.getStatus() != 1 && enterprise.getStatus() != 2) { throw new BusinessException("当前状态不允许审批通过"); } enterprise.setStatus(enterprise.getStatus() + 1); saveApprovalRecord(enterpriseId, "APPROVE", operatorName, "审批通过"); }3.3 文件上传:本地存储还是对象存储
企业入驻要上传营业执照,服务申报要上传材料附件,所以文件上传功能是逃不掉的。这个项目我用的是本地存储,把文件存到服务器的一个指定目录,数据库里只保存相对路径。
本地存储的好处是部署简单,不需要额外开通对象存储服务;坏处是服务器空间有限,迁移时要把整个上传目录一起拷走。我后来写部署文档时会专门强调:文件路径不要写死在代码里,要放在配置文件里。
文件上传后,前端拿到的是相对路径,真正访问时通过 Nginx 或者后端接口映射。我在后端做了静态资源映射,把本地的上传目录映射到/files/**路径上,前端用服务器地址 + /files + 相对路径就能访问,不需要走 Controller,效率也更高。
这里有一个我踩过的坑:文件上传时,前端 FormData 里的字段名必须和后端 MultipartFile 的接收参数名一致,否则会报Required request part is not present。排查这种问题最快的方式是先打开浏览器 Network 面板看请求体,再对字段名,而不是直接改配置。
3.4 数据看板:从统计 SQL 到 ECharts 展示
数据看板是给运营方和管理员看整体情况的,通常包括累计入驻企业数、待审批数量、本月服务申报量、公告发布趋势等。我前端用了 ECharts,后端提供统计数据接口。
统计数据接口最忌讳的是每次都全表查询。我的做法是提前约定好统计口径,然后用聚合查询一次性查出来。比如待审批数量,本质上是查询企业表中状态为待初审和待终审的数据数量;服务申报量则按月份分组。
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS total FROM service_order WHERE create_time >= #{startTime} GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month;这里提醒一个细节:统计类的 SQL 如果能用数据库聚合完成,就不要在 Java 代码里循环累加。有一次我发现看板数据刷新很慢,排查到最后是因为我用 for 循环挨个月查数据库,三个月的数据也要查三次。改成一条分组 SQL 之后,响应时间从几百毫秒降到了几十毫秒。
3.5 前端状态同步:列表页刷新是高频 Bug 来源
前端最容易出的问题不是页面写不出来,而是操作完成之后列表不刷新。比如企业入驻审批通过后,列表里这条数据的状态还是“待初审”。常见的解决办法是在调用成功之后调用查询接口重新拉列表数据,或者让表格绑定的数据源重新赋值。
我的习惯是封装一个fetchList()方法,在增删改操作成功后统一调用。这个方法不写在组件内部到处复制,而是放在一个公共混入对象里,所有列表页面都复用。这样后续要加“接口异常后自动重试”也好统一处理。
4. 从开发到交付:部署文档、配套论文和讲解视频的整理方法
很多人在代码写完之后就以为项目结束了,其实真正花时间的还有另外三件事:把项目部署到服务器、把设计思路写成论文、把操作过程录成讲解。这三件事做得好不好,直接决定这个项目在别人眼里是“完整作品”还是“半成品”。
4.1 本地开发环境怎么搭最简单
本地开发我推荐直接用一个集成环境把 MySQL 和 Redis 起起来,再用 IDEA 跑后端,用命令行启动前端开发服务器。
- JDK 8+
- Maven 3.6+
- MySQL 8.0
- Redis 5+
- Node.js 14+
后端配置文件里把数据库连接、Redis 连接、JWT 密钥、文件上传路径都做成可配置项。特别是文件上传路径,本地开发时我习惯用./upload/,部署到服务器后改成绝对路径,这样不会因为环境不同产生路径错乱。
4.2 服务器部署:从打包到 Nginx 反向代理
我把部署过程总结为五步,写进文档里,每一步都配上实际命令。
第一步,后端打包。在项目根目录执行:
mvn clean package -DskipTests打包成功后 target 目录下会生成 jar 包。我习惯把 jar 包放在服务器的/opt/app目录,然后用nohup启动:
nohup java -jar smartpark-server.jar \ --spring.profiles.active=prod \ --server.port=8080 \ --spring.datasource.url=jdbc:mysql://localhost:3306/smartpark \ --spring.datasource.username=root \ --spring.datasource.password=你的密码 \ > /opt/logs/smartpark.log 2>&1 &第二步,初始化数据库。我准备了一份init.sql,里面包含建库建表和初始化管理员账号的语句。部署时直接执行:
mysql -uroot -p < /opt/init.sql第三步,前端打包。Vue 项目执行:
npm install npm run build打包后生成的 dist 目录就是所有静态文件。
第四步,配置 Nginx。把 dist 目录指到 Nginx,同时把/api开头的请求反向代理到后端 8080 端口:
location / { root /opt/smartpark/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意这里我用了proxy_pass http://127.0.0.1:8080/;,末尾的斜杠会把请求路径里的/api前缀去掉,后端 Controller 里就不用额外加/api前缀。前后端的接口路径约定要在开发一开始就定好,不然部署时改起来很痛苦。
第五步,配置 HTTPS。现在新平台基本都被要求用 HTTPS 访问,我在部署文档里会附上申请免费证书和修改 Nginx 配置的步骤。这一步不做,后面讲解环节很容易被问住。
4.3 部署中最容易被忽略的问题
部署看起来步骤少,但每一条都可能让你卡一个下午。
第一个坑是跨域。前后端分离项目如果前端直接访问后端地址,会触发跨域问题。开发环境我用了 Vue 的 devServer 代理,生产环境用 Nginx 反向代理,这样浏览器看到的始终是同源地址。如果你在后端代码里也配置了跨域,可能会和 Nginx 代理叠加出奇怪的问题,最好是只保留一条链路。
第二个坑是数据库字符集。MySQL 建库时如果没有设置 utf8mb4,中文和特殊符号可能出现乱码或插入失败。我在 init.sql 里建库语句直接带上字符集:
CREATE DATABASE smartpark DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第三个坑是文件上传目录权限。Linux 服务器上,后端进程如果没有上传目录的写权限,上传会静默失败或报权限异常。部署文档里我会专门叮嘞一句:先创建目录,再确认运行用户有写权限。
4.4 配套论文怎么写才不会变成“流水账”
配套论文(lw)是这类交付里最有技术含量的一环。很多人把论文写成了功能介绍列表,第二章是环境安装,第三章是表格展示,第四章是操作截图,答辩老师一问原理就沉默了。
我的论文结构一般是这样:
- 第一章写背景与意义。不要只写“随着社会发展”,要写具体痛点:人工审批周期长、材料重复提交、进度不透明、数据统计靠人工。
- 第二章写需求分析。画用例图,列功能需求和非功能需求,讲清楚不同角色的边界。
- 第三章写系统设计。技术架构、功能模块设计、数据库设计、接口设计。
- 第四章写核心实现。把登录鉴权、审批状态机、文件上传、数据看板四块写深,每块都要讲设计的出发点和关键代码逻辑。
- 第五章写系统测试。功能测试、接口测试、性能测试至少要有基础的数据记录。
写论文的核心原则是“每个结论都有出处”。你说系统性能好,就要贴接口响应时间的数据;你说权限设计安全,就要讲清楚后端拦截器是怎么实现的。这一部分写扎实了,答辩讲解时你会非常轻松。
4.5 讲解视频和讲解稿:一条主线讲到底
讲解环节最容易犯的错是照着代码逐行念。真正有效的讲解,是一条主线走到底:先讲清楚谁在用这个系统,再讲用户怎么进来,然后讲后台怎么处理,最后讲管理员怎么看数据。
我录制讲解视频时一般分四段:
- 第一段:展示项目整体界面,登录、首页、菜单,建立起直观印象。
- 第二段:走完一条完整业务流,比如从企业注册到入驻审批通过,录屏加口述。
- 第三段:打开核心代码说明重点逻辑,比如审批状态机、权限拦截器。
- 第四段:展示部署过程,启动后端、前端打包、Nginx 配置。
这里要特别提醒一句:讲解稿和演示数据要提前准备好。演示企业资料、审批账号、待办数据都要在录制前造好,否则现场临时输入资料会非常占用时间,看起来也不专业。
我在这类项目上做过不止一次,最大的体会是:这个平台的难度不在某个被单独拿出来说的高深算法,而在于你能不能把一条完整业务链从数据库字段一路串到页面按钮。企业入驻审批流转到哪一步、该由谁处理、处理完以后前台哪些数据要跟着变,这些才是真正决定用户体验和评审判分的地方。如果你正准备动手做类似系统,我建议你先把第 2 章的数据表设计和第 3 章的审批状态机想清楚再写代码,后面会少走很多弯路。