又到毕业设计选题季,我陆续收到不少同学的私信,问题高度相似:“想做一个XX管理系统,用SSM框架行不行?”“单体架构的后台管理系统,答辩时会不会被老师觉得太简单?”说实话,SSM单体写一套增删改查确实不难,但真正到答辩或面试时,老师很可能追问一句:“如果多个部门同时审批,系统卡顿怎么办?”“权限是怎么做细粒度控制的?”“流程状态流转的代码在哪一层?”这些问题,如果只停留在单体架构,往往答得没有底气。
今年有一个毕业设计题目值得拆解——基于SpringCloud+Layui+AI的智能政务微服务审批管理系统。这个题目最大的价值在于它没有停留在“管理系统”的舒适区,而是把三条技术线组合在一起:SpringCloud做微服务治理,Layui做后台管理界面,AI作为创新点接入审批业务。从实际工程角度来看,这个组合兼顾了毕业设计的查重性、演示效果和答辩深度。
本文不打算吹嘘这套项目有多“完美”,而是想从技术拆解的角度说清楚:微服务为什么这样拆、SpringCloud核心组件分别解决什么问题、AI能力到底能落在哪些审批环节、Layui在这种前后端分离程度不高的项目里怎么用最合适。如果你正在准备类似的毕业设计,或者想从单体项目往微服务方向过渡,这篇文章可以当成一份“技术预演”来读。全文不依赖某个特定版本,重点讲清楚思路和关键配置,你拿到任何一套SpringCloud环境都能对应上。
1. 为什么这个题目踩中了主流技术栈
先说一个比较扎心的现象:每年毕业设计里,“XX管理系统”占了相当大的比例,但其中很多系统还是传统的单体架构——一个SpringBoot应用,配上MyBatis和Thymeleaf或者JSP,前端页面直接丢在静态资源目录里。这样的系统用来演示“能跑”是没问题的,但放到“技术含金量”的维度看,确实缺少可聊的空间。
这个题目真正的聪明之处,在于它用一套业务系统,同时覆盖了三类技术关键词:
SpringCloud微服务。这是目前企业在数字化转型中最常用到的技术栈之一。业务拆分、服务注册、网关路由、远程调用、配置中心,这些概念在面试题里反复出现,但在毕业设计里能完整落地的不多。政务审批场景天然适合拆微服务,因为参与方多、流程长、模块边界清晰:用户登录认证是一块,审批业务是一块,流程流转是一块,AI辅助能力又是一块。
Layui后台管理。不少人觉得Layui“老”,但“老”不等于“不好用”。对于毕业设计和中小型后台系统,Layui的组件化思路、自带表格分页、表单验证、弹出层,开发效率极高。它不需要Node构建链,也不需要Webpack配置,引入layui.js和layui.css就能开始写页面。在时间紧张的情况下,用Layui比用Vue+Element UI从零搭一套工程更稳妥。
AI能力接入。现在的毕业设计如果没有一点AI元素,答辩时容易被问“你的系统有什么亮点”。这个题目把AI放在审批场景里,已经有了很自然的切入点:材料预审、智能问答、文本查重、审批意见生成。AI不是飘在业务之外的花架子,而是真正嵌在审批流程里。
所以这个题目的选题逻辑是:用微服务架构承载一套真实业务,用Layui保证后台开发效率,用AI能力制造创新点。它不追求技术上的“新”,而是追求技术组合上的“合理”。
2. 政务审批场景与微服务架构的匹配逻辑
在展开技术细节之前,有必要先把业务场景讲清楚。政务审批管理系统,典型的使用对象是办事群众和审批人员。办事群众在线提交材料,系统对材料做初步检查,然后流转到审批人员那里,经过多个节点——部门初审、分管领导审批、终审——最后给出审批结果。
这个场景放在单体架构里能不能做?能。但如果考虑更多真实因素,单体的短板就暴露出来了:
- 审批高峰期,材料提交和审批查询集中在同一时段,数据库连接和接口响应容易被拖垮。
- 审批材料、用户信息、流程实例、日志记录,这几类数据的访问模式和量级差别很大,放在同一个数据库里,索引和表结构互相牵制。
- 不同审批节点可能需要不同的权限策略,单体架构中权限拦截通常写在Filter或者Interceptor里,时间长了会越来越难维护。
- AI能力如果要接入,比如材料预审,调用的是外部大模型API,响应时间比普通数据库查询慢一个数量级,如果放在原有请求链路里,很容易把整个系统拖慢。
微服务架构不是万能的,但在这个场景里,它的多数特性都能派上用场:
| 微服务特性 | 在审批系统中的实际位置 |
|---|---|
| 服务注册与发现 | 各业务服务启动后自动注册到Nacos,网关按服务名转发 |
| 网关路由与鉴权 | 所有外部请求先经过Gateway,再分发到对应服务 |
| 远程调用 | 审批服务调用用户服务获取审批人信息,调用AI服务完成材料预审 |
| 配置中心 | 各服务的数据库配置、API Key、业务开关统一管理 |
| 独立部署 | AI服务可以独立扩容,即使AI服务暂时不可用,审批主流程也能继续走 |
对于毕业设计来说,这个场景的最大好处是:每个微服务都能找到明确的业务归属,不会出现“为了微服务而微服务”的尴尬。答辩时被问到“为什么把这个模块拆出来”,可以拿出业务层面的理由,而不是只说“老师让我这么做的”。
3. 系统总体架构与技术选型
这个项目的架构分层比较清晰,可以概括为“入口-治理-业务-数据-智能”五个层面。
入口层:Layui前端页面 + Nginx静态资源服务。页面运行在浏览器里,通过HTTP请求访问后端接口。Layui本身只是一个前端UI框架,不负责页面渲染,也不承担路由跳转,它只需要通过ajax与后端交互。
治理层:SpringCloud Gateway网关 + Nacos注册中心/配置中心。网关负责统一入口、跨域处理、简单鉴权、路由转发。Nacos负责服务注册发现和配置管理。
业务服务层:服务按业务边界拆为多个独立模块。用户认证服务负责登录、权限、用户信息;业务审批服务负责审批任务的创建、流转、查询;流程引擎服务负责状态机的推进;AI服务负责与大模型API交互,输出预审结果或智能问答内容。
数据层:MySQL存储业务数据,Redis缓存用户会话和热点审批数据。各微服务可以共用同一个MySQL实例,但逻辑上通过不同数据库或不同表前缀做隔离。毕业设计阶段不建议每个服务独立拆一个数据库实例,那样会显著增加部署复杂度。
智能层:通过HTTP调用大模型API,对审批材料做预审分析,对群众常见问题做智能答复。
下面这张表汇总了项目里最核心的技术组件:
| 技术组件 | 在本项目中的作用 |
|---|---|
| Spring Boot | 各微服务的基础开发框架 |
| Spring Cloud Alibaba | 提供微服务治理能力 |
| Nacos | 服务注册发现 + 配置中心 |
| Spring Cloud Gateway | 统一API网关,路由和跨域 |
| OpenFeign | 服务之间声明式远程调用 |
| MySQL | 用户信息、审批业务数据存储 |
| Redis | 登录Token、审批缓存、限流计数 |
| MyBatis-Plus | 数据持久层,简化单表CRUD |
| Layui | 后台管理界面UI框架 |
| 大模型API | AI材料预审、智能问答(通常以OpenAI兼容接口对接) |
从技术栈来看,它并没有引入太冷门的东西,每个技术国产化率也很高,Nacos、MyBatis-Plus在中文技术社区的资料非常丰富。这一点对于毕业设计尤其重要,遇到问题能搜到解决方案。
这里要多说一句版本兼容的问题:Spring Boot 2.x和Spring Cloud Hoxton/2021.x搭配比较稳定,Spring Boot 3.x则要求Spring Cloud 2022.x以上,同时JDK需要17以上。到写代码之前,先查一下官方版本兼容矩阵,不要凭感觉乱搭版本。本文的代码以Spring Boot 2.x风格演示,如果你用Spring Boot 3.x,把javax.servlet相关改成jakarta.servlet即可。
4. 环境准备与前置条件
环境准备这一步是毕业设计里最容易出问题的地方,很多同学在配置环境上花的时间比写代码还多。下面按推荐顺序列出需要准备的环境。
4.1 JDK环境
推荐JDK 8或者JDK 11。如果你打算用较新的Spring Boot 3.x,那么JDK 17是基础。安装后配置好JAVA_HOME环境变量,命令行执行java -version确认版本。
4.2 Maven环境
Maven负责依赖管理。建议配置阿里云镜像,否则下载SpringCloud相关依赖会非常慢。在settings.xml里加入镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>4.3 MySQL
安装MySQL 5.7或8.0。本项目的核心业务数据都存MySQL,字符集建议用utf8mb4,排序规则utf8mb4_general_ci,这样能兼容中文和一些特殊字符。
4.4 Redis
Redis在本项目中的职责是缓存登录用户信息、维护Token有效期,也可以用来做接口限流。安装后默认端口6379,密码可以设置,但毕业设计阶段不设置密码也能跑通。
4.5 Nacos
Nacos在整个微服务架构中扮演两个角色:注册中心和配置中心。下载Nacos Server后,Windows环境直接进入bin目录,双击startup.cmd启动,Linux/Mac环境执行sh startup.sh -m standalone。启动成功后浏览器访问http://localhost:8848/nacos,默认账号密码都是nacos。
4.6 开发工具
推荐IntelliJ IDEA。创建父工程后,在IDEA里同时打开多个子模块,启动时按依赖顺序逐个启动服务。
5. 父工程搭建与微服务模块划分
这一节直接进入代码实操。先创建一个Maven父工程,用来统一管理所有子模块的依赖版本。
文件路径:pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>gov-approval-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <properties> <spring.boot.version>2.7.18</spring.boot.version> <spring.cloud.version>2021.0.8</spring.cloud.version> <spring.cloud.alibaba.version>2021.0.5.0</spring.cloud.alibaba.version> <mybatis.plus.version>3.5.3.1</mybatis.plus.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <modules> <module>gov-common</module> <module>gov-gateway</module> <module>gov-auth</module> <module>gov-approval</module> <module>gov-ai</module> </modules> </project>这里的模块划分对应实际业务:
| 模块 | 职责 |
|---|---|
gov-common | 公共工具类、统一返回结果、异常处理 |
gov-gateway | 网关服务,负责路由转发和跨域 |
gov-auth | 用户认证与权限服务 |
gov-approval | 审批业务服务,核心流程 |
gov-ai | AI能力服务,对接大模型API |
父工程只是依赖管理容器,不写业务代码。这个阶段最大的坑是版本组合,如果Spring Boot、Spring Cloud、Spring Cloud Alibaba三者版本不兼容,会出现启动报错或者组件不可用。上面的版本组合经过大量项目验证,稳妥性较好。
6. 服务注册与发现:Nacos接入
微服务架构里,服务启动后需要注册到注册中心,其他服务才能通过服务名找到它。这个项目选择Nacos,除了配置中心的集成能力强之外,还有一点非常实际:Nacos控制台自带中文界面,启动后能看到每个服务是否在线、实例数量是多少,演示效果比Eureka更直观。
以gov-approval服务为例,在pom.xml中加入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>配置文件采用bootstrap.yml,原因在于Nacos配置中心的客户端需要在Spring容器初始化之前连接配置服务器。
文件路径:gov-approval/src/main/resources/bootstrap.yml
spring: application: name: gov-approval cloud: nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos discovery: namespace: public group: DEFAULT_GROUP config: namespace: public group: DEFAULT_GROUP file-extension: yaml启动类的写法与普通SpringBoot一致,重点是加上服务发现注解:
package com.example.govapproval; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class GovApprovalApplication { public static void main(String[] args) { SpringApplication.run(GovApprovalApplication.class, args); } }启动后,在Nacos控制台的“服务管理”页面应该能看到gov-approval服务已经注册,状态为健康。如果没有出现,优先检查三件事:Nacos是否真的启动成功、server-addr地址是否写对、application.name是否与Maven中模块名一致。
7. 网关路由与跨域配置
服务一旦多了,前端不能直接请求每一个服务的端口,否则跨域问题会让人崩溃。网关的作用就是把所有请求收敛到一个端口,然后按路径前缀转发到不同的服务。
文件路径:gov-gateway/src/main/resources/application.yml
server: port: 8080 spring: application: name: gov-gateway cloud: nacos: server-addr: 127.0.0.1:8848 gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: auth-route uri: lb://gov-auth predicates: - Path=/api/auth/** filters: - StripPrefix=1 - id: approval-route uri: lb://gov-approval predicates: - Path=/api/approval/** filters: - StripPrefix=1 - id: ai-route uri: lb://gov-ai predicates: - Path=/api/ai/** filters: - StripPrefix=1配置里要注意几个细节:
lb://前缀表示使用负载均衡方式调用服务,SpringCloud Gateway会从Nacos拿到服务实例列表,然后按照负载均衡策略分发请求。不写lb://,网关就不知道去哪里找服务。
**StripPrefix=1**指去掉路径中的第一个前缀。比如前端请求/api/auth/user/info,经网关转发到gov-auth服务时,实际路径变成了/user/info。如果服务端的Controller里写的是/api/auth/user/info,那么可以不启用这个过滤器,前后保持一致即可。
跨域配置是前后端分离项目的重点。Layui页面如果部署在8081端口,请求网关8080,必然触发跨域。网关层统一处理跨域比较省事:
package com.example.govgateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; @Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }这里真正容易踩坑的地方是:addAllowedOrigin("*")和setAllowCredentials(true)同时使用时会报错,SpringCloud Gateway不支持这种组合。所以推荐用addAllowedOriginPattern("*")。
8. 审批服务间的远程调用:OpenFeign
在微服务架构里,服务间调用最常见的方式是OpenFeign。比如审批业务里有一个常见需求:创建审批任务时,需要根据审批人ID去用户服务查询审批人的姓名、部门、岗位。如果审批服务直接连用户数据库,就破坏了微服务的边界,所以要让审批服务通过Feign调用用户服务接口。
首先在gov-approval的pom.xml中加入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>在启动类上添加@EnableFeignClients注解:
package com.example.govapproval; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class GovApprovalApplication { public static void main(String[] args) { SpringApplication.run(GovApprovalApplication.class, args); } }然后写一个Feign客户端接口:
package com.example.govapproval.feign; import com.example.common.result.R; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "gov-auth", fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/api/auth/user/{id}") R<UserInfoVO> getUserById(@PathVariable("id") Long id); }Fallback类用于服务降级:
package com.example.govapproval.feign; import com.example.common.result.R; import org.springframework.stereotype.Component; @Component public class UserClientFallback implements UserClient { @Override public R<UserInfoVO> getUserById(Long id) { return R.fail("用户服务暂时不可用"); } }要启用Feign的降级能力,还需要在配置文件中打开熔断开关:
feign: circuitbreaker: enabled: true一个小建议:Feign接口的路径和返回值一定要和服务端实际接口保持一致。很多同学在联调时出现“Feign调用404”的问题,排查时发现Controller路径写的是/user/{id},Feign客户端却写成了/user/get/{id}。这种问题多看服务端日志就能定位。
9. 审批流程核心实现
审批流程是这个系统的业务核心,也是答辩时最能体现“系统设计能力”的部分。项目里可以设计一个清晰的状态机,保证审批任务从提交到结束的每一步都有状态记录。
审批状态可以这样定义:
| 状态 | 说明 |
|---|---|
| PENDING_SUBMIT | 待提交 |
| UNDER_REVIEW | 部门初审中 |
| LEADER_REVIEW | 领导审批中 |
| APPROVED | 已通过 |
| REJECTED | 已驳回 |
| WITHDRAWN | 已撤回 |
文件路径:gov-approval/src/main/java/com/example/govapproval/enums/ApprovalStatus.java
package com.example.govapproval.enums; public enum ApprovalStatus { PENDING_SUBMIT(0, "待提交"), UNDER_REVIEW(1, "部门初审中"), LEADER_REVIEW(2, "领导审批中"), APPROVED(3, "已通过"), REJECTED(4, "已驳回"), WITHDRAWN(5, "已撤回"); private final Integer code; private final String desc; ApprovalStatus(Integer code, String desc) { this.code = code; this.desc = desc; } public Integer getCode() { return code; } public String getDesc() { return desc; } public static ApprovalStatus of(Integer code) { for (ApprovalStatus status : values()) { if (status.code.equals(code)) { return status; } } return null; } }审批任务的Service层不必把逻辑堆在Controller里。一个比较合理的Controller可以只负责接收参数和返回结果,状态流转放在Service层:
package com.example.govapproval.service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.govapproval.entity.ApprovalRecord; import com.example.govapproval.enums.ApprovalStatus; import com.example.govapproval.mapper.ApprovalRecordMapper; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @Service public class ApprovalRecordService extends ServiceImpl<ApprovalRecordMapper, ApprovalRecord> { /** * 提交审批材料 */ public void submit(Long recordId) { ApprovalRecord record = getById(recordId); if (record == null) { throw new RuntimeException("审批记录不存在"); } if (!ApprovalStatus.PENDING_SUBMIT.getCode().equals(record.getStatus())) { throw new RuntimeException("当前状态不允许提交"); } record.setStatus(ApprovalStatus.UNDER_REVIEW.getCode()); record.setSubmitTime(LocalDateTime.now()); updateById(record); } /** * 审批通过 */ public void approve(Long recordId, Long approverId, String comment) { ApprovalRecord record = getById(recordId); if (record == null) { throw new RuntimeException("审批记录不存在"); } if (!ApprovalStatus.UNDER_REVIEW.getCode().equals(record.getStatus()) && !ApprovalStatus.LEADER_REVIEW.getCode().equals(record.getStatus())) { throw new RuntimeException("当前状态不允许审批"); } record.setStatus(ApprovalStatus.APPROVED.getCode()); record.setApproverId(approverId); record.setApproveComment(comment); record.setApproveTime(LocalDateTime.now()); updateById(record); } /** * 驳回 */ public void reject(Long recordId, Long approverId, String comment) { ApprovalRecord record = getById(recordId); if (record == null) { throw new RuntimeException("审批记录不存在"); } record.setStatus(ApprovalStatus.REJECTED.getCode()); record.setApproverId(approverId); record.setApproveComment(comment); record.setApproveTime(LocalDateTime.now()); updateById(record); } }状态机用枚举加显式校验,比在代码里散落魔法数字要清晰很多。答辩时可以直接说:审批状态的合法性由状态机保证,每个状态都定义在枚举类中,流转操作有前置校验,这样非法状态变更在业务入口就被拦住了。
10. AI能力如何接入审批系统
这个项目的名字里带“AI”,所以这一节需要重点讲清楚AI到底落在哪里。合理的落点有三个:
第一,材料预审。群众提交审批材料后,系统调用大模型API,检查材料是否完整、格式是否合规、内容是否存在明显矛盾。这个环节的价值在于减少审批人员的重复劳动。
第二,智能问答。办事群众在提交材料前经常会问“这个材料需要几份”“审批需要多少天”,与其写死FAQ,不如接入大模型做语义理解。
第三,审批意见辅助生成。审批人员填写意见时,系统根据材料摘要生成一段建议性描述,审批人员确认后再提交。
为了方便维护,AI调用统一放在gov-ai服务中,其他服务通过Feign或HTTP调用。
大模型API的对接方式通常是HTTP调用。这里用一个简化示例演示,假设模型服务提供了OpenAI兼容接口:
文件路径:gov-ai/src/main/java/com/example/govai/service/AiReviewService.java
package com.example.govai.service; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.Map; @Service public class AiReviewService { @Value("${ai.api.url}") private String apiUrl; @Value("${ai.api.key}") private String apiKey; @Value("${ai.api.model}") private String model; private final RestTemplate restTemplate; public AiReviewService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } /** * 材料预审 */ public String preReview(String materialContent) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); JSONObject payload = new JSONObject(); payload.put("model", model); JSONObject userMessage = new JSONObject(); userMessage.put("role", "user"); userMessage.put("content", "你是政务审批材料预审助手。请检查以下材料是否存在以下问题:" + "1. 材料是否完整;2. 是否有明显格式错误;3. 是否有信息矛盾。\n\n材料内容:\n" + materialContent); payload.put("messages", List.of(userMessage)); HttpEntity<String> requestEntity = new HttpEntity<>(payload.toJSONString(), headers); ResponseEntity<String> response = restTemplate.exchange( apiUrl, HttpMethod.POST, requestEntity, String.class ); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { JSONObject body = JSON.parseObject(response.getBody()); return body.getJSONArray("choices").getJSONObject(0) .getJSONObject("message") .getString("content"); } return "AI预审服务暂时不可用,请稍后重试"; } }这里需要提醒一件事:真实项目中,大模型API的Key绝对不能硬编码在代码里。正确做法是放在Nacos配置中心,或者本地的application.yml里,并在.gitignore中排除;同时调用AI必须设置超时时间。因为大模型API的响应速度通常在几秒到几十秒,如果不设超时,一个慢请求可能挂住整个线程。建议统一配置连接超时和读超时:
ai: api: url: https://your-llm-api.example.com/v1/chat/completions key: ${AI_API_KEY} model: gpt-4o-miniRestTemplate的超时配置:
package com.example.govai.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.net.SocketTimeoutException; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(30000); return new RestTemplate(factory); } }AI接入还有一个容易被忽视的问题:降级策略。如果大模型API超时或者说服务不可用,不能因此阻塞审批主流程。正确的做法是捕获异常后给前端返回一个“AI预审暂不可用,请人工预审”的提示,让流程继续走人工通道。这也是答辩时值得展开讲的技术点。
11. Layui后台管理页面的关键写法
Layui在这个项目里负责后台管理界面。虽然现在Vue很流行,但Layui在快速开发场景下确实高效——不需要Node环境,不需要安装依赖,一个JS文件全部搞定。
Layui页面加载时,顶栏和侧边栏用layui.layout组件,内容区用iframe嵌入子页面,这是一种非常经典的后台布局方式。
子页面中典型的表格渲染方式是table.render。这里重点讲两个高频问题。
第一个问题:Layui table 单个列怎么加点击事件?
很多人往table里塞一个“操作”列,然后在done回调里绑定点击事件,其实Layui本身提供了table.on('tool(...))事件。解决办法是:在列配置中通过templet指定一个自定义按钮,然后在table.on('tool(filter)')里监听点击。
table.render({ elem: '#approvalTable', url: '/api/approval/list', page: true, cols: [[ { field: 'id', title: '审批编号', width: 100 } , { field: 'title', title: '事项名称' } , { field: 'statusName', title: '当前状态', width: 120 } , { field: 'createTime', title: '提交时间', width: 180 } , { title: '操作', width: 200, templet: function (d) { return '<button class="layui-btn layui-btn-sm" lay-event="detail">详情</button>' + '<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="approve">审批</button>'; }} ]] });注意lay-event属性定义了按钮的事件名。然后通过table.on('tool(filter)', callback)监听:
table.on('tool(approvalTable)', function (obj) { var data = obj.data; if (obj.event === 'detail') { layer.open({ type: 2, title: '审批详情', area: ['600px', '500px'], content: '/page/approval/detail.html?id=' + data.id }); } else if (obj.event === 'approve') { layer.open({ type: 2, title: '审批处理', area: ['700px', '550px'], content: '/page/approval/handle.html?id=' + data.id }); } });这个方式的优势在于:不用操作DOM、不用绑定多余的onclick,事件完全由Layui统一管理,页面刷新后事件依然有效。
第二个问题:Layui table 的数据格式要求。
Layui table对后端返回的数据格式有严格要求,必须是一个符合以下结构的JSON:
{ "code": 0, "msg": "", "count": 100, "data": [ { "id": 1, "title": "企业设立登记", "statusName": "部门初审中" }, { "id": 2, "title": "建设项目审批", "statusName": "领导审批中" } ] }其中code必须为0,否则Layui会认为请求失败;count是总记录数,用于分页;data是当前页的列表数据。
所以后端封装统一返回结果时,字段名要严格对齐。比如定义一个R类:
package com.example.common.result; import lombok.Data; @Data public class R<T> { private Integer code; private String msg; private Long count; private T data; public static <T> R<T> ok(T data, Long count) { R<T> result = new R<>(); result.setCode(0); result.setMsg("成功"); result.setCount(count); result.setData(data); return result; } public static <T> R<T> fail(String msg) { R<T> result = new R<>(); result.setCode(500); result.setMsg(msg); return result; } }注意这里count用了Long类型,如果数据库分页用的Page对象,把page.getTotal()直接放进去就行。
12. 系统启动顺序与验证方式
微服务项目启动有先后顺序,顺序不对会有一堆注册失败的错觉。推荐按以下顺序启动:
第一步启动Nacos。Nacos不启动,后面所有服务都会报连接超时。
第二步启动gov-auth。用户服务是基础服务,审批服务和AI服务启动时可能需要调用它。
第三步启动gov-approval和gov-ai。这两个服务依赖Nacos注册中心,启动后会自动注册。
第四步启动gov-gateway。网关最后启动,这样网关在初始化路由时,下游服务的实例信息已经全部就绪。
验证方式可以分三层:
第一层看注册中心。打开Nacos控制台的服务管理页面,确认gov-auth、gov-approval、gov-ai、gov-gateway都显示在线。
第二层看网关转发。直接请求网关地址,例如:
curl "http://localhost:8080/api/auth/user/1"如果响应正常,说明网关把请求正确转发到了gov-auth服务。
第三层看前端页面。浏览器打开Layui后台页面,进入审批列表,看表格数据是否正常加载。可以顺手测试一下“详情”按钮是否能弹出层。
13. 常见问题与排查思路
微服务项目出了问题,不要急着重启服务,先看日志。下面把这类项目最容易遇到的问题按现象整理成一张表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务启动报“找不到配置” | Nacos配置中心没有建对应的Data ID | 检查Nacos控制台配置列表是否存在 | 手动创建Data ID,格式为服务名.yaml |
| 注册中心看不到服务实例 | 服务启动失败或Nacos地址错误 | 查看服务日志,检查bootstrap.yml | 修正server-addr,重新启动 |
| 网关转发返回503 | 下游服务不在线或负载均衡拉取不到实例 | 检查下游服务是否注册成功 | 等下游服务启动后重试,或检查服务名 |
| 在iframe子页面里Layui表格不渲染 | 子页面没有引入layui.js或table模块 | 浏览器F12控制台看JS报错 | 在子页面重新layui.use(['table']) |
| Layui表格显示“返回数据格式不正确” | 后端没有返回统一格式 | 看Network响应体 | 将code置为0,补充count和data |
| Feign调用404 | 路径不一致,或服务名写错 | 看服务端日志确认请求是否到达 | 统一接口路径;检查@FeignClient(name) |
| AI接口调用超时 | 大模型API响应慢 | 看gov-ai日志,检查调用耗时 | 设置读超时时间,加入降级逻辑 |
| 跨域报错 | 网关CORS配置不正确或浏览器拦截 | 看浏览器Network响应头是否有CORS头 | 使用addAllowedOriginPattern("*")配置 |
这里重点说一个容易迷惑的现象:网关或服务首次启动时,控制台可能会出现红色报错,但Nacos页面里服务却显示在线。这通常是因为SpringCloud的某些组件启动时会尝试连接一些可选依赖,连接失败后自动降级。不要一看到红色日志就紧张,先确认关键组件是否就绪,再根据实际功能是否受影响决定是否处理。
14. 项目设计上的几个加分项
这部分内容是给答辩准备的。同样的项目,为什么有人能拿优秀,有人只拿及格?差别往往在于有没有把设计思考讲出来。
第一个加分项:服务拆分的业务依据。答辩时不要只说“我把系统拆成了几个微服务”,要讲清楚边界在哪里。比如用户认证服务独立出来,是因为它是一个被所有业务服务调用的基础能力;AI服务独立出来,是因为它的响应时效模型和普通数据库请求差异很大,独立部署可以避免互相拖累。这种回答体现的是架构能力。
第二个加分项:数据一致性的处理。审批系统里,如果审批服务调用AI服务做预审,AI服务返回了结果,但审批服务在保存时出了异常,两边数据就不一致了。这里可以引入一个简单的方案:把AI预审结果先保存为草稿,审批人员确认后再更新审批状态。这样就在不引入分布式事务组件的情况下,把一致性问题通过业务编排化解了一部分。分布式事务组件Seata在这个项目里可以提,但没必要真引入,毕业设计阶段复杂度会明显上升。
第三个加分项:权限设计。政务审批系统天然需要角色权限。这里可以讲RBAC模型:用户-角色-菜单/权限的三层关联。JWT Token放在请求头里,网关层做统一鉴权,业务服务里做细粒度校验。能讲清楚权限模型的层次关系,是管理系统的加分项。
第四个加分项:安全兜底。生产环境中数据库操作、审批通过、驳回操作都要有操作日志和审计记录。在日志里记录操作人、操作时间、操作内容和结果,出了问题能追溯。这是政务系统非常看重的点。
15. 写在最后
如果把这套毕业设计当成一个纯“管理系统”来做,那就浪费了题目的架构价值。SpringCloud让你理解服务拆分的边界,Nacos让你感受注册中心和配置中心的存在意义,Gateway让你知道网关不是一层简单的转发,Layui帮你理解传统后台管理页面的开发效率,AI接入则逼着你去思考外部能力如何与业务系统集成。
如果你也想做一个类似的系统,我的建议是:先不要急着写代码。找一张纸,把审批业务的角色、状态、流程节点列出来,再对照本篇文章的结构画出服务边界,然后逐步填充代码。把“单体跑通”和“微服务拆分”分成两个阶段来推进,能显著降低调试复杂度。一个技术上成熟、答辩思路清晰的毕业设计,靠的不是代码堆数量,而是每一步选择都有明确的理由。