1. 项目概述与方案选型拆解
做Java Web开发的人,十有八九都跟电商项目打过交道。但市面上大部分教程项目都是“通用型商城”——商品、购物车、订单、后台管理一套模板走天下,做完也不知道自己到底学会了什么业务。这个项目不一样的地方在于:它把商城主题锁定在篮球装备这条垂直赛道上,后端用SSM(Spring + SpringMVC + MyBatis)组合,前端视图层用JSP,整体是一个可以跑通的完整闭环:用户注册登录浏览商品、加购物车、下单支付(模拟)、后台发货管理,每一步都有对应代码支撑。
先说结论:如果你正在学Java Web,想找一个既有业务深度、又不需要自己从零设计架构的项目来练手,或者你正在准备课程设计、毕业设计答辩,这个基于SSM的篮球商城系统是很合适的参考对象。它不烧硬件、不依赖微服务那套复杂生态,用一台普通电脑加一个Tomcat就能跑起来。选题上篮球主题也给项目增加了辨识度——答辩时候你能聊球鞋SKU、球衣号码定制、库存扣减,而不是千篇一律的“我的商品表有id和name”。
为什么选SSM这套组合而不是Spring Boot?站在学习的角度,SSM把Spring的IoC容器、SpringMVC的请求分发、MyBatis的数据访问三层拆得非常明显。你在JSP页面里发一个请求,能看到请求是怎么经过DispatcherServlet、进入Controller、调用Service、最终由Mapper落到数据库的。这套链路在Spring Boot里被大量自动化配置掩盖了,初学者反而不容易建立“请求-处理-响应”的整体认知。JSP同理,虽然现在企业里用前后端分离更多,但JSP + EL表达式 + JSTL标签这种“服务端渲染”的方式,对理解Session、Cookie、请求转发这些Web基础概念极其友好。用这个项目打底,以后学Spring Boot、Vue甚至微服务,都有个清晰的对照。
再说篮球垂直领域带来的业务细节。通用商城的商品表只需要存标题、价格、图片,但篮球装备不一样:一双篮球鞋可能有多个尺码和配色的库存组合,一件球衣需要区分主客场版本和印号服务,一个篮球本身又分室内牛皮、室外PU材质。这意味着你不能只做一张商品表,必须引入SKU(库存量单位)的概念。这个“小复杂”恰好是电商系统最重要的核心点之一,也是很多商城Demo没有做深的地方。下面我从功能模块、表结构、核心代码、调试避坑几个维度把这个项目完整拆一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块与数据表设计思路
2.1 前台与后台的功能边界划分
商城系统最忌讳的就是把用户看得见和看不见的功能混在一起写。这个项目在设计上把前台和后台分成了两套逻辑:前台面向普通用户,后台面向管理员,两者入口不同、权限不同、操作的数据域也不同。
前台功能围绕“逛 - 选 - 买 - 查”四个动作展开。用户注册登录后,可以在首页看到篮球分类下的商品轮播和推荐位;可以按“篮球”“球鞋”“球衣”“护具”“配件”分类筛选;商品详情页展示主图、价格、库存、规格选择;购物车支持增删改数量和批量结算;订单模块覆盖从提交订单到确认收货的完整生命周期;个人中心里能维护收货地址、查看历史订单。这些功能看起来常规,但每一块都有值得设计的细节。比如购物车在未登录状态下应该允许使用吗?如果允许,用户登录后本地购物车和数据库购物车怎么合并?这些问题项目里都有对应的处理逻辑,而不是像很多教程项目那样只做“登录后才能加购”的阉割版。
后台功能则面向管理员:商品分类管理、商品信息维护、SKU库存调整、上下架操作、订单审核发货、用户列表管理。后台的订单列表和前台最大的区别在于操作维度:管理员需要根据订单状态做“发货”动作,把订单从“已付款”推进到“已发货”;如果用户申请取消,管理员还要做退款审批。这里涉及到一个重要的状态机设计思路,后面会细说。
2.2 数据表设计:从商品到SKU再到订单的拆解
数据表设计是整个项目的地基。我见过太多人一上来就建表,结果写到订单模块发现库存字段不知道该放哪张表。这个项目合理的表结构至少应该包含以下这些核心表:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, phone, avatar, create_time | 用户表,密码不能明文存储 |
| category | id, name, parent_id, sort | 商品分类表,支持两级分类 |
| product | id, category_id, name, main_image, description, status | 商品主表,status控制上下架 |
| product_sku | id, product_id, spec_key, spec_value, price, stock | SKU表,具体规格与库存价格 |
| cart_item | id, user_id, sku_id, quantity, checked | 购物车表,checked标记是否选中结算 |
| address | id, user_id, receiver, phone, province, city, detail | 收货地址表 |
| orders | id, order_no, user_id, address_snapshot, total_amount, status, create_time | 订单主表,地址存快照防止地址修改影响历史订单 |
| order_item | id, order_id, sku_id, product_name, spec, price, quantity | 订单明细表,冗余商品快照 |
这里有两个设计点值得拿出来重点说。第一是商品和SKU分离。在商城业务里,一个商品是逻辑概念,真正卖的是具体规格。以篮球鞋为例,同一双鞋可能有41、42、43三个尺码,每个尺码的库存和价格可能不同——41码断货了不代表整双鞋下架。project里product表存商品的公共属性(名称、主图、描述),product_sku表存规格维度的属性(尺码、颜色)和价格库存。这样在商品详情页选择规格时,前端根据选中的SKU动态刷新价格和库存,下单时锁定的是SKU而不仅仅是商品ID。
第二个设计点是订单地址快照。很多初学者会把订单直接关联到address表的id上,用户一旦修改了收货地址,历史订单的地址也跟着变了,这在实际业务中是绝对不允许的。正确做法是在创建订单时,把地址信息冗余一份到orders表的address_snapshot字段。同样地,订单明细表里也要冗余商品名称、规格描述、成交价格,而不是只存一个SKU ID。因为商品可能改名、调价、下架,但历史订单必须保持当时的样子。这种“业务快照”思想是电商项目区别于课程Demo的重要标志,值得在文档里专门说明。
2.3 订单状态流转:用状态机思维管理生命周期
订单状态是商城系统里最容易被写乱的地方。有些人用int字段存0、1、2、3,然后在Controller里直接if判断改状态,改着改着就分不清当前订单到底能执行哪些操作了。这个项目采用的是状态机思路:预定一个清晰的状态序列,每个状态下只允许特定的操作。
从用户视角看,订单依次经历:待付款(已创建)→ 已付款/待发货 → 已发货/待收货 → 已完成。伴随商业环节还有:已取消(用户付款前取消或超时未付)、退款中/已退款(管理员处理售后)。每个状态对应一个具体的触发动作:提交订单触发“创建”,模拟支付触发“付款”,管理员发货触发“发货”,用户确认收货触发“完成”。代码实现上可以在Service层做一个统一的状态流转方法,每次更新状态前先校验当前状态是否允许该操作,不允许就直接抛异常提示。这样既防止了用户通过伪造请求把订单状态从“待付款”直接改成“已完成”,也让后续维护状态管理逻辑时不用到处找散落的update语句。
3. 关键代码实现与开发过程复盘
3.1 开发环境与项目工程结构
先把搭建环境列出来,这些是经过实测比较稳妥的组合:JDK 8 + Maven 3.6 + MySQL 5.7 + Tomcat 8.5 + IDEA。JDK 8是SSM项目最舒服的版本,Spring 5.x对JDK 8支持完美,Tomcat 8.5对应Servlet 3.1规范,也够JSP使用。MySQL 5.7是考虑到很多教学环境还在用这个版本,如果本地装的是MySQL 8.0,记得驱动包用8.x版本并且JDBC连接串要加上时区参数,不然会报时区错误。
工程结构建议采用Maven的标准分层:
code复制ssm-basketball-mall
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com.xxx.mall
│ │ │ ├── controller
│ │ │ ├── service
│ │ │ ├── mapper
│ │ │ ├── pojo
│ │ │ ├── common
│ │ │ └── interceptor
│ │ ├── resources
│ │ │ ├── jdbc.properties
│ │ │ ├── spring-mybatis.xml
│ │ │ ├── spring-mvc.xml
│ │ │ └── mapper
│ │ └── webapp
│ │ ├── WEB-INF
│ │ │ ├── web.xml
│ │ │ └── views
│ │ └── static
│ └── test
一个容易被忽略但很重要的点是:mybatis的mapper接口和mapper XML文件要放在同一个包路径下,并且在spring-mybatis.xml里配置mapper-locations来指定XML的位置。很多人喜欢把XML单独扔到某个文件夹,结果运行时一直报“Invalid bound statement (not found)”,其实就是Mapper接口和XML没对应上。
3.2 购物车与订单核心链路代码拆解
购物车的核心是“加购 - 勾选 - 结算”这条链路。前端在商品详情页点“加入购物车”时,通过Ajax把skuId和数量发给后端,Controller接收后交给Service处理:查询SKU是否存在、库存是否充足,再查购物车是否有相同SKU记录,有则累加数量,没有则新增记录。下面这段是加购Service层的关键逻辑,佐以注释便于理解:
java复制public Result addCart(Long userId, Long skuId, Integer quantity) {
Sku sku = skuMapper.selectByPrimaryKey(skuId);
if (sku == null) {
return Result.error("商品规格不存在");
}
if (sku.getStock() < quantity) {
return Result.error("库存不足");
}
CartItem exist = cartMapper.selectByUserIdAndSkuId(userId, skuId);
if (exist != null) {
exist.setQuantity(exist.getQuantity() + quantity);
cartMapper.updateByPrimaryKey(exist);
return Result.success("已更新购物车数量");
}
CartItem item = new CartItem();
item.setUserId(userId);
item.setSkuId(skuId);
item.setQuantity(quantity);
item.setChecked(true);
cartMapper.insert(item);
return Result.success("加入购物车成功");
}
这里有个业务细节:CartItem表的checked字段表示该商品在购物车中是否处于勾选状态。因为很多商城购物车不是一次性全部结算,用户可能今天只想买其中一双鞋,而不是把购物车里的篮球、护具、球衣一起下单。所以结算时只计算“被勾选”的条目,而不是所有购物车数据。这个点看起来不起眼,但在写结算接口的时候如果没这个字段,代码逻辑就会变得很别扭。
下单是整个系统最需要谨慎对待的环节,因为它涉及多张表的写操作:创建订单主表、创建订单明细表、扣减库存、清空购物车中已结算条目。任何一步失败都会造成数据不一致,所以必须加事务。Spring里最直接的方式就是在Service方法上标注@Transactional注解,并且指定rollbackFor为Exception.class,这样任何RuntimeException之外的受检异常也会触发回滚。库存扣减是高并发场景下的核心关注点,我这里用一个常见的防超卖写法:在扣减库存的SQL语句里加上stock >= 当前购买数量的条件,让数据库层面来保证不出现负数库存。
xml复制UPDATE product_sku
SET stock = stock - #{quantity}
WHERE id = #{skuId} AND stock >= #{quantity}
这样写的作用是:如果库存不够,更新语句影响行数为0,Java代码中判断update返回的int值,为0就抛异常触发事务回滚。比起先用select查库存再update的写法,这种做法在并发场景下更安全。虽然单机部署的项目并发量不会很大,但养成这种习惯对以后做高并发系统很有帮助。
3.3 登录拦截、权限校验与安全性细节
商城系统天然区分普通用户和管理员两种角色,这意味着需要一套权限控制机制。这个项目用的是SpringMVC拦截器(HandlerInterceptor)方案。在spring-mvc.xml中配置拦截规则:所有以/order/、/cart/、/user/开头的路径都要求登录,未登录则重定向到登录页;管理后台以/admin/开头的路径要求管理员权限。
拦截器里校验身份的逻辑其实就几行:从Session里取当前登录用户对象,取不到就说明未登录。但这里有一个很多人忽略的安全细节:后台管理员的权限校验不能只判断“Session里有没有人”,还要判断这个人是不是管理员。因为普通用户登录后Session里同样有用户信息,如果拦截器只做了非空判断,普通用户直接在地址栏输入/admin/product/list就可能进入后台。所以项目里的管理员拦截器要额外判断Session里的用户角色字段。
密码存储也是一个必须强调的点。早期很多Web项目的user表直接用MD5加密存密码,这在今天看来是不够安全的:MD5碰撞风险高,彩虹表攻击也很容易破解简单密码。这个项目建议的做法是MD5加盐:每个用户注册时生成一个随机盐值,将盐值和密码拼接后再做MD5,数据库同时存盐值和密文。虽然比不了BCrypt这类专业密码哈希算法,但在SSM教学项目里属于“比大多数课程设计规范”的做法。如果你想把安全性再往上提一档,可以自己研究一下Spring Security或者Shiro框架,但为了保持项目结构简洁,用拦截器加盐加密已经是性价比很高的方案了。
4. 调试过程与常见问题排查实录
4.1 环境搭建阶段的高频报错
我把调试过程中遇到频率最高的问题整理成下面这张速查表,每一项都是实操中真实踩过的坑:
| 现象 | 根本原因 | 解决方法 |
|---|---|---|
| 启动Tomcat后访问页面404 | 部署时没有配置正确的Application context | IDEA中Artifact的Application context设为 /,或用项目名访问 |
| MyBatis报Invalid bound statement | Mapper接口与XML文件路径不对应 | 确保XML放在resources/mapper目录,且namespace和接口全限定名一致 |
| 页面汉字全部乱码 | JSP和数据库、Tomcat编码不一致 | JSP统一UTF-8,Tomcat的URIEncoding设为UTF-8,数据库连接串加characterEncoding=utf8 |
| 静态资源CSS/JS加载失败 | SpringMVC前端控制器拦截了静态资源 | spring-mvc.xml配置<mvc:resources>放行/static/**路径 |
| 数据源连不上数据库 | MySQL版本与驱动版本不匹配 | MySQL 5.7用5.x驱动,8.0用8.x驱动并加serverTimezone参数 |
| 启动报ClassNotFoundException: jstl | pom中缺少JSTL依赖或版本冲突 | 添加javax.servlet:jstl:1.2依赖,并排除tomcat自带的jasper-el |
第一项404要重点展开说。IDEA中部署Web项目时,如果不小心把Deployment页面的Application context设置成了/ssm_war_exploded,那访问首页就必须带上一长串路径。很多人明明代码没问题,就是在这里折腾半天。建议直接改成根路径/,然后用http://localhost:8080/访问。另一个容易踩的坑是Tomcat热部署时JSP改动没生效,因为JSP的编译缓存机制比较特殊,有时候必须把work目录清掉再重启Tomcat。
4.2 业务逻辑层面的隐蔽Bug
还有一类问题不报错,但是跑出来的结果不对,这类问题最折磨人。我遇到过几个典型的:
第一个是金额计算精度问题。购物车结算和订单总额如果直接用double或者float计算,在涉及小数时会出现0.1 + 0.2 != 0.3的问题。虽然商城金额只有两位小数,但累计多个商品后误差会被放大。正确做法是金额字段在数据库用decimal类型,Java侧用BigDecimal进行所有运算。刚开始不分青红皂白全用double写的人,迟早会在对账环节发现平不了账。
第二个是修改购物车数量后库存溢出。前端传入的数量是通过input框手填的,用户完全可以输入一个1000,如果后端不加校验就拿去更新数量,库存就爆了。所以更新购物车数量时,后端必须再查一次SKU库存,当前端传入的数量大于库存时直接拒绝并提示。
第三个是未登录状态加购逻辑。前面提到过购物车未登录和登录状态地的合并问题。简单做法是不允许未登录加购,只能先登录再操作。有些同学想做得更完整一点,让未登录也能加购,把数据存在Cookie或localStorage里,那么登录成功后就要做合并:把本地购物车数据和数据库购物车按SKU维度合并,数量相加,重复项去重。这部分的坑在于:如果本地购物车里的SKU在数据库中已经下架或者删除了,合并时要做过滤,否则会出现空壳商品。
4.3 数据库与MyBatis的调试技巧
数据库层面的问题往往比代码层面更隐蔽。建议在项目调试阶段把MyBatis的SQL日志打印出来,这样你就能直观看到每条SQL实际执行了什么。在resources里加一个logback.xml或log4j.properties,把mapper包下的日志级别设为DEBUG:
properties复制log4j.logger.com.xxx.mall.mapper=DEBUG
这样控制台会打印每个Mapper方法对应的SQL语句和参数,排查“查出来的数据不是想要的”这类问题时效率翻倍。比如你发现订单列表里用户ID一直是同一个,一看日志发现SQL里的where条件没写USER_ID,或者参数没传给Mapper。这类问题通过看SQL日志基本一眼就能定位。
MyBatis还有一个高频踩坑点:多参数方法。如果你在Mapper接口里写了一个方法,参数是两个以上的普通类型参数,比如selectByUserIdAndStatus(Long userId, Integer status),XML里写#{userId}会报错,因为MyBatis不知道这个值对应哪个参数。解决方法有两个:一是用@Param("userId")注解标明参数名,二是把这些参数封装成一个对象。前者更常用,代码写起来也直白。
5. 二次开发与扩展建议:从课程作业到工程化项目
项目跑通之后,很多人的下一步是想让它在简历上或者答辩中更有分量。这时候就需要在现有骨架基础上做一些“锦上添花”的扩展,而不是另起炉灶。我个人建议优先做下面这几个方向。
第一,引入Redis做热点商品缓存。商城首页和商品详情页是访问量最高的页面,每次都查数据库会很吃力。可以引入Spring Data Redis,把热门商品信息在首次查询后写入Redis,设置缓存过期时间,比如10分钟。如果商品价格或库存发生了变更,还要考虑缓存失效策略——最简单的做法就是后台修改商品时主动删除对应缓存,下一次查询自动回源数据库再写入新缓存。这个“Cache Aside”模式是面试问答的高频考点。
第二,把管理后台的统计功能做出来。在后台加一个简单的数据看板:今日订单数、今日销售额、销量Top5商品。这些统计SQL本身不复杂,几条group by加order by就能写出来。但它的价值在于:让项目从“能卖东西”进阶到“能分析经营情况”,这也是很多优秀毕设和简历项目的常见区分点。
第三,订单支付从模拟走向接口化。目前项目里的支付是模拟的——点击支付按钮直接把订单状态改成已付款。如果你想在项目里对接真实支付接口,可以研究一下支付宝沙箱环境或者微信支付沙箱。注意,企业级支付接口需要应用ID和密钥,沙箱环境则比较宽松,适合个人开发者学习。这块的核心难度不在支付本身,而在于支付回调的处理逻辑:支付平台异步通知你的服务器支付结果,你要验证签名、修改订单状态、防止重复通知导致库存重复扣减。这个流程做下来,你对电商整个闭环的理解会上一个台阶。
第四,也是我愿意特别推荐的方向:做一次前后端分离改造。JSP版本跑通后,你已经理解了服务端渲染的模式。接着可以只保留后端提供的JSON接口,把前端换成一个Vue或React的单页应用。你会发现,后端Controller的返回值从ModelAndView变成@ResponseBody返回JSON,SpringMVC的这部分功能其实早就准备好支持这种模式了。改造过程会逼着你重新思考接口设计、跨域问题、登录状态怎么保持(从Session到Token),这些都是企业开发中的真实挑战。
写在最后的实操体会
这个项目从头到尾做下来,我自己最大的感受是:真正让它有价值的不是SSM框架本身,而是围绕篮球商城业务延伸出来的那些设计决策——SKU拆分的粒度、订单地址快照、状态机流转、事务与防超卖、权限拦截。很多初学者容易陷入“框架用法”的汪洋大海,背了一堆注解却不知道它们用在什么样的业务场景里。而这个项目的好处在于,它用一套足够真实的业务把这些知识点串了起来:你写@Transactional的时候知道自己是在保护一笔真实订单的完整链路,写@Param的时候知道自己在处理购物车查询的多参数绑定。
最后分享一个我在调试时养成的小习惯:每完成一个模块,比如购物车加购、订单创建、后台发货,就立刻用浏览器和数据库来回验证一遍数据的完整性。看到数据库里新增一条订单记录,明细表跟着生成对应条目,SKU库存相应减少,这种“全链路走通”的正反馈比任何教程都有说服力。如果你正准备拿这个项目做毕业设计或者求职作品,建议不要停留在“能跑就行”的层面,而是沿着本文第5部分提到的扩展方向挑一两个动手改造,把它真正变成你自己的作品。
