SpringBoot篮球用品网购系统开发实录:从零搭建到上线全记录
做电商系统开发这么多年,帮人改过的购物车代码比我吃过的饭都多。但每次拿到类似SpringBoot篮球用品网购系统这种垂直类电商项目,我还是会觉得有意思——因为它的逻辑复杂度和大厂电商没有本质区别,只是业务规模更聚焦。今天我想把这个项目的完整开发过程和设计思路合盘托出,从前台的商品浏览、购物车、订单支付,到后台的商品管理、库存维护、订单处理,每一步都附上实测过的配置和踩坑记录。不管你是准备拿它做毕业设计、接外包定制,还是纯粹想用SpringBoot跑通一个完整的电商闭环,这篇都能给你一份可以直接照着抄的作业。
我自己平时也会接触PHP、Python、C#的项目,但做这类系统我几乎无脑选Java技术栈。不是别的语言不行,而是SpringBoot这套生态实在太适合跑电商了——事务处理稳定、并发支撑成熟、现成的组件丰富,社区资料多到你能搜到的坑基本都有人踩过。而且对刚入门的朋友来说,学SpringBoot顺带练手一个完整系统,这个技术投资是真的值。
1. 项目定位与功能全景
1.1 为什么选择垂直品类做电商系统
很多初学者一上来就想做一个"淘宝全品类"的通用商城,我劝你冷静。篮球用品网购系统这种垂直定位的电商项目,其实更容易把每个环节做扎实。买篮球鞋、篮球服的用户需求非常明确,搜索关键词集中,库存SKU数量可控,订单量级适合单机部署——这些特点都让它成为教学和毕设场景下的理想选择。
从商品维度看,一个篮球用品店通常只有球类、鞋类、服饰、护具、配件这几个大类。每类的属性差异不像服装那样细碎,商品规格可以控制在颜色、尺码、型号这几项,极大降低了SKU管理的复杂度。但从业务流程看,它又是一个完整的B2C闭环:用户注册登录、浏览商品、加入购物车、下单、结算、模拟支付、订单查询、后台发货,一步都不能少。
我接手这个项目时最开始的定位就是"麻雀虽小五脏俱全",系统要覆盖电商核心链路,但不做大而全的会员积分、优惠券裂变这类边缘功能。把主营业务跑通、跑稳,比堆功能更有价值。
1.2 系统核心功能模块拆解
我相信对于绝大多数学习者来说,最关心的就是这个系统到底有哪些功能。我直接给你一张功能全景图:
前台用户端:
- 用户注册与登录(手机号或邮箱注册,BCrypt加密存储)
- 首页轮播图与热门商品推荐位
- 商品分类导航(篮球、球鞋、服饰、护具、配件)
- 商品列表页:支持关键词搜索、价格区间筛选、按销量/价格/上架时间排序
- 商品详情页:多图展示、SKU规格选择、库存展示
- 购物车:加入、修改数量、删除、批量结算
- 订单确认页:收货地址管理、订单金额明细
- 模拟支付流程:对接支付宝沙箱或简单的余额支付
- 个人中心:订单列表、订单详情、取消订单、确认收货
后台管理端:
- 管理员登录与权限校验
- 商品管理:发布、上下架、编辑、库存调整、多图上传
- 类目管理:树形分类的增删改查
- 订单管理:订单列表、查看详情、发货处理
- 用户管理:用户列表、状态禁用与启用
- 轮播图管理:前端首页Banner的配置
这些模块加起来也就是十几张表的规模,但每个模块都有设计细节。比如商品上下架时库存怎么联动、订单取消后库存怎么回滚,这类问题才是真正考验开发功力的地方。
1.3 项目的三种典型使用场景
那么问题来了,这套系统究竟适合什么人?我总结了三类最常见的场景:
第一类是毕业设计,比例大约占七成。一个SpringBoot + Vue的完整前后端分离项目,功能完整、技术栈主流、文档好写,答辩时能讲的东西很多——数据库设计、事务控制、并发处理、前后端交互,随便拿一个点都能展开讲十几分钟。
第二类是外包接单,要么直接定制定做,要么在这个基础上替换成PHP、Python或C#的版本。这种垂直电商的逻辑是通用的,换语言只是语法不同,业务模型可以直接平移。
第三类是个人学习。SpringBoot入门容易,但想真正理解一个业务系统的完整链路,没有比"从零实现一个电商"更好的练手方式了。学完框架语法之后,拿一个真实项目把知识点串起来,效果甩看十套教程几条街。
2. SpringBoot技术选型与项目搭建全流程
2.1 技术栈选型的底层逻辑
用SpringBoot做这类系统,技术选型的核心是"够用、好维护、教程多"。在实际项目中我基本固定用这样一套组合:SpringBoot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis + MinIO + Vue 2/Vue 3 + Element UI。
早期版本的SpringBoot 3.x引入了Jakarta命名空间,很多老教程的示例代码直接抄会报错,对于新手非常不友好,所以我更推荐先用2.7.x版本跑通业务,后期再平滑升级。Java版本选JDK 8或JDK 11,稳定可靠。MyBatis-Plus确实是国内开发者的福音,单表CRUD几乎不需要写SQL,条件构造器让动态查询变得非常直观。Redis用于缓存热门商品和轮播图数据,缓解数据库压力;MinIO则负责商品图片的存储,后面我会专门讲为什么不用传统本地路径存储。
选这套技术栈的另一个原因是社区资料极其丰富。你搜"SpringBoot整合XX",前十条结果几乎都是有效内容。这一点在排错时比什么高深架构都重要,因为别人踩过的坑你大概率也会踩一遍。
2.2 从零搭建SpringBoot项目的关键步骤
项目初始化我建议直接走Spring Initializr(start.spring.io),别手动建目录。选择Java 8、Spring Boot 2.7.18,依赖勾选Spring Web、MySQL Driver、Spring Data Redis、Lombok、Validation。生成后手动引入MyBatis-Plus的依赖,因为这不在初始化器的选项里。
pom.xml里需要加的核心依赖大概是这样的:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>配置文件我也直接给出一份实测可用的application.yml核心片段:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/basketball_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里有个特别容易踩的坑:MySQL连接URL一定要指定serverTimezone,不然系统时间和数据库时间对不上,可能导致日期字段错乱。我见过太多人因为少了这个参数,订单时间整整齐齐差了8个小时。
2.3 Java与PHP、Python、C#方案的技术横评
标题里提到了Java、PHP、Python、C#四个方向的方案,很多朋友会纠结做毕设时到底选哪条路。我四个方向都做过交付,给你一份我自己的横向对比:
| 技术路线 | 学习门槛 | 电商开发效率 | 并发与事务能力 | 部署环境要求 | 典型适用人群 |
|---|---|---|---|---|---|
| Java + SpringBoot | 中高 | 高(组件生态全) | 强 | JVM,稍吃内存 | 想做长期后端开发的 |
| PHP(ThinkPHP/Laravel) | 低 | 很高(上手快) | 中 | 轻量,便宜 | 追求快速交付、低成本部署 |
| Python(Django/Flask) | 低 | 中 | 中 | 中 | 偏向数据分析、AI方向的 |
| C#(.NET Core) | 中 | 高 | 强 | Windows/Linux均兼容 | Windows生态重度用户 |
我个人对PHP的评价是"做小项目真的快",数据模型一条命令生成,后台管理框架现成的多,如果你只是想要一个能演示的商城,PHP可能一天就能跑起来。但SpringBoot的优势在复杂业务的可靠性。电商系统最怕的不是写不出来,而是并发一高就出乱七八糟的数据问题。Java对线程、事务、锁的支持,以及强大的生态积累,是长期演进最稳的方案。
还有一点比较功利但很现实:招聘市场上Java后端岗位最多,简历里写"SpringBoot电商项目"的认可度,要比PHP/Python项目高一个档次。如果你是技术新人,仅仅从求职角度讲,我也会建议你选Java路线。
3. 数据库设计与核心业务模块落地
3.1 篮球用品系统的表结构设计思路
数据库设计直接决定了业务逻辑能不能顺畅实现。对于一个垂直电商系统,我建议最少设计这些表:
| 表名 | 核心字段 | 业务说明 |
|---|---|---|
| t_user | id, username, password, nickname, phone, avatar, status | 用户表,status区分是否禁用 |
| t_address | id, user_id, receiver_name, receiver_phone, province, city, detail | 收货地址表,用户可配多个 |
| t_category | id, parent_id, name, sort | 类目表,树形结构 |
| t_product | id, category_id, name, subtitle, main_image, price, stock, sales, status, detail | 商品主表 |
| t_product_image | id, product_id, image_url, sort | 商品图片表,一个商品多图 |
| t_cart | id, user_id, product_id, quantity, checked | 购物车表 |
| t_order | id, order_no, user_id, total_amount, pay_amount, status, address_snapshot, create_time | 订单主表 |
| t_order_item | id, order_id, product_id, product_name, product_image, price, quantity | 订单明细表 |
| t_carousel | id, image_url, link_url, sort | 首页轮播图表 |
有两个设计细节我必须特别强调。订单地址我用了"快照"的方式,也就是下单那一刻把收货地址的完整信息复制一份存到订单表里,而不是只存一个address_id。因为用户的收货地址后续可能修改,如果只存ID,订单历史数据就会出现"地址漂移"问题——下单的地址和后来看到的地址对不上。商品名称和图片同样做快照,将来商品改名或下架,老订单依然能正常展示历史信息。
这种"快照思想"在整个系统里其实很重要。电商业务里数据是动态的,而订单是静态的历史事实,两者必须分开处理。每次下单都从实时数据生成快照,这样即使主表数据变动,你的订单永远是好查的。
3.2 商品SKU与库存扣减的并发控制
库存扣减是电商系统的经典考题,也是面试官最爱问的点。篮球鞋有尺码,篮球服有颜色,所以SKU设计通常有两种做法:简单一点是直接在商品表里放总库存,复杂一点是单独建SKU表,每个规格组合一条记录。
对于毕设和中小型项目,我建议折中处理:单规格商品直接库存放在商品表上,多规格商品用SKU表管理。SKU表字段大概是:id, product_id, spec_info(JSON格式,比如{"color":"红色","size":"42"}), stock, price。这样既能应对多规格场景,又不至于让系统结构过于臃肿。
库存扣减必须考虑并发安全。最经典的写法是乐观锁:
// 扣减库存,注意stock > 0条件,防止超卖 int count = productMapper.deductStock(productId, quantity); if (count == 0) { throw new BusinessException("库存不足"); }对应的SQL逻辑在Mapper里:
<update id="deductStock"> update t_product set stock = stock - #{quantity}, sales = sales + #{quantity} where id = #{productId} and stock >= #{quantity} </update>这个写法的巧妙之处在于它把"检查库存"和"扣减库存"合并成了一个原子操作,利用数据库的行锁保证并发扣减不超卖。在实际压测里,这种方案在单机和主从复制场景下的表现完全够用。别再问我为什么不用悲观锁——性能差一截,而且这种场景用乐观锁恰到好处。
3.3 购物车到订单的完整事务链路
购物车和订单是电商系统里最容易写乱的部分。我建议把逻辑分层:购物车只管"存什么",订单管"怎么结算"。购物车的增删改查没有事务复杂度,核心是下单那一刻的逻辑要设计严谨。
下单接口的处理流程我整理成了七步:
- 校验用户登录态和购物车中的勾选商品
- 遍历勾选商品,预扣库存(用上面提到的乐观锁)
- 计算订单金额(商品单价 × 数量,注意判空防止价格不合法)
- 生成唯一订单号并创建订单主记录
- 批量创建订单明细
- 清空已结算的购物车记录
- 全部成功则提交事务,任一步失败则回滚
这里必须加@Transactional注解。我见过最典型的问题是:下单时库存扣了,但是购物车没清空,或者订单明细没写进去,导致数据对不上账。给整个方法加上事务,任何一步抛出异常都会被自动回滚,不会出现"库存少了但订单没生成"这种事故。
@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, List<Long> cartIds) { // 1. 查询购物车信息 // 2. 预扣库存 // 3. 生成订单号 // 4. 创建订单与明细 // 5. 清理购物车 }有一个细节要提醒:订单号和订单金额一定要在事务内生成,不能直接拿前端传过来的金额入库。这是安全底线——你把价格参数暴露给前端,数据包被别人改了怎么办?正确做法是后端根据商品库存表实时计算金额,前端传的金额只做页面展示参考,后端必须重新计算。
4. 前后端分离部署与MinIO对象存储实战
4.1 商品图片存储:为什么放弃本地路径直存
我见过太多SpringBoot教程里商品图片传着传着就存在了E:/upload/这种本地路径,数据库里存个http://localhost:8080/upload/xxx.jpg完事。诚然,本地直存最简单,但部署时就成了大麻烦:换服务器需要迁移图片,集群部署时文件不一致,服务器故障图片全丢,而且还涉及静态资源的路径配置权限问题。
实际上,商品图片这类非结构化数据,正确的姿势是用对象存储。大厂用阿里云OSS,个人开发者用MinIO搭建私有对象存储最合适。MinIO是一个开源的S3兼容对象存储服务,本地一台服务器几分钟就能跑起来,社区免费版没有功能阉割,用来做毕设和中小项目绰绰有余。
打个比方吧,如果把应用服务器比作一个便利店,本地存储就是在便利店仓库里堆杂物,货架满了、仓库搬家都麻烦;MinIO就是物美价廉的独立物流仓库,便利店只管收货和验货,商品集中存储在仓库中,哪个门店需要就从仓库调货,完全不占门店空间。
4.2 SpringBoot整合MinIO的关键步骤和代码
把MinIO加到SpringBoot项目里,核心就是配好依赖、定义客户端、写工具类。首先是依赖,我在pom.xml里用的是8.5.7版本,连接地址直接用9000端口。
配置文件的写法如下:
minio: endpoint: http://localhost:9000 access-key: yourAccessKey secret-key: yourSecretKey bucket-name: basketball-shop工具类封装我给出一个精简可用的版本:
@Component public class MinioUtil { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Value("${minio.bucket-name}") private String bucketName; private MinioClient client; @PostConstruct public void init() { client = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } public String upload(MultipartFile file, String objectName) throws Exception { // 保证bucket存在 boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!exists) { client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } client.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint + "/" + bucketName + "/" + objectName; } }对象名建议用UUID或者时间戳拼接,避免中文文件名和重复文件覆盖问题。另外注意一件事:MinIO的endpoint地址,在前端展示图片时必须是前端能直接访问到的地址。如果前端部署在80端口,而后端MinIO在9000端口,跨端口访问会涉及跨域和网络策略问题,实测中建议给MinIO配一个nginx反向代理路径,用统一的域名入口解决。
4.3 Vue打包产物与SpringBoot统一部署
很多朋友用Vue写了前端,但不知道最后怎么把前后端合并部署。这里我直接说结论:开发期用Vite/Webpack的代理转发放后端,上线期把Vue产物直接丢进SpringBoot的src/main/resources/static目录,或者通过nginx反向代理指向两者端口。
最省事的方式是Vue打包后,把dist目录里的文件复制到SpringBoot的static目录,然后配置一个WebMvcConfigurer放行静态资源。这样用户访问8080端口就能同时拿到前端页面和调用后端接口,一个端口搞定全部。
前端路由如果用了history模式,还需要补充一个接口把404请求转发到index.html,否则刷新二级页面会白屏。这个问题新手必踩,提前帮你排掉:
@Component public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html"); } }这样做的好处就是——部署只需要一个SpringBoot的jar包加一个MinIO服务,结构非常清爽。推荐有条件的朋友直接用docker-compose把MySQL、Redis、MinIO、SpringBoot四件套编排起来,一条命令启动整栈,演示时是真的省心。
5. 核心业务逻辑与Java高频技巧实战
5.1 商品搜索排序场景的MyBatis-Plus实践
商品列表页的业务逻辑看起来简单,实际写代码时有不少细节。前端会传当前页码、每页条数、分类ID、搜索关键词、价格区间、排序字段,这些参数组合起来就是一次典型的动态SQL查询。
MyBatis-Plus的LambdaQueryWrapper在这个场景非常好用:
public Page<ProductVO> pageProducts(ProductQuery query) { Page<Product> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 分类筛选 if (query.getCategoryId() != null) { List<Long> categoryIds = categoryService.getChildCategoryIds(query.getCategoryId()); wrapper.in(Product::getCategoryId, categoryIds); } // 关键词搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } // 价格区间 if (query.getMinPrice() != null) { wrapper.ge(Product::getPrice, query.getMinPrice()); } if (query.getMaxPrice() != null) { wrapper.le(Product::getPrice, query.getMaxPrice()); } // 排序 if ("price_asc".equals(query.getSort())) { wrapper.orderByAsc(Product::getPrice); } else if ("price_desc".equals(query.getSort())) { wrapper.orderByDesc(Product::getPrice); } else if ("sales_desc".equals(query.getSort())) { wrapper.orderByDesc(Product::getSales); } else { wrapper.orderByDesc(Product::getCreateTime); } return productMapper.selectPage(page, wrapper); }这里有个经验之谈:排序字段不要直接拼接前端传参,要用白名单映射。前端要什么排序就先映射成固定选项,再把对应的排序条件写死进代码。否则用户传一个"order=name desc, password", 配合拼接可能就出SQL注入的洞,这类安全底线不能破。
5.2 订单号生成与字符串截取处理的实战细节
有些朋友会直接拿数据库自增ID当订单号,这样做的风险显而易见:订单号可以被猜测,竞争对手能通过订单号推算你的日单量。正确的做法是生成独立的业务订单号,我常用的方案是"时间戳 + 用户ID + 随机数"组合:
public String generateOrderNo(Long userId) { // 格式:年月日时分秒 + 用户ID后四位 + 四位随机数 String timePart = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); String userPart = String.format("%04d", userId % 10000); String randomPart = String.format("%04d", new Random().nextInt(10000)); return timePart + userPart + randomPart; }这个订单号方案生成出来的长度是22位左右,可读性好、不重复、不容易被推算出业务量,在中小项目里非常够用。订单号还有一个场景需要处理:取消订单和退款时,用什么保证请求的幂等性?每次对同一个订单发起取消,不能重复回滚库存。我的做法是在订单表加一个status字段,用UPDATE t_order SET status = 5 WHERE id = ? AND status = 3这种带条件的更新,如果更新影响行数为0,说明订单状态已变化或不允许取消,直接返回失败。这种做法非常朴素但极其可靠,简单业务场景就不需要上分布式锁了。
说到字符串处理,Java里截取、格式化、拼接这些操作在业务开发中比算法题里的所谓"高级用法"出现频率高得多。比如截取订单号的日期部分、处理手机号脱敏(中间四位打码)、把URL参数里的字符串转数字——这类通过String.substring、Integer.parseInt、StringBuilder就能解决的问题,多写几轮业务代码自然就熟练了。
5.3 事务边界、幂等设计与并发防护的组合拳
SpringBoot里做事务管理,核心就是搞清楚"什么方法该加事务、事务边界放在哪"。我总结的实操原则很简单:
事务的粒度要尽可能小,放在服务层,不要放在控制器层。控制器层是接口入口,如果在这里加@Transactional,增大了事务范围,还会让接口的异常处理和事务逻辑耦合在一起,代码很难看。在Service层方法上加上@Transactional(rollbackFor = Exception.class),一旦方法内部抛出异常,所有数据库操作都会回滚。rollbackFor要指定成Exception.class,因为Spring默认只对运行时异常回滚,对于检查异常默认不会回滚,这个坑很隐蔽。
购物车到订单的链路能不能串起来,核心就在于用户状态、商品状态、库存状态这三者的联动。我在这里用了一个小技巧:用商品ID + 用户ID + 添加时间作为购物车行的唯一排查线索,一旦出现"加购商品和下单商品不一致"的诡异问题,可以通过日志快速定位。
幂等设计里比较典型的就是支付回调。如果用支付宝沙箱,支付成功后的异步回调可能会重复通知。处理方式是在回调接口里先查订单当前状态,如果已经是"已支付",直接返回成功,不再重复处理。这样既能保证幂等,也能避免重复更新订单导致异常。
6. 安全防护、部署优化与问题排查实录
6.1 用户认证与权限控制的落地做法
电商系统最怕什么?最怕用户随意篡改数据、越权访问他人订单。我用JWT做登录态管理,登录成功后签发一个Token返回给前端,前端每次请求都在请求头带上Authorization: Bearer xxx,后端定义拦截器统一解析校验。
JWT的核心逻辑不复杂,我常用jjwt实现:
public String createToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }密码存储的问题也必须重视。我见过一些老项目把密码明文存数据库,一旦数据泄露全部账号裸奔,这种低级失误不能犯。更不要自己写什么"加密算法",直接用业界公认的BCrypt就可以,用法也简单:注册时BCrypt.hashpw加密入库,登录时BCrypt.checkpw比对。这种加盐哈希的方案在暴力破解面前安全性远比MD5、SHA1可靠得多。
权限控制方面,后台管理接口必须校验管理员身份,而不是只靠前端隐藏按钮。思路也很朴素:定义一个@RequireAdmin注解配合拦截器,对/admin/**路径统一校验Token的role字段。别小看这一行配置,没有它你的后台管理接口等于裸奔——任何人只要知道接口路径就能调用。
6.2 SQL注入、越权与接口防刷的防护清单
我总结了电商系统上线前必须要过的安全检查,缺任何一个都可能出事:
- SQL注入:MyBatis-Plus的
LambdaQueryWrapper天然防注入,但手写SQL拼接时必须用#{}而不是${}。传表名、排序字段这种必须动态的部分,就做白名单校验。 - 越权访问:查询订单详情必须先校验订单的
user_id是否等于当前登录用户。光靠前端隐藏按钮防不了直接发HTTP请求,后端必须层层校验。 - 接口防刷:登录接口和发送验证码接口容易被脚本刷。简单方案是Redis存接口调用计数,同一IP一分钟内超过N次直接拒绝。不追求性能的话,用Guava RateLimiter做单机限流也行。
- 文件上传:检查文件扩展名和Content-Type,限制上传大小,上传目录禁止解析脚本。如果上传了恶意文件被当作静态资源访问,那就是妥妥的服务器沦陷了。
- 接口参数校验:用JSR 303的
@NotNull、@Min、@Max这种注解在Controller层做基础校验,业务层再校验业务规则,防御纵深才有意义。
我还补一个很隐蔽但常见的坑:商城项目里redis存的购物车临时数据,key一定要加userId区分,不然用户A登录后能看到用户B的购物车。
6.3 高频部署问题排查速查表
最后送你一份我自己项目上线的避坑清单,全是实测遇到过的:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端能打开但接口请求404 | 前端路由history模式刷新失败 | 配置forward到index.html |
| 图片上传成功但页面不显示 | MinIO地址未做路径代理,或桶权限是私有 | 设置桶访问策略为public,或nginx代理 |
| 数据库中文乱码 | URL未指定UTF-8或表字符集不对 | 连接URL加characterEncoding=utf8,表用utf8mb4 |
| 登录后接口返回401 | 前端没带Token头,或Token过期 | 检查拦截器放行白名单,前端统一请求拦截器加头 |
| 端口被占用 | 上一次运行的进程没杀掉 | `netstat -ano |
| 应用启动报连接池错误 | MySQL没启动或账号密码错 | 先本地客户端测试连接,再排查配置 |
| 静态资源加载缓慢 | 文件没走CDN/反代 | nginx部署静态资源并开启gzip压缩 |
| 部署后时间差8小时 | 时区配置不一致 | JVM参数加-Duser.timezone=GMT+8,数据库连接加serverTimezone |
从建表到下单,从上传到部署,整个SpringBoot篮球用品网购系统做下来,我最大的感触是:框架本身没那么神奇,真正值钱的是对业务细节的把控。我在一个实际项目里被"订单金额对不上账"这个问题折磨过整整一天——最后发现是因为前端把商品单价传给后端,而我只是想当然地直接用了这个值。从那以后所有金额一律以数据库为准,前端传上来的数字只做展示校验。
最后再分享一个小技巧:开发阶段一定把MyBatis-Plus的SQL日志打开,用StdOutImpl打印到控制台,调试时看一眼实际执行的SQL比什么都直观。等你SQL日志看熟了,很多"玄学bug"其实一眼就能定位是逻辑问题还是数据问题。
这个系统后续的扩展空间也不小,你可以加一个秒杀模块练练Redis分布式锁,也可以加一个数据统计面板,用定时任务算出每日销量走势。技术在迭代,业务是相通的,把这套电商流程吃透了,以后接什么垂直品类的单子都不慌。