☰
SSM + Vue 实战:从零构建汉服文化平台与踩坑指南
2026/10/8 12:15:09 网站建设 项目流程

简介:基于SSM+Vue的汉服文化平台网站是一套可直接用于毕业设计的完整Java项目,面向计算机相关专业正在做毕设的学生及需要项目实战练习的Java学习者,也可作为课程设计或期末大作业。项目经严格调试,确保可正常运行。压缩包共867个文件,约21.74MB,包含Java源码、Vue组件、JavaScript脚本、HTML页面、数据库SQL脚本及开发文档等,类型覆盖前端、后端、配置与文档,结构清晰。内容涵盖管理员、用户及前台首页三大模块,实现汉服知识管理、服装展示、用户相册、论坛交流、购物车、订单管理等完整功能。另有演示视频、部署视频、代码讲解视频及全套开发软件,并附有环境说明(JDK1.8、Tomcat7、MySQL5.7),方便快速启动项目。已有138人学习,适合需要完整可运行毕设方案和实战练习的读者。

1. 从「毕设选题」到「可上线的小型电商站」:SSM + Vue 的汉服平台到底能做到什么程度

一个汉服文化平台网站,表面上是商品展示加下单,实际上一旦开始动手,你会碰到的是一整套 Web 工程问题:用户注册登录、商品分类与检索、购物车与订单状态机、后台管理、图片上传与回显、前后端联调与跨域、部署上线。市面上很多脚手架要么只有 Spring Boot 单体,要么只有 Vue 管理端,真正把 SSM(Spring + SpringMVC + MyBatis)和 Vue 组合起来、又能跑通完整业务闭环的参考项目其实不多。这篇文章用一套「基于 SSM + Vue 的汉服文化平台」的典型设计,把从建表到联调再到部署的关键步骤拆开讲,重点放在那些新手会卡住、熟手也容易忽略的边界细节上。适合正在做 Java Web 毕设的在校生,也适合刚接手 SSR 老项目、想快速理解前后端分离架构的初级工程师——你可以照着这套思路复现一个最小可用版本,再按自己的业务去扩展。

2. 为什么用 SSM + Vue,而不是 Spring Boot + 模板引擎:选型逻辑与整体架构

2.1 SSM 三件套各自管什么:Spring、SpringMVC、MyBatis 的分工边界

先明确一个容易被考到也容易被搞混的点:SSM 不是一个框架,而是三个框架的组合。Spring 负责对象管理和依赖注入,SpringMVC 负责 HTTP 请求到方法的映射,MyBatis 负责 SQL 与 Java 对象之间的转换。一句话总结:Spring 管装配,SpringMVC 管路由,MyBatis 管数据。

汉服平台的典型场景是:前端 Vue 发一个POST /api/user/login请求,Tomcat 收到后交给 DispatcherServlet,DispatcherServlet 通过 HandlerMapping 找到 LoginController 里的对应方法,方法里调用 UserService,UserService 再通过 UserMapper 接口找到 MyBatis 生成的代理对象,最终执行 XML 里写好的 SQL,把结果逐层返回。这一套链路里,任何一个环节的配置断了,前端只会看到 404 或 500,而不会告诉你到底是哪一层出了问题——这也是很多新手第一次联调时最沮丧的地方。

我在实际排查这类项目时,首先会确认web.xml里的 DispatcherServlet 配置和spring-mvc.xml的组件扫描路径是否一致。比如 controller 包是com.hanfu.controller,但扫描路径写成了com.hanfu.service,那所有接口都会直接 404,而且后端日志没有任何异常。这种问题不是代码逻辑错误,而是配置漂移。

2.2 Vue 2 还是 Vue 3:给一个不纠结的答案

很多拿到这类项目的人会问:Vue 2 还是 Vue 3?答案取决于你的参考项目本身。如果项目里用的是 Vue 2 + Element UI + vue-router 3 + Vuex 3,你在没有充分理由的情况下升级到 Vue 3 是给自己挖坑——Vue 3 的 Composition API、Element Plus、vue-router 4 在生态上已经有很大变化,升级过程中你会花大量时间处理「看着差不多但就是不工作」的细节。

但如果你是从零开始搭,我建议 Vue 3 + Vite + Pinia。原因很实际:Vue 2 已经停止维护,新装的依赖经常和 Node 版本冲突;Vue 3 的setup语法让组件逻辑更集中,对一个商品列表加购物车的页面来说,代码量反而更少。不过要注意,如果你在 IDEA 里打开的是一个 Vue 2 老项目,npm install时报错别急着升级版本,先看 Node 版本是不是太高了,Vue 2 项目配合 Node 16 通常比 Node 18 更省心。

2.3 分层架构:为什么 controller 不能直接写 SQL

汉服平台这种规模的业务,最忌讳的就是 controller 里揉进业务逻辑和 SQL。我会严格按照 controller → service → mapper → entity 四层来组织后端代码。controller 只接收参数、调用 service、返回统一结果;service 写业务规则,比如下单时检查库存、计算总价;mapper 只写 SQL 和参数映射;entity 对应数据库表结构。

这样做的好处在排错时特别明显。比如订单金额算错了,如果你把价格计算写在 controller 里,那前端传什么参数就按什么算,没有任何防御;如果写在 service 里,你就可以在 service 层加日志、加单元测试,不用动 controller 和 SQL 就能定位问题。另一个好处是复用——后台管理端和前台商城可能都需要查订单列表,两个 controller 共用一个 service 方法,参数不同而已。

依赖方向必须单向:controller 依赖 service,service 依赖 mapper,谁都不能反向。如果你发现某个 mapper 方法里又调了 service,说明设计已经乱了,趁早重构。

3. 从数据库设计到第一个能跑的登录接口:核心表与 MyBatis 实战

3.1 汉服平台的表结构设计:用户、商品、订单、收藏四张表就够了

一套能支撑「静态展示 + 购物下单 + 后台管理」的汉服平台,最少需要这几张表:用户表user、汉服商品表costume、订单表order、订单明细表order_item、收藏表favorite,再加一个分类表category。不要一开始就设计十几张表,业务没到那一步,表越多联调越痛苦。

用户表的设计有个容易踩的坑:密码字段。明文存储是绝对不能接受的,我用的是 BCrypt 加密,Spring Security 里自带BCryptPasswordEncoder,不引 Security 的话可以用 jBCrypt 的独立库。加密的意义在于数据库泄露时,攻击者拿到的是一串随机散列而不是用户的原始密码。对于毕设项目,这是评委一定会问的点。

商品表需要注意的字段是「上下架状态」和「库存」。状态字段用TINYINT,0 表示下架、1 表示上架,比用字符串有效率得多。库存字段不能只是展示用,下单时要扣减,所以它必须参与事务。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL, `nickname` VARCHAR(50) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `role` TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `costume` ( `id` INT NOT NULL AUTO_INCREMENT, `category_id` INT DEFAULT NULL, `name` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `image` VARCHAR(255) DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '0-下架 1-上架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这两张表的细节提醒:所有字符串字段用utf8mb4而不是utf8,否则用户填的生僻字或 Emoji 表情会报Incorrect string value错误。密码字段长度预留 100 是因为 BCrypt 加密后的字符串长度是 60,但保险起见给足余量。UNIQUE KEY加在用户名上是必须的,否则注册接口会重复插入数据。

3.2 用 MyBatis 写动态 SQL:商品列表和条件筛选

商城的商品列表页不可能只有一个SELECT * FROM costume,用户会按分类筛选、按价格排序、搜关键字。这些条件组合在 MyBatis 里用<where>标签加<if>判断来实现,避免手动拼接 SQL 时漏掉WHERE关键字或者多出一个AND。

<select id="listByCondition" resultType="com.hanfu.entity.Costume"> SELECT id, category_id, name, description, price, stock, image, status FROM costume <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> <if test="orderBy != null and orderBy != ''"> ORDER BY ${orderBy} </if> LIMIT #{offset}, #{pageSize} </select>

这里有两个关键点。第一,#{}和${}的区别:#{}是预编译参数占位符,MyBatis 会生成?,能防 SQL 注入;${}是字符串拼接,直接把值拼进 SQL,永远不要用${}接收前端传的值。上面代码里ORDER BY ${orderBy}是有意为之,因为排序字段不能参数化,但必须在 service 层做白名单校验,只允许传入price ASC、price DESC、create_time DESC这几个固定值。

第二,CONCAT('%', #{keyword}, '%')不要写成'%${keyword}%',后者有注入风险。模糊搜索在数据量小的时候用 LIKE 没问题,数据量过百万再考虑全文索引或 Elasticsearch,现阶段不用过度设计。

3.3 登录接口的前后实现与 Postman 验证

登录是第一个联调接口,跑通了它,后面所有接口的套路都是一样的。后端用一个LoginController接收参数,service 里校验用户名密码,成功则返回用户信息和一个 token。

@RestController @RequestMapping("/api/user") public class LoginController { @Resource private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { if (dto.getUsername() == null || dto.getPassword() == null) { return Result.error("用户名和密码不能为空"); } User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } // 登录成功,生成 token(实际项目用 JWT 或 Redis 会话) String token = UUID.randomUUID().toString().replace("-", ""); return Result.success(token, user); } }

LoginDTO是专门接收前端请求体的对象,不要直接把User实体拿来接收——实体里可能有role、createTime这些前端不应该传的字段,用 DTO 隔离是防手贱传参的好办法。Result是统一返回类,里面至少包含code、message、data三个字段,前端拦截器只需要判断code是不是 200。接口写完后,用 Postman 发一个 JSON 请求体{"username":"admin","password":"123456"},先确认后端通不通,再去做前端页面,不要一上来就打开 Vue 项目调接口,否则分不清是前端还是后端的错。

4. 前端工程化与联调:Vue 项目的路由、Axios 封装、跨域三个关键动作

4.1 从零初始化 Vue 3 项目并安装依赖

在 IDEA 里开发 Vue 项目,我习惯直接用 Vite 创建,不需要 Vue CLI。Vite 启动速度快,配置直观,Vue 3 生态的中小型项目基本都转向 Vite 了。命令如下:

npm create vite@latest hanfu-web -- --template vue cd hanfu-web npm install npm install vue-router@4 pinia axios element-plus

安装依赖是这个环节最容易翻车的地方。国内网络环境下,npm install经常卡死或报ETIMEDOUT,解决办法是切换镜像源,在项目根目录建.npmrc文件写入registry=https://registry.npmmirror.com,再重新装。另一个高频报错是版本冲突,比如vue-router装成了 3.x,和 Vue 3 根本不兼容。Vue 3必须用vue-router@4,别小看这个数字,它就决定了你路由写法是createRouter还是new Router。

4.2 Axios 拦截器统一处理 token 和错误码

前端的每个需要登录态的接口都得带 token,如果每个页面写一遍axios.get(url, { headers: { token: xxx } }),那是噩梦。用 Axios 拦截器统一注入,这算 Vue 前端面试题里被问烂了但仍然有人写不对的典型题。

// request.js import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { // token 失效,跳转登录页 if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.message)) } return res }, error => { console.error('请求异常:', error) return Promise.reject(error) } ) export default request

这段代码的逻辑说明:request实例的baseURL写成/api,意思是所有请求路径省略前缀,由 Vite 的 proxy 把/api开头的请求转发到后端。这样前端代码里写的是request.post('/user/login'),实际发出去的是POST http://localhost:8080/api/user/login。拦截器把 token 放进请求头,后端 Filter 或拦截器统一校验,比每个 controller 方法里手动取 token 干净得多。响应拦截器把code === 200的业务成功和 HTTP 状态码的成功区分开,避免前端到处写if (res.code === 200)的重复判断。

4.3 联调阶段的跨域问题:Vite proxy 与后端 CORS 的实际配置

前后端分离必然遇到跨域。Vue 项目跑在 5173 端口,后端跑在 8080 端口,直接用 Axios 请求http://localhost:8080,浏览器会拦截响应。解决方式有两种:在 Vite 配置里做代理,或者在后端开启 CORS。

我推荐开发阶段用 Vite proxy,因为它不改后端代码,且在生产环境也不需要它——前端打包后由 Nginx 托管,Nginx 反向代理到后端服务,根本没有跨域一说了。Vite 配置如下:

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, // 不需要 rewrite,后端接口本身带 /api 前缀 } } } })

changeOrigin: true的意思是让后端看到的请求头 Host 变成localhost:8080,避免某些后端框架基于 Host 做判断时误伤。如果后端接口路径不是以/api开头,需要加rewrite: path => path.replace(/^\/api/, '')。但更省事的做法是后端所有接口统一加/api前缀,这样前端和后端用的是同一套路径规则。如果前端请求发出去了,浏览器 Network 面板显示 200,但页面报跨域错误,那说明 proxy 没生效,先重启 Vite,再确认请求路径确实以/api开头。

5. SSM + Vue 汉服平台踩坑记录:5 个真实翻车案例

5.1 商品图片上传到本地磁盘,前端却死活访问不到

现象:后端接口上传图片成功,数据库里存的是D:/hanfu/images/xxx.jpg,前端<img src="D:/hanfu/images/xxx.jpg">显示破图。原因:浏览器不能直接访问服务器本地磁盘路径,必须通过 HTTP 访问,且 SpringMVC 默认不对磁盘路径做静态资源映射。解决:在 SpringMVC 配置里加资源映射,把/images/**映射到本地磁盘目录。同时把图片 URL 存成相对路径,即/images/xxx.jpg,前端拼上后端地址即可访问。

<mvc:resources mapping="/images/**" location="file:D:/hanfu/images/" />

如果是 Spring Boot 项目则用web.config.addResourceHandlers,但 SSM 项目的核心就是这个<mvc:resources>。另一个常见坑是上传文件名直接用原文件名,用户传1.jpg和2.jpg都覆盖了,所以上传后必须用 UUID 重命名。

5.2 前端传过来的时间字段,后端拿到的是 null

现象:用户在前端选了一个预计归还日期(汉服租赁场景),提交订单后数据库里return_time是空。原因:前端传的日期格式是2025-06-30,但 SpringMVC 默认的日期转换格式不支持,或者实体字段类型是Date,而 Jackson 反序列化时没配置格式。解决:在实体字段上写@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),保证前端传来的字符串能正确映射。如果前端传的是时间戳数字,则把字段类型改为Long,在 setter 里转成Date,避免序列化层与业务层互相污染。

5.3 订单提交重复点击,生成了两条记录

现象:用户下单时连续点击两次「提交订单」,数据库出现两条一模一样的订单。原因:前端按钮没有做防抖,后端接口也没做幂等处理。解决:前端在提交后立即disabled按钮并加 loading 状态;后端在 service 层加一个校验,比如同一用户 5 秒内不能重复提交相同金额的订单,或者在订单表加一个业务唯一键。对于真正的商城项目,更可靠的做法是使用分布式锁或在创建订单前校验 token 的幂等状态,但毕设项目做到前后端双重拦截已经够了。注意:只做前端防抖是防不住绕过前端的刷单请求的,评审问到时一定要能说出后端幂等方案。

5.4 首页商品列表加载快,但是点击分类后页面白屏

现象:路由跳转到分类页面后,浏览器没有报 JS 错误,但页面内容是空的。原因:大概率是路由匹配参数的问题。如果分类页路径是/category/:id,而你在组件里用this.$route.params.id获取到的是第一次路由的缓存值,或者路由表里把/category/:id写成了/category,跳转时匹配不上。解决:在组件里监听$route变化,重新拉取数据,而不是只在onMounted里加载一次。

watch( () => route.params.id, (newId) => { if (newId) { loadList(newId) } } )

用 Vue 3 的组合式 API 时,route.params.id是一个响应式对象,上面的写法没问题。Vue 2 项目则是watch: { '$route.params.id': function() { ... } }。这类问题在控制台里往往没有任何报错,排查思路就是先打日志看route.params到底是什么。

5.5 后端 MyBatis 查询正常,但前端总提示 500 且日志里有 SQL 异常

现象:调用商品列表接口,前端显示 500,后端日志提示Error querying database。原因:通常有下面几种——数据表字段名和实体属性映射不上,MyBatis 返回的结果集里有一个字段在实体里没有对应属性,自动映射默认只匹配驼峰和下划线;或者多表联查时重复列名,导致映射字段错乱。解决:先拿 SQL 去 Navicat 跑一遍,确认是不是 SQL 本身的问题。SQL 没问题就看resultType对应的实体类,是否所有查询字段都有对应的setter。开启 MyBatis 下划线转驼峰配置能省去大量resultMap:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>

这个配置的作用是自动把create_time映射到createTime,如果你的数据库字段是按snake_case命名的、实体属性是camelCase,这个开关能解决 80% 的映射崩溃问题。剩余 20% 是类似order这种数据库关键字,SQL 里必须加反引号。

6. 让代码真正属于你自己:二次开发与部署验证的关键技巧

拿到或者写完一套 SSM + Vue 汉服平台,最重要的不是跑起来,而是能改得动。我的经验是:先按数据流把这条链路画出来——用户打开首页、请求商品列表、加入购物车、提交订单、后台审核发货——然后对照代码在每一个环节上找出「现在的实现是什么、如果我要改需求该动哪个文件」。比如把「购物车存前端 localStorage」改成「购物车存后端数据库」,你就需要动 Vue 的 store 层、后端新增 cart 表和对应的 controller / service / mapper。能说清楚每次改动涉及哪些文件,才说明你读懂了这套代码。

部署上,本地开发用 IDEA 启动 Tomcat 加 Vite dev server,生产交付则建议前端npm run build生成dist目录,放到 Nginx 的html目录下,Nginx 配置反向代理把/api请求转发到 Tomcat 的 8080 端口。这样把静态资源请求和动态接口请求分开,性能上好、也符合汉服文化平台这类展示加交易网站的常规部署结构。如果导师或客户要求提供一个「一键启动」的演示环境,那就把后端打成 WAR 包放到 Tomcat webapps 下,前端 build 完也丢进同一个 Tomcat,配置好上下文路径即可。

验证阶段,我会建议做三件事:用 Postman 跑一遍核心接口的异常用例——注册重复用户名、登录错误密码、下单超卖场景、未登录访问需要鉴权的接口;用浏览器的 Vue Devtools 检查路由跳转和 Pinia/Vuex 的状态变化;再用生产模式npm run build后在本地起一个服务看有没有资源路径 404 的问题。这三个动作做完,代码才算真正能交付。

最后说一个我的习惯:每次接到这类项目,我都会先把数据库导出一份初始化脚本,再把前端页面走一遍把所有静态资源路径记录成表。这个习惯帮我避过无数次「部署新环境发现少了个图片文件夹」「用户说页面样式乱了但其实引用的是绝对路径」的翻车事故。希望这套思路也能让你在二次开发时少走点弯路,把更多时间花在真正有价值的功能上,而不是和依赖、路径、编码较劲。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询