在开发这个Springboot篮球文化商铺系统之前,我其实已经帮人做过好几个类似的教学型商城项目。但这次不一样的地方在于,需求方明确提到“程序+源码+数据库+调试部署+开发环境”全流程交付,说明这是一个典型的毕设或者作品集项目,不是单纯写个Demo就完事,而是要能从零开始复现、能讲得出设计逻辑、能直接在机器上跑起来给老师或者面试官看。整篇文章我会按照我自己实际做完一整套系统的工作路径来讲,从业务拆解到技术选型,再到数据库设计、核心代码落地、调试部署踩坑,每个环节都有具体的操作细节,希望对正在做Spring Boot项目的朋友有直接帮助。
1. 先理清业务:一家篮球文化主题商铺到底需要哪些功能
1.1 使用场景和角色梳理
很多人拿到这类题目就急着建项目写代码,结果做着做着发现功能冗余、逻辑混乱,最后自己都不知道系统里哪个模块是干嘛的。我的习惯是第一步先把角色和使用场景列清楚。
篮球文化商铺系统的典型场景是一个围绕篮球周边商品销售的在线商铺,商品包含但不限于球衣、篮球鞋、球星卡、手环、护具、主题水杯这类货品。与传统电商不同的是,篮球文化商铺的商品有一定潮流属性和限量属性,所以除了常规的浏览、下单、支付之外,还需要考虑“商品系列”和“限量标签”这类文化展示维度。
我把系统的用户划分为三类:
| 角色 | 核心需求 | 对应功能 |
|---|---|---|
| 游客 | 浏览商品、了解商铺信息 | 首页、商品列表、商品详情 |
| 注册用户 | 购买、管理个人订单 | 登录注册、购物车、下单、订单查询 |
| 管理员 | 维护商铺后端运营 | 商品分类管理、商品管理、库存管理、订单处理、用户管理 |
这个划分看起来简单,但决定了后面所有模块的设计边界。比如“游客要不要能加购物车”这种问题,有了角色划分后答案就很清晰:游客没有登录态,不能加购物车,只能看,要加购物车必须走登录流程。这不是技术做不到,而是业务逻辑上必须清晰。
1.2 模块划分与边界
基于上面的角色分析,系统模块划分为:
- 用户模块:注册、登录、个人信息查看与修改、密码加密存储。
- 商品模块:商品分类管理、商品信息维护、商品上下架、图片管理、库存数量维护。
- 购物车模块:加入购物车、修改购物车商品数量、删除购物车条目、选中结算。
- 订单模块:创建订单、订单状态流转、取消订单、订单列表查询、订单详情。
- 管理后台模块:面向管理员的商品信息CRUD、订单状态更新发货、用户列表管理。
- 首页展示模块:轮播推荐、热销商品、新品上架、分类导航。
这里有一个很关键的划分原则:用户端和管理端共享底层数据表,但接口分开。很多人做系统的时候喜欢把管理员功能混在用户接口里,或者干脆做两个完全不同的项目,这样会增加大量冗余代码。正确的做法是控制层分开路径前缀,例如用户接口用 /api/user,管理接口用 /api/admin,但service层是同一套,只是管理端调用时不校验用户token而是校验管理员身份。
记住,单一商铺系统的核心是“商品——库存——订单”这条链,所有其他模块都是围绕这条链做支撑。我见过不少同学把精力浪费在复杂的优惠券和积分体系上,结果库存扣减和订单状态这种基本功反而做得稀烂,这属于本末倒置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是Spring Boot、MySQL、以及配套方案
2.1 后端框架选型思路
现在Spring Boot已经成为Java后端开发的事实标准。它并不是新技术,而是把Spring框架中繁琐的XML配置、依赖管理、部署方式做了高度封装,让开发者能够以“约定大于配置”的方式快速启动一个独立的Web服务。针对商铺系统这种典型CRUD加业务状态的场景,Spring Boot天然合适。
版本选择上,目前比较稳定且学习资料丰富的是Spring Boot 2.7.x系列。需要注意的是,Spring Boot 3.x要求JDK 17及以上,如果你的开发环境是JDK 8,那就不要盲目追新。很多毕设或者课程作品最终跑不起来,问题并不在代码,而是Spring Boot 3 + JDK 8这种组合从一开始就是错的。我这次选择的是Spring Boot 2.7.18 + JDK 1.8,这个组合经过了大规模生产验证,资料多,出问题容易搜到答案。
2.2 数据库与其他组件的取舍
数据库选择MySQL,理由很简单:MySQL体积可控、跨平台、SQL标准支持好,对于商铺这类业务有非常成熟的数据建模方案。如果是为了方便答辩展示,MySQL 5.7或8.0均可,建议直接上8.0,因为8.0在窗口函数、字符集优化方面更强,而且新版Navicat对8.0支持更友好。
持久层框架方面,MyBatis-Plus是当前使用频率很高的选择。它继承了MyBatis的灵活SQL能力,又内置了通用Mapper和分页插件,单表CRUD基本不需要手写SQL,大大缩短了开发时间。比直接用原生MyBatis省掉大量XML配置,比JPA更容易控制SQL细节,适合这种业务不算特别复杂但需要清晰掌控查询逻辑的系统。
前端页面这块,如果目标只是演示功能完整,可以不用前后端分离,直接使用Thymeleaf服务端模板渲染,后端把数据和页面一起返回。这样做的好处是项目结构更简单,不需要额外启动Node环境,对于想快速跑通效果的同学非常友好。如果对自己的前端能力有信心,也可以采用Vue + Axios的方式做一套独立前端,但代价是前后端联调工作量和部署复杂度都会上一个台阶。
2.3 开发工具组合
我推荐的开发环境组合:
- JDK 1.8
- Maven 3.6.3或3.8.x
- IntelliJ IDEA 2022.1及以上
- MySQL 8.0
- Navicat 或者 MySQL Workbench
- Redis(可选,非必需,如果只做基础功能可以不引入)
这个组合里面最容易出问题的其实是Maven仓库和IDEA的JDK配置。经常遇到的情况是:代码写好了,启动时报 UnsupportedClassVersionError 或者 invalid source release: 11,十有八九是Project SDK和Modules language level没有统一,这里后面会在部署章节具体讲。
3. 数据库模型设计:商品、会员、订单、库存四条核心链路
我设计数据库时不会一上来就整一堆表,而是先把核心业务对象和它们之间的关系画出来。对于这个商铺系统,最核心的四个对象是:用户、商品、购物车、订单。订单下面又需要订单明细来记录订单中包含哪些商品及购买时的价格快照。
3.1 核心表结构说明
数据库我创建为 basketball_store,主要表如下:
1. user 用户表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键自增 |
| username | VARCHAR(50) | 登录用户名,唯一 |
| password | VARCHAR(100) | BCrypt加密后的密码 |
| nickname | VARCHAR(50) | 昵称 |
| phone | VARCHAR(20) | 手机号 |
| avatar | VARCHAR(255) | 头像地址 |
| role | TINYINT | 角色,0普通用户,1管理员 |
| created_time | DATETIME | 注册时间 |
密码一定不要明文保存,用BCrypt加密。Spring Security自带的 BCryptPasswordEncoder 可以直接调用,也可以用jBCrypt库。好处是相同密码每次加密结果不同,即使数据库泄露也不能反推明文。
2. category 商品分类表
篮球文化商铺的分类不应该简单叫“上衣”“裤子”,而应该结合场景设置。我的做法是分类字段里加一个 type 区分“服装类”“鞋类”“配件类”,再加一个 style_tag 做文化属性标签,比如“复古”“街头”“联名”“经典”。
3. product 商品表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 商品ID |
| category_id | BIGINT | 分类ID |
| name | VARCHAR(100) | 商品名称 |
| subtitle | VARCHAR(255) | 商品副标题 |
| main_image | VARCHAR(255) | 主图 |
| price | DECIMAL(10,2) | 售价 |
| stock | INT | 库存 |
| sales | INT | 销量 |
| status | TINYINT | 上下架状态 |
| detail | TEXT | 商品详情文本 |
| created_time | DATETIME | 创建时间 |
这里有个细节:stock库存字段到底放商品表还是独立的库存表?如果商铺没有复杂的多仓库需求,合并放在product表即可。如果以后要扩展多仓库、锁定库存这类能力,再单独拆库存表。作为单个商铺系统,把库存直接挂在商品上是合理的。
4. shopping_cart_item 购物车表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| user_id | BIGINT | 用户ID |
| product_id | BIGINT | 商品ID |
| quantity | INT | 数量 |
| checked | TINYINT | 是否选中结算 |
| created_time | DATETIME | 加车时间 |
5. order / order_item 订单表
订单表 orders:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 订单ID |
| order_no | VARCHAR(64) | 订单号 |
| user_id | BIGINT | 用户ID |
| total_price | DECIMAL(10,2) | 订单总金额 |
| status | TINYINT | 状态枚举,见下表 |
| receiver_name | VARCHAR(50) | 收货人 |
| receiver_phone | VARCHAR(20) | 收货电话 |
| receiver_address | VARCHAR(255) | 收货地址 |
| created_time | DATETIME | 下单时间 |
| paid_time | DATETIME | 支付时间 |
| delivery_time | DATETIME | 发货时间 |
订单状态我用整数表示并建立常量类管理,推荐状态枚举值:
| 值 | 含义 |
|---|---|
| 0 | 待付款 |
| 1 | 待发货(已付款) |
| 2 | 已发货 |
| 3 | 已签收 |
| 4 | 已取消 |
| 5 | 已退款 |
之所以用通用的 order 表而非“篮球商品订单”专用表,是为了保留可扩展性。以后商铺如果卖门票、卖电子卡券,这些字段也能复用。
订单明细表 order_item:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| order_id | BIGINT | 订单ID |
| product_id | BIGINT | 商品ID |
| product_name | VARCHAR(100) | 商品名称快照 |
| product_image | VARCHAR(255) | 商品图片快照 |
| price | DECIMAL(10,2) | 成交单价快照 |
| quantity | INT | 购买数量 |
| total_price | DECIMAL(10,2) | 小计 |
订单明细里必须冗余商品名称和价格的快照字段,绝对不能去关联product表实时查。因为商品可能改名、下架、改价,如果订单明细实时关联商品表,历史订单显示就会出错。这一点非常体现一个开发者的谨慎程度。
3.2 数据库索引和外键的处理建议
主键一律用BIGINT自增,不使用UUID做主键。UUID作为主键在InnoDB中会引起页分裂,影响插入性能;而且订单号是业务字段,可以单独用时间戳加随机数生成,没必要把业务字段当主键。
外键方面我的建议是:逻辑外键,不用数据库物理外键。也就是说在实体里维护 category_id、user_id、order_id,但不建立数据库层面的 FOREIGN KEY 约束。原因是一旦建立物理外键,数据删除和批量操作会受到较强制约,而且项目导入导出数据时经常因为外键顺序问题报错。逻辑外键靠代码层保证,对中小型项目完全够用。
索引方面,常用的查询路径要建索引:
sql复制ALTER TABLE product ADD INDEX idx_category_id (category_id);
ALTER TABLE product ADD INDEX idx_status (status);
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
ALTER TABLE orders ADD UNIQUE KEY uk_order_no (order_no);
ALTER TABLE order_item ADD INDEX idx_order_id (order_id);
ALTER TABLE shopping_cart_item ADD UNIQUE KEY uk_user_product (user_id, product_id);
uk_user_product 的意思是一个用户同一件商品只能有一行购物车记录,如果重复加入应该做数量累加,而不是插一条新数据。
4. 核心业务代码从搭建到落地的关键步骤
4.1 Spring Boot工程初始化与目录组织
使用IDEA新建Spring Initializr项目,Group填写 com.store,Artifact填写 basketball-store。依赖勾选 Spring Web、MyBatis Framework、MySQL Driver、Validation、Lombok。
标准的包层级结构如下:
code复制com.store.basketball
├── config // 配置类,跨域、拦截器、MyBatisPlus配置
├── common // 通用返回结果、异常枚举、业务异常类
├── controller // 控制层
│ ├── admin // 管理端接口
│ └── api // 用户端接口
├── service // 业务层
├── mapper // 持久层接口
├── entity // 实体类
├── dto // 数据传输对象
├── vo // 视图对象
└── utils // 工具类
很多同学写代码喜欢把Controller里的逻辑写得非常长,然后Service层空空如也。对于课程设计和作品集来说,这种代码质量在答辩时几乎就是送分题给老师质疑。正确做法是Controller只做参数接收、参数校验、调用Service、包装返回结果,真正的业务判断都放在Service层。
4.2 用户注册登录与密码安全
用户注册时对参数做校验,用户名长度、密码长度不要低于6位,手机号格式校验。密码加密使用 BCryptPasswordEncoder:
java复制@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
@Override
public void register(RegisterDTO dto) {
Long count = userMapper.countByUsername(dto.getUsername());
if (count != null && count > 0) {
throw new BusinessException("用户名已存在");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(encoder.encode(dto.getPassword()));
user.setNickname(dto.getNickname());
user.setRole(0);
user.setCreatedTime(LocalDateTime.now());
userMapper.insert(user);
}
}
登录接口这里我没有引入完整的Spring Security框架,只用拦截器和JWT做轻量鉴权,理由是商铺系统的安全需求主要在“接口不裸奔”,而不是复杂的OAuth和权限模型。引入完整Spring Security反而会让新手在过滤器链配置上陷入泥潭。
4.3 基于JWT的登录态保持与拦截器设计
用户在登录成功后,后端返回一个JWT令牌,前端后续请求在Header携带 Authorization: Bearer <token>。拦截器统一从Header中解析用户信息,放入 ThreadLocal 或 RequestContext。
生成JWT的核心代码如下:
java复制String token = Jwts.builder()
.setSubject(user.getId().toString())
.claim("username", user.getUsername())
.claim("role", user.getRole())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000L))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
拦截器判断Token是否有效、是否过期、当前用户角色是否匹配接口权限。比如管理端 /api/admin/** 的接口必须在解析出 role==1 时放行,否则返回403。
下面是一个重要心得:JWT虽然解决了无状态鉴权,但一旦签发后无法在服务端主动让它失效。如果你需要“强制下线”“修改密码后踢出旧Token”这类功能,需要引入Redis黑名单机制。普通课程设计不做这个也够用,自己心里要知道边界在哪里。
4.4 商品浏览与分类检索
商品列表接口是面向用户的高频接口。需要注意如果MySQL查询条件只有“分类”、“关键字”,那不需要上ES。直接在Mapper层用 @Select 注解写动态SQL或者在MyBatis-Plus中用LambdaQueryWrapper构造条件。
一个带有搜索词、分类ID、价格区间组合条件的查询,用MyBatis-Plus如下:
java复制public Page<ProductVO> pageProducts(ProductQuery query) {
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(query.getCategoryId() != null, Product::getCategoryId, query.getCategoryId())
.like(StringUtils.hasText(query.getKeyword()), Product::getName, query.getKeyword())
.between(query.getMinPrice() != null && query.getMaxPrice() != null,
Product::getPrice, query.getMinPrice(), query.getMaxPrice())
.eq(Product::getStatus, 1)
.orderByDesc(Product::getCreatedTime);
return productMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), wrapper);
}
注意这里的“只查status=1商品”,也就是说上下架的商品对用户不可见,但对管理员可见。这样同一个商品表就能支撑两端需求,而不需要额外加 is_deleted 之类的软删除标记。
篮球文化商铺的首页我希望有些展示变化,比如轮播Banner图、编辑推荐位、新品速递,为此额外建了 banner 表和 recommend_product 关联表,但这属于锦上添花,我放在基础功能全部完成后才做,不会一上来就动这种非核心表。
4.5 购物车模块的实现细节
加入购物车的代码看起来简单,但要做合并逻辑。用户如果已经添加过同一商品,再次加入时应该做数量累加并判断上限,而不是产生两条独立记录:
java复制public void addCart(Long userId, Long productId, Integer quantity) {
CartItem item = cartItemMapper.selectByUserAndProduct(userId, productId);
if (item == null) {
// 新增购物车记录
} else {
// 数量累加,并校验不能超过库存
int newQuantity = item.getQuantity() + quantity;
Product product = productMapper.selectById(productId);
if (newQuantity > product.getStock()) {
throw new BusinessException("商品库存不足,当前库存" + product.getStock());
}
item.setQuantity(newQuantity);
cartItemMapper.updateById(item);
}
}
购物车选中结算时,前端把选中行的ID列表传给后端,后端批量查询这些CartItem,再关联Product表检查价格和库存,算出总价,这一步是为创建订单做预校验,避免用户在前端看到的是旧价格。
4.6 创建订单与扣库存的事务控制
创建订单是整个系统中最需要严谨对待的环节,它涉及多张表的写入,必须保证原子性。如果创建订单成功但扣减库存失败,会导致超卖;如果库存扣了但订单没创建成功,会导致用户钱付了货没生成。
我在订单Service方法上直接加 @Transactional(rollbackFor = Exception.class),并且按照“预校验商品状态 -> 生成订单主表 -> 生成订单明细 -> 扣减库存 -> 清空对应购物车”这个顺序执行。
核心代码:
java复制@Transactional(rollbackFor = Exception.class)
public OrderVO createOrder(Long userId, CreateOrderDTO dto) {
List<CartItem> cartItems = cartItemMapper.selectByIdsAndUser(dto.getCartItemIds(), userId);
// 1. 计算总价,校验库存
BigDecimal total = BigDecimal.ZERO;
List<OrderItem> orderItems = new ArrayList<>();
for (CartItem cartItem : cartItems) {
Product product = productMapper.selectById(cartItem.getProductId());
if (product == null || product.getStatus() != 1) {
throw new BusinessException("商品已下架");
}
if (product.getStock() < cartItem.getQuantity()) {
throw new BusinessException(product.getName() + "库存不足");
}
total = total.add(product.getPrice().multiply(new BigDecimal(cartItem.getQuantity())));
// 填充订单明细快照
}
// 2. 生成订单号
String orderNo = generateOrderNo();
// 3. 插入orders主表
// 4. 批量插入order_item
// 5. 扣减库存
for (CartItem cartItem : cartItems) {
productMapper.reduceStock(cartItem.getProductId(), cartItem.getQuantity());
}
// 6. 删除已购买的购物车记录
cartItemMapper.deleteBatchIds(dto.getCartItemIds());
return orderVO;
}
这里必须关注一个隐藏问题:如果在高并发下直接执行 UPDATE product SET stock = stock - #{num} WHERE stock >= #{num},会比先查库存再改的方式更安全。MySQL的行锁机制保证了一次update的原子性,所以商品表提供的扣减方法应该是带条件更新,而不是先select后update。
java复制@Update("UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} " +
"WHERE id = #{productId} AND stock >= #{quantity}")
int reduceStock(@Param("productId") Long productId, @Param("quantity") int quantity);
如果update返回0,说明库存不够,这个时候应该抛出异常并回滚整个事务,否则就会出现超卖。
4.7 管理端后台的商品管理与订单状态流转
管理员登录后,通过管理端接口维护分类和商品。由于管理员是同一个用户表里的role字段区分,所以管理端删除商品不是物理删除,而是将status改为0下架,这样用户端立即不可见,订单历史里的商品快照不受影响。
订单状态流转要设计好状态机约束。我写了一个简单的状态变更方法:
java复制public void updateOrderStatus(Long orderId, int targetStatus) {
Orders order = ordersMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 检查从当前状态是否允许变更为目标状态
if (!canChange(order.getStatus(), targetStatus)) {
throw new BusinessException("当前订单状态不能执行该操作");
}
// 更新状态和时间戳
}
状态机校验表设计:
| 操作 | 原状态 | 目标状态 | 说明 |
|---|---|---|---|
| 取消订单 | 0待付款 | 4已取消 | 用户取消 |
| 付款 | 0待付款 | 1待发货 | 模拟支付 |
| 发货 | 1待发货 | 2已发货 | 管理员操作 |
| 确认收货 | 2已发货 | 3已签收 | 用户操作 |
| 退款 | 1待发货/2已发货 | 5已退款 | 管理员操作 |
状态机校验最大的价值是防止非法跳转。很多不成熟项目都有一个致命bug:用户只要调一下接口就能把待付款订单直接改成已签收,这在答辩时一旦被老师抓到,印象分直接崩掉。
5. 从编码完成到本地调试的实测记录
5.1 开发环境的配置细节
很多人从网上拉一个Spring Boot项目到自己电脑上运行不起来,90%是环境变量、JDK、Maven仓库的问题,代码本身反而是完好的。
首先要检查IDEA中项目的 File -> Project Structure:
- Project SDK:选择1.8
- Project language level:选择8
- Modules -> 当前模块 -> language level:也选8
- Java Compiler -> Bytecode version:选择8
这个三个位置必须保持一致,否则启动时会编译错误或者版本报错。我见过学生机器上项目SDK选的JDK17,Module编译级别却写的8,结果一路报错,找了一天不知道原因。
其次是Maven仓库。国内网络环境下,强烈建议配置阿里云镜像,在 settings.xml 里加入:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
如果不配置镜像,首次加载Spring Boot依赖很可能长时间卡住或者下载失败,这个时间的损失完全不值得。
5.2 application.yml配置要点
Spring Boot的配置文件我需要区分开发和演示环境。一个有效做法是创建不同环境的配置文件:
yaml复制# application.yml 主配置
spring:
profiles:
active: dev
yaml复制# application-dev.yml 开发环境
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/basketball_store?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
这里的 serverTimezone=Asia/Shanghai 必须设置,否则MySQL连接会报时区错误。allowPublicKeyRetrieval=true 是MySQL 8.0以上版本连接时常需要的参数,否则会报 “Public Key Retrieval is not allowed”。
MyBatis-Plus相关配置:
yaml复制mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.store.basketball.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
其中 map-underscore-to-camel-case 必须开启,这样数据库字段 created_time 就能自动映射到实体类属性 createdTime,不用手动写一堆ResultMap。开发阶段开启日志 StdOutImpl 方便看到SQL执行,部署到生产环境时记得关掉。
5.3 数据库初始化工具脚本设计
项目交付时必需附带一份完整的数据库初始化脚本 schema.sql,方便接收方快速建库建表。我的做法是把初始化脚本分为两部分:
schema.sql:创建库、建表语句,可重复执行(通过CREATE TABLE IF NOT EXISTS)。data.sql:初始化分类数据、管理员账号、示例商品数据。
为了让演示效果好看,data.sql里的商品数据要足够有代入感。示例数据准备好8到10件商品,覆盖服装、鞋类、配件三类,名称要带篮球文化属性,比如“城市限定篮球文化T恤”“复古涂鸦连帽卫衣”“全明星主题运动手环”这类。图片地址我建议引用本地 static/upload 目录下的占位图片,或者直接使用无版权的占位图URL,避免加载外部站点图片时因网络问题显示失败,影响首次打开印象。
5.4 启动时常见的异常与处理
我在跑这种项目时几乎必然遇到下面几个问题,提前写在这里供大家排查。
问题1:控制台报 Access denied for user 'root'@'localhost' (using password: YES)
原因很简单,application.yml里的数据库密码和本地MySQL密码不一致。排查方法是先用Navicat测试能否连接成功,如果Navicat能连而项目连不上,检查配置中的URL是否写错库名。如果连库名都不存在,项目启动会报 Unknown database,需要先执行schema.sql建库。
问题2:端口被占用,Port 8080 was already in use
开发环境最常见,IDEA终端执行:
bash复制netstat -ano | findstr 8080
找到占用8080的PID后,在任务管理器结束对应进程,或者直接改配置 server.port=8081,二选一即可。更省事的方案是Spring Boot自带随机端口配置:server.port=0,但这样每次启动端口都不同,调试前后端联调不方便,不建议用作常规手段。
问题3:使用Postman测试POST接口时报403或者404
403通常是跨域拦截器或JWT拦截器挡掉了,404多半是Controller路径写错。注意Spring Boot 2.7里如果引入了Spring Security,默认所有接口都要鉴权,如果没有正确放行登录接口,登录请求也会403。本系统如果用拦截器方案,需要在拦截器配置里放行 /api/user/login、/api/user/register以及静态资源路径 /static/**。
问题4:页面能打开,但所有静态资源CSS/JS加载404
这通常是模板路径配置问题。如果用Thymeleaf,页面放 templates 目录,CSS/JS放 static 目录。在HTML中使用链接时要写相对路径 /static/css/style.css 或者Thymeleaf表达式 th:href="@{/css/style.css}",不要把绝对磁盘路径写进去。
5.5 模拟支付功能的业务处理
真正对接支付宝或者微信支付需要企业资质,个人开发者在毕设和作品展示阶段一般不做真实支付。我的方案是在支付按钮触发后进入一个模拟支付页,用户点击“确认支付”由后端更新订单状态。这里要注意模拟支付的代码也要走状态机校验,只允许待付款订单变更为待发货状态,同时记录支付时间。以后如果接入真实支付,只需要在支付成功回调接口里调用同样的状态更新方法即可。
6. 打包部署:从IDEA到一台能跑起来的演示环境
6.1 使用Maven打jar包
这个项目我最终以可执行Jar方式部署。在IDEA右侧Maven面板执行 clean 然后 package,或者在项目根目录执行:
bash复制mvn clean package -DskipTests
构建成功后,在 target 目录下会看到一个 basketball-store-0.0.1-SNAPSHOT.jar 文件。以Spring Boot内置Tomcat方式运行的Jar包不需要额外安装Tomcat,极大简化部署流程。需要特别注意的是,如果项目里引入了JSP相关依赖,就不能简单打成Jar运行了,所以我坚持使用Thymeleaf模板,天然支持打包成Jar。
生成Jar文件的大小一般在60到90MB,因为包含了所有依赖库。这个大小是完全正常的,不用惊讶。首次传输到服务器如果慢,可以考虑用内网传输或者压缩分包上传。
6.2 服务器端环境预检与启动脚本
服务器上需要提前装好JDK 1.8和MySQL 8.0。JDK安装后执行 java -version 确认版本;MySQL启动后执行 mysql -uroot -p 登录,并把数据库脚本导入:
bash复制mysql -uroot -p < /opt/basketball_store/schema.sql
mysql -uroot -p < /opt/basketball_store/data.sql
上传Jar到服务器后,我习惯写一个简单的启动脚本 start.sh:
bash复制#!/bin/bash
nohup java -jar /opt/basketball_store/basketball-store-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod \
--server.port=8080 \
> /opt/basketball_store/logs/run.log 2>&1 &
echo "started pid: $!"
用 nohup 启动后,即使SSH断开进程也不会被杀掉。日志输出到 logs/run.log,方便后期排错。如果关闭进程,可以执行 jps 找到Jar对应的PID,然后 kill -9 PID,更规范的做法是用Spring Boot Actuator的shutdown端点或kill进程组,但课程演示阶段直接kill也是可以接受的。
6.3 Nginx反向代理与静态资源缓存
如果服务器上80端口被Nginx占据,或者希望通过域名直接访问系统而不用输入8080端口,可以配置Nginx反向代理:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static/ {
alias /opt/basketball_store/static/;
expires 30d;
}
}
这里把 /static/ 路径直接映射到服务器磁盘目录,由Nginx直接返回静态文件,不经过Java进程,减少不必要的Java层开销。如果项目里用户上传了图片并保存到本地 upload 目录,记得把该目录同样映射到Nginx,否则页面上的图片会加载失败。
6.4 演示系统上线后的自查清单
部署完成后,我会按以下顺序做一遍完整验收:
- 访问首页,看商品列表是否展示,图片是否正常。
- 注册一个新账号,验证用户名重复提示。
- 登录后把一件商品加入购物车,修改数量后再结算。
- 提交订单,在订单列表看到待付款状态。
- 模拟支付,确认库存减少、销量增加,购物车内项被清除。
- 用管理员账号登录后台,修改商品价格并上架一个新商品,刷新前端确认生效。
- 管理员对待发货订单执行发货,用户端刷新订单详情,确认状态从待发货变为已发货。
- 直接访问
/api/user/orders,确认未登录访问返回401,已登录返回200。
如果这八步全部通过,这个系统的交付质量就能达到一个能打分的水平。
我在交付时还会额外写一份简短的README,把启动步骤、默认管理员密码、测试账号、JDK/MySQL版本要求写清楚。这个README文件本身就是项目的一部分,它能让接收者不在你身边的情况下也能自己把系统跑起来,真正做到“程序+源码+数据库+调试部署+开发环境”全流程闭环。很多问题表面上看起来是技术水平不够,实际上是交付习惯和思考完整性上差了一口气。希望这篇完整的实现记录,能帮大家在类似Spring Boot商铺项目的开发部署过程中少走几个坑。
