☰
本地版生鲜电商源码:Spring Boot前后端分离完整部署指南
2026/10/18 15:35:58 网站建设 项目流程

简介:慕慕生鲜项目是一套面向生鲜电商场景的本地化源码工程,适合开发者用于学习 Spring 技术栈、理解商品、订单、购物车等核心业务模块,也可作为教学案例或二次开发基础。压缩包共 434 个文件,约 14.11MB,主要包含 Java 源码、XML 配置、HTML 页面、JS/CSS 前端资源、图片与 SQL 脚本,并附带 Maven 构建脚本、pom.xml 及 IntelliJ IDEA 工程配置,解压后即可在本地完成编译、调试与启动。已有 1021 人学习下载。从内容预览看,项目覆盖用户、商品分类、购物车、订单等典型生鲜电商链路,包含 Controller、Service、Mapper 等分层实现,适合作为 Spring Boot 或 Spring MVC 项目的练手素材;同时 target 目录与 class 文件也保留了构建产物,便于对照代码检查编译结果。无论是学生课程设计、初级开发者入门,还是团队内部复盘,都能从中获得完整可运行的电商后端参考骨架。

1. 项目定位:一份能直接跑起来的本地版生鲜电商源码

做过电商系统的人都知道,生鲜项目比普通商城麻烦的地方不在业务复杂度,而在状态流转和库存扣减。这份慕慕生鲜的本地版源码,解决的不是“怎么设计一个生鲜平台”,而是“怎么在没买云服务器、没配域名的时候,把一整套前后端分离的商城系统在自己电脑上完整跑起来”。它自带管理后台和用户端,商品上架、分类、购物车、下单、订单跟踪、库存扣减都有现成的实现。适合刚转做电商方向的后端开发,也适合毕设需要一套完整系统参考的同学。拿到手不用连生产环境,改完配置就能启动,学习和二次开发的成本都很低。

2. 整体架构与源码结构:从Spring Boot到Vue的本地闭环

2.1 技术栈与设计定位

先看技术选型。本地版不等于阉割版,它的前后端分离结构是完整的。后端基于 Spring Boot,持久层用的是 MyBatis Plus,数据落在 MySQL,缓存走 Redis,前端是 Vue + Element UI 的管理后台混合开发模式。这套组合在中小型电商项目里出现频率很高,原因很实在:Spring Boot 适合快速搭接口,MyBatis Plus 把单表 CRUD 的样板代码压缩到最少,Vue + Element UI 能直接拿来当后台管理页面的组件库。

本地版的核心定位是“可运行、可调试、可扩展”。它在设计上没有引入微服务和分布式事务,而是把所有模块收拢在一个工程里,服务间通过本地接口调用,这样不会出现“代码看着合理但一启动就缺依赖”的问题。但数据模型和接口设计仍然保留了生产项目的习惯,比如订单表拆主表和明细表、商品表带 SKU 维度,后面如果真要上生产,改造成分布式部署时不用推翻重来。

关于 JDK 版本,比较稳的选择是 JDK 8,配 Spring Boot 2.x。因为生鲜项目里大量用到 MyBatis Plus 的分页插件和 Redis 缓存注解,Spring Boot 3 的项目在这类源码包里还比较少见,JDK 8 的生态兼容性最好,也最不容易在本地环境上翻车。

2.2 源码目录结构拆解

拿到源码后先别急着启动,把目录结构过一遍,能省掉后面很多找代码的时间。本地版的目录组织通常是按“后端服务 + 前端页面 + 数据库脚本”三层来放的。

fresh-food/ ├── backend/ │ ├── src/main/java/com/mumu/ │ │ ├── controller/ # 接口层,接收HTTP请求 │ │ ├── service/ # 业务逻辑层,事务边界在这里 │ │ ├── mapper/ # MyBatis Plus的Mapper接口 │ │ ├── entity/ # 数据库实体类 │ │ ├── config/ # 跨域、Redis、拦截器配置 │ │ └── common/ # 统一返回结果、异常处理、工具类 │ ├── src/main/resources/ │ │ ├── mapper/ # XML形式的复杂SQL │ │ └── application.yml # 数据源、Redis、端口配置 ├── frontend/ │ ├── admin/ # 管理后台,Vue项目 │ └── app/ # 用户端,Vue项目 └── sql/ └── fresh_food.sql # 建库建表脚本,含初始数据

controller 目录里能看到典型模块划分:有商品模块、类目模块、购物车模块、订单模块、用户模块和地址模块。每个模块的 controller、service、mapper 三件套是配齐的。重点看订单模块,它包含了下单接口、订单列表接口、订单取消接口和对支付回调的模拟接口。在本地版里支付回调往往是写死的模拟逻辑,因为不可能真的接微信支付沙箱,这个后面改造时要留意。

配置文件 application.yml 是所有启动问题的源头,后面第 4 章会详细讲怎么改。这里只需要记住一个原则:拿到源码先看 resources 目录里的 mapper 文件夹,如果里面有大量 XML 文件,说明这个项目的复杂 SQL 集中在 XML 里,改动报表类需求时的入口就在这里。

2.3 核心数据模型:生鲜订单为什么这么设计

生鲜电商和普通电商的数据库设计差异不在商品表,而在订单和库存的关联方式。生鲜商品有保质期、按重量计价、可能拆成分装单位,所以商品表和 SKU 表必须分离。看建表脚本,核心表有这么几张:

表名作用关键字段
product商品主表id、name、category_id、cover_img
product_sku商品规格表id、product_id、spec_name、price、stock
user_address收货地址表id、user_id、receiver、phone、detail
order_master订单主表id、order_no、user_id、total_amount、status
order_item订单明细表id、order_id、product_id、sku_id、quantity、price

order_master 和 order_item 是一对多的关系,拆两张表的原因很直接:一个订单可能包含多件不同商品,而订单状态只需要在主表上维护一份。明细表记录下单当时的商品快照,包括商品名称、规格、价格、数量。注意明细表不会去关联现在的 product 表查价格,因为在生鲜场景里价格波动是常态,下单后如果再查商品表,可能出现订单金额对不上的问题。

库存设计这里有个值得学习的地方。product_sku 表里的 stock 字段是唯一库存依据,下单扣减时用乐观锁做条件更新。生鲜商品损耗率高,经常要调整库存,用乐观锁比用 Redis 预扣库存更适合本地版,因为在没有缓存依赖的情况下,事务回滚更干净。建表脚本里对 order_master.status 字段用数字表示订单状态,0 待支付、1 已支付、2 配送中、3 已完成、4 已取消,这个约定在代码里会反复用到。

3. 核心模块拆解:一个订单从选购到结算的完整链路

3.1 商品列表接口:缓存与库存的取舍

用户端第一个打到的接口是商品列表。生鲜商品列表有个特殊性:同一件商品有多个 SKU,比如“1kg装”和“5kg装”,前端要看到的是商品维度,详情页才展开 SKU。所以列表接口返回的是 product 表数据,并且会带上一个可用的最低价格,用来在列表页显示“起购价”。

/** * 商品列表接口 * 支持按分类筛选和关键字搜索 */ @GetMapping("/product/list") public Result<List<ProductVO>> list(@RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") Integer page) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getStatus, 1) .eq(categoryId != null, Product::getCategoryId, categoryId) .like(StringUtils.isNotBlank(keyword), Product::getName, keyword) .orderByDesc(Product::getUpdateTime); // 分页查询,pageSize固定为20 IPage<Product> productPage = productMapper.selectPage( new Page<>(page, 20), wrapper); List<ProductVO> result = productPage.getRecords().stream().map(product -> { ProductVO vo = new ProductVO(); BeanUtils.copyProperties(product, vo); // 查询当前商品的库存和最低价 ProductSku minSku = skuMapper.selectOne( new LambdaQueryWrapper<ProductSku>() .eq(ProductSku::getProductId, product.getId()) .orderByAsc(ProductSku::getPrice) .last("limit 1")); vo.setMinPrice(minSku.getPrice()); vo.setTotalStock(skuMapper.selectList( new LambdaQueryWrapper<ProductSku>() .eq(ProductSku::getProductId, product.getId())) .stream().mapToInt(ProductSku::getStock).sum()); return vo; }).collect(Collectors.toList()); return Result.success(result); }

这段代码的逻辑点在前端展示和后端查询之间做了取舍。没有直接缓存整个列表,因为生鲜商品的库存和价格变化频繁,缓存命中率低,反而容易让用户看到过期价格。常见做法是只在极端流量场景下对首页做短时间缓存,比如 Redis 缓存 30 秒。参数说明:categoryId 和 keyword 都允许为空,为空时不作为过滤条件,这是 MyBatis Plus 的条件构造器设置 eq 第一个参数为 boolean 的效果;page 参数默认值是 1,避免前端不传页码时数据落不了分页。

3.2 下单接口:事务、库存扣减与订单号生成

商品加入购物车之后,最后的关键一步是提交订单。下单接口是整份源码里包含最多设计细节的代码,也是面试官最常追问的模块。本地版的实现用的是数据库事务加乐观锁,代码逻辑可以拆成三个步骤:生成订单号、插入订单主表和明细表、扣减 SKU 库存。这三个步骤必须在一个事务里完成。

@Transactional(rollbackFor = Exception.class) public String createOrder(OrderCreateRequest request) { // 校验收货地址属于当前用户 UserAddress address = addressMapper.selectById(request.getAddressId()); if (address == null || !address.getUserId().equals(request.getUserId())) { throw new BizException("收货地址不存在"); } // 生成订单号,规则:yyyyMMddHHmmss + 用户ID后四位 + 随机四位 String orderNo = generateOrderNo(request.getUserId()); OrderMaster master = new OrderMaster(); master.setOrderNo(orderNo); master.setUserId(request.getUserId()); master.setStatus(0); // 遍历明细,计算总金额并检查库存 int totalAmount = 0; List<OrderItem> items = new ArrayList<>(); for (OrderItemRequest item : request.getItems()) { ProductSku sku = skuMapper.selectById(item.getSkuId()); if (sku == null || sku.getStock() < item.getQuantity()) { throw new BizException("商品库存不足"); } OrderItem orderItem = new OrderItem(); orderItem.setOrderNo(orderNo); orderItem.setProductId(sku.getProductId()); orderItem.setSkuId(sku.getId()); orderItem.setPrice(sku.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setProductName(sku.getSpecName()); // 金额保留两位小数,先按分计算再转回元 totalAmount += sku.getPrice() * item.getQuantity(); items.add(orderItem); } master.setTotalAmount(totalAmount); orderMasterMapper.insert(master); // 逐条写入明细 items.forEach(orderItemMapper::insert); // 扣减库存,用乐观锁条件防止超卖 for (OrderItem item : items) { ProductSku sku = skuMapper.selectById(item.getSkuId()); int rows = skuMapper.updateStock(item.getSkuId(), sku.getStock() - item.getQuantity()); if (rows == 0) { throw new BizException("库存扣减失败,请重试"); } } return orderNo; }

事务注解上事务边界是整个方法的默认边界,出现任何异常都会整体回滚。库存扣减用了一个自定义 SQL 方法 updateStock,它的关键点不是计算差值,而是 WHERE 条件里带上原来的库存值,这样并发环境下后提交的请求会因为匹配不到数据而返回影响行数 0,从而触发异常回滚。这是乐观锁的常见实现,不锁表,性能损耗小。订单号用时间加用户 ID 加随机数拼接,避免出现重复订单号,这个规则在本地足够用,真正的生产环境可以换成雪花算法。

3.3 订单状态机的推进:支付回调和超时取消

订单创建之后状态为 0,也就是待支付。生鲜订单最怕的不是用户不付钱,而是用户不付钱但库存被占用。源码里实现了一个定时扫描逻辑,每五分钟扫描一次超过三十分钟仍未支付的订单,将其自动取消并回滚库存。

@Component public class OrderTimeoutTask { @Scheduled(fixedDelay = 300000) public void cancelTimeoutOrders() { // 查询超过30分钟仍然待支付的订单 LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(30); List<OrderMaster> orders = orderMasterMapper.selectList( new LambdaQueryWrapper<OrderMaster>() .eq(OrderMaster::getStatus, 0) .lt(OrderMaster::getCreateTime, cutoffTime)); for (OrderMaster order : orders) { // 更新订单状态为已取消 orderMasterMapper.updateStatus(order.getOrderNo(), 4); // 回滚明细对应的库存 List<OrderItem> items = orderItemMapper.selectList( new LambdaQueryWrapper<OrderItem>() .eq(OrderItem::getOrderNo, order.getOrderNo())); for (OrderItem item : items) { skuMapper.rollbackStock(item.getSkuId(), item.getQuantity()); } } } }

这个定时任务用的是 Spring 自带的 @Scheduled,fixedDelay 单位是毫秒,所以 300000 表示五分钟执行一次。运行时必须保证应用没有被多副本部署,否则会出现多台机器重复取消同一笔订单。本地版不在意这个,但改造上生产时要注意,常见做法是加一个分布式锁或者把定时任务独立成一个服务。回滚库存的前提是订单明细还在,而且是按当前库存做增加,不会因为商品后来被改价而受影响。

4. 本地部署与启动:从零把源码跑起来

4.1 环境准备和版本对齐

先把运行环境准备好,版本不要乱用,这是最省事的办法。后端需要 JDK 8 以上和 Maven 3.6,数据库用 MySQL 5.7 或 8.0 都行,缓存需要 Redis,版本不限但必须启动,前端基于 Vue 2 项目,需要 Node 14 到 16 的版本。Node 版本太高会出现 webpack 编译报错,这是 Vue 2 项目的老问题,后面避坑里会专门讲。

组件推荐版本用途
JDK1.8Spring Boot 运行的底层环境
Maven3.6+管理后端依赖
MySQL5.7 / 8.0业务数据落库
Redis5.x+缓存和登录态存储
Node14.x / 16.x前端编译运行

本地版的好处在这一步就能体现出来,不需要安装 Docker,不需要配置 Nginx,全部用开发模式启动。数据库安装在默认端口 3306,Redis 如果没有改过配置,默认端口 6379,密码为空,源码里的配置可以直接连上。

4.2 数据库初始化与配置文件修改

启动服务之前,先初始化数据库。用 Navicat 或命令行执行 sql 目录下的 fresh_food.sql 脚本,这个脚本会创建 fresh_food 数据库,并把表结构和测试数据一起写入。执行完之后,打开后端项目的 application.yml 文件,修改两处地方。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fresh_food?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 password: database: 0 servlet: multipart: max-file-size: 20MB max-request-size: 40MB

datasource 配置中,username 和 password 要写成自己本机 MySQL 的用户名密码。url 参数里的 characterEncoding=utf8 是保证中文不乱码的关键,useSSL=false 是避免 MySQL 8.0 连接时报 SSL 告警。Redis 的密码如果本地没设置,就留空一个空格,不要写password: null,那样会被当成字符串处理。multipart 配置是针对生鲜商品图片上传的,本地测试时页面会一次性传多张图,所以把上传上限调大了一些。

4.3 后端启动步骤

后端是标准的 Spring Boot 工程,启动方式没有花活。命令行进入 backend 目录,先执行 Maven 打包,再启动主类。如果你用的是 IDEA,直接打开 backend 文件夹,等 Maven 加载完依赖后运行 FreshFoodApplication 这个入口类即可。如果你不习惯 IDE,可以走命令行:

cd backend mvn clean package -DskipTests java -jar target/fresh-food-1.0.0.jar

打包参数-DskipTests是跳过单元测试,本地起服务没必要跑一遍测试套件。启动成功的标志是控制台出现Tomcat started on port(s): 8080这一段日志。如果你的机器上别的程序占用了 8080 端口,控制台会直接报Port 8080 was already in use,解决方式有两种:关掉占用端口的进程,或者把 application.yml 里的 server.port 改掉。改端口之后前端项目的代理配置也要跟着改,这个要记住。

4.4 前端启动与访问

前端有两个项目,先启动管理后台,在 frontend/admin 目录下执行两条命令:

npm install npm run dev

npm install 首次执行会下载几百兆依赖,耗时长短取决于网络。装完依赖后 npm run dev 会把项目跑起来,默认监听 9527 端口,浏览器访问 http://localhost:9527 打开管理后台登录页。用户端在 frontend/app 目录下,同样执行 npm install 和 npm run dev,访问 http://localhost:9528 看到的是面向消费者的商城界面。

管理后台登录账号一般在 sql 脚本的初始化数据里有,默认账号和密码通常是 admin/123456。在本地版里,登录态通过 Redis 保存,所以部署顺序最好按“先 Redis,再 MySQL,再后端,最后前端”来。前端项目的请求代理配置在 vue.config.js 里,如果你后端改了端口,前端这里也要同步修改。

5. 避坑指南:本地跑生鲜项目最常见的五个坑

5.1 控制台和数据库的中文乱码

现象:接口返回的数据里,商品名称和管理后台的菜单名都显示成问号,或者数据库里存的直接是乱码字符串。

原因:数据库连接 URL 没有指定字符集,MySQL 默认的 client 字符集不是 UTF-8,导致写入和读取的编码不一致。代码本身是 UTF-8 的,所以问题一定出在连接配置上。

解决:给 application.yml 里的 datasource URL 末尾加上characterEncoding=utf8,并确认 MySQL 建库的时候用的字符集是 utf8mb4。修改后重启后端服务,把已经乱码的测试数据删掉重新插入。

5.2 前端请求接口一直报跨域

现象:浏览器控制台出现Access to XMLHttpRequest at 'http://localhost:8080/...' has been blocked by CORS policy,页面拿不到任何数据。

原因:前端页面跑在 9527 端口,后端接口跑在 8080 端口,属于跨源请求。如果源码里没有全局跨域配置,这类请求会被浏览器拦截。

解决:在本地开发环境下,正确解法是看前端项目的 vue.config.js 里有没有 devServer.proxy 配置。如果有,保证前端请求的 baseURL 是/api,这样浏览器发出的请求是同源的,再由 Node 代理转发到后端的 8080。如果源码没有代理配置,也可以在后端加一个跨域过滤器,用代码统一放开,但改动后端不如配置前端代理干净。注意这里说的代理不是网络代理,是前端开发服务器的转发机制,别混淆概念。

5.3 npm install 报 ERESOLVE 错误

现象:在 frontend 目录执行 npm install 时,控制台抛出大量ERESOLVE unable to resolve dependency tree的报错,依赖装不上来。

原因:Node 版本过高。npm 7 及以上对依赖冲突的处理策略更严格,而项目中某个第三方包存在 peerDependencies 版本冲突,旧版 npm 会跳过警告,新版直接中断。

解决:用 nvm 切换 Node 版本到 14.x,然后清理缓存重装。命令行里执行nvm use 14,删除 node_modules 文件夹和 package-lock.json,重新执行 npm install。这类错误和源码质量无关,属于环境兼容性问题,也是本地版最常见的启动障碍。

5.4 启动后端时 Redis 连接不上

现象:后端启动日志刷出一连串Unable to connect to Redis, 然后控制台卡住一段时间后整个服务启动失败。

原因:Redis 服务没有启动,或者 Redis 端口不是默认的 6379。本地版缓存依赖 Redis 保存登录状态,连不上它就会启动不了项目。

解决:按操作系统的启动方式把本机 Redis 启动起来,Windows 平台直接执行 redis-server.exe 看到版本号输出就可以。如果 Redis 设置了密码,要把密码填到 application.yml 的 spring.redis.password 字段,否则 Spring Boot 的 Redis 自动配置会创建一个空密码的连接工厂,握手失败。判断 Redis 是否正常也可以在命令行执行redis-cli ping,返回 PONG 就代表服务可用。

5.5 上传商品图片后详情页图片不显示

现象:管理后台选择图片上传,提示上传成功,但前台的商品图片区域显示一个破损图标的占位图。

原因:本地版没有配置专门的静态资源访问路径,图片被写入了本地磁盘的某个目录,但前端域名解析不到这个文件路径。浏览器实际上发出了请求图片的 HTTP 请求,返回 404。

解决:在 application.yml 里配置静态资源映射,把本地磁盘目录映射成/images/**的访问路径。图片上传模块推荐使用本地存储而不是传到日志目录,否则系统重启后图片可能被清掉。配置完以后,前端只需要输出相对路径/images/xxx.jpg,回显就能正常。如果前端代码里拼接的是绝对地址,还要检查上传回显接口返回字段和前端使用的字段名是否统一。

6. 二次开发改造:把本地版改成多商户平台的三个切入口

6.1 从本地版到多商户:把shopId从业务中提出来

如果要把这套源码改成一个多商户入驻的生鲜平台,最大改动点不在页面,而在数据隔离方式。当前所有商品和订单都在一张表里,对应的是一个商户维度。改造的通常做法是增加 shop 表以及相应的商户 ID 字段,让商品表持有 shop_id,并确保所有查询都带上入驻商户隔离条件。订单主表里也要增加 shop_id 字段保存到商户维度,方便每个商户只看到自己的订单数据。

改动顺序建议是先加表、后加字段、再改 Mapper 查询条件。不要一开始就去改造所有前端页面,商品接口后端改完后,前端管理后台增加一个商户选择下拉就能跑通初步模型。

6.2 增加小程序端需要改什么

现在前端用户端是基于 Vue 的 Web 页面,如果要换成微信小程序,后端接口是可以直接复用的。小程序请求方式和 Web 请求差异在于登录态认证,本地版用的是 header 传 token,小程序也支持这种方式,改造时只需要把小程序端的登录回调替换成自己的用户体系,并保证请求拦截器携带 token。订单相关接口无需改动,生鲜业务核心逻辑全部在后端,前端只是展示层,这一点得益于前后端分离的架构,你不需要重写任何下单事务代码。

6.3 验证改动的快速回归清单

改造完之后自己心里要有一张回归清单:至少跑一遍商品列表、商品详情、加购、下单、取消订单、重新上架这个链路。另外,可执行一段 SQL 去验证库存是否只被扣减一次,多线程模拟并发下单的场景来确认超卖异常是否真的被拦截。我自己每次都先改一个无关紧要的字段,完整走一遍启动部署流程,确认链路无异常再开始动业务代码。从那以后我拿到任何源码,都强制先看一眼数据库初始化脚本有没有独立跑通,再打开编辑器,已经成了一个不会忘的习惯。希望帮到你。

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

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

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

立即咨询