ARTICLE DETAIL

资讯详情

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

前后端分离全栈实战:SpringBoot、Vue、MyBatis和MySQL构建科研管理系统

前后端分离全栈实战:SpringBoot、Vue、MyBatis和MySQL构建科研管理系统 搞过前后端分离项目的人应该都有这种感觉单看SpringBoot、Vue、MyBatis、MySQL这几个词每一个都不算陌生但真要把它们揉进一个能跑、能交付、能部署的完整系统里中间隔着的东西比想象中多得多。这套科研管理系统就是在这种背景下攒出来的项目本身不算大但五脏俱全登录鉴权、动态菜单、分页检索、文件上传、多角色权限、打包部署一条链路走下来几乎把前后端分离项目里最常见的那批坑都踩了一遍。所以我写这篇东西的思路很直接不贴几十页流水账代码而是把这个项目的设计取舍、核心实现、部署过程、坑点排查完整讲清楚。如果你正准备做毕业设计、课程设计或者想找一套能直接上手改造的前后端分离脚手架这篇内容应该能帮你省下大量翻文档和试错的时间。1. 项目整体设计与思路拆解先说清楚这套系统到底做了什么。科研管理系统面向的是高校或科研单位的日常科研业务管理场景核心用户有四类学生、教师、科研秘书、系统管理员。业务上主要围绕科研项目从立项、中期检查到结题的全生命周期展开同时包含论文成果登记、经费使用记录、数据统计导出等功能。1.1 前后端分离到底分离了什么很多初学者对“前后端分离”的理解停留在“用了Vue当框架、用了SpringBoot写接口”这个层面其实分离的核心在于交互方式变了。传统JSP或Thymeleaf那套是服务端渲染页面跳转、数据填充、模板解析全在服务器上完成前后端分离之后前端工程独立运行通过HTTP接口与后端通信数据以JSON格式传输页面路由由前端控制。这套系统里前端工程用Vue CLI构建开发环境下通过代理解决跨域生产环境把打包后的静态文件扔给Nginx再由Nginx反向代理到后端服务。所以前端和后端在物理上就是两个独立部署的单元Vue项目跑在80或8080端口SpringBoot服务跑在8080或9090端口两者之间只有JSON请求和响应。这样设计的好处很明显前后端可以并行开发互不阻塞后端接口可以同时被Web端、小程序端甚至App端复用部署时可以单独扩容哪边压力大就扩哪边。代价则是联调成本变高、跨域问题必须处理、鉴权不能再依赖传统的SessionCookie要换成Token机制。1.2 核心业务模块划分科研管理系统的业务不算复杂但模块之间有关联设计时我按“以项目为主线”的思路来拆分系统管理模块用户管理、角色管理、菜单管理、数据字典。这是后台系统的底座也是动态路由的数据来源。项目申报模块学生或教师提交科研项目立项申请填写项目名称、类型、预算、成员等信息上传项目任务书附件。项目审核模块科研秘书对申报材料进行初审按角色权限决定是否通过流程上支持退回修改。项目过程管理立项后的中期检查、变更申请、结题申请每个节点都有状态字段控制。成果登记模块论文、专利、获奖等科研成果的登记和审核。经费管理模块记录每笔经费支出按项目汇总统计。数据统计模块按部门、按年份、按项目类型统计科研产出生成表格数据供前端展示。模块划分的原则遵循“高内聚、低耦合”每个模块对应一组Controller、Service、Mapper接口路径统一以/api/module/action方式命名例如/api/project/apply、/api/review/approve。这样无论前端还是后端看一眼路径就知道在做什么。1.3 角色权限与数据隔离权限模型采用经典的RBAC设计用户-角色-菜单权限三张核心表外加两张关联表。管理员给角色分配菜单权限用户登录后根据角色ID查询菜单列表动态生成前端路由。数据隔离方面普通用户只能看到自己提交的项目和成果科研秘书能看到全院的申报数据系统管理员则拥有全部权限。这个隔离不做成复杂的行级权限框架而是在Service层通过判断当前用户的角色ID在SQL查询条件中动态拼上user_id ?或dept_id ?简单直接对于这种规模的项目完全够用。2. 技术选型深挖SpringBoot、Vue、MyBatis、MySQL这套系统的技术栈是市面上最常见的一档网上随便一搜就是一大把同名项目。但常见不等于简单真正动手时版本选择、框架搭配、工程结构都是需要想清楚的事。2.1 SpringBoot版本选择与工程结构项目采用了SpringBoot 2.7.x版本。之所以卡在这个版本线而不是直接上SpringBoot 3.x主要是因为生态兼容问题。SpringBoot 3要求JDK 17起步且对javax包名改为jakarta很多老牌第三方库的兼容性还没完全跟上。如果你是跟着教程做毕业设计选SpringBoot 2.7.x JDK 1.8的组合是最稳的因为大部分教学资源、封装好的工具类、网上搜到的解决方案都基于这套环境。如果确实想用SpringBoot 3建议同时升级到对应的MyBatis Starter版本并测试好所有依赖的兼容性否则光是一个包名改起来就够折腾的。后端工程结构采用标准的分层分包方式com.example.research ├── controller # 控制层接收请求、返回结果 ├── service # 业务层处理核心逻辑 │ └── impl ├── mapper # MyBatis数据访问层接口 ├── entity # 数据库实体类 ├── dto # 前端请求/响应数据传输对象 ├── config # 配置类跨域、拦截器、WebMvc ├── common # 通用工具、统一返回结构、异常处理 ├── utils # JWT工具、文件处理工具等控制层只做参数接收和结果封装业务逻辑写在Service中数据库操作走Mapper接口。这样做的好处是隔离清晰、后续好扩展比如需要加入缓存或消息队列时可以直接在Service层做切面不用动Controller。2.2 用MyBatis而不是MyBatis-Plus的取舍现在很多新项目直接上MyBatis-Plus确实省事单表CRUD基本零SQL。这套系统选型时我特意回归了原生MyBatis原因有两个一是科研管理系统里的核心查询大多是多表关联MP虽然后来也支持但XML里写SQL的灵活度和可控性更强二是原生MyBatis能让你对SQL的执行过程有更清晰的认识对理解#{}和${}的区别、一级缓存、二级缓存这些基础概念更有帮助。当然这不意味着我要否定MyBatis-Plus实际开发中如果在做纯单表操作占多数的系统用MP能提升不少效率。这里只是说明选型需要结合业务场景。MyBatis的坐标我用了mybatis-spring-boot-starter版本在2.3.x与SpringBoot 2.7.x配合良好。Mapper接口与XML文件通过Mapper注解扫描注册在application.yml中配置了mapper-locations指向classpath:mapper/*.xml。2.3 前端工程Vue 2还是Vue 3这是一个绕不开的选择题。这套系统用的是Vue 2 Element UI的组合。从技术上看Vue 3 Element Plus已经是新项目的默认选项但如果你的目标是把项目跑起来、快速完成毕业设计或后端方向的学习Vue 2的教程数量、问题解决方案、组件库成熟度依然是最好的。如果你选择Vue 3需要注意的点包括main.js的创建方式从new Vue()变成了createApp()路由和Vuex的引入方式从Vue.use()改为app.use()Element Plus的组件注册方式也略有不同。这些差异在写页面时都会遇到。我这套项目里单独封装了request.js模块基于Axios统一处理请求地址、超时时间、请求头、响应拦截。前端请求流程统一为页面调用API模块中的函数API模块通过Axios发出请求后端返回统一JSON结构前端通过响应拦截器判断业务码。3. 数据库设计与MyBatis落地细节数据库设计是这套系统里最不能马虎的部分。前后端分离项目接口返回的数据结构基本由表结构决定表设计不合理后面所有接口都要跟着遭殃。3.1 核心表结构设计要点科研管理系统的主要数据表包括用户表、角色表、菜单表、科研项目表、项目参与者表、中期检查表、结题申请表、成果登记表、经费记录表、数据字典表。这里挑几张核心表说几个设计要点第一用户表不直接挂角色字段。用户与角色之间用中间表关联因为用户可能身兼多职比如一个硕士生同时是某项目的参与者、某论文的第一作者。中间表方案能保持数据的规范化。第二科研项目表带一个status字段来记录审批状态取值范围在数据字典中定义比如0草稿、1待初审、2初审通过、3待中期、4待结题、5已结题、6已驳回。状态字段配数据字典前端就能根据状态码渲染对应的标签颜色和操作按钮。第三所有的业务表统一带上create_time、update_time、deleted这三个字段。前两个用于排序和审计deleted用于逻辑删除。这不是科研系统特有的要求但很多初学者会忽略。第四经费记录表和项目表是多对一关系经费表里必须包含project_id外键同时记录支出类型、金额、经手人、备注。统计时通过GROUP BY project_id汇总接口层再按项目回填数据。3.2 XML映射与多表联查MyBatis的核心在于SQL映射这套系统里我把所有的多表查询都写在XML里。例如查询项目列表需要同时附上申报人姓名、所在部门、项目类型名称、当前状态名称就要从项目表关联用户表、部门表、字典表。SQL大概长这样select idselectProjectList resultTypecom.example.research.dto.ProjectDTO SELECT p.id, p.project_name, p.project_type, p.budget, p.status, u.real_name AS applicant_name, d.dept_name, dict.label AS status_label FROM research_project p LEFT JOIN sys_user u ON p.create_by u.id LEFT JOIN sys_dept d ON u.dept_id d.id LEFT JOIN sys_dict dict ON dict.type project_status AND dict.value p.status where if testprojectName ! null and projectName ! AND p.project_name LIKE CONCAT(%, #{projectName}, %) /if if teststatus ! null AND p.status #{status} /if if testuserId ! null AND p.create_by #{userId} /if /where ORDER BY p.create_time DESC /select值得注意的细节是SQL里的动态条件如果当前登录用户是普通教师Service层会往查询参数里塞一个userIdXML通过if标签动态拼接过滤条件从而实现数据隔离如果用户是科研秘书则不传这个参数直接查全院数据。#{}在这里是预处理参数MyBatis会转成?占位符可以防止SQL注入。而${}是字符串拼接虽然有时写排序字段比较方便但绝不能直接拼用户输入。3.3 MyBatis缓存机制与坑点MyBatis默认开启一级缓存也就是SqlSession级别的缓存同一个SqlSession中两次相同的查询会直接命中缓存。但在实际项目中每个请求都会创建新的SqlSession所以一级缓存的作用范围非常有限对系统性能提升基本没有帮助。真正有意义的是二级缓存它是Mapper级别的缓存多个SqlSession可以共享。在XML中加一行配置就能开启cache evictionLRU flushInterval60000 size512 readOnlytrue/不过我要提醒一句科研管理系统这种低并发、多业务表关联的系统我并不建议贸然开二级缓存。原因在于当关联表中的数据发生变更时缓存不会自动感知特别容易查出脏数据。比如用户修改了姓名之前缓存过的项目列表可能还存着旧姓名要等缓存过期才能刷新。如果你确实想用二级缓存建议只用在高频读取且极少更新的字典表、部门表上同时接受最多一分钟的延迟刷新。4. 后端核心模块实现从登录鉴权到文件上传后端是整个系统的发动机。这里挑四个我认为最具代表性的模块详细展开JWT登录鉴权、分页查询、项目审批流程、文件上传。4.1 JWT登录鉴权与拦截器前后端分离项目的登录鉴权不能用Session原因很简单Session依赖CookieCookie依赖同源策略跨域场景下处理起来非常别扭。所以这套系统采用JWTJSON Web Token机制。登录流程是前端调用/api/auth/login传入用户名和密码。后端校验用户名密码成功则生成一个Token返回给前端。前端把Token存到localStorage或sessionStorage里。后续每个请求在Axios请求拦截器里带上Authorization: Bearer token。后端通过拦截器解析Token取出用户ID和角色信息放入ThreadLocal供后续业务代码使用。Token生成的核心代码依赖io.jsonwebtoken:jjwt这个库public String generateToken(Integer userId, String username) { MapString, Object claims new HashMap(); claims.put(userId, userId); claims.put(username, username); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000L * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }拦截器继承HandlerInterceptorAdapter在preHandle方法中检查请求头里的Token。如果Token不存在或已过期直接返回401状态码前端响应拦截器捕获后跳转到登录页。这里有一个实际开发中容易踩的坑拦截器只拦截/api/**路径但登录接口/api/auth/login必须放行否则用户连登录都进不去。另外如果是前后端分离部署别忘了配置跨域否则浏览器会在CORS策略这关把请求拦下来。4.2 分页查询与条件检索列表接口是后台管理系统最普遍的接口类型科研项目的列表页需要支持项目名称模糊搜索、状态条件筛选、申报人精确匹配、按时间排序、分页展示。我用PageHelper这个分页插件来简化分页操作。PageHelper.startPage(pageNum, pageSize); ListProjectDTO list projectMapper.selectProjectList(queryDTO); PageInfoProjectDTO pageInfo new PageInfo(list);PageHelper.startPage后面必须紧跟第一条查询语句这个顺序不能乱否则分页会失效。分页插件底层是通过MyBatis拦截器自动在SQL后面拼接LIMIT实现的所以如果你在调用后额外执行了别的查询分页参数就跑到那条SQL上去了。封装返回给前端的分页数据结构一般长这样{ code: 200, data: { list: [], total: 100, pageNum: 1, pageSize: 10, pages: 10 } }前端拿到total值用来计算分页组件的总页数list用于渲染表格。这种结构几乎适用于所有的列表接口建议在后端封装一个公共的PageResult类避免每个接口返回结构不一致。4.3 科研项目审批流程的状态机设计审批流程的核心是状态流转控制。科研项目的状态不能乱跳比如草稿状态不能直接跳到已结题待初审状态不能直接退回草稿以外的其他状态。如果把这些判断散落在各个Service方法里代码会越来越难维护。我的做法是在Service层抽出一个状态校验逻辑用Map定义合法流转关系private static final MapInteger, SetInteger TRANSITION_MAP new HashMap(); static { TRANSITION_MAP.put(0, Set.of(1, 6)); // 草稿 - 待初审, 已驳回 TRANSITION_MAP.put(1, Set.of(2, 6)); // 待初审 - 初审通过, 已驳回 TRANSITION_MAP.put(2, Set.of(3, 4)); // 初审通过 - 待中期, 待结题 TRANSITION_MAP.put(3, Set.of(4)); // 待中期 - 待结题 TRANSITION_MAP.put(4, Set.of(5, 6)); // 待结题 - 已结题, 已驳回 }每次执行流转操作前先判断当前状态和目标状态是否在合法映射表中不合法直接抛出业务异常。这种方式把“散装”的状态判断集中到了一个地方后面要加新的状态节点只需要改这一张映射表即可。4.4 文件上传本地存储与MinIO整合科研项目申报时必须上传任务书、预算表这类附件结题时还要传结题报告。这套系统最初用的是最简单的本地存储方案上传的文件保存到服务器某个目录数据库里存文件的访问路径。但部署到多台服务器或容器环境时本地存储的弊端就暴露了——文件只存在于某一台机器上其他请求访问不到。后来我引入了MinIO来做对象存储。MinIO是开源的兼容S3协议部署非常简单一条Docker命令就能拉起来。SpringBoot整合MinIO的步骤也不复杂引入io.minio:minio依赖配置一个MinioClient的Bean然后封装上传、下载、删除方法。MinIO接入的核心代码大概是这样public String uploadFile(MultipartFile file) { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String objectName UUID.randomUUID().toString().replace(-, ) suffix; minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return objectName; }对象名用UUID重新生成避免文件名冲突和中文乱码问题。数据库里保存的是objectName前端展示时再通过后端接口生成临时访问URLpresignedGetObject这样还可以控制访问权限。如果只是做本地部署演示继续用本地存储也完全可行但要约定好统一的存储目录并且用绝对路径保存防止相对路径导致文件找不到。5. 前端核心实现动态路由、状态管理与交互前端这部分的核心难度不在页面样式而在工程化的组织方式。Vue项目写得久了就会明白无非是路由配置、状态管理、接口封装、组件复用这么几件事。5.1 动态路由与权限菜单动态路由是前后端分离后台系统的标志性功能。普通的静态路由适合内容固定的页面但带权限的后台系统必须做到不同角色登录后看到的菜单不同且不能通过在地址栏输入URL绕过菜单直接访问无权页面。实现思路是这样的路由表分为两部分基础路由登录页、404页、首页在代码里静态定义业务路由在本地建立完整的“路由映射表”。用户登录成功后后端返回该用户有权限访问的菜单列表数据结构是树形父菜单子菜单。前端拿到菜单树后递归遍历菜单树根据每个菜单项上的component字段从本地映射表中找到对应的路由组件动态注册到Vue Router中。菜单栏渲染的数据源也是这棵菜单树不是手写死的。这里引入了一个Vue Router的动态添加方法router.addRoute这是实现的核心。权限菜单的完整流程可以总结为登录接口返回菜单权限前端根据权限动态注册路由同时将菜单结构交给侧边栏组件渲染。要注意的一个坑直接刷新页面时Vue实例会重新创建vuex里的菜单数据会丢失动态添加的路由也不复存在。所以刷新时必须重新获取用户信息和菜单权限再走一遍动态路由注册流程。否则刷新后当前URL指向的页面会因为路由不存在而跳转到404。5.2 Axios封装与Token处理Axios封装是所有前后端分离项目前端的标准操作。我通常会单独建一个utils/request.js统一配置const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未授权)) } if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { Message.error(error.message) return Promise.reject(error) } )这样做的核心收益是所有请求的鉴权头、错误处理都在一个地方管理页面代码不需要重复处理错误弹窗和登录失效跳转。尤其是401这个状态码一旦后端统一返回前端只需在这一个位置做跳转处理不需要每个页面都写一遍。5.3 列表页、表单页、分页组件的套路化写法后台管理系统的页面高度重复大部分页面就是表格搜索栏弹窗表单的组合。我整理了一套固定的写法套路新模块页面基本就是复制改改。列表页的Vue组件模板大致是template div el-form :inlinetrue :modelqueryParams el-form-item label项目名称 el-input v-modelqueryParams.projectName placeholder请输入项目名称 / /el-form-item el-form-item label状态 el-select v-modelqueryParams.status placeholder请选择状态 el-option v-foritem in statusOptions :keyitem.value :labelitem.label :valueitem.value / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form el-table :datapageList v-loadingloading el-table-column propprojectName label项目名称 / el-table-column propapplicantName label申报人 / el-table-column propbudget label预算金额 / el-table-column propstatusLabel label状态 / el-table-column label操作 template slot-scopescope el-button typetext clickhandleEdit(scope.row)编辑/el-button /template /el-table-column /el-table el-pagination :current-pagequeryParams.pageNum :page-sizequeryParams.pageSize :totaltotal current-changehandlePageChange / /div /template把搜索、列表、分页绑定的逻辑写一次之后你会发现后续的每个功能模块页面都只是字段不同结构完全可以复用。这也是为什么框架类的项目代码看起来总是“千篇一律”但这种千篇一律恰恰意味着需要调试的地方更少。6. 部署上线从本地到生产环境代码写得再好跑不起来没有任何价值。这一部分我完整走一遍部署流程从服务器环境搭建到前端打包、Nginx配置、后端Jar包部署。6.1 服务器环境准备部署用的是Linux服务器CentOS 7或Ubuntu都可以需要安装以下环境JDK 1.8用yum install java-1.8.0-openjdk或者上传tar包手动解压配置环境变量。MySQL 5.7或8.0按官方教程配置好注意设置字符集为utf8mb4。Nginx通过yum install nginx或编译安装。MinIO可选通过Docker一行命令启动。MySQL安装后有一个特别常见的坑本地连接正常但SpringBoot启动时报SSL连接错误。这是因为MySQL 8.0默认开启SSL而JDBC驱动在连接时默认尝试使用SSL。解决方法是连接串中显式关闭SSLjdbc:mysql://localhost:3306/research?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai另外一个容易遗漏的是数据库授权。建议单独创建应用账号只授权当前数据库不要用root直连权限最小化原则在数据库层面同样适用。6.2 前端打包与Nginx静态代理前端构建需要本地Node环境执行以下命令npm install npm run build打包完成后dist目录就是发布物。把这个目录上传到服务器修改Nginx配置将/路径指向该目录对应的静态文件位置。顺带处理一个常见场景如果前端使用了BrowserRouter模式即访问/project/list这种路径刷新页面会出现404因为Nginx找不到这个路径对应的物理文件。解决方案是在Nginx配置中加入location / { root /usr/share/nginx/html/research; index index.html; try_files $uri $uri/ /index.html; }try_files会先找物理文件找不到就回退到index.html这样前端路由就能接管了。6.3 后端Jar包部署与脚本SpringBoot项目使用Maven打包mvn clean package -DskipTests打出来的research-system.jar就是最终产物。上传到服务器后用nohup命令后台启动nohup java -jar research-system.jar --spring.profiles.activeprod logs/app.log 21 生产环境的数据库连接、Redis地址等配置放在application-prod.yml中通过--spring.profiles.activeprod激活。日志输出到一个固定文件方便排查问题。还有一个小问题是JVM内存。如果服务器本身内存只有2G建议限制JVM堆内存否则容易导致被系统杀掉进程nohup java -Xms512m -Xmx512m -jar research-system.jar logs/app.log 21 Nginx配置了反向代理后前端请求路径/api会转发到后端端口。前后端的联通性通过curl http://localhost:8080/api/auth/login来验证如果返回JSON而不是404说明后端服务没问题。7. 常见问题与排查技巧实录这部分内容全是从实际部署和使用中攒出来的经验。整理成表格方便对照排查。现象可能原因解决方案前端页面能打开列表无数据后端跨域未配置或接口报错F12看网络请求排查CORS错误和后端日志登录接口报USER_NOT_FOUND数据库中无该用户或用户名参数传递错误用navicat或命令行查询库中用户表确认页面刷新后404前端路由未配置try_files修改Nginx配置加上try_files $uri $uri/ /index.htmlSpringBoot启动报端口被占用默认8080端口被其他进程占用用netstat -tunlpMyBatis报Invalid bound statementMapper接口与XML文件未匹配检查XML的namespace、接口方法名和XML中id是否一致时间字段显示相差8小时数据库时区与JVM时区不一致在Jdbc连接串上加serverTimezoneAsia/Shanghai文件上传成功但访问404MinIO未配置公网访问策略用presignedGetObject生成临时访问URL上面这张表只是最常见的问题真实项目里遇到的问题远比这个复杂。比如查一个项目列表接口返回401原因可能是Token过期但Axios没有走统一跳转处理又比如上传文件后预览显示乱码原因是Content-Type没设置对。再单独说一个排查思路层面的建议。前后端分离项目的问题排查一定要学会看三个地方浏览器开发者工具里的Network面板、后端日志、前端Console报错。按照“请求发出去没有-后端有没有收到-后端处理有没有报错-响应数据对不对-前端渲染有没有报错”的顺序排查90%的问题都能定位。我在实际开发中最深的一个体会是这类项目最难的不是某个框架的API用不熟而是多个组件串联起来之后的状态一致性。比如用户登录后拿到了Token但刷新页面后Token还在、菜单信息却丢了这个时候需要同时检查前端的store持久化策略、路由动态注册逻辑、后端的Token有效期设计。三个环节只要有一个没对齐表现出来就是一个莫名奇妙的Bug。所以如果你正在做类似的项目我的建议是先把一条完整链路跑通哪怕只有一个登录接口、一个列表接口让前端通过代理访问到后端、后端查到数据库、数据返回页面渲染出来再逐步添加模块。这也是我拿到任何脚手架时首先做的事情。整套系统打包源码项目里都有按照文档里的部署顺序走一遍本地起一个完整的科研管理系统不是问题。最后分享一个小技巧SpringBoot项目启动时如果怀疑是某个Bean初始化失败但又不知道是哪个类出问题可以在启动参数里加上--debugSpringBoot会把自动配置的匹配和不匹配日志全打出来。这个参数看起来不起眼但在排查环境差异导致的启动失败时命中率极高。
返回列表