
市面上的企业级管理系统源码多如牛毛但真正把 SpringBoot Vue MyBatis MySQL 这套组合讲清楚并且能直接落地的“完整版”并不多。最近我在梳理一套企业级 BB 平台管理系统源码简单说就是围绕 B 端业务协作场景搭建的一套后台管理 前端界面 数据库设计一体的工程适合做 OA、CRM、供应链协同、工单流转这类系统的二次开发底座也适合想彻底搞懂前后端分离实战的同学拿来练手。下面我会从源码结构、环境搭建、后端细节、前端联调、数据库设计、部署上线到常见问题排查把自己在实操里踩过的坑和验证过的思路完整过一遍。1. 项目概述与源码价值拆解1.1 BB 平台到底是什么为什么强调“企业级”不同团队对“BB”的叫法不太一样有些叫 Business-to-Business 业务协同平台有些叫运营管理后台还有的干脆是一家公司的产品代号。名字不重要重要的是这类系统背后的逻辑它不只是给一个人用的工具而是要支撑企业内部的多个角色在同一个流程里协作。典型场景包括客户资料管理、合同审批、订单流转、结算对账、消息通知、操作日志审计等功能模块。我一直强调“企业级”这三个字不是产品经理写 PPT 用的形容词而是会直接影响技术决策。企业级系统要面对多角色权限隔离、数据审计可追溯、接口并发与稳定性、上线后的可维护性这些问题。比如一个订单表如果连创建人、修改人、版本号都没有出了问题根本没法定位是谁在什么时间改了什么数据。这套源码既然标了完整版它的价值就不在于某个页面多漂亮而在于这些底层规则有没有被考虑进去菜单权限、角色分配、数据权限、日志留痕、软删除字段、统一返回结构这些才是真正能复用到下一个项目里的资产。1.2 这套技术栈为什么能撑住这类场景SpringBoot 解决的是后端基础设施的复杂度。以前搭一个 SSM 项目要手写一堆 XML、配置事务、配置数据源SpringBoot 用 starter 机制把这些默认值都准备好了真正的业务代码反而更集中。Vue 是目前国内后台管理系统里普及度最高的前端框架生态成熟、资料多招人也好招。MyBatis 和 JPA 的最大区别是 SQL 完全由自己控制企业里经常要写复杂统计报表和动态条件查询用 MyBatis 的 XML 动态 SQL 能精确掌控每一条语句这对 DBA 和资深后端更友好。MySQL 不需要我多说部署简单、运维成本低、文档丰富配合 InnoDB 事务引擎在企业内部系统这个量级完全够用。这套组合不是最时髦的但它是目前企业里“最容易上线、最容易接手、最容易外包交付”的组合之一。如果你非要问我为什么不直接上微服务加各种中间件我的回答很简单一个几十个功能模块的管理系统单体应用加上合理分层比一上来拆十几个服务要好维护得多。等业务量真正有需要了再把单点拆分出来也不迟。1.3 拿到完整版源码后第一步该看什么很多人拿到源码第一件事就是点 Run报错了就开始焦虑。我的习惯是先把三样东西找出来README、数据库初始化脚本、全局配置文件。大部分完整版项目会把启动步骤写在 README 里依赖哪些中间件、用什么版本的 JDK、数据库脚本在哪都有说明。第二步是看数据库脚本这比看代码更能快速还原业务全貌。表之间怎么关联、哪些字段是通用审计字段、哪些模块是主流程一目了然。第三步才是全局配置 application.yml确认端口、数据库连接、Redis、上传路径这些环境信息。先把这些看完再启动你会少走很多弯路。跑通之后再按“用户登录 - 菜单加载 - 新增一条业务数据 - 审批流转 - 日志记录”这条主链路去读代码基本就能把一个完整版源码吃透。2. 环境准备与项目初始化2.1 开发环境版本搭配这套源码涉及的组件不算多但版本组合非常关键。我见过不少项目启动失败不是代码有问题而是拿 JDK17 去跑 Spring Boot 2.6 的旧项目或者用 Spring Boot 3.2 的高版本去兼容老团队的 MyBatis Starter结果各种别扭。这里给出一个相对稳妥的版本参考组件推荐版本说明JDK1.8 或 11Spring Boot 2.x 默认支持如果是 Spring Boot 3.x必须 JDK 17Maven3.6稳定即可3.8 以上没太大问题Spring Boot2.7.x比较折中的企业版本第三方 starter 兼容性最好MyBatis Starter2.3.x配合 Spring Boot 2.7 实测稳定MySQL8.05.7 也可以用但 8.0 更推荐Node.js16/18对应 Vue3 Vite 或者 Vue CLI 5Vue2.7 或 3.4如果源码基于 Vue2建议 2.7它是最后一个大版本关于“SpringBoot 版本太高”这个问题我要多说一句。不是新版本不好而是企业项目的依赖树太深高版本往往意味着命名空间从 javax 切成 jakarta底层 Tomcat、Netty、MyBatis 插件的兼容性都要重新验证。如果源码是给团队交付用的求稳比求新更重要你把 Spring Boot 升到 3.5却发现某个报表组件只支持到 2.7那就很尴尬了。2.2 Spring Boot jar 反编译成可阅读项目的具体步骤有些时候你拿到的不是源码而是一个已经打包好的 Spring Boot fat jar。这时候如果想把它转成 IDE 里能阅读、能参考的工程通常要经历反编译这一步。完整版源码应该直接有 java 文件但很多交付物确实只给了 jar所以我专门说一下这部分的实操方法。我常用 CFR 工具做反编译命令很简单java -jar cfr-0.152.jar bb-platform.jar --outputdir ./bb-src如果是 Spring Boot fat jar里面会分成 BOOT-INF/classes 和 BOOT-INF/lib 两部分。上面的命令会把这些 class 都反编译出来但目录结构可能不是标准的 Maven 结构需要自己再把 BOOT-INF/classes 下面的包路径整理成 src/main/java配置文件扔到 src/main/resourceslib 里的依赖 jar 可以通过 Maven 坐标一个个补回 pom.xml。反编译不是万能的局部变量名、注释、开发时的原始目录结构基本都会丢失有些泛型和 Lambda 表达式还原出来也会比较难看。我的做法是反编译代码只用来做业务逻辑分析和接口还原真正要二次开发的话还是建议花点时间按照原结构重写一个干净工程把反编译来的类作为参考。资源文件比如 mapper XML 如果被打进了 BOOT-INF/classes反而能直接从 jar 里原样解压出来这是最容易恢复的部分。2.3 MySQL 安装与客户端选择MySQL 安装看起来简单实际上很多问题都在安装这一步埋下了。下载时优先去官网拿 MySQL Installer 或者对应系统的 tar 包别用搜索引擎里来路不明的“一键安装包”。安装时我建议字符集直接选 utf8mb4别用 utf8因为 utf8mb4 才是真正的完整 Unicode 支持企业表里存用户昵称、带 emoji 的备注甚至一些生僻字都不会变成问号。安装完顺手确认几个点端口是不是被占用服务能不能正常启动密码策略是否符合团队习惯。有些团队会配一个独立的账号给项目用而不是直接用 root这个习惯很好比如这样CREATE DATABASE bb_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER bb_userlocalhost IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON bb_platform.* TO bb_userlocalhost; FLUSH PRIVILEGES;客户端工具方面Navicat 确实好用但我还是提醒一句别去下载破解版。破解版被植入后门的事不是新鲜事数据库客户端掌握着你的连接信息和数据这是企业环境的大忌。用官方试用版或者直接用开源的 DBeaver日常开发和看表结构完全够用。3. 后端架构SpringBoot 与 MyBatis 的落地细节3.1 后端包结构与分层职责看一套源码质量高不高先看包结构。企业级项目的包结构通常是这样com.bb.platform ├── common // 通用返回体、异常、常量、工具类 ├── config // 全局配置比如 MyBatis、CORS、拦截器 ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务层事务注解基本都在这层 ├── mapper // MyBatis 接口 ├── entity // 数据库实体 ├── dto // 入参、出参对象 └── vo // 视图层返回对象我比较看重 service 层有没有把业务规则沉淀下来。很多初学者喜欢把业务写进 controller一个接口里查表、算金额、改状态全干完短期看没问题一旦同一条规则要被另一个接口复用就只能在别处再写一遍。完整版源码里更多是“Controller 薄、Service 厚、SQL 稳”的风格这样至少改起来不慌。dto 和 vo 分开也是我判断源码是否完整的重要指标。直接用 entity 往外返回容易被前端拿到不该看到的字段比如密码散列、内部状态码。企业级系统接口层应该做字段收敛用 vo 控制最终返回这也是一个被很多 demo 项目忽略的点。3.2 MyBatis 的初始化流程与 XMLConfigBuilder热词里很多人搜“mybatis xmlconfigbuilser 的工作流程”其实就是想搞懂 MyBatis 启动时到底干了什么。MyBatis 的初始化可以简单理解成“读配置、建对象、存容器”三个动作。流程大致是SqlSessionFactoryBuilder接收配置文件输入流创建XMLConfigBuilder。XMLConfigBuilder解析 mybatis-config.xml把settings、typeAliases、plugins、environments、mappers等节点逐个解析成 Java 对象。解析到mappers时根据实际配置去加载 Mapper XMLXMLMapperBuilder 会把每条 SQL 解析成MappedStatement保存到 Configuration 对象里。把 Mapper XML 的 namespace 和对应的 Mapper 接口做绑定注册到 MapperRegistry最终生成 MapperProxy 代理对象。所以Mapper 接口本身没有实现类Spring 注入的其实是 JDK 动态代理。你调用userMapper.selectById(1)的时候实际进入的是 MapperProxy 的拦截逻辑然后根据接口方法名找到配置里对应的 MappedStatement再通过 SqlSource 生成 SQL交给 JDBC 执行。理解这一步对排查“Invalid bound statement”这类问题特别有帮助因为大多数时候是 XML 没有加载进 Configuration或者 namespace 和接口全限定名对不上。3.3 用 TypeHandler 处理 JSON、枚举等特殊字段TypeHandler 是 MyBatis 里一个不起眼但是特别实用的扩展点。它的作用一句话就能说清当 Java 类型和数据库类型不一致时由它来做双向转换。企业级系统里最常见的场景是 JSON 字段。比如一张表里有个extra_info字段类型是 JSON业务上需要存一组标签。如果每次都在 service 里手动调 Jackson 转来转去很容易写散掉。正确做法是自定义一个 TypeHandlerMappedTypes(Object.class) public class JsonTypeHandler extends BaseTypeHandlerObject { private final ObjectMapper objectMapper new ObjectMapper(); Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, objectMapper.writeValueAsString(parameter)); } Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName) null ? null : objectMapper.readValue(rs.getString(columnName), Object.class); } }然后在 Mapper XML 或注解里指定resultMap idbaseResultMap typecom.bb.platform.entity.BizOrder result propertyextraInfo columnextra_info typeHandlercom.bb.platform.handler.JsonTypeHandler/ /resultMap这段代码看起来不长但很实用。用 TypeHandler 的好处是转换逻辑收敛在一处查询和写入都走同一个规则不会出现“写入是 JSON 字符串读出来变成 Map 后又无法序列化”的怪问题。3.4 MyBatis 缓存机制应该怎么用MyBatis 缓存是很多人面试时非常爱问、实际项目里又很少用好的点。它分两级缓存级别作用范围生命周期注意事项一级缓存SqlSession 级别同一个 SqlSession 内共享默认开启跨方法时通常失效二级缓存namespace 级别同一个 Mapper 命名空间下多个 SqlSession 共享默认关闭需要显式开启实体最好可序列化一级缓存之所以经常被讨论是因为它容易引起“查询结果不变”的错觉。你在同一个事务里两次查询同一条数据第二次可能走的是缓存但这个缓存中间不会感知其他事务提交的修改导致读到旧数据。解决方案是好习惯不依赖一级缓存做业务判断必要的时候清掉缓存。二级缓存真正适合的场景是不常变动的字典表、配置类数据。对于高频更新的业务表我建议直接放弃 MyBatis 自带二级缓存改用 Redis。因为 MyBatis 的二级缓存是进程内缓存单体状态下勉强能用一旦后面拆服务或者多节点部署就会出现每个节点缓存不一致的诡异问题。企业级系统里“缓存一致性”远比“缓存性能”重要这是我在实际项目里反复踩坑换来的经验。3.5 事务控制与分页查询的配置心得Spring Boot 集成 MyBatis 之后事务用Transactional就完事了。但我见过不少人只写注释不写参数遇到异常不回滚查半天发现默认只回滚 RuntimeException 和 Error受检异常不会回滚。我的习惯是统一写成Transactional(rollbackFor Exception.class) public void auditOrder(OrderAuditDTO dto) { orderMapper.updateStatus(dto.getId(), dto.getStatus()); auditLogMapper.insert(buildLog(dto)); }另一个冷门但很常见的坑是“事务自调用失效”。同一个类里 methodA 调用了本类的 methodBmethodB 上的 Transactional 是不生效的因为 Spring 事务基于代理代理对象才能触发事务逻辑方法内部直接调 this 是绕过了代理。碰到这种场景要么把 methodB 拆分到另一个 Service要么用AopContext.currentProxy()显式获取代理。分页查询我习惯用 PageHelper。配置上比较关键的是这几项dialect 指定数据库方言reasonable 设为 truesupportMethodsArguments 设为 true。使用时分页参数紧跟查询语句之后即可PageHelper.startPage(pageNum, pageSize); ListOrderVO list orderMapper.selectPage(dto); PageInfoOrderVO pageInfo new PageInfo(list);PageHelper 的原理是拦截 Executor自动改写 SQL。它好用但也要注意如果 startPage 之后跟随的并不是你预想的那条 SQL分页就会错乱比如中途调用了别的查询。所以分页参数和业务查询要贴着写中间不要夹其他 Mapper 操作。3.6 消息通知用 ActiveMQ 解耦异步任务企业级系统里登录日志、短信提醒、待办通知这类操作如果每次都同步执行接口响应会变慢。我看这套源码里预留了消息通知扩展点如果你后续想引入消息队列Spring Boot 整合 ActiveMQ 是比较轻的方案。引入依赖后主要配置就是 broker-url比如本地默认地址tcp://localhost:61616然后注入 JmsTemplate 发送消息用 JmsListener 监听队列。ActiveMQ 和 RabbitMQ、Kafka 相比不算先进但它胜在部署简单、文档多内部管理系统使用绰绰有余。用它把非核心链路异步化是比较平滑的演进方式。4. 前端实现Vue 路由、请求封装与播放器集成4.1 Vue 项目结构与路由权限设计Vue 项目拿到手先看 src 目录。常规划分是 api 目录放接口请求router 目录放路由store 放全局状态views 放页面组件components 放公共组件。这套源码里的权限设计一般是“后端返回菜单 前端动态注册路由”。开发环境里路由可以先写死生产环境则往往需要根据角色动态生成。核心写法大概是这样const menuList await getMenuList() const routes generateRoutes(menuList) routes.forEach(route router.addRoute(route))动态路由有个点要注意如果用户刷新页面动态注册的路由会丢掉所以在路由守卫里需要先判断 store 里有没有菜单没有就先拉菜单再放行。同时要处理 addRoute 重复添加的问题刷新后旧路由不会被自动覆盖最好加个 hasRoutes 之类的标识或者通过router.getRoutes()检查是否已存在再决定要不要 add。另一个常见问题是 404 页面。静态路由里通常最后会配一个path: *, component: 404但动态路由是在它之后注册的所以可能出现权限菜单能正常产生却没被 404 兜底逻辑覆盖的情况。碰到这种细节我会先检查静态路由和动态路由的注册顺序再确认 404 的匹配位置。4.2 Axios 封装和开发环境代理前后端分离项目里Axios 不封装直接到处用后期改一个公共超时时间都要全局搜索非常痛苦。我的标准做法是在 src/utils/request.js 里创建一个 axios 实例统一设置 baseURL、超时时间再通过请求拦截器把 token 加到请求头响应拦截器统一处理业务码错误和 HTTP 错误。const service axios.create({ baseURL: process.env.VUE_APP_BASE_API || /api, timeout: 10000 }) service.interceptors.request.use(config { const token store.getters.token if (token) config.headers.Authorization Bearer ${token} return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 0) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )开发环境联调最常遇到的就是跨域。Vite 里配置代理很方便不用改后端 CORS直接这样写server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这个“代理”是开发服务器转发的意思和系统层面的网络代理完全是两码事。用代理的好处是前端请求路径保持相对路径生产环境再让 Nginx 去转发联调和部署都不用在代码里写死后端地址。4.3 与 Electron 结合时的 Vue 角色热词里有人问 Electron 主进程、渲染进程和 Vue 到底什么关系。简单说Vue 负责渲染页面和交互Electron 负责把页面装进桌面壳并提供一些浏览器没有的能力比如访问本地文件、调用系统命令、和操作系统原生窗口交互。Vue 和 Electron 的主进程没有直接关系。Vue 运行在渲染进程里它要和主进程通信必须借助 Electron 提供的 IPC。现在比较推荐的做法是 preload 脚本里用 contextBridge 暴露安全 APIconst { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(electronAPI, { selectFolder: () ipcRenderer.invoke(select-folder) })渲染进程里也就是 Vue 组件中这样使用const folder await window.electronAPI.selectFolder()如果后续你想用 VSCode 开发 Vue 的移动端 App这个思路也能迁移。Vue 本身只负责 UI移动端能力要交给 Capacitor 或 uni-app 那类容器去承接。所以不要纠结 Vue 和 Electron 谁影响谁它们是“页面框架 宿主容器”的合作关系。4.4 M3U8 流媒体播放的免安装方案很多业务系统里要播放监控回放、培训视频M3U8 是一种非常常见的流媒体格式。所谓“免安装播放器”意思是不要用户装 Flash 或者其他浏览器插件直接用 H5 播放能力加 JS 库来处理。浏览器原生 video 标签支持 MP4但对 HLS 的支持并不统一尤其在桌面端需要依赖 hls.js。最简单的实现是这样import Hls from hls.js const video document.getElementById(player) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(https://example.com/live/stream.m3u8) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () video.play()) }实际项目里坑也不少一是 M3U8 返回时如果服务端 CORS 配置不对hls.js 会被浏览器拦截甚至报跨域二是自动播放策略要求视频必须静音或用户有过交互video.muted true可以先保证播放再让用户手动开声音三是某些播放器组件和 hls.js 的版本会冲突最好按官方文档推荐版本锁定依赖。视频在后台管理系统里看起来是小功能但一旦播放卡住或黑屏用户对整套系统的评价都会拉低所以我建议单独用一个播放器组件来封装别到处复制代码。5. 数据库设计MySQL 表结构、索引与连接问题5.1 核心表设计这套源码既然叫管理系统核心表离不开用户、角色、菜单、业务单据这些基础。用户角色权限是三张基础表加两张关联表sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu。业务表则看具体场景比如订单、合同、工单、审批记录。设计上我建议保留几个通用字段CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录用户名, password varchar(128) NOT NULL COMMENT 密码密文, real_name varchar(64) DEFAULT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, deleted tinyint NOT NULL DEFAULT 0 COMMENT 软删除标记, create_by bigint DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;我特别看重deleted和create_time这类字段。企业系统里物理删除风险太高误删后根本没有补救机会。软删除虽然多写一点条件但只要在 MyBatis XML 里统一加where deleted 0/where整体成本并不高。审计字段 create_by、update_time 则是排查问题的救命稻草别嫌它啰嗦。5.2 索引设计真正该注意什么索引不是越多越好这个话我说再多都不嫌多。很多源码里的表明明只有几万行数据却建了十几个单列索引写数据时索引维护成本远超查询收益。我的设计原则是从实际 SQL 出发先看 where、order by、join 条件再决定要不要加索引。比如一个订单查询前端筛选项是“状态 创建时间”那就可以建联合索引(status, create_time)。如果你再单独建status索引和create_time索引查询路径不一定能组合好反而可能出现索引合并或者走错索引。还有个很常见的误区在低基数列上建索引。比如status字段只有“已提交、审核中、已通过、已驳回”几个值单独给 status 建索引扫描行数可能还是一大堆优化器可能直接放弃索引走全表。这种场景不如和前端的查询条件一起设计联合索引或者用覆盖索引来减少回表。5.3 连接池参数与 SSL 报错处理企业级应用用 HikariCP 是常态Spring Boot 默认数据源就是它。常见配置我会写成这样spring: datasource: url: jdbc:mysql://localhost:3306/bb_platform?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: bb_user password: your_password hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000很多人在测试环境被useSSLfalse救了然后直接复制到生产环境。这不是好习惯生产环境应该用useSSLtrue并让连接校验服务器证书否则账号密码走明文链路存在风险。MySQL 8 驱动默认认证插件是 caching_sha2_password如果你的驱动版本太老会报 unable to load authentication plugin 之类的问题解决办法是升级 mysql-connector-j 到 8.x而不是回退密码插件回退是治标不治本。SSL 连接错误还有一个常见场景客户端报 “SSL connection error” 或 “Communications link failure”通常就是驱动版本、useSSL 参数、服务器端 SSL 配置这几类问题。排查时先看错误堆栈里有没有 SSL 关键词有的话优先检查 JDBC URL 参数。5.4 Windows 服务启动报 e0434352 怎么排查有同学说 MySQL 在 Windows 上服务启动失败错误码 e0434352。这个错误码不是 MySQL 自己报的业务错误而是 Windows 服务宿主进程的 .NET Runtime 错误。MySQL 服务包装层依赖 VC 运行库如果系统里的 Microsoft Visual C Redistributable 缺失或者损坏启动时就会出现这个错误。排查思路记住三件事打开 Windows 事件查看器找到对应时间的 .NET Runtime 错误记录看具体异常信息。确认 MySQL 安装目录下的 bin 路径有没有权限问题服务账户对数据目录是否可读可写。重新安装 VC 运行库最好把 2015-2022 合集版本装全再重启服务。这个问题经常被误判为 MySQL 配置文件或数据目录损坏其实很多时候只是运行库问题。安装完再去看事件日志错误如果没有继续出现服务基本就能正常起来。6. 部署上线打包、Nginx 与对象存储6.1 前端打包与 Nginx 配置前端部署本质上三句话构建产物、静态托管、转发后端接口。执行npm run build之后dist 目录就是可以直接交给 Nginx 的静态文件。部署时要注意 history 路由刷新 404 的问题如果用的是 Vue Router 的 history 模式Nginx 必须配置 try_files否则用户刷新一个子页面就直接白屏。server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html是必须的它让所有匹配不到文件的路径都回退到 index.html交给前端路由去解析。/api/的转发则把前端请求转发到后端 Spring Boot这样浏览器不会出现跨域。部署完如果发现前端页面能打开但接口 404多半是 proxy_pass 的路径写歪了比如反向代理时把 /api 前缀也带到了后端而后端接口前缀是另外一个路径。6.2 Spring Boot 打包和启动参数后端打包用 Maven 命令就够了mvn clean package -DskipTests打出来的 jar 在 target 目录。启动时不要裸执行 java -jar最好至少带上堆内存参数和运行环境java -Xms512m -Xmx1g -jar bb-platform.jar --spring.profiles.activeprod--spring.profiles.activeprod会触发 application-prod.yml 里的配置这样可以把生产数据库密码、日志级别等按照环境隔离。我不太喜欢把所有环境的连接串都写在同一个配置文件里因为太容易误操作多人复制时光改一处漏改另一处的事我见过太多次。顺带提一个提升体验的小技巧Spring Boot 启动时的横幅 banner 完全可以用在线 banner 生成器做一个项目名替换掉 resources/banner.txt。这个对功能没有实际影响纯属团队归属感但每次启动看到自己的项目名字确实比默认的 Spring 图标舒服一点。6.3 MinIO 集成到 Spring Boot 做统一文件存储企业系统里上传头像、附件、导入模板最忌讳把文件直接塞进 MySQL 的 blob 字段我建议统一走对象存储。MinIO 是市面上兼容 S3 协议的产品里部署成本最低的尤其适合内网私有化部署。Spring Boot 集成 MinIO 的核心是引入客户端dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency然后在配置类里创建 MinioClientBean public MinioClient minioClient(Value(${minio.endpoint}) String endpoint, Value(${minio.accessKey}) String accessKey, Value(${minio.secretKey}) String secretKey) { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); }上传的核心方法是client.putObject(...)但这里我想提醒的是桶权限问题。很多人为了省事把桶权限直接设为 public附件链接虽然是能访问了但内网里的敏感文档等于裸奔。更好的方案是桶设为 private业务系统通过 MinIO 生成预签名 URL短时间有效既能控制访问范围又不需要让文件完全私有导致前端无法预览。文件存储在企业系统里是低频但高敏感的功能别把省事放在安全前面。7. 常见问题排查速查表7.1 后端常见问题现象可能原因解决思路Invalid bound statement not foundMapper 接口方法和 XML 的 id 对不上或 XML 没被打进 classpath检查 namespace 和接口全限定名确认 target/classes 里有 mapper XMLMapper 接口注入为 nullMapperScan 没有覆盖到对应包在启动类确认 MapperScan 路径或每个接口加 Mapper事务没有回滚受检异常没有指定 rollbackFor或方法自调用绕过代理统一 rollbackForException.class拆分 Service 或用 AopContextSpring Boot 版本过高导致依赖冲突新版本切到 jakarta 命名空间老 starter 不兼容换回 Spring Boot 2.7.x 对应 starter 版本或全面升级依赖SQL 日志打不出来MyBatis 配置没开日志在 application.yml 里加 mybatis.configuration.log-impl 和 mapper 包日志级别 debug我在排查后端问题时第一步永远是看日志而不是猜。启动时有没有 mapper 映射警告请求时 SQL 有没有执行异常堆栈指向哪个类这些信息比任何经验都靠谱。如果日志没有输出 SQL先解决日志配置再继续往下查。7.2 前端常见问题现象可能原因解决思路刷新子页面 404Nginx 没有配置 history 回退location 里加 try_files $uri $uri/ /index.html请求跨域前端端口和后端端口不一致开发用 Vite/Nginx 代理生产用同域反向代理动态路由刷新丢失动态注册的路由没有持久化保存路由守卫里先请求菜单再 addRoute用 hasRoutes 防重复接口返回正常但页面没数据字段名大小写下划线不匹配后端开启驼峰映射 map-underscore-to-camel-casetrue前端确认字段名M3U8 黑屏无法播放CORS 跨域、自动播放策略、hls.js 版本问题检查服务端跨域头静音播放锁定播放器依赖版本前端问题里有一半都是“看得到现象找不到链路”造成的。我的做法是打开浏览器开发者工具先看 Network 面板请求到底有没有发出去返回值是什么再看 Console 报错大部分播放和路由问题都会直接提示原因。不要一上来改代码先还原现场。7.3 数据库与部署常见问题现象可能原因解决思路Too many connections连接池过大或连接泄漏调小 maximum-pool-size检查程序是否释放连接MySQL 8 认证插件报错驱动太旧不支持 caching_sha2_password升级 mysql-connector-j 到 8.x时区差 8 小时驱动和系统时区设置不一致JDBC URL 加 serverTimezoneAsia/Shanghai容器设置 TZ 环境变量Windows 服务启动报 e0434352VC 运行库或 .NET Runtime 异常重装 VC Redistributable查看事件日志Docker 里连不上 MySQL容器网络模式或防火墙问题用 host 模式或 docker network 连通确认端口映射部署类问题最怕的是“环境不一致”。本地好好的一到服务器就挂大概率是操作系统时间、字符集、网络、运行库这些隐性因素。排查时把问题拆成“代码层面还是环境层面”会清晰很多。8. 收尾一些源码阅读与二次开发建议这套完整版源码如果只是用来当毕业设计或者练手项目我建议你把重点放在“一个请求从 Vue 页面到 MySQL 再返回”的完整链路上先别急着加功能。把用户登录那个流程走通你会同时理解 Spring 拦截器、MyBatis 动态代理、Vue 路由守卫、Axios 拦截器、数据库索引这些概念是怎么串起来的。如果是拿来做企业项目二次开发我的建议是不要在原代码上大改。先把核心业务表结构吃透然后把你自己的业务模块按照原有分层写进去。保留原来的权限体系和通用返回结构替换业务逻辑这样既能借力现成底座又不会越改越混乱。所有数据库变更都写增量 SQL 脚本别直接改初始化脚本否则之后部署新环境会对不上。最后再分享一个我坚持很久的排查套路从前端 Network 发起请求开始看到后端 Controller 接收参数继续跟 Service 方法再看 MyBatis 是否打印 SQL最后确认 SQL 能否在数据库客户端里正常执行。沿着这条链路逐层打日志或者加断点绝大多数看起来玄学的问题最后都会落在字段名不一致、时区没配置、缓存脏数据、权限拦截这类小事情上。源码只是起点真正值钱的是你基于源码沉淀出的这一套调试和实践能力。