资讯动态

SpringBoot 3 + Vue 3 汉服文化宣传系统开发实战

发布时间:2026/10/9 3:18:20 来源:尧图企业网站定制
1. 项目定位与技术选型思路接手这个汉服文化宣传系统时我首先想清楚一件事它本质上不是一个普通的增删改查管理后台而是一个内容和文化的展示窗口。汉服文化这几年热度一直在涨但很多相关网站和系统要么偏电商、要么内容陈旧真正能把汉服的形制、历史、活动资讯这些内容系统化展现出来的项目并不多。用 SpringBoot 做后端接口、Vue 做前端展示刚好能把内容管理和文化展示这两件事都做好。标题里写2026最新这其实是个信号——如果你还在照着网上那些 2020 年左右的教程选型大概率会踩坑。比如 SpringBoot 2.x 的老教程还在用 Java 8 和javax命名空间但 2026 年的环境下SpringBoot 3.x 已经成为绝对主流JDK 17 是标配命名空间也换成了jakarta。Vue 这边也一样Vue CLI 官方已经不再维护现在初始化项目基本都是 Vite。所以这篇文章里我用的版本组合是组件版本选择说明JDK17SpringBoot 3.x 强制要求SpringBoot3.2.x / 3.5.x选稳定版别追最新小版本Vue3.x配合 Vite用script setup写法构建工具Vite 5/6代替 Webpack 老方案UI 组件库Element Plus后台管理页面开发效率高数据库MySQL 8.x内容类系统够用ORMMyBatis-Plus分页和 CRUD 效率高这个技术栈适合谁来参考我觉得有三类人一是准备做毕业设计的在校生这个项目既能体现业务设计能力又能展示前后端分离的完整流程二是想入门 SpringBootVue 全栈开发的初学者因为我下面会写很多实操细节和踩坑记录三是真的想做一个汉服文化内容平台的个人开发者可以拿这套代码作为基础版本去迭代。选 SpringBootVue 而不是其他的方案核心原因是生态成熟度。遇到任何问题搜索引擎上基本都能找到解决方案。尤其是 SpringBoot 3.x 虽然有一些配置和 2.x 不一样但资料已经足够多了Vue 3 的周边生态像 Vue Router、Pinia、Element Plus也都进入了稳定期。对于内容宣传类系统来说稳定压倒一切我们没有必要去尝试太新锐的方案。2. 后端工程搭建与数据访问2.1 单体项目还是多模块这个项目规模不大就是一个前后端分离的内容宣传系统所以我选择的是单体 Maven 工程按包结构分层。有人可能看到网上讲springboot modules多模块工程觉得很高级但对于这种体量的项目多模块反而增加了理解成本——你需要在父 POM 和子模块之间来回切换IDEA 的构建配置也更繁琐。我的后端目录结构如下hanfu-server/ ├── pom.xml ├── src/main/java/com/hanfu/ │ ├── HanfuApplication.java │ ├── controller/ │ │ ├── ArticleController.java │ │ ├── CostumeController.java │ │ ├── ActivityController.java │ │ └── VideoController.java │ ├── service/ │ │ ├── ArticleService.java │ │ └── impl/ │ ├── mapper/ │ ├── entity/ │ ├── config/ │ │ ├── MybatisPlusConfig.java │ │ ├── WebMvcConfig.java │ │ └── InterceptorConfig.java │ └── common/ │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java └── src/main/resources/ ├── application.yml └── mapper/每个包只干一件事controller只做参数接收和结果返回service放业务逻辑mapper放数据库操作方法entity放表对应的实体类。common包放统一返回结构和全局异常处理这个是前后端对接的关键——如果每个接口返回的数据格式都不一样前端 Axios 拦截器就没法统一处理错误了。2.2 SpringBoot 版本选型与依赖配置我建项目用的是 Spring Initializrstart.spring.io直接选 SpringBoot 3.2.5。为什么不选 3.5因为 3.x 的版本迭代非常快对于教学和稳定项目来说选择一个已经发布一段时间、社区反馈充分的版本更稳妥。SpringBoot 3.5 虽然功能更新但没必要为了新而新。pom.xml 核心依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies !-- Web 支持包含内置 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 针对 SpringBoot3 的 starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok 简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这里有个非常关键的坑MyBatis-Plus 针对 SpringBoot 3 的 starter 包名是mybatis-plus-spring-boot3-starter不是老的mybatis-plus-boot-starter。如果沿用老版本照抄启动时直接会报找不到SQLSessionFactory相关的错误。很多人被SpringBoot 版本太高的问题卡住十有八九就是栽在这种依赖坐标不一致上。2.3 数据库表设计与实体映射汉服文化宣传系统的核心内容可以分为四大块文化百科、服饰图鉴、活动资讯、视频资料。围绕这几个业务模块我设计的数据表如下t_article文化百科表CREATE TABLE t_article ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL COMMENT 文章标题, category VARCHAR(50) COMMENT 分类如形制/历史/礼仪, content TEXT COMMENT 正文内容, cover_image VARCHAR(500) COMMENT 封面图, source VARCHAR(100) COMMENT 来源, view_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME, update_time DATETIME );t_costume服饰图鉴表CREATE TABLE t_costume ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) COMMENT 服饰名称, dynasty VARCHAR(50) COMMENT 所属朝代, style VARCHAR(50) COMMENT 形制如直裾、曲裾、齐胸襦裙, image_url VARCHAR(500) COMMENT 展示图片, detail TEXT COMMENT 详细介绍, is_hot TINYINT DEFAULT 0 COMMENT 是否热门推荐 );t_activity活动资讯表和t_video视频表结构类似后者多一个video_url字段我建议存储 m3u8 格式的播放地址后面会专门讲为什么。实体类用 Lombok 简化以Costume为例Data TableName(t_costume) public class Costume { TableId(type IdType.AUTO) private Integer id; private String name; private String dynasty; private String style; private String imageUrl; private String detail; private Integer isHot; }注意TableName注解如果实体类名和表名不一致比如Costume对应t_costume必须写上否则 MyBatis-Plus 会默认去查costume这张表直接报Table doesnt exist。这类错误很低级但很常见排查起来也不难看控制台 SQL 日志就知道了。2.4 数据访问层与分页实现数据访问层我选 MyBatis-Plus 而不是 Spring Data JPA原因很简单分页方便、CRUD 不需要写 SQL、中文资料多。汉服文化系统的列表页和搜索结果页都需要分页MyBatis-Plus 自带分页插件自己写手写 SQL 的话还得维护 count 查询效率低很多。application.yml里配置数据源和 MyBatis-Plusspring: datasource: url: jdbc:mysql://localhost:3306/hanfu_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto分页插件配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }有了这个配置分页查询就变得非常简洁。以服饰图鉴接口为例Service public class CostumeServiceImpl implements CostumeService { Autowired private CostumeMapper costumeMapper; Override public PageCostume getCostumePage(int pageNum, int pageSize, String dynasty) { LambdaQueryWrapperCostume wrapper new LambdaQueryWrapper(); // 支持按朝代筛选 if (StringUtils.hasText(dynasty)) { wrapper.eq(Costume::getDynasty, dynasty); } wrapper.orderByDesc(Costume::getIsHot).orderByAsc(Costume::getId); return costumeMapper.selectPage(new Page(pageNum, pageSize), wrapper); } }LambdaQueryWrapper的好处是写实体字段名不会打错——它用的是方法引用而不是字符串编译期就能发现问题。如果条件为空就查全部有筛选条件就动态拼接 SQL。整个链路的逻辑非常直观前端只需要传pageNum、pageSize和筛选参数即可。3. 前端工程搭建与页面交互3.1 Vue 环境准备与 Vite 初始化前端做完环境和工程配置再开始写页面。先说环境配置这块看似简单实际坑最多——很多人卡在vue安装及环境配置上不是代码写错了而是 Node 和 npm 版本不匹配。Vite 5 要求 Node.js 18 才能跑Vite 6 则要求 20建议直接用最新的 LTS 版本20.x 或 22.x。我本地用的是 Node 20实测运行速度和依赖安装都正常。安装 Node 的时候注意Windows 用户尽量去官网下载 LTS 安装包不要用某些包管理器里的旧版本否则启动 Vite 时直接给你报错。初始化项目npm create vitelatest hanfu-web -- --template vue cd hanfu-web npm install这里有两个容易踩的坑。第一npm create vite如果提示 EEXIST 或者权限问题多半是已有同名目录或者 npm 缓存问题换个目录名或者执行npm cache clean --force再试。第二国内网络环境下直接npm install很容易超时我建议先配置淘宝镜像npm config set registry https://registry.npmmirror.com配置完镜像我实测安装 Element Plus、Vue Router 这些依赖速度能从几分钟降到十几秒。然后依次安装项目需要的核心库npm install vue-router4 pinia axios element-plus3.2 路由设计与访问控制前端工程目录我按模块划分方便后面维护hanfu-web/ ├── src/ │ ├── api/ # 接口请求封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面视图 │ │ ├── home/ # 门户首页 │ │ ├── article/ # 文化百科 │ │ ├── costume/ # 服饰图鉴 │ │ ├── activity/ # 活动资讯 │ │ ├── video/ # 视频专区 │ │ └── admin/ # 管理后台 │ ├── App.vue │ └── main.jsrouter/index.js是核心配置import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(../views/home/index.vue) }, { path: /article, name: ArticleList, component: () import(../views/article/list.vue) }, { path: /article/:id, name: ArticleDetail, component: () import(../views/article/detail.vue) }, { path: /costume, name: CostumeList, component: () import(../views/costume/list.vue) }, { path: /costume/:id, name: CostumeDetail, component: () import(../views/costume/detail.vue) }, { path: /admin/login, name: AdminLogin, component: () import(../views/admin/login.vue) }, { path: /admin, name: AdminHome, component: () import(../views/admin/layout.vue), meta: { requiresAuth: true }, children: [ { path: article/edit, component: () import(../views/admin/ArticleEdit.vue) }, { path: costume/edit, component: () import(../views/admin/CostumeEdit.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 登录守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(adminToken) if (to.meta.requiresAuth !token) { next(/admin/login) } else { next() } }) export default router这里我用了路由懒加载() import(...)按需加载页面组件首页打开速度会快不少。参数传递方面详情页通过this.$route.params.idComposition API 里是useRoute().params.id接收非常直观。有个细节值得说管理端页面我单独嵌套了一个layout.vue作为父路由左侧菜单、顶部栏、内容区用子路由填充。这样做的好处是管理后台的公共布局只写一次所有子页面只需要关注自己的内容区代码结构清晰很多。3.3 公共组件封装与插槽应用前端开发中最容易忽略的就是组件复用。汉服文化门户首页有大量的卡片展示服饰图鉴卡片、活动资讯卡片、百科文章卡片如果每个页面都写一遍卡片布局后期改样式就要改三四处。我的做法是封装一个通用的ContentCard.vue充分利用 Vue 插槽template div classcontent-card clickhandleClick div classcard-image img :srcimage :alttitle / !-- 默认插槽用于放置角标、收藏按钮等覆盖元素 -- slot nameoverlay/slot /div div classcard-body h3 classcard-title{{ title }}/h3 p classcard-desc{{ description }}/p !-- 默认插槽子组件可以往底部塞额外内容 -- slot/slot /div /div /template script setup import { useRouter } from vue-router const props defineProps({ image: String, title: String, description: String, link: String }) const router useRouter() const handleClick () { if (props.link) { router.push(props.link) } } /script使用的时候服饰图鉴页面这样调用ContentCard image/images/qixiong.jpg title齐胸襦裙 description唐代女性主流服饰高腰系带裙摆及地。 link/costume/12 template #overlay span classhot-tag热门/span /template div classcard-footer唐代 · 襦裙/div /ContentCard为什么用插槽而不是直接传所有参数因为插槽让组件有了留白的能力。默认情况下卡片是统一的但需要扩展时比如加个朝代标签、热门角标、底部操作按钮子页面可以通过插槽往里面塞自己的内容而不用去改公共组件的代码。这就是 Vue 组件化的核心思想——把变的部分交给插槽把不变的部分封装成组件。3.4 Axios 封装与接口对接前端调用后端接口我统一封装了 Axios 实例。不做事前的统一封装每个页面自己去写请求会产生大量重复代码而且后端返回错误时前端处理逻辑会乱掉。api/request.jsimport axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带 token request.interceptors.request.use(config { const token localStorage.getItem(adminToken) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理业务码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(adminToken) router.push(/admin/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request后端统一返回结构很重要。我定义的ResultT是这样的 JSON 格式{ code: 200, message: success, data: ... }。所有接口都返回这个结构前端拦截器统一处理code不等于 200 的情况业务页面拿到的直接就是data部分不用每个请求都自己判断一遍。4. 核心业务模块设计与实现4.1 汉服文化百科模块文化百科是汉服宣系统的内容底座类似一个小型 CMS。用户进入门户首页能看到文章分类导航形制科普、历史故事、礼仪文化、妆容配饰。点进分类后走的是列表分页接口。后端的列表接口我这样设计RestController RequestMapping(/api/article) public class ArticleController { Autowired private ArticleService articleService; GetMapping(/page) public ResultPageArticle getArticlePage( RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String category, RequestParam(required false) String keyword) { PageArticle page articleService.getArticlePage(pageNum, pageSize, category, keyword); return Result.success(page); } GetMapping(/{id}) public ResultArticle getArticleById(PathVariable Integer id) { // 查询详情同时浏览量 1 return Result.success(articleService.getAndIncrementViewCount(id)); } }搜索功能对于内容类系统必不可少。我直接用LambdaQueryWrapper的 like 条件没有上 Elasticsearch因为数据量还没有大到需要全文检索引擎的程度if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Article::getTitle, keyword) .or().like(Article::getContent, keyword)); }这里的逻辑是关键词匹配标题或者匹配正文只要命中其中一个就返回。对于汉服这种内容垂直领域几千几万条数据的搜索响应时间用 MySQL 的 like 完全够用——毕竟我们不是做全网搜索引擎不需要为想象中的高并发做过度设计。页面上用户点击文章卡片后跳转到详情页根据/article/:id路由获取数据。我还会展示浏览量浏览量字段的递增逻辑放在后端做前端只负责展示避免用户恶意刷新页面刷数据。4.2 服饰图鉴模块与图片上传服饰图鉴是汉服文化系统里最有视觉吸引力的模块。我按朝代和形制两个维度组织数据朝代筛选汉、晋、唐、宋、明形制标签直裾、曲裾、齐胸襦裙、圆领袍、马面裙等。用户进入图鉴页可以用筛选条件组合查看比如只看唐朝的齐胸襦裙。管理端需要支持图片上传。最简单的方案是后端接收 MultipartFile保存到服务器本地目录然后返回访问路径PostMapping(/upload) public ResultString uploadImage(RequestParam(file) MultipartFile file) { // 校验文件类型和后缀 if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); // 限制图片格式 SetString allowedSuffix Set.of(.jpg, .jpeg, .png, .webp, .gif); if (!allowedSuffix.contains(suffix.toLowerCase())) { return Result.error(不支持的图片格式); } // 生成文件名避免中文和特殊字符问题 String newFileName UUID.randomUUID().toString().replace(-, ) suffix; // 按日期分目录存储 String dateDir new SimpleDateFormat(yyyyMMdd).format(new Date()); String dirPath uploadRoot / dateDir; File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newFileName)); String url /uploads/ dateDir / newFileName; return Result.success(url); }注意几个关键点文件名一定要用 UUID 重命名原因很简单——用户上传的图片名称可能包含中文、空格、特殊字符直接作为 URL 访问会产生编码问题按日期分目录存储既方便管理也能避免单目录文件过多影响访问性能。前端上传时我用 Element Plus 的el-upload组件:action指向/api/uploadon-success回调里拿到返回的图片地址再和其他表单字段一起提交。4.3 活动资讯与视频播放模块m3u8 处理汉服文化活动经常有线下集会、走秀、讲座的录像视频内容是宣传的重要组成部分。这里就要讲一下 m3u8 这个格式了。m3u8 本质是一个播放列表文件文本文件里面记录了一串 .ts 视频分片文件的地址。为什么活动录像要切分片因为一场汉服走秀可能一两个小时如果整段作为单一 mp4 文件用户网络稍有波动就得从头缓冲体验很差。切片之后播放器可以逐段下载播放网络好就流畅播放网络差也不会整个视频卡死。浏览器原生虽然支持 mp4但原生播放器不支持 m3u8 协议。所以前端需要引入 hls.js 或者 video.js 的 HLS 插件。我用的是hls.js轻量、无依赖配合原生 video 标签非常方便。template div classvideo-player video refvideoRef controls classvideo-element/video /div /template script setup import { ref, onMounted, onBeforeUnmount } from vue import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) const videoRef ref(null) let hls null onMounted(() { const video videoRef.value // 判断浏览器是否原生支持 HLS比如 Safari if (video.canPlayType(application/vnd.apple.mpegurl)) { // 原生支持直接赋值 video.src props.src } else if (Hls.isSupported()) { // 主流浏览器用 hls.js 加载 hls new Hls() hls.loadSource(props.src) hls.attachMedia(video) // 监听错误便于排查 hls.on(Hls.Events.ERROR, (event, data) { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: hls.startLoad() break case Hls.ErrorTypes.MEDIA_ERROR: hls.recoverMediaError() break default: hls.destroy() break } } }) } else { console.error(当前浏览器不支持 HLS 播放) } }) onBeforeUnmount(() { if (hls) { hls.destroy() } }) /script这段代码做了三层兼容iOS Safari 原生支持 m3u8直接video.src赋值即可其他浏览器如果支持hls.js就按loadSourceattachMedia方式播放如果都不支持打出错误日志。实际部署中我发现一个问题跨域会导致 HLS 请求被浏览器拦截。如果后端接口和前端页面不在同一个域需要在后端配置 CORS 允许跨域访问。这个我在第 5 节的部署部分会详细讲。视频资源的来源管理端可以录入 m3u8 的 URL 地址。比如活动主办方把录制视频切片后托管到 CDN系统只需要记录播放地址即可。视频封面图单独存储列表页先用封面撑场面点击播放时才加载真正的视频流这样首页加载速度不会被大视频拖垮。4.4 管理端权限与内容发布管理端不是开放给所有人的得有一个简单的登录认证。汉服文化系统这种规模我不会引入 Spring Security 加 JWT 的重型方案否则光配置 Security 的过滤链就得花掉大量时间。我用的是拦截器 Token的轻量方案管理员输入账号密码后端校验成功后生成一个 UUID Token存到 Redis 并设置有效期如果没接 Redis存内存 Map 也行但重启会失效。前端登录后把 Token 存 localStorage每次请求通过拦截器放到 Header。后端写一个AuthInterceptor拦截所有/api/admin/**请求校验 Header 里的 Token不合法直接返回 401。拦截器注册Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/admin/**) .excludePathPatterns(/api/admin/login); } }这样设计的好处是门户展示接口/api/article/**、/api/costume/**完全开放任何人可看管理接口新增、修改、删除需要登录认证。前后端分离模式下这是一个性价比很高的权限控制方案。5. 打包部署与上线细节5.1 开发联调与跨域配置开发阶段最烦人的问题就是跨域。前端页面跑在http://localhost:5173后端接口跑在http://localhost:8080浏览器的同源策略会直接挡住请求。我建议开发阶段用 Vite 的代理转发而不是在后端开全局 CORS。因为开 CORS 意味着所有来源都能访问后端接口对安全性不友好。Vite 配置如下// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })Axios 的 baseURL 写成/api开发时代理到 8080部署时再由 Nginx 转给后端服务。这样前后端的接口路径始终保持一致切换环境只需要改代理配置不用改任何代码。如果确实需要后端支持跨域比如手机端或者第三方要调用可以用 CorsFilter 配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }但注意addAllowedOriginPattern(*)和setAllowCredentials(true)一起用时不能用addAllowedOrigin(*)旧写法否则浏览器会报错。这也是排查跨域问题时很多人忽略的细节。5.2 前端构建与 Nginx 配置开发完成后前端构建npm run build构建产物在dist/目录下。部署时把它上传到服务器用 Nginx 托管静态文件。Nginx 配置有几个关键点server { listen 80; server_name your-domain.com; # 前端静态资源 root /opt/hanfu-web/dist; index index.html; # 关键的 location解决 Vue Router history 模式刷新 404 location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 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; } # 上传文件访问 location /uploads/ { alias /opt/hanfu-uploads/; } }try_files $uri $uri/ /index.html;这行是必须写的。如果漏掉用户访问/article/3这种二级路由时Nginx 找不到对应的物理文件会返回 404。加上这行配置后所有路径都回退到前端入口由 Vue Router 自己解析路由刷新页面就不会出问题了。5.3 Docker Compose 与宝塔部署服务器上部署用 Docker 更省事。我写了一个 docker-compose.yml把 MySQL、后端程序、前端 Nginx 都编排进去version: 3 services: mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: hanfu_db volumes: - ./mysql-data:/var/lib/mysql ports: - 3306:3306 server: build: context: ./hanfu-server depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/hanfu_db?useUnicodetruecharacterEncodingutf8 ports: - 8080:8080 web: build: context: ./hanfu-web depends_on: - server ports: - 80:80后端 DockerfileFROM maven:3.9-eclipse-temurin-17 AS builder COPY . /build WORKDIR /build RUN mvn package -DskipTests FROM eclipse-temurin:17-jre COPY --frombuilder /build/target/hanfu-server.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]不使用宝塔的话直接在面板里装 Docker然后上传文件执行docker-compose up -d就能完成部署。用宝塔部署也可以流程是宝塔安装 Nginx 和 Java 环境前端 dist 目录配置到网站根目录后端 jar 包用 Supervisord 托管启动。两种方式我都实际跑过个人更推荐 Docker因为容器隔离了环境差异换服务器迁移成本低——我直接把 compose 文件拷到别的机器docker-compose up命令一执行环境就全部拉起来了。6. 常见问题排查实录6.1 SpringBoot 启动失败与应用配置问题这是我遇到最多的类型。SpringBoot 版本太高导致的配置失效典型表现有三种第一javax.servlet包找不到。这个问题原因非常明确SpringBoot 3.x 把javax命名空间换成了jakarta如果你还是从百度复制的老博客代码源码里写import javax.servlet.*编译直接报错。解决办法是把所有javax.servlet改成jakarta.servlet。这个报错信息通常非常显眼IDE 会自动红标基本不会漏。第二MyBatis-Plus 依赖版本不匹配。如果你用的 SpringBoot 3.x却引入了mybatis-plus-boot-starter而不是mybatis-plus-spring-boot3-starter启动时会报Failed to configure a DataSource或者 Mapper 扫描不到。解决办法就是换依赖坐标版本选 3.5.5 及以上。第三allowPublicKeyRetrieval连接 MySQL 时报错。这个跟 SpringBoot 版本没关系和 MySQL 8 的认证方式有关。如果你的数据库 URL 没加参数连接时可能会报Public Key Retrieval is not allowed。在 JDBC URL 后面加上allowPublicKeyRetrievaltrueuseSSLfalse即可。排查这类问题的通用思路先看完整的堆栈错误信息看到底是哪一行加载失败再去 Maven 仓库检查依赖的版本不要盲目升级 SpringBoot 小版本。6.2 Vue 安装与依赖问题前端安装阶段最常见的报错npm install太慢或者直接卡死换镜像源npm config set registry https://registry.npmmirror.com后重新执行。Vite启动时提示 Node 版本过低这个版本要求很明确Vite 5 需要 Node 18Vite 6 需要 Node 20。解决方式是去 Node 官网下载新版本跨大版本升级后最好删掉node_modules和package-lock.json重新安装。failed to load tsconfig vue/tsconfig/tsconfig.web.json这个问题出现在使用 TypeScript 模板创建项目时本地缺少vue/tsconfig依赖或者版本对不上。解决办法是npm install -D vue/tsconfig如果还不行删除node_modules和package-lock.json后重新npm install。依赖装好之后还有一类问题是编译时报Module not found。比如你npm install element-plus了但在某个组件里import ElementPlus from element-plus找不到基本可以确定是依赖没装全或者 IDE 缓存问题。先确认node_modules里有没有对应的包再用npm install修复。6.3 前后端联调与样式问题联调阶段的问题主要集中在三个方面跨域报错浏览器控制台出现Access-Control-Allow-Origin字样就是跨域问题。开发阶段确认 Vite 代理配置是否正确部署阶段确认 Nginx 是否有对应的location /api配置。接口返回但页面没数据这种时候不要急着改前端先用浏览器开发者工具看 Network 面板的实际响应。如果响应体是{code: 500, message: ..., data: null}那就是后端异常去后端控制台看完整堆栈。如果接口返回 404大概率是路径不对检查后端RequestMapping和前端请求路径是否一致。我遇到过最无语的问题前端写的是/api/article/list后端接口路径是/api/article/page两边没对齐页面就一直空着。样式冲突Vue 组件默认是全局样式如果不同组件里定义了同名 CSS 类后加载的会覆盖先加载的。解决办法是给style标签加scoped属性让样式只作用于当前组件。如果需要对子组件内部样式做穿透用:deep()选择器。这是 Vue 入门必经的一课我早期就被 Element Plus 的弹窗样式和自定义样式冲突困扰过后来统一给业务组件加scoped问题基本绝迹。6.4 视频播放黑屏或卡顿m3u8 播放还是踩了不少坑我总结三类跨域导致播放器加载失败HLS 加载 .ts 分片是走 HTTP 请求的如果 CDN 没配置 CORS浏览器会拦截。检查 CDN 响应头是否包含Access-Control-Allow-Origin: *。控制台报Hls.ErrorTypes.MEDIA_ERROR但页面没处理这个通常是因为视频分片文件损坏或格式不规范。建议先找一个可靠的测试地址比如公开示例 m3u8验证代码本身没问题再排查视频源。播放器延迟启动首帧加载慢一般是因为 m3u8 切片太长。切片时长建议控制在 4~6 秒太长会拖慢首帧速度太短又会影响播放器拉流的效率。6.5 部署后刷新页面 404这个前面已经说过部署后访问首页正常但部署后访问/article/3或/admin这类二级路径按 F5 刷新就 404。这就是 Nginx 的try_files配置缺失导致的。补充如下location / { try_files $uri $uri/ /index.html; }配置好之后别忘执行nginx -t检查语法然后nginx -s reload重新加载。添加完这个配置再刷新任何路由都不会出现 404 了。7. 一些实操心得与扩展思路这个系统从规划到上线我做下来最大的体会是项目规模不在于代码量大而在于链路完整。一个 SpringBoot Vue 的汉服文化宣传系统麻雀虽小但它覆盖了一个完整产品从需求、设计、编码、联调、部署到运维的全部环节。做完这一个项目前后端分离开发的核心流程基本就摸透了。另外我从这个项目里学到一个挺重要的经验版本选择决定项目体验。如果你一开始就选 SpringBoot 2.x 搭配旧版 Vue CLI做完确实轻车熟路但过两年再回头看升级成本就变成一笔纯消耗。选 SpringBoot 3.x 和 Vue 3虽然起步会多踩几个坑但文档和生态都已经成熟踩坑的过程恰恰是理解框架机制的过程——比如报错让你改javax为jakarta你才知道 SpringBoot 3 做了命名空间的迁移。后续如果想在这个系统上做扩展我建议可以从这几个方向入手第一增加 3D 服饰展示。汉服图鉴现在是静态图片如果加入 Three.js 或模型展示用户可以直接 360 度旋转查看服饰细节这对文化宣传的感官冲击力是质的提升。第二增加原型尺码比对。汉服的朝代、形制很多用户容易混淆可以在详情页加入形制对比功能把不同朝代同类型服饰的图片、文字、特点放在同一张表里对比展示交互体验会好很多。第三做活动报名和日历。现在活动资讯只是展示后续可以扩展成用户可以在线报名参加线下汉服活动后台统计报名人数这就能让系统从内容宣传走向社区运营。这些都是我在做完基础版本之后开始想的扩展。技术的终点不是把功能堆出来而是通过功能把用户真正想看的文化内容顺畅地展示出来。汉服文化本身内容足够丰富系统稳定跑起来之后内容运营才是长期工作的重心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑