看到这个标题,估计不少正在准备毕业设计、或者想找个完整项目练手的朋友会会心一笑。SSM239,一看就是典型的课程设计编号,前三个字母代表了技术栈——Spring、SpringMVC、MyBatis,后面跟着的项目内容“二手母婴商品交易系统Vue”,则把业务场景和前端技术都交代清楚了。这个项目我前后带过几个同学做完,也自己完整重构过一版,可以说它表面上是“基础款”,但真正把它做扎实、做完整,里面能挖的东西远比想象中多:从数据库设计到订单状态流转,从图片上传到前后端联调,几乎把Java Web全栈开发的常见知识点都串了一遍。
这篇文章我想抛开那些课程设计报告里的套话,纯粹从一个实际动手做过的人的角度,把这个项目怎么拆、怎么设计、怎么一步步实现、以及最容易踩的坑都讲清楚。适合谁看?正在做类似选题的学生、想快速上手前后端分离开发的新手、以及想了解SSM+Vue项目完整链路的朋友,这篇文章应该都能给你一些参考。
1. 项目整体设计与技术选型
1.1 为什么是SSM + Vue,而不是Spring Boot
先说一个很现实的问题:现在企业里用Spring Boot的越来越多,为什么还要做SSM?从我接触的情况来看,很多高校的Java Web课程至今还是以SSM为教学主线,框架原理讲得细,配置也要求手动完成。这其实是好事,因为SSM的配置全都是“显式声明”,你能清清楚楚看到Spring的容器是怎么装起来的、SpringMVC的拦截器是怎么挂上去的、MyBatis的SqlSessionFactory是怎么和数据源绑定的。换成Spring Boot,很多东西被自动配置遮住了,反而不容易理解底层联动。
从项目本身来说,SSM的骨架足够支撑这样一个中型的交易系统。用户管理、商品发布、订单流程、后台管理,模块划分清晰,事务边界可控。SpringMVC负责接口路由和参数绑定,MyBatis负责SQL操作,Spring负责把Service和Mapper串起来,各司其职。对练手和毕设答辩来说,这种“每个环节都能讲出道理”的技术栈,反而是加分项。
前端选择Vue,现在的版本一般都用Vue2 + Element UI,或者Vue3 + Element Plus。如果学校课程讲的是Vue2,那就用Vue2,生态稳定、资料多、遇到坑好搜。Vue负责页面渲染和数据交互,通过axios调用后端接口,整体就是典型的前后端分离开发模式。
1.2 系统角色与权限边界
二手母婴商品交易系统虽然看起来业务不算复杂,但角色划分必须清晰。我习惯分三类:
- 普通用户(买家):浏览商品、搜索筛选、收藏、加入购物车、下单付款、确认收货、发表咨询。
- 卖家用户:商家自家也是普通用户注册而来,但比买家多出“商品发布”“商品管理”“订单发货”“查看买家咨询”这几个能力。
- 管理员:登录后台,审核商品上架、管理用户状态(禁用/启用)、管理商品分类、查看交易统计。
这里有个关键设计点:卖家和买家不是两张用户表,而是在用户表里用字段区分身份,比如role字段:0代表普通用户,1代表卖家,2代表管理员。用户注册时默认是普通用户,但普通用户也可以“申请成为卖家”,这个在个人中心里做一个入口即可,后台审核通过后role变为1。这样实现简单,也符合电商系统的常见设计——先有买家,再升级为卖家。
权限控制怎么做?后端用拦截器(HandlerInterceptor)拦截需要登录的接口,从Session里取用户信息,取不到直接返回未登录状态码。管理员接口则额外校验角色的值是否为2。前端配合路由守卫,根据登录状态和角色控制页面跳转和菜单显示。前后端都做校验,但真正的安全底线一定在后端。
1.3 核心功能模块清单
我习惯在动手写代码前先把功能模块铺开,不然写到哪里算哪里,代码很容易乱。
| 端 | 模块 | 功能点 |
|---|---|---|
| 前台 | 用户 | 注册、登录、退出、个人资料修改、头像上传 |
| 前台 | 商品 | 分类浏览、关键词搜索、价格筛选、成色筛选、商品详情、收藏 |
| 前台 | 购物车 | 加入购物车、修改数量、删除、批量结算 |
| 前台 | 订单 | 提交订单、订单支付(模拟)、卖家发货、买家确认收货、取消订单 |
| 前台 | 商品发布 | 卖家发布商品、图片上传、编辑、上架/下架 |
| 前台 | 咨询 | 买家对商品发起询问,卖家在商品管理里回复 |
| 后台 | 管理 | 用户管理、商品审核、分类管理、订单总览、数据统计 |
这个清单基本就是项目的全部业务范围。把每个模块的接口列表列出来,开发的时候照着做就行,不容易漏功能。
2. 数据库设计与核心表结构
2.1 设计原则与字段规划
二手母婴商品交易系统的数据量不会很大,但表关系要设计得规范。我设计表的时候遵循几点:
第一,业务主键用自增ID,订单号单独用一个唯一字符串。订单号要展示给用户看,自增ID太暴露业务量,而且多个表都可能引用,所以订单表用order_no字段,格式就是时间戳加随机数。
第二,状态字段全用整型。比如商品状态:0待审核、1在售、2已下架、3已售出。订单状态:0待付款、1待发货、2待收货、3已完成、4已取消。用数字存状态,代码里定义常量或者枚举,比直接用字符串科学得多。
第三,删除用逻辑删除。用户列表、商品列表都可能涉及“删除”,但物理删除会破坏数据关联。所以每个核心表加一个deleted字段或者直接用status来表示失效状态。比如用户禁用就是status=0,商品下架就是status=2,都是逻辑层面的操作,数据还留在库里。
第四,金额字段用BigDecimal。这一点必须强调,我见过有人用double存价格,最后计算总额出现0.1+0.2不等于0.3的经典问题。数据库里用decimal(10,2),Java对象里用BigDecimal,避免浮点精度问题。
2.2 核心表结构说明
把表结构直接写出来,也是后续开发的重要参考。用户表字段如下:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加盐MD5)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像路径', `role` tinyint(4) DEFAULT '0' COMMENT '角色:0普通用户 1卖家 2管理员', `status` tinyint(4) DEFAULT '1' COMMENT '状态:1正常 0禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';密码单独说一句,真实项目肯定不能明文存储。我用了MD5加盐:取用户名作为盐的一部分,再加上一串固定密钥,和密码拼起来后做MD5。虽然MD5不算强的加密算法,但应对课程设计、演示项目足够,答辩的时候还能把“为什么加盐”讲清楚。真要考虑安全性,换BCrypt即可,思路是一样的。
商品表是核心业务表,字段比较多:
CREATE TABLE `goods` ( `id` int(11) NOT NULL AUTO_INCREMENT, `seller_id` int(11) NOT NULL COMMENT '卖家用户ID', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `title` varchar(100) NOT NULL COMMENT '商品标题', `description` text COMMENT '商品描述', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价/参考价', `sell_price` decimal(10,2) NOT NULL COMMENT '售价', `condition_level` tinyint(4) DEFAULT '0' COMMENT '成色:1全新 2九成新 3七成新 4五成及以下', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图', `images` varchar(1000) DEFAULT NULL COMMENT '图片列表,逗号分隔', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0待审核 1在售 2已下架 3已售出', `view_count` int(11) DEFAULT '0' COMMENT '浏览次数', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';图片字段这里多提一嘴:单图用cover_image,多图用images字符串存逗号分隔的路径列表,查询时拆一下即可。这比单独建一张商品图片表省事,而且对这个量级的项目完全够用。
订单相关表是另一个核心模块。订单主表和订单明细表分开设计:
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `buyer_id` int(11) NOT NULL COMMENT '买家ID', `seller_id` int(11) NOT NULL COMMENT '卖家ID', `total_price` decimal(10,2) DEFAULT NULL COMMENT '总金额', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0待付款 1待发货 2待收货 3已完成 4已取消', `receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货人电话', `receiver_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `remark` varchar(255) DEFAULT NULL COMMENT '买家备注', `create_time` datetime DEFAULT NULL, `pay_time` datetime DEFAULT NULL COMMENT '付款时间', `ship_time` datetime DEFAULT NULL COMMENT '发货时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';订单明细表记录每一件商品的信息:
CREATE TABLE `order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `goods_id` int(11) NOT NULL, `goods_title` varchar(100) DEFAULT NULL COMMENT '商品标题(快照)', `goods_image` varchar(255) DEFAULT NULL COMMENT '商品图片(快照)', `price` decimal(10,2) NOT NULL COMMENT '成交单价', `count` int(11) DEFAULT '1' COMMENT '购买数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';注意明细表里的商品标题和图片,这是“快照”字段。为什么冗余?因为订单生成之后,卖家完全可能修改商品标题、更换图片甚至删除商品,订单数据不能被这些操作影响。这在电商系统里是标准的设计思路,面试和答辩时能讲清楚这一点会加分不少。
购物车表和收藏表结构相对简单:
CREATE TABLE `cart` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `goods_id` int(11) NOT NULL, `count` int(11) DEFAULT '1' COMMENT '数量', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表'; CREATE TABLE `favorite` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `goods_id` int(11) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';2.3 订单状态流转设计
二手交易和标准电商最大的不同在于角色结构:买卖双方都是普通用户,平台(管理员)主要负责审核和仲裁,不直接参与交易流程。所以订单状态的流转要围绕“买家-卖家”两端交互来设计。
我的设计中,订单生命周期如下:
待付款 -> 待发货 -> 待收货 -> 已完成 | | | | | +---> 已取消(买家在待发货前可取消) +---------+--------------> 已取消(超时未付款,简单演示可跳过)买家提交订单后状态为待付款,点击“立即支付”模拟支付成功(这里不需要接真实支付网关,直接用接口把状态改成待发货即可,答辩时说明是为了演示而简化)。卖家在自己的订单列表里看到待发货订单,点击“发货”填写物流单号(演示项目也可不填),状态变为待收货。买家确认收货后,状态变为已完成。待付款状态买家可以取消订单,待发货状态如果卖家还没发货,也可以由卖家关闭订单。
这个状态机的核心是:每次状态变更必须有明确的前置条件,而且更新订单状态时SQL要加上状态条件,防止并发导致状态错乱。比如:
UPDATE orders SET status = 2, ship_time = NOW() WHERE id = #{orderId} AND status = 1 AND seller_id = #{sellerId}这样即使买家同时点了取消、卖家同时点了发货,数据库层面也只会有一个操作生效。
3. 后端核心功能实现
3.1 SSM整合配置要点
SSM整合的配置文件比较多,第一次做的人容易被绕晕。我的经验是分四个配置文件:web.xml管SpringMVC和字符编码过滤器;spring-mvc.xml管控制器扫描、注解驱动、视图解析器;spring-mybatis.xml管数据源、SqlSessionFactory、Mapper扫描、事务管理器;mybatis-config.xml管驼峰映射、缓存配置。
web.xml里一定不能漏掉的配置是两个:一个是SpringMVC的DispatcherServlet,另一个是字符编码过滤器。如果漏了编码过滤器,你会发现前端传过来的中文全是乱码。DispatcherServlet的url-pattern如果配成/,要注意静态资源放行,不然图片、CSS、JS都会404。我在spring-mvc.xml里这样配置:
<mvc:default-servlet-handler/>这表示SpringMVC处理不了的请求交给容器默认Servlet处理,静态资源才能正常访问。
spring-mybatis.xml里数据源我用的是Druid连接池。数据库连接池这个选型不复杂,Druid的好处是自带监控页面,答辩演示时可以打开SQL监控面板,展示项目真实执行的SQL,这个细节很加分。关键配置大概是:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mall_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> </bean>数据库连接串里的serverTimezone=Asia/Shanghai不能省。MySQL 8的驱动如果不指定时区,连数据库的时候会直接报错,这是新手最容易卡住的地方之一。
MyBatis的Mapper扫描和事务管理器要配套:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mall.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>事务管理一定要配。订单生成时要同时写入订单主表、订单明细表、删除购物车对应商品、扣减商品库存(如果有库存概念),这四步任何一个失败都应该回滚,不配事务的话数据就乱了。
3.2 用户注册登录与拦截器
用户注册的核心逻辑是三步:校验用户名是否已存在、对密码做加盐MD5、插入用户记录。这里唯一的坑在用户名唯一性判断上,建议把username字段设置为唯一索引,这样即使并发注册,数据库也会兜底,不会出现两个同名的用户。
登录接口的处理方式是:根据用户名查出用户,比对密码,比对通过后把用户对象存入Session。我在Service层返回一个统一的结果对象Result,包含code、message、data三个字段。前端根据code判断登录是否成功,成功就跳转首页,失败就提示具体原因。
拦截器是这个项目的权限核心。我写一个LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}"); return false; } return true; } }注意这里直接输出JSON而不是重定向,因为前后端分离的项目里,前端是通过axios请求的,如果后端重定向到登录页,前端拿到的是HTML而不是JSON,处理起来很麻烦。统一返回401,前端axios拦截器里判断到401就跳转登录页,体验才正常。
拦截器注册时要注意放行路径。注册、登录、商品列表、商品详情、商品搜索这些接口不需要登录,都必须放行。购物车、订单、个人中心、发布商品这些接口才需要登录校验。管理员接口再额外加一个管理员拦截器,校验session里的用户角色。
3.3 商品发布与图片上传
商品发布是卖家端的核心操作,涉及表单提交和图片上传两个部分。我在设计时把这两个放在同一个接口里处理,前端以multipart/form-data方式提交:表单字段用普通的键值对,图片文件用文件字段。
上传文件的保存路径是另一个常见的坑。很多新手把文件写到项目目录下,然后启动Tomcat部署的时候发现文件路径不对,或者重启Tomcat后上传的图片不见了,因为重新部署会把原目录清空。正确做法是把图片保存到服务器之外的独立路径,比如Linux下的/usr/local/upload/,Windows下的D:/upload/,然后通过配置映射让Tomcat能访问到这个目录。
在spring-mvc.xml中对静态资源做映射:
<mvc:resources mapping="/upload/**" location="file:D:/upload/"/>这样前端访问http://localhost:8080/upload/xxx.jpg就能直接看到图片,而文件本身存放在项目部署目录之外,重启不丢失。
文件名称也要处理。直接用原始文件名不安全,而且可能重名。我的工具类里用UUID重命名,保留原文件扩展名。这样既避免重名,也防了路径穿越。
商品发布的业务逻辑里还有一步:状态设置为待审核(status=0)。这是二手平台的一个典型规则——管理员审核通过之后商品才能在首页展示。所以卖家发布完成之后,在自己的“我的商品”列表里能看到该商品,但前台首页和搜索列表不会出现,除非管理员在后台审核通过。
3.4 订单流程与事务处理
订单生成的接口是项目中事务最重的接口,逻辑如下:
- 从请求里获取收货人信息、备注、以及本次结算的商品ID列表(可以来自购物车,也可以直接从前端传商品ID和数量)。
- 遍历商品ID,查询商品信息,校验商品状态必须是在售状态,校验卖家不能是自己(自己买自己的二手商品没有意义)。
- 计算总价,生成订单号。
- 插入订单主表,拿到自增主键。
- 批量插入订单明细表。
- 删除购物车中对应的记录。
- 如果有库存概念,扣减库存;这个项目二手商品大多只有一件,所以下单后直接把商品状态改为已售出。
整个方法加@Transactional注解。有一个细节:调用商品查询时,如果商品在第一步查询显示在售,但下单过程中别人抢先买走,状态已经变成已售出怎么办?所以更新商品状态的SQL要加上条件:
UPDATE goods SET status = 3 WHERE id = #{goodsId} AND status = 1如果影响行数为0,说明商品已经被别人买走,抛异常回滚整个订单,并向用户提示“商品已售出”。这就是乐观锁的思路——用条件更新代替锁表,在这个场景下完全够用。
订单号生成我用的是:
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String orderNo = sdf.format(new Date()) + (int)((Math.random() * 9 + 1) * 10000);18位基本不会重复,而且一眼能看出下单时间,比UUID更适合展示给用户。
3.5 后台管理功能实现
后台管理的核心是商品审核。管理员从商品列表里筛选status=0的记录,查看商品详情和图片,决定通过或驳回。通过就是status=1,驳回是status=2(或者单独一个状态,此处简化)。这里我加了一个可选的“驳回原因”字段,方便用户了解原因。前端页面上,后台管理系统我用了一套独立的布局:左侧菜单(商品审核、用户管理、分类管理、数据统计),右侧内容区,同样基于Element UI搭建。
用户管理主要是禁用和启用用户。禁用用户时,同时要考虑到这个用户还有在售商品怎么办?我的处理是:后台禁用一个用户时,自动把该用户所有在售商品下架,代码里就是两个UPDATE操作放在一个事务里。否则会出现“用户被封禁但商品还在卖”的逻辑漏洞,这个细节在答辩时可以主动提。
分类管理是简单的CRUD,一个分类对应商品表里的category_id。商品列表和首页的分类导航都通过分类ID关联。新增分类时注意分类名的唯一性,前端加校验,后端也做查重。
数据统计我做得比较轻量:统计注册用户总数、商品总数、订单总数、交易总额,再按近七天的订单量做一张简单的柱状图。数据接口用SQL的聚合函数就能实现,前端ECharts画图。
4. 前端Vue页面实现与接口联调
4.1 项目搭建与路由设计
Vue项目我用vue-cli创建,选Vue2版本。依赖方面安装vue-router、axios、element-ui、qs(或者直接手写序列化),开发依赖装sass-loader和node-sass做样式预处理。
路由设计要考虑不同角色的权限。路由表分为三个层级:公共页面、登录后页面、管理员页面。我用vue-router的beforeEach守卫做登录校验和角色判断。核心逻辑是:
router.beforeEach((to, from, next) => { const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; if (to.meta.requiresAuth && !user) { next('/login'); return; } if (to.meta.requiresAdmin && (!user || user.role !== 2)) { next('/'); return; } next(); });前后端都要做权限控制,前端做的是体验优化,后端才是安全防线。这个点写进文档或者答辩讲清楚就行。
组件设计上,我建议页面组件和通用组件分开。通用组件比如商品卡片(展示商品图、标题、价格、成色、链接到详情页),在首页、商品列表页、我的收藏页都要用,抽出来复用能省大量重复代码。
4.2 axios封装与接口调用
axios一定要统一封装,不要每个页面各自写一遍。我在utils/request.js里创建一个axios实例,配置baseURL、请求超时时间、拦截器。请求拦截器负责往header里加token(如果后端用了无状态方案);响应拦截器统一处理code:等于200正常返回数据,等于401跳转登录页,其他code用Element UI的Message组件弹出错误信息。
import axios from 'axios'; import { Message } from 'element-ui'; import router from '@/router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res; } else if (res.code === 401) { localStorage.removeItem('user'); router.push('/login'); return Promise.reject(res); } else { Message.error(res.message || '请求失败'); return Promise.reject(res); } }, error => { Message.error(error.message || '网络异常'); return Promise.reject(error); } ); export default service;这里baseURL是/api,后面联调环节会说为什么这样配。
4.3 前后端联调与跨域处理
SSM后端默认跑在8080端口,Vue开发服务器跑在8081端口。端口不同,前端直接请求后端接口会触发跨域。解决办法有两个:后端配CORS,或者前端用devServer代理。我推荐前端代理,因为部署到生产环境时,前后端通常部署在同一域下,代理配置只需要在开发阶段生效,生产环境把前端打包后放到Tomcat的webapps下,或者用Nginx反向代理,同域下就没有跨域问题。
vue.config.js里的代理配置:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };这样前端请求/api/user/login,开发环境下会被代理转发到http://localhost:8080/user/login,后端接口路径不用加/api前缀。等到生产环境直接部署,如果前后端都在8080端口下,/api/user/login这个路径会直接404,所以生产部署时还要在SpringMVC配置里加一个路径处理,或者在nginx层做rewrite。我在实际部署时更常后端接口直接带/api前缀,前端所有请求天然统一口径,最省心。
跨域还有一个小坑:如果后端用了Session存用户信息,axios请求默认不携带cookie,会导致每次请求都是新会话。解决方法是给axios加上withCredentials: true,同时后端的CORS配置中不能使用*通配符,必须指定具体的域名。用代理方案可以完全避开这个问题,这也是我推荐代理的原因之一。
4.4 核心页面实现要点
首页的结构比较简单:顶部导航栏(Logo、搜索框、分类菜单、登录状态区)、中间分类导航、下面是最新商品和热卖推荐。首页数据接口返回商品列表,每次进首页调用一次,商品卡片点击后跳详情页。
商品列表页要支持筛选,筛选条件包括分类、成色、价格区间、排序方式(最新发布、价格升序/降序)。后端接口接收这些查询参数,用MyBatis的动态SQL拼接查询条件。前端用Element UI的Select和Input组件绑定条件,点击筛选时重新调用接口。
商品详情页是信息量最大的页面:左侧图片轮播(多图)、右侧标题价格、成色、卖家信息、收藏按钮、加入购物车和立即购买按钮、下方是详细描述和买家咨询列表。这里的咨询功能是一个测试常见但容易漏掉的模块:买家可以针对商品提问,卖家在商品管理页看到提问并回复。数据表用goods_id关联,查询时按时间排序。
购物车页面要做“是否选中”和“合计金额”的联动。我在前端用一个对象存储每个条目的选中状态,用computed属性计算总价。结算时把选中的商品ID列表传给后端。
个人中心包括我的信息、我的订单、我要卖货、我的商品、我的收藏几个子页面。用Tab切换。我的订单要区分买家视角和卖家视角:同一个订单,买家关心的是付款、确认收货;卖家关心的是发货。所以前端要根据当前用户角色显示不同的操作按钮。
后台管理页面是独立的一套布局,左侧菜单固定,右侧内容区根据菜单切换组件。商品审核列表用表格展示,每条记录带“查看详情”和“通过/驳回”按钮。用户管理表格里显示用户名、手机号、角色、状态,操作列是“禁用/启用”。
5. 项目部署与本地运行
5.1 环境准备
我建议的环境配置是:JDK 1.8、Maven 3.6+、MySQL 5.7或8.0、Tomcat 8.5或9.0、Node 14+(前端构建用)。这些版本相互兼容性好,网上资料也多,不推荐一上来就追求JDK 17、Tomcat 10这些新版本,容易遇到兼容性问题。
数据库初始化脚本要提前准备好。包括建库、建表、插入必要的初始化数据:一个管理员账号(如admin/admin123)、一个测试卖家账号、几个商品分类(婴儿车、婴儿床、玩具、衣物、喂养用品等)、若干测试商品。这样项目启动后不用手动造数据就能直接看效果。
5.2 部署步骤
后端部署分为两步:Maven打包成war包,放到Tomcat的webapps目录下,启动Tomcat后war包自动解压部署。注意项目打包名称最好改成好记的,比如mall,访问路径就是http://localhost:8080/mall/。
前端部署也分两步:先在项目根目录执行npm run build,生成dist目录,然后把dist目录下的文件拷贝到Tomcat的webapps下某个目录中。如果希望和前端在同一端口下部署,可以在webapps下建一个mall目录,把打包产物放进去,同时保证后端接口路径和前端请求路径能对得上。
我自己运行测试时经常不改动Tomcat,直接用IDEA的Tomcat插件启动后端,然后用npm run serve启动前端开发服务器,开发体验很好。要演示给老师或者同学看时,再打一个正式包部署。
5.3 联调过程中必须验证的流程
项目做完后,不要直接交差,我建议把核心流程完整走一遍。重点验证这几个业务闭环:
- 用户注册登录、退出登录、刷新页面后登录状态保持。
- 卖家发布商品,管理端能收到待审核商品,审核通过后前台能搜到,审核不通过则前台不可见。
- 买家搜索商品、查看详情、加入购物车、提交订单、模拟支付。
- 卖家在订单列表看到新订单,执行发货,买家确认收货。
- 商品在售出后,再次搜索这个商品,状态显示已售出或直接下架,不能被重复购买。
- 管理员禁用一个用户后,该用户无法登录,其商品全部下架。
每一个流程跑通,项目才算真正能用。
6. 常见问题与排查技巧实录
6.1 启动报错与配置类问题
最常遇到的启动报错是数据库连接失败。排查思路是固定的:先看MySQL服务有没有启动,再看账号密码对不对,再看连接串时区配置有没有问题。如果报错信息里有Public Key Retrieval is not allowed,在连接串末尾加上allowPublicKeyRetrieval=true即可,这是MySQL 8的新特性导致的问题。
第二个常见的启动报错是Spring容器创建失败,多半是Mapper接口扫描不到或者Bean注入失败。观察控制台报错提到的类名,检查是否漏了@MapperScan或者MapperScannerConfigurer的basePackage指向了错误的包。还有一个小问题是ServiceImpl和Controller都没有加注解,检查时容易忽略,养成写完一个模块就启动一次的习惯能快速定位。
6.2 MyBatis相关的坑
MyBatis最典型的问题是驼峰属性映射。数据表的字段是create_time,Java属性是createTime,如果mybatis-config.xml里没有开启驼峰映射,查询结果里的Create_time永远是null。一定要加:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>还有一种问题是SQL拼接错误。动态SQL里用了<if>标签,但条件的写法不对,导致整个查询查不出数据。排查技巧是,在MyBatis的日志配置里打开SQL输出,看实际生成的SQL语句,是哪里拼错了一眼就能看到。日志配置在logback或log4j.properties里设置对应mapper包级别为DEBUG。
6.3 图片上传与回显问题
图片上传报错有几种情况。一种是文件大小超限,SpringMVC默认单文件最大1MB,需要配multipart配置:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="10485760"/> <property name="maxInMemorySize" value="4096"/> </bean>另一种是图片能上传但访问404。原因通常是静态资源映射没配置对,路径中的file:前缀缺失。还有Tomcat目录权限问题,如果映射的目录没有写权限,上传操作直接报错。
回显问题主要在于图片路径的拼接。数据库里存的是相对路径/upload/xxx.jpg,前端渲染时直接拼接口域名即可。我建议数据库里就存完整可访问的URL,比如http://localhost:8080/upload/xxx.jpg,前端不用再拼接,省得开发环境和生产环境之间来回改。
6.4 前端联调中的细节问题
前端最烦的两个问题一个是跨域,一个是数据格式。跨域问题前面已经讲过了,用代理基本能根治。数据格式问题主要在时间字段上。后端返回的Date类型默认序列化成一串时间戳数字,前端显示很不友好。解决方法是配置Jackson的日期格式:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <property name="objectMapper" ref="objectMapper"/> </bean> </mvc:message-converters> </mvc:annotation-driven>或者给实体类的日期字段加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解。我习惯用注解,简单直接,每个日期字段上加上就行。
另一个联调细节是前端提交表单数据时,如果提交的是JSON对象,后端用@RequestBody接收;如果提交的是FormData格式,后端用普通参数接收,混用会导致参数绑定为null。在购物车勾选结算、商品发布表单、订单提交这几个接口上,我要么统一JSON,要么统一FormData,前后端定义一个约定,开发时就不会乱。
6.5 项目答辩时的加分技巧
如果你做这个项目是为了毕业答辩,除了功能完整外,我建议在技术点讲解上多准备几个亮点。比如订单生成的乐观锁处理、订单数据中的快照字段、图片独立目录存储方案、拦截器的多角色权限设计、前端路由守卫与后端拦截器双重校验,这几点随便展开都能讲一两分钟,而且能体现你对项目有真正的理解。
再准备一套完整的演示数据和脚本,答辩现场不要临时敲命令。数据库里准备好账号:管理员、卖家、买家各一个,每一个角色的核心操作都练熟,演示的时候流畅不卡壳,这比代码本身更容易给老师留下好印象。
7. 写在最后的几点体会
这套SSM + Vue的二手母婴交易系统,我前前后后带人做过好几轮,每一次重做都会发现一些新问题、新优化点。最大的感受是:这类“经典毕业设计”项目虽然技术栈看起来不新,但它把Web开发中最重要的一整套链路都涵盖了——从需求拆解到数据库建模,从接口设计到前端交互,从本地联调到打包部署。认真做完一遍,你对Java后端开发的整体认知会清晰很多,这比单纯背面试题更管用。
我个人在实际操作中养成了一个习惯:每做完一个模块,就立刻把前后端联调跑一遍,不要等所有功能都写好再统一调试。比如用户模块做完,马上注册一个账号,登录、退出、改资料,全套验证通过再做商品模块。模块之间独立开发但尽早集成,能避免最后联调时一堆问题挤在一起,定位起来特别痛苦。
最后再分享一个小技巧:项目目录结构从一开始就规划清楚,后端按controller、service、mapper、pojo、utils分层,前端按views、components、router、api、utils分层。这个习惯跟我做这个项目的时候一模一样,后面扩展功能、换人接手时,大家都省心。要是你正在做类似的项目,遇到具体问题卡住了,欢迎按这个思路对照排查,大部分问题其实都在前面几节提到的坑里。