做毕设或者课程设计的时候,最怕的不是题目有多难,而是手里的项目跑不起来、讲不清楚。我见过太多人选了商城系统,结果答辩的时候被老师一问“你的库存扣减怎么防超卖”就卡住。所以今天这篇就拿一套能直接用的 Java 调料品网上商城系统来拆,从技术选型、数据库设计到核心逻辑实现、常见排坑,一次性捋清楚。这套项目我实际跑过,配套有完整源码和演示录像,你拿回去照着搭就能跑通,重点是里面的设计思路也能支撑你应付答辩。
1. 项目设计思路与技术选型
1.1 这套系统能解决的问题
这套调料品网上商城,本质是一个 B2C 的电商系统,只是把商品品类限定在了调味品这个垂直领域,比如酱油、醋、辣椒酱、火锅底料、十三香这类商品。对于毕设来说,垂直品类有一个很大的好处:业务模型足够清晰,但又不会像全品类商城那样功能膨胀。你不需要做秒杀、拼团、优惠券那一大堆营销玩法,把商品展示、购物车、下单、支付模拟、订单管理、后台管理这几条核心链路做扎实,就已经是一个完整度很高的项目。
从学习角度来说,它覆盖的知识点非常密集:Spring Boot 的后端分层架构、MyBatis-Plus 的持久层操作、JWT 的登录鉴权、Redis 的缓存使用、Vue 的前后端分离、订单状态机设计、库存并发控制。这些点每一个都是 Java 岗面试和毕设答辩的高频考点,所以它不只是一套“能交差”的毕设,更是一份可以写进简历的项目经历。
1.2 技术选型:为什么是 Spring Boot 单体架构
我直接说结论:毕设和课程设计场景,不要一上来就上微服务。很多同学觉得 Spring Cloud 那套东西听起来高端,但微服务带来的分布式事务、服务治理、网关配置,在单机演示环境下只会成为你跑不起来、讲不清楚的负担。
这套系统用的是标准的 Spring Boot + MyBatis-Plus + MySQL + Redis 单体架构,前端管理后台用 Vue 3 搭配 Element Plus,用户端则用 uni-app 做了一套可以编译到小程序和 APP 的代码。这个选型的好处很实际:
- Spring Boot 3 是目前的主流版本,简历上写出去匹配度高;
- MyBatis-Plus 极大减少手写 SQL 的工作量,分页查询、条件构造器这些功能对新手极其友好;
- Redis 用来做首页轮播图、热门商品的缓存,以及购物车的临时存储,属于“跳一跳够得着”的加分项,不会太难,但答辩有得讲;
- uni-app 一套代码同时覆盖 H5、微信小程序和 Android/iOS APP,直接响应了标题里“可做小程序APP”的要求。
有人可能会问:为什么不直接用 Sa-Token 或者 Spring Security?其实 JWT 就够了。对于商城系统这种场景,JWT 无状态、前端 localStorage 存 token、后端拦截器校验,逻辑直观,演示时也方便解释。Spring Security 当然更“正规”,但配置复杂度高出不少,对毕设来说性价比不高。
1.3 角色划分与核心业务链路
整个系统拆成三个端来看会非常清晰。
- 管理员端:登录后台,管理商品分类、维护商品信息(价格、库存、规格、图片)、处理订单发货、查看用户列表和销售统计。
- 用户端(PC/H5/小程序/APP):注册登录、浏览商品、搜索筛选、加购物车、提交订单、模拟支付、查看订单状态、确认收货。
- 后端服务端:提供 RESTful API,负责鉴权、业务逻辑、数据持久化、缓存处理。
核心业务链路就是一条线:用户登录 → 浏览/搜索商品 → 查看详情 → 加入购物车 → 提交订单 → 支付(模拟) → 商家发货 → 用户收货 → 完成。你把这个链路上的每一个环节都做通了,再把订单状态机梳理清楚,答辩的时候按着这条线讲,没有任何问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:调料品商城的核心表结构
2.1 调味品商品模型有什么特殊之处
商城系统的数据库设计,照着通用电商模型套就能覆盖九成场景。但既然做的是调料品垂直商城,有一个细节必须注意:商品规格和保质期。
调味品的规格五花八门。同样是酱油,有 500ml 瓶装、1L 桶装、袋装补充装,价格和库存都不一样。如果只设计一张 commodity 表,把规格写死在商品详情里,那库存和订单就会变得极其混乱。所以必须引入 SKU(Stock Keeping Unit,库存量单位)的概念:一个商品条目(SPU)下挂多个 SKU,每个 SKU 对应一个具体的规格和独立库存。
另外一个容易被忽略的点是保质期。调料品是食品,数据库里建议给商品加上 shelf_life(保质期)字段和生产日期字段,虽然不参与核心交易逻辑,但能让你的系统在业务完整性上比普通“图书商城”高出一个档次,答辩时也是一个小亮点。
2.2 核心数据表与关键字段
这套项目实际使用的核心表大致有以下这些,我列一下每张表的作用和关键字段,你建表的时候可以直接对照参考。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| category | 商品分类,支持两级分类 | id, parent_id, name, sort_order |
| commodity | 商品 SPU 表,描述一件商品 | id, name, category_id, main_image, description, status, shelf_life |
| sku | 商品 SKU 表,描述具体规格库存 | id, commodity_id, spec_name, price, stock, barcode, image |
| user | 用户表 | id, username, password, nickname, phone, avatar, create_time |
| cart_item | 购物车表 | id, user_id, sku_id, quantity, selected, add_time |
| address | 收货地址表 | id, user_id, receiver, phone, province, city, district, detail |
| orders | 订单主表 | id, order_no, user_id, address_id, total_amount, status, pay_time, delivery_time |
| order_item | 订单明细表 | id, order_id, sku_id, commodity_name, price, quantity, image |
| admin_user | 管理员表 | id, username, password, role, last_login_time |
这里面最值得细说的是 orders 表的 status 字段。我在项目中使用的是整数状态机:
- 0:待支付
- 1:已支付 / 待发货
- 2:已发货 / 待收货
- 3:已确认收货 / 已完成
- 4:已取消
这个状态机的优点是简单明确,每张订单在任何时刻只处于一个状态,后端用状态值做条件更新,前端根据状态值展示不同的操作按钮。比如状态为 0 时用户可以取消订单或去支付;状态为 1 时只有管理员能看到“发货”按钮;状态为 2 时用户能看到“确认收货”按钮。这种设计在答辩时就是标准的“订单状态机设计”考点。
2.3 商品、SKU、库存三者之间的关系
很多新手在写商城项目时,喜欢在 commodity 表里直接加一个 stock 字段,这是大忌。一旦商品有多个规格,库存就会对不上。正确做法是库存只挂在 sku 表上,commodity 表里的“总库存”是通过 SUM(sku.stock) 计算出来的,或者干脆不展示总库存,只展示每个规格的库存。
例如“海天生抽”这个商品,SPU 记录的是品牌、分类、描述这些公共信息,SKU 表里则是三行:
- 海天生抽 500ml / 12.9元 / 库存300
- 海天生抽 1L / 22.9元 / 库存150
- 海天生抽 1.9L / 32.9元 / 库存80
用户在前端看到“海天生抽”详情页,选择不同规格,实际选中的是某个 SKU,加进购物车的也是 SKU ID。下单时锁定的是 SKU 的库存,而不是 SPU 的库存。这个模型想通了,整个商城的数据骨架就立住了。
3. 后端核心模块实现与关键逻辑
3.1 基于 JWT 的登录鉴权
登录这块我用的是 JWT(JSON Web Token)。流程并不复杂:
- 用户提交用户名密码到 /api/user/login;
- 后端校验通过后,生成一段 JWT 字符串返回给前端;
- 前端把 token 存到 localStorage;
- 后续每次请求在 HTTP Header 里带上 Authorization: Bearer
; - 后端通过拦截器或过滤器统一校验 token,解析出用户 ID 后放行。
关键代码大致长这样:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
request.setAttribute("userId", claims.get("userId"));
return true;
} catch (Exception e) {
// token 无效或过期
}
}
response.setStatus(401);
return false;
}
}
这里有两个细节值得注意。第一,JWT 的 secretKey 一定不能硬编码在业务代码里,应该放到 application.yml 配置文件中,答辩时可以提一句“密钥配置在配置文件中,通过 @Value 注入”,显得你有安全意识。第二,拦截器要配置放行路径:登录、注册、商品列表、商品详情这些接口必须放行,否则用户不登录就什么都看不到了。我见过很多同学做前后端分离项目,最后卡在“为什么一打开页面全是401”,十有八九是拦截器路径没配好。
对于密码存储,项目里用的是 BCrypt 加密而非明文存储。BCrypt 是 Spring Security 里常用的加密算法,每次生成的哈希值都带随机盐,即使两个用户密码相同,存储的密文也不同,安全性比 MD5 高很多。这个点很小,但体现的项目规范程度是完全不一样的。
3.2 商品分类、搜索与分页
用户端的商品列表是整个系统访问量最高的接口,一定不能写得粗暴。这里我用的方案是 MyBatis-Plus 的分页插件加 LambdaQueryWrapper 动态条件构造。
java复制public PageResult<CommodityVO> queryCommodityPage(Integer pageNum, Integer pageSize,
Long categoryId, String keyword) {
Page<Commodity> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Commodity> wrapper = new LambdaQueryWrapper<>();
// 分类筛选
if (categoryId != null) {
wrapper.eq(Commodity::getCategoryId, categoryId);
}
// 关键词搜索,模糊查询商品名称
if (StringUtils.hasText(keyword)) {
wrapper.like(Commodity::getName, keyword);
}
wrapper.orderByDesc(Commodity::getCreateTime);
commodityMapper.selectPage(page, wrapper);
// 返回分页数据与 VO 转换
return convertToPageResult(page);
}
为什么要用 LambdaQueryWrapper 而不是手写 XML SQL?因为查询条件是动态拼接的:分类可能传也可能不传,关键词可能是酱油也可能是“辣”,如果手写动态 SQL,if 标签会写一大堆。LambdaQueryWrapper 通过 Java 代码链式拼接,可读性更好,也不容易出现 SQL 拼接错误。
还有一个细节:热门商品的缓存。首页展示的轮播图和热销商品是高频读、低频写的数据,每次查数据库没必要。我在 Redis 里放了一个 hot_commodity_list 的 key,缓存半小时,后台修改商品时可以删除对应缓存。这里要注意缓存和数据库的一致性问题,对于毕设来说,设置合理的过期时间 + 修改后主动删缓存,已经是够用的方案了。
3.3 购物车与下单的完整流程
购物车和下单是商城系统的核心交易链路,需要保证数据的一致性。
先看购物车。购物车表的核心字段就是 user_id + sku_id + quantity。加购的接口逻辑:
- 根据 userId 和 skuId 查购物车是否已有该商品;
- 有则数量累加,没有则新增一条记录;
- 返回购物车数量。
这里需要注意,购物车操作一定要带上 userId,不能只依赖前端传的 skuId。因为购物车是用户维度的数据,如果接口设计成前端把 skuId 传过来就能加,那别人只要知道你的接口地址,就可以伪造请求往自己的购物车加东西了。好在后端有 JWT 拦截器,可以从 token 里解析出 userId,这是比较标准的设计。
再看下单。订单这一块我用了一个比较稳妥的流程:
- 前端提交:地址 ID + 购物车选中的 SKU 列表(SKU ID 和数量);
- 后端接收请求后,先查一遍购物车,以服务端数据为准重新计算价格;
- 生成订单主表和订单明细表,OrderNo 用时间戳 + 用户ID + 随机数生成,保证唯一;
- 扣减 SKU 库存;
- 清空已下单的购物车记录。
这里最容易被问到的问题是:为什么不直接信任前端传的价格?答案很简单,前端传的价格可以被篡改。服务端必须从数据库读取商品价格,再根据数量重新计算总金额。你把这一点做好,答辩的时候跟老师说“我的下单金额是服务端计算的,没有信任前端数据”,这就是一个非常加分的回答。
3.4 防超卖:乐观锁扣库存的写法
库存并发控制是商城系统最经典的高频面试题。场景是这样的:两个用户同时买最后一件商品,如果代码写成“先查库存,再扣库存”,两个请求都读到库存为 1,都通过判断,然后都执行 update 减 1,库存就变成 -1 了,这就是超卖。
我在项目里用的是乐观锁方案,也就是在扣库存的 SQL 语句里加库存条件:
sql复制UPDATE sku
SET stock = stock - #{quantity}
WHERE id = #{skuId} AND stock >= #{quantity}
注意最后这个 AND stock >= #{quantity},它保证了只有库存充足时更新才会成功,影响行数为 1;如果库存不足,影响行数为 0,业务层根据返回结果判断并抛出库存不足的异常。这种方式不需要加数据库悲观锁,也不需要分布式锁,在单机部署的毕设项目里完全够用,而且思路非常清晰。
事务也要加上。下单、生成订单明细、扣库存、清购物车,这四步必须放在同一个事务里,任何一个环节失败都要整体回滚。很简单,在 Service 方法上加上 @Transactional 注解就行。Spring 的声明式事务默认在遇到 RuntimeException 时回滚,所以业务异常建议继承 RuntimeException,避免出现“更新数据库成功但业务中途中止”的问题。
4. 前端页面与管理后台
4.1 用户端核心页面拆解
用户端我把它拆成 6 个页面,这也是绝大多数商城项目的标准页面结构:
- 首页:轮播图、热门商品推荐、分类入口;
- 商品列表页:左侧分类栏 + 右侧商品列表,支持按销量和价格排序;
- 商品详情页:轮播图、商品名称、价格、规格选择、库存显示、数量加减;
- 购物车页:勾选商品、修改数量、删除、计算合计;
- 订单确认页:选择收货地址、展示商品明细、提交订单;
- 个人中心:待付款、待发货、待收货、全部订单、收货地址管理。
页面虽然多,但前端逻辑不难。商品列表页通过 Vue Router 接收 categoryId 参数,请求后端接口时带上这个参数;商品详情页通过路由参数拿 commodityId,然后请求详情接口,同时拉取该商品下的所有 SKU 列表,用户点击不同规格时切换价格和库存。
这里要给新手一个建议:前端在渲染商品价格时,一定要以 SKU 维度的数据为准。很多项目为了省事,把最低价直接放在商品列表接口里返回,详情页打开后商品价格却变来变去,这种细节很容易被答辩老师挑刺。正确做法是列表页展示“起售价”,详情页展示用户选择的 SKU 的具体价格。
4.2 管理后台的商品与订单管理
管理后台用的是 Vue 3 + Element Plus,整体界面就是经典的后台管理系统布局:左侧菜单栏,右侧内容区。
商品管理这个模块包含:商品列表(支持搜索、上下架)、新增商品(需要填写 SPU 信息 + 动态添加多个 SKU)、编辑商品、删除商品。新增商品的时候,SKU 列表是动态增减的,前端用一个数组来绑定表单,每行包括规格名称、价格、库存、条码。提交时把 SPU 数据和 SKU 数组一起通过 JSON 传给后端,后端分批插入数据。
订单管理这个模块,管理员的权限主要就是“发货”。用户付款后,订单状态变为“待发货”,管理员在后台看到订单详情,点击发货按钮后订单状态置为 2,同时写入发货时间。这里要注意权限控制:普通用户接口和管理员接口在拦截器上要做区分,比如 /api/admin/** 的路径要求 token 解析出的角色为 ADMIN,否则一律 403。
4.3 小程序/APP 端的适配与打包
标题里写了“小程序APP”,所以用户端我用的是 uni-app 框架。uni-app 的好处是编写一套 Vue 语法代码,通过 HBuilderX 可以分别编译成微信小程序和 Android/iOS APP。在开发阶段,所有接口可以直接请求本地后端的 localhost,等真机调试时再把 baseURL 改成电脑的局域网 IP。
如果你要用这套代码做小程序端演示,有一个点必须注意:微信小程序要求所有请求域名都必须备案并在小程序后台配置 request 合法域名,本地开发时可以勾选“不校验合法域名”,但真机预览和线上发布必须要用 HTTPS 的正式域名。毕设演示阶段用开发者工具的“不校验”选项即可,但一旦涉及演示录像的正式展示,建议提前确认好这一项。
APP 端的话,用 HBuilderX 云打包生成 apk 安装包是比较省事的方案,不需要本地配置复杂的 Android 开发环境。首次云打包需要注册 DCloud 账号,打包排队时间一般在几分钟到十几分钟不等,建议提前做,不要等到答辩前一天才开始。
5. 本地复现、常见问题与答辩建议
5.1 本地环境准备
要把这套项目跑起来,你本机需要准备这么几样东西:
- JDK 1.8 或更高版本,我推荐 JDK 8 或者 JDK 17,这两个在 Spring Boot 3 和旧版框架项目里兼容性最好;
- Maven 3.6+,用于拉取项目依赖;
- MySQL 5.7 或 8.0,用于数据存储;
- Redis(Windows 版本可以直接用压缩包解压运行,不需要安装);
- IDEA 或 Eclipse,IDEA 社区版就够用;
- HBuilderX(如果要改小程序端 / APP 端);
- 微信开发者工具(如果要跑小程序端)。
JDK 安装完一定要配置环境变量 JAVA_HOME,把 %JAVA_HOME%\bin 加到 Path 里。我遇到过很多同学下载了 JDK 但命令行 java -version 不认,十有八九是环境变量没配。配置完环境变量后,记得新开一个终端窗口再验证,因为环境变量的修改不会自动刷新到已打开的终端。
5.2 从源码到跑起来需要做的四步
拿到源码之后,完整的启动流程其实只有四步,但每一步都可能踩坑。
第一步,导入数据库。用 Navicat 或命令行执行项目里的 sql 脚本,创建数据库和表结构,注意核对脚本里的数据库名称是否和 application.yml 中配置的一致。
第二步,修改配置。打开 application.yml,把 MySQL 的用户名密码、Redis 的地址端口改成自己本机的。如果 MySQL 密码是 123456,你就填 123456,重点是别改错文件,我见过有人改了 application-test.yml 结果发现根本没生效。
第三步,启动 Redis。Windows 下双击 redis-server.exe 就行,Mac /Linux 下执行 redis-server 命令,注意默认端口 6379 不要被占用。
第四步,运行后端主类。在 IDEA 中右键运行 Application 主类,看到控制台输出“Started Application in x.xxx seconds”就代表后端启动成功了。然后启动前端,Vue 项目在终端执行 npm install 安装依赖,再执行 npm run dev 启动开发服务器,浏览器访问 localhost:5173 即可。
5.3 高频报错与排查速查表
我把这套系统跑起来过程中最常见的几个问题整理一下,遇到类似的直接对照排查。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 后端启动报 Access denied for user | MySQL 用户名或密码不对 | 检查 application.yml 里数据库账号密码 |
| 后端启动报 Unknown database | 数据库没创建成功 | 确认已执行建库语句,数据库名与配置一致 |
| 前端 npm install 报错 | Node 版本过低或镜像源问题 | 使用 Node 16+,设置淘宝镜像源后重装 |
| 前端请求接口全部 404 | 后端没启动或端口不一致 | 确认后端已启动,检查前端 baseURL |
| 请求接口报 401 | 未登录或 token 过期 | 重新登录,检查拦截器放行路径配置 |
| 中文乱码 | 数据库连接未指定 UTF-8 | 在 JDBC URL 加 characterEncoding=utf8 |
前端跨域问题也要提前说清楚。前后端分离项目,前端跑在 5173 端口,后端跑在 8080 端口,如果不做任何处理,浏览器会拦截跨域请求。后端的解决方案是配置一个 CorsFilter 全局允许跨域,或者写一个 WebMvcConfigurer 的实现类来允许指定源。如果你用的是旧版方案,在 Controller 上加 @CrossOrigin 注解也可以,但一个个加太麻烦,全局配一次更省事。
5.4 毕业设计答辩时可以讲的亮点
这套系统拿去答辩,有几个点是天然的加分项。
第一个是 JWT 无状态鉴权。可以重点讲“服务端不需要存储会话,token 自包含用户信息,减轻了服务端压力”,但也要能回答出 JWT 的缺点,比如无法主动失效,这反而能体现你的深入理解。
第二个是乐观锁防超卖。这是最经典的高并发问题,讲清楚“UPDATE stock SET stock = stock - 1 WHERE id = ? AND stock >= 1”这一条 SQL 的原理,老师通常会追问“如果库存是负数还需要其他手段吗”,只要能回答出“可以配合数据库层唯一约束或引入消息队列做异步削峰”,就已经超出大部分同学的深度了。
第三个是订单状态机设计。讲清楚五个状态的流转条件、不同角色能执行的操作,配合前端按钮的展示逻辑,整体上就很完整。
第四个是 Redis 缓存的应用。首页热点数据缓存、购物车会话缓存,都是常见的缓存场景,能答出“缓存穿透、缓存击穿、缓存雪崩”这几个概念的基本防护手段即可。
如果还有余力,建议把项目里的商品搜索从 MySQL 的 LIKE 查询,尝试替换成 Elasticsearch 或者直接用 Redis 的 key 做前缀匹配,哪怕只是提一句“后续扩展方向”,都能让答辩老师觉得你有工程视野。
我个人在实际带项目的过程中,最深的体会是:商城系统这种题目一点都不虚,它的技术含量取决于你把哪一块做透。如果只是把所有功能都草草做一遍,那就是个 CRUD 项目;但如果把 JWT、乐观锁、事务、状态机这些点讲透,它就是一份含金量很高的 Java 后端项目经验。这套源码我建议拿到手以后,先不要急着改功能,按上面的思路把核心代码读一遍,再自己动手跑起来,遇到问题再回来看这篇文章的排查表,基本就能吃透了。
