
简介一份基于SpringBoot与Vue.js的果蔬电商平台完整工程包面向Java方向毕业设计或课程设计人群可用于学习前后端分离开发、电商业务建模与部署实践。包内共有751个文件压缩后大小16.6MB涵盖Java后端源码、Vue前端页面、FTL模板、CSS样式、SQL数据库脚本、XML配置以及项目说明文档等结构清晰便于按需查阅。目前已有29人浏览学习适合需要快速搭建果蔬商城原型并理解SpringBootMySQL技术栈的读者。项目不仅包含可运行的前后端代码和数据库表设计还附带了README说明与界面素材能够帮助使用者从数据模型、接口实现到页面展示完整掌握一个轻量级电商平台的构建思路。1. 为什么果蔬电商要用SpringBoot而不是SSH或者Node很多人做电商毕设时第一反应是找一套通用商城代码改改。但果蔬电商有个容易被忽略的特点库存波动大、保质期短、订单状态变化频繁最难的不是增删改查而是并发下单时如何防止库存变负数。这个项目基于SpringBoot 2.x Vue.js MySQL后端利用SpringBoot自动装配和Starter机制减少配置前端用Vue组件化搭页面是标准的前后端分离结构。无论是做课程设计、毕业设计还是入门SpringBoot项目实战都可以直接拿它当骨架但前提是先搞懂自动装配、表设计和事务边界。2. SpringBoot后端核心自动装配、分层结构与商品接口2.1 SpringBoot自动装配与项目初始化SpringBoot最常被提起的就是自动装配。SpringBootApplication实际上由SpringBootConfiguration、EnableAutoConfiguration和ComponentScan三个注解组合而成。EnableAutoConfiguration在启动时读取classpath下的META-INF/spring.factories文件按条件注解判断哪些自动配置类需要生效。比如引入spring-boot-starter-web后内嵌Tomcat、DispatcherServlet、Jackson消息转换器都会被自动配置好开发者不用再手动编写web.xml和springmvc.xml。初始化项目时常见做法是使用Spring Initializr在IDEA中生成也可以手工创建Maven工程。下面是一个最小可运行的pom.xml片段parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.13/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependenciesspring-boot-starter-parent负责统一管理依赖版本不用手动指定mysql-connector-java的版本避免多个库之间版本不一致。选择MyBatis-Plus而不是JPA是因为这个项目的商品列表经常要按分类、价格、销量组合筛选MyBatis-Plus的LambdaQueryWrapper能把这些条件以类型安全的方式写出来JPA在这种动态查询场景下反而需要写Query注解处理。mybatis-plus-boot-starter就是SpringBoot整合MyBatis-Plus的Starter。注意SpringBoot 2.7.13是基于javax命名空间的网上大部分资料和SSM课程设计案例都可以直接复用。如果换成SpringBoot 3.xjavax要改成jakarta很多老代码要调整这是刚接触SpringBoot的人最容易踩的版本坑。2.2 SpringBoot分层架构与常用注解项目后端按controller/service/mapper/entity四层组织复杂一点的增加dto和vo。Controller层负责接收HTTP请求、做参数校验、返回统一结果Service层写业务规则和事务边界Mapper层继承BaseMapper获得通用CRUD能力。切分原则是Controller不能直接操作数据库Mapper也不能写业务判断否则后续扩展时改动面会不可控。下面是一组在SpringBoot项目里出现频率最高的注解。注解出现位置作用RestControllerController类处理HTTP请求并直接返回JSONRequestMapping类或方法映射URL地址Autowired字段或方法注入Spring容器中的BeanService业务类标记为业务组件MapperMapper接口让MyBatis扫描到该接口Transactional方法或类声明事务边界ConfigurationProperties配置类绑定application.yml里的配置项这组注解比网上流行的一段式代码更容易维护。以商品列表接口为例Controller只负责接收分页参数和分类参数RestController RequestMapping(/api/product) public class ProductController { Autowired private IProductService productService; GetMapping(/list) public ResultListProductVO list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String category) { return Result.success(productService.pageList(page, size, category)); } }RestController是Controller和ResponseBody的组合方法返回值会被Jackson序列化成JSON。RequestParam的defaultValue可以让page和size在缺省时分别取1和10category可空。Result是一个统一包装类包含code、msg、data三个字段避免每个接口返回结构不一致。真正的查询逻辑放在Service实现里Service public class ProductServiceImpl extends ServiceImplProductMapper, Product implements IProductService { Override public ListProductVO pageList(Integer page, Integer size, String category) { LambdaQueryWrapperProduct wrapper Wrappers.lambdaQuery(); if (StringUtils.hasText(category)) { wrapper.eq(Product::getCategory, category); } wrapper.orderByDesc(Product::getCreateTime); PageProduct p this.page(new Page(page, size), wrapper); return p.getRecords().stream().map(ProductVO::from).collect(Collectors.toList()); } }LambdaQueryWrapper用方法引用替代字符串列名category为空时不会拼接条件这样既防SQL注入又不会产生多余的“where 11”。orderByDesc保证新品排在前面。ProductVO::from是实体到视图对象的转换方法可以隐藏数据库字段结构不让前端感知到内部实现。2.3 参数配置与商品列表接口调试application.yml是SpringBoot的配置入口。果蔬电商的本地开发配置一般是这样的server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fruit_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl里的characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai避免MySQL 8驱动与服务器时区不一致带来的时间偏移useSSLfalse省去SSL证书告警。map-underscore-to-camel-case开启后数据库的create_time字段会自动映射到实体的createTime属性不用写一堆ResultMap。log-impl配置成StdOutImpl后控制台会直接打印SQL调试列表接口时能很直观地看到limit参数和where条件。如果要把项目交给其他人跑建议把配置文件拆成application-dev.yml和application-prod.yml然后在application.yml里用spring.profiles.active选择环境。默认密码不要用123456至少改成强密码后面第五章还会提加密。接口启动后可以直接访问http://localhost:8080/api/product/list?page1size10验证。如果返回JSON且data有值说明数据库连接、SQL映射和Jackson序列化都正常。这时再看控制台SQL日志可以确认分页查的是哪张表、有没有走索引。3. Vue前端与SpringBoot的跨域联调组件化页面与接口对接3.1 Vue项目结构与组件化页面前端使用Vue CLI创建项目内部按职责划分目录。下表是这类前后端分离商城最常见的结构目录职责src/api按模块封装接口请求函数src/views页面级组件路由直接对应src/components可复用组件比如商品卡片、数量选择器src/router路由配置与守卫src/storeVuex状态管理存储购物车和用户信息src/utilsaxios实例、工具函数商品列表页不会把整块UI写在同一个文件里通常会把商品卡片抽成组件。一个简单的ProductCard组件如下template el-card :body-style{ padding: 0px } shadownever img :srcproduct.img classproduct-img / div classproduct-info div classproduct-name{{ product.name }}/div div classproduct-price{{ product.price }} / {{ product.unit }}/div /div /el-card /template script export default { name: ProductCard, props: { product: { type: Object, required: true } } }; /scriptprops表示父组件传入的数据子组件内部不能修改props只能通过$emit向父组件抛事件。这个单向数据流保证商品对象在多个组件间不会被无意覆盖。果蔬商品必须把单位展示出来“斤”“盒”“份”直接影响下单数量这是通用商城很少注意的地方。3.2 axios封装与接口调用页面里直接发axios请求会导致错误处理代码重复。常见做法是封装一个request对象统一维护baseURL、超时时间和拦截器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( res { const code res.data.code; if (code 200) { return res.data.data; } return Promise.reject(new Error(res.data.msg)); }, err Promise.reject(err) ); export default request;baseURL固定为/api是为了配合开发环境的代理转发也方便生产环境用Nginx统一加前缀。请求拦截器把登录后保存的token放进Authorization头响应拦截器把后端包装的Result中code字段解掉页面拿到的是真实的data不需要每个页面都重复写if (res.data.code ! 200)这种判断。接口定义放在src/api/product.js中import request from /utils/request; export function getProductList(params) { return request({ url: /product/list, method: get, params }); }这样列表页只需要调用getProductList({ page: 1, size: 10, category: fruit })参数由组件内部生成接口模块负责URL和HTTP方法。3.3 前后端联调的跨域与代理配置开发过程中前端运行在8081端口后端运行在8080端口属于跨域请求。最干净的做法不是在后端写CorsFilter而是在vue.config.js中配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };注意pathRewrite的写法前端请求/api/product/list代理转发到后端时会重写为/product/list所以后端Controller里不要带/api前缀。很多springboot vue前后端分离项目联调失败就是因为后端写了/api代理也保留/api最后实际请求变成/product/list的两次拼接返回404。如果生产环境用Nginx也要注意proxy_pass是否带斜杠location /api/ { proxy_pass http://127.0.0.1:8080/; }这里的斜杠会把/api/去掉和后端接口路径保持一致。如果漏掉尾部斜杠Nginx会原样传递/api/...接口直接404。除了跨域还需要处理登录态。前端路由守卫只保证页面能被访问到真正的权限判断必须由后端接口完成router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requireAuth !token) { next(/login); } else { next(); } });守卫里检查的是本地有没有token并不能防止用户篡改。所以下单、支付这类接口必须在后端校验token对应的用户身份前端守卫只用来改善体验。4. 果蔬电商的数据模型与订单库存处理4.1 核心数据表结构果蔬电商的数据表不需要设计得像通用商城那么庞大核心就是用户、商品、订单、订单项和购物车。下面给出商品表和订单表的建表SQLCREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 商品名称, category varchar(50) NOT NULL COMMENT 分类蔬菜/水果/肉类, price decimal(10,2) NOT NULL COMMENT 价格, unit varchar(10) NOT NULL DEFAULT 斤 COMMENT 单位, stock int NOT NULL DEFAULT 0 COMMENT 库存, sales_count int NOT NULL DEFAULT 0 COMMENT 销量, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, order_no varchar(32) NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3完成 4取消, address_snapshot varchar(255) DEFAULT NULL COMMENT 收货地址快照, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;各表职责如下表名作用关键字段user用户账号信息id, username, passwordproduct商品信息price, stock, categoryorders订单主表order_no, status, total_amountorder_item订单明细表order_id, product_id, product_namecart购物车user_id, product_id, count价格字段必须用decimal而不是float否则浮点误差会让对账变成灾难。订单表冗余address_snapshot是因为用户地址可能修改但历史订单需要保留下单那一刻的地址。status用数字表示比字符串节省空间也方便在代码里写枚举。4.2 订单流程与事务控制下单是电商平台最核心的流程查商品、扣库存、写订单、写订单明细。任何一个步骤失败前面已经扣掉的库存都要恢复。所以createOrder必须是一个事务方法Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, Long productId, Integer count) { Product product productMapper.selectById(productId); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } int rows productMapper.deductStock(productId, count); if (rows 0) { throw new BusinessException(库存不足); } Orders order new Orders(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(count))); order.setStatus(0); ordersMapper.insert(order); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(productId); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setCount(count); orderItemMapper.insert(item); return order.getId(); }Transactional(rollbackFor Exception.class)是SpringBoot里最容易忽略的参数。Spring默认只对RuntimeException回滚如果业务代码抛的是检查异常不加rollbackFor不会回滚库存就会被白白扣掉。订单明细表保存product_name和price快照这样商品改价或改名称后历史订单仍能显示当时的商品信息。generateOrderNo()一般生成时间戳加随机数比如yyyyMMddHHmmss加6位随机数。更严格的场景可以引入雪花算法但在毕设项目里时间戳加随机数加数据库唯一索引已经够用。唯一索引uk_order_no的作用是防止网络重试时同一请求被处理两次。4.3 库存扣减与高并发场景事务方法里的“先select再update”其实有并发漏洞。假设库存只剩1件两个请求同时读到库存为1都通过判断然后各自把库存改成0就会超卖。正确做法是把库存判断和扣减放在一条UPDATE语句里update iddeductStock update product set stock stock - #{count} where id #{id} and stock #{count} /update这条SQL利用InnoDB的行锁保证同一时刻只有一个事务能执行扣减。如果影响行数为0说明库存不够业务层立刻抛异常回滚。调用逻辑如下public interface ProductMapper extends BaseMapperProduct { int deductStock(Param(id) Long id, Param(count) Integer count); }注意这里不能用MyBatis-Plus自带的updateById因为那种方式会先查一次再组装UPDATE无法保证stock count这个条件。如果项目里没有XML也可以用UpdateWrapperproductMapper.update(null, Wrappers.ProductlambdaUpdate() .setSql(stock stock - {0}, count) .eq(Product::getId, productId) .ge(Product::getStock, count));两种方式原理相同。如果场景继续升级到秒杀MySQL扣减扛不住时才考虑Redis的decr预扣减加异步订单落库。对课程设计和毕业设计来说数据库条件扣减已经是合格方案。5. 从毕设到上线安全加固与部署验证5.1 防SQL注入与XSSMyBatis的#{}是PreparedStatement占位符能挡住绝大部分SQL注入。真正要小心的是${}比如动态排序字段。排序字段不适合用预编译常见做法是用白名单映射private static final MapString, String ORDER_MAP Map.of( price, price, sales, sales_count, new, create_time ); String orderField ORDER_MAP.getOrDefault(sort, create_time);请求里的sort参数只能映射到固定的三个列传任何其他值都回退到create_time。XSS方面Vue的插值表达式会自动转义v-html要尽量少用。如果商城的商品详情允许富文本前端口罩需要做清理接口也要过滤script标签。5.2 敏感配置与打包部署application.yml里的数据库密码不能明文写在仓库里。可以引入jasypt-spring-boot-starter用ENC()包裹密文启动时通过环境变量传入解密盐jasypt: encryptor: password: ${JASYPT_SALT}这样即使源码泄露没有环境变量也解不出密码。打包使用Maven命令mvn clean package -DskipTests生成的jar文件位于target目录。部署时用nohup启动并把日志写到文件nohup java -jar target/fruit-shop.jar --spring.profiles.activeprod logs/app.log 21 注意SpringBoot 2.7.13对应JDK 8或11服务器上不要直接装最新版JDK 21。前端构建产物由Nginx静态托管接口请求走/api代理到后端服务。5.3 接口验证清单启动后先验证商品列表curl http://localhost:8080/api/product/list?page1size10如果返回code为200且data有记录说明数据库连接和Mapper正常。再验证下单接口的异常回滚传一个超过库存的count调用后查询库存和orders表库存应保持不变订单表也不会有新数据。用JMeter开100个并发线程对同一个商品下单请求结束后查product.stock只要没有负数就说明库存扣减逻辑合格。这些验证做完这个项目再拿去提交或演示才算真正站得住。本文还有配套的精品资源点击获取