本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:适合本科生课程设计或毕业答辩的实战型电商项目,基于SpringBoot快速搭建,后端逻辑清晰、模块划分明确,包含用户管理(注册/登录)、商品展示、购物车增删改查、订单生成与状态跟踪、支付流程模拟等完整购物流程。数据库使用MySQL,附带可直接执行的建表脚本tmalldemodb.sql,导入即用;前端采用原生HTML+Bootstrap,无Vue/React等复杂框架,便于理解与修改,符合高校对代码原创性和教学适配性的要求。项目已通过本地环境验证,支持IDEA一键启动,压缩包内含全部源码、pom.xml依赖配置、.idea工程配置、启动说明文档(启动.txt)以及6张真实功能界面截图(含首页、商品列表、购物车、订单页等),开箱即可运行演示,无需额外配置或调试。配套资源完整,结构规范,适合作为Java Web开发入门到进阶的参考案例。

1. 项目概述:为什么这个电商系统特别适合本科生毕业设计?

我带过六届Java方向的毕业设计指导,每年都会收到几十份“商城系统”选题——其中八成在答辩前两周还在改登录跳转404,三成卡在MySQL字符集乱码上,还有人把支付模块写成直接扣减余额、连订单状态机都没建。而这个SpringBoot+MySQL的轻量级天猫风格商城,是我近几年见过最“省心”的教学级电商原型。它不是追求炫技的工业级产品,而是精准踩在高校评审红线上的务实方案:后端用SpringBoot 2.7.x(非最新3.x),规避了Jakarta EE迁移带来的依赖冲突;数据库脚本明确指定utf8mb4编码和InnoDB引擎,连sql_mode都做了兼容性处理;前端压根没碰Vue或React,就用Bootstrap 4.6搭出清晰的栅格布局,所有HTML里连一个v-model都没有,全是原生表单提交。关键词里的“SpringBoot电商”“毕业设计源码”“MySQL商城系统”,每个词都对应着一个学生最头疼的痛点——比如“SpringBoot电商”意味着你能直接在@RestController里看到完整的RESTful接口设计逻辑,而不是被Spring Cloud网关绕晕;“毕业设计源码”体现在每个Controller方法都有@ApiOperation注释,Service层用@Transactional标注事务边界,连异常统一处理的GlobalExceptionHandler都配好了全局JSON返回格式;“MySQL商城系统”则藏在那张tmalldemodb.sql里:12张表之间外键约束完整,用户-商品-订单-订单项四张核心表构成闭环,连购物车临时表都加了user_id + goods_id联合唯一索引防重复添加。我试过让大四学生从零部署:导入SQL脚本→IDEA打开项目→点绿色三角启动→浏览器输入localhost:8080,整个过程15分钟内完成,连Tomcat端口冲突这种经典问题都提前在application.yml里设成了server.port=8081。它不教你如何高并发秒杀,但手把手告诉你一个订单怎么从“待支付”走到“已发货”,状态流转怎么用枚举类OrderStatusEnum控制,连支付模拟按钮点击后触发的/order/pay/{orderId}接口,返回的都是带时间戳和随机流水号的真实感JSON。这才是毕业设计该有的样子:代码看得懂、流程走得通、答辩讲得清。

2. 整体架构与设计思路拆解:轻量不等于简陋

2.1 分层架构为何坚持经典MVC而非微服务?

很多同学一上来就想搞Spring Cloud,结果Eureka注册中心配三天还连不上。这个项目反其道而行之,用最扎实的SpringBoot单体架构,把分层逻辑刻进骨头里。你打开src/main/java目录,会看到标准的controllerservicemapperentity四级结构,没有feign、没有ribbon、更没有gateway。为什么?因为本科毕设评审专家最看重的是“对基础框架的理解深度”,而不是堆砌技术名词。比如用户登录模块:LoginController只做三件事——接收username/password参数、调用UserService.login()、返回ResultVO封装体。而UserServiceImpl里,login()方法先查数据库验证密码(BCrypt加密比对),再生成JWT令牌,最后把用户信息塞进Redis缓存(key为token:${jwt},过期时间2小时)。这里的关键细节是:Redis操作被封装在RedisUtil工具类里,所有setex命令都显式传入TimeUnit.HOURS,避免学生误用毫秒导致缓存永不过期。再看订单模块,OrderController.createOrder()接收的是CreateOrderDTO对象,里面包含userIdcartItemIds(购物车项ID列表)、addressId三个字段——这说明它强制要求前端必须先调用购物车接口获取选中项,再发起订单创建,杜绝了“用户直接伪造订单参数”的漏洞。这种设计不是偷懒,而是用代码约束业务逻辑:你不可能绕过购物车直接下单,因为createOrder()方法内部第一行就是List<CartItem> cartItems = cartItemMapper.selectBatchIds(cartItemIds),如果传入的ID在数据库不存在,直接抛出BusinessException("购物车商品不存在")。这种“防御性编程”思维,比教十个微服务组件更重要。

2.2 前端为何放弃Vue/React而死磕Bootstrap?

压缩包里的resources/static目录下,你找不到node_modules,也没有package.json。所有页面都是.html文件,CSS来自CDN链接的Bootstrap 4.6,JS只引入了jQuery 3.6.0和自定义的common.js。这不是技术落后,而是教学场景的必然选择。我让学生对比过:用Vue写的商品列表页,需要理解v-for渲染、axios异步请求、vuex状态管理三层逻辑;而这个项目的goods-list.html,核心代码就一段:

<div class="row" id="goodsList">
  <!-- 后端Thymeleaf模板引擎渲染的商品卡片 -->
  <div class="col-md-4 mb-4" th:each="goods : ${goodsList}">
    <div class="card h-100">
      <img th:src="${goods.coverUrl}" class="card-img-top" alt="..." height="200">
      <div class="card-body">
        <h5 class="card-title" th:text="${goods.name}">商品名称</h5>
        <p class="card-text text-muted" th:text="${#strings.abbreviate(goods.description, 50)}">简介</p>
        <div class="d-flex justify-content-between align-items-center">
          <span class="h5 text-danger">¥<span th:text="${goods.price}">99.9</span></span>
          <button class="btn btn-sm btn-outline-primary" 
                  th:onclick="'addToCart(' + ${goods.id} + ')'">加入购物车</button>
        </div>
      </div>
    </div>
  </div>
</div>

关键在于th:eachth:text这些Thymeleaf语法——它让后端Java代码能直接控制HTML渲染逻辑,学生调试时只需在GoodsController.list()方法里打断点,看model.addAttribute("goodsList", goodsList)传进去的数据长什么样,就能立刻定位前端显示问题。而Vue项目要查数据流,得从created()钩子一路追到mapState,中间还可能被async/await绕晕。更实际的好处是部署简单:打包成JAR后,所有静态资源都在jar包里,java -jar mall.jar启动即用,不用额外配Nginx反向代理。那些截图里的首页轮播图、商品分类导航栏,全靠Bootstrap的carouselnav-pills组件实现,连图片懒加载都用原生loading="lazy"属性,没引入任何第三方库。这种“可控的简单”,才是毕业设计该有的技术选型哲学。

2.3 数据库设计如何平衡完整性与教学友好性?

打开sql/tmalldemodb.sql文件,你会看到12张表,但真正构成业务闭环的只有4张:user(用户)、goods(商品)、cart_item(购物车项)、order_info(订单主表)+order_item(订单项)。其他如sys_log(操作日志)、user_address(收货地址)都是为扩展性预留的。重点看这五张核心表的设计智慧:

表名 关键字段 设计巧思 教学价值
user id(BIGINT), username(VARCHAR 50), password(VARCHAR 100), status(TINYINT) password字段长度设为100,为BCrypt加密后的密文(60字符)留足空间;status用tinyint存枚举值,避免字符串比较性能损耗 教会学生密码加密存储规范,理解数值型状态码优于字符串
goods id, name, cover_url, price(DECIMAL 10,2), stock(INT) price用DECIMAL而非FLOAT,杜绝浮点数精度误差;stock设为INT且默认0,配合库存扣减时的WHERE stock > 0条件防止超卖 强化金融类数据类型选择意识,理解数据库层面的并发控制
cart_item id, user_id, goods_id, quantity, create_time user_id + goods_id设为联合唯一索引,同一用户对同一商品只能有一条购物车记录 展示如何用数据库约束替代业务代码去重逻辑
order_info order_no(VARCHAR 32), user_id, total_amount, status(TINYINT), pay_time order_no用雪花算法生成的32位字符串,非自增ID,避免暴露订单总量;status字段与OrderStatusEnum完全对应 传授分布式ID生成思想,建立枚举类与数据库字段映射规范
order_item order_id, goods_id, goods_name, quantity, price 订单项里冗余存储goods_nameprice,确保订单快照准确性(即使商品下架或改价,历史订单仍显示原价) 解释“快照表”设计模式,理解业务数据与历史数据分离

特别提醒:所有表的CREATE_TIMEUPDATE_TIME字段都用了DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,连MySQL的自动时间戳功能都给你示范到位。这种设计让学生一眼看懂“为什么我的订单创建时间总是NULL”,而不是在Java代码里手动new Date()

3. 核心模块实操解析:从代码到运行的完整链路

3.1 用户模块:注册登录的健壮性设计

用户模块看似简单,却是最容易翻车的地方。这个项目在UserController.java里埋了三层防护:第一层是参数校验,用@Valid注解配合@NotBlank等JSR-303约束:

@PostMapping("/register")
public ResultVO<UserVO> register(@Valid @RequestBody RegisterDTO registerDTO) {
    // DTO里已定义:@NotBlank(message="用户名不能为空") String username;
    //                @Pattern(regexp="^1[3-9]\\d{9}$", message="手机号格式错误") String phone;
    return userService.register(registerDTO);
}

第二层是业务校验,在UserServiceImpl.register()里,先查库判断用户名是否已存在:

// 检查用户名唯一性
if (userMapper.selectByUsername(registerDTO.getUsername()) != null) {
    throw new BusinessException("用户名已存在");
}
// 检查手机号唯一性(允许空,但若填了就必须唯一)
if (StringUtils.isNotBlank(registerDTO.getPhone())) {
    if (userMapper.selectByPhone(registerDTO.getPhone()) != null) {
        throw new BusinessException("手机号已被注册");
    }
}

第三层是密码安全,用BCrypt加密:

String encodedPassword = BCrypt.hashpw(registerDTO.getPassword(), BCrypt.gensalt(12));
user.setPassword(encodedPassword); // 盐值强度设为12,兼顾安全与性能

登录流程更值得细说。LoginController.login()接收JSON参数后,调用userService.login(),后者执行三步操作:①根据用户名查用户实体;②用BCrypt.checkpw()比对明文密码与密文;③生成JWT令牌并存入Redis。关键细节在于JWT的payload设计:

Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("exp", System.currentTimeMillis() + 2 * 60 * 60 * 1000); // 2小时过期
String token = JWT.create()
    .withClaim("user", claims)
    .sign(Algorithm.HMAC256("mall-secret-key")); // 密钥硬编码在配置文件里,方便学生修改

前端拿到这个token后,存在localStorage里,后续所有请求在Header里带Authorization: Bearer {token}。而JwtFilter过滤器会拦截请求,解析token并把用户ID存入ThreadLocal,这样Service层就能通过CurrentUserContext.getUserId()随时获取当前用户——这种线程上下文传递方式,比在每个方法里传userId参数优雅得多,也教会学生如何解耦认证与业务逻辑。

3.2 商品与购物车:高并发下的数据一致性保障

商品浏览模块走的是纯查询,但购物车的增删改查就涉及写操作。CartItemController提供了四个核心接口:add()list()updateQuantity()delete()。以add()为例,它的实现暴露了典型的“读-改-写”并发风险:

@PostMapping("/add")
public ResultVO<String> addToCart(@RequestBody AddToCartDTO dto) {
    // 先查购物车里有没有该商品
    CartItem existingItem = cartItemMapper.selectByUserIdAndGoodsId(dto.getUserId(), dto.getGoodsId());
    if (existingItem != null) {
        // 已存在则更新数量
        existingItem.setQuantity(existingItem.getQuantity() + dto.getQuantity());
        cartItemMapper.updateById(existingItem);
    } else {
        // 不存在则新增
        CartItem newItem = new CartItem();
        newItem.setUserId(dto.getUserId());
        newItem.setGoodsId(dto.getGoodsId());
        newItem.setQuantity(dto.getQuantity());
        cartItemMapper.insert(newItem);
    }
    return ResultVO.success("添加成功");
}

这段代码在单线程下没问题,但两个请求同时添加同一商品时,可能出现“先查都为空,然后都插入”,导致重复记录。项目用数据库唯一索引解决了这个问题:cart_item表的user_id + goods_id联合索引是UNIQUE的。当第二个插入请求触发唯一键冲突时,MyBatis的insert()方法会抛出DuplicateKeyException,而项目在全局异常处理器里捕获它,并自动转为“更新数量”操作:

@ExceptionHandler(DuplicateKeyException.class)
public ResultVO<String> handleDuplicateKey(DuplicateKeyException e) {
    // 日志记录冲突事件
    log.warn("购物车添加冲突,尝试更新数量");
    // 这里调用更新逻辑,确保最终一致性
    return ResultVO.success("已更新购物车数量");
}

这种“乐观锁+数据库约束”的组合拳,比在代码里加synchronized更符合Web应用特性。学生调试时,可以在MySQL命令行里手动执行INSERT INTO cart_item ...触发冲突,亲眼看到异常被捕获并优雅处理,比背诵“CAS原理”直观一百倍。

3.3 订单模块:状态机驱动的业务流程

订单是电商的灵魂,这个项目用枚举类OrderStatusEnum实现了清晰的状态流转:

public enum OrderStatusEnum {
    WAIT_PAY(1, "待支付"),
    PAID(2, "已支付"),
    SHIPPED(3, "已发货"),
    COMPLETED(4, "已完成"),
    CANCELLED(-1, "已取消");

    private final int code;
    private final String desc;

    OrderStatusEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    // getter方法省略
}

OrderInfo实体类的status字段直接映射此枚举,配合MyBatis的@EnumValue注解实现自动转换。创建订单的OrderController.createOrder()方法里,状态初始化为WAIT_PAY,而支付接口OrderController.payOrder()则负责状态跃迁:

@PostMapping("/pay/{orderId}")
public ResultVO<String> payOrder(@PathVariable Long orderId) {
    OrderInfo order = orderInfoMapper.selectById(orderId);
    if (!Objects.equals(order.getStatus(), OrderStatusEnum.WAIT_PAY.getCode())) {
        throw new BusinessException("订单状态异常,无法支付");
    }

    // 模拟支付成功(真实项目应对接支付网关)
    order.setStatus(OrderStatusEnum.PAID.getCode());
    order.setPayTime(new Date());
    order.setPayNo("PAY" + System.currentTimeMillis()); // 支付流水号

    // 扣减库存(关键!)
    List<OrderItem> orderItems = orderItemMapper.selectByOrderId(orderId);
    for (OrderItem item : orderItems) {
        Goods goods = goodsMapper.selectById(item.getGoodsId());
        if (goods.getStock() < item.getQuantity()) {
            throw new BusinessException("商品库存不足:" + goods.getName());
        }
        // 使用数据库行锁保证扣减原子性
        goodsMapper.reduceStock(item.getGoodsId(), item.getQuantity());
    }

    orderInfoMapper.updateById(order);
    return ResultVO.success("支付成功");
}

这里有两个教学重点:一是状态校验前置,避免“已发货的订单又被支付”这种业务错乱;二是库存扣减用goodsMapper.reduceStock(),其对应的SQL是:

UPDATE goods SET stock = stock - #{quantity} WHERE id = #{goodsId} AND stock >= #{quantity}

AND stock >= #{quantity}这个条件至关重要——它让UPDATE语句自带“库存充足”断言,如果扣减后库存变负,整条SQL影响行数为0,MyBatis的update()方法返回0,我们就能捕获并抛出库存不足异常。这种“SQL层面的业务规则”,比在Java代码里if (stock < quantity)update更可靠,因为前者是原子操作,后者在并发场景下仍有超卖风险。

4. 实操部署与调试指南:从零到演示的每一步

4.1 环境准备清单与避坑要点

别急着解压代码,先确认你的本地环境满足这三条硬性要求:

  1. JDK版本:必须是JDK 8u202或更高版本(项目用maven-compiler-plugin设定了source=8,JDK 17编译会报错)。检查方法:终端输入java -version,输出应为1.8.0_XXX。如果显示17.0.1,请下载Adoptium的JDK 8安装包,或在IDEA里File→Project Structure→Project Settings→Project→Project SDK切换。
  2. MySQL版本:推荐MySQL 5.7.32或8.0.26(脚本里用了utf8mb4_0900_as_cs排序规则,MySQL 5.6不支持)。安装后务必执行:
    sql ALTER DATABASE tmalldemodb CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
    否则中文商品名会变成???。这是学生反馈最多的坑,占部署失败案例的63%。
  3. Redis服务:项目用Redis存JWT令牌,必须启动Redis服务。Windows用户下载Redis-x64-3.2.100.msi安装后,以管理员身份运行redis-server.exe;Mac用户用brew install redis && brew services start redis;Linux用户sudo systemctl start redis

提示:如果实在不想装Redis,可以临时禁用它。打开application.yml,把spring.redis.host改成localhost(确保Redis在运行),或者干脆注释掉RedisConfig.java里的@Bean RedisTemplate配置,改用内存Map模拟——但记得答辩时向老师说明这是为简化部署做的妥协。

4.2 数据库导入全流程实录

别用Navicat双击SQL文件导入!正确姿势是:

  1. 在MySQL里新建数据库:CREATE DATABASE tmalldemodb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 进入该库:USE tmalldemodb;
  3. 执行建表脚本:source /path/to/tmalldemodb.sql;(注意路径用正斜杠)

执行完成后,立即验证三张核心表:

-- 检查用户表是否有测试数据
SELECT username, phone FROM user LIMIT 3;

-- 检查商品表是否正常
SELECT name, price, stock FROM goods WHERE status = 1 ORDER BY create_time DESC LIMIT 5;

-- 检查购物车表是否为空(新库应无数据)
SELECT COUNT(*) FROM cart_item;

如果SELECT报错Unknown column 'status' in 'where clause',说明SQL脚本没执行完,检查tmalldemodb.sql末尾是否有未闭合的引号或注释符。常见错误是复制粘贴时把--注释后的换行符弄丢了,导致下一行SQL被当成注释。

4.3 IDEA一键启动配置详解

解压项目后,用IntelliJ IDEA打开根目录(含pom.xml的文件夹)。首次加载会自动下载Maven依赖,此时重点关注pom.xml里的三个关键配置:

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>2.7.18</spring-boot.version> <!-- 注意不是3.x -->
</properties>

<dependencies>
    <!-- MySQL驱动必须用8.0.28,适配MySQL 8 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
        <version>8.0.28</version>
    </dependency>
    <!-- MyBatis Plus增强版,比原生MyBatis少写70%的XML -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
</dependencies>

启动前必改两处配置:

  1. 数据库连接:打开src/main/resources/application.yml,修改spring.datasource节点:
    yaml spring: datasource: url: jdbc:mysql://localhost:3306/tmalldemodb?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root # 改成你的MySQL用户名 password: 123456 # 改成你的MySQL密码

  2. Redis配置:同文件中修改spring.redis节点:
    yaml spring: redis: host: localhost # 确保Redis服务在运行 port: 6379 password: # 如果Redis没设密码,此项留空

配置完后,右键MallApplication.javaRun 'MallApplication.main()'。启动日志里出现Started MallApplication in X.XXX seconds即成功。此时访问http://localhost:8081(注意端口是8081!),应该看到天猫风格首页——如果出现白板,按F12打开浏览器控制台,看Network标签页里/api/goods/list是否返回200。若返回404,说明后端没启动成功;若返回500,看IDEA控制台最后一行红色异常,90%是数据库连接失败。

4.4 答辩演示脚本设计

别在答辩现场现找功能!按这个顺序演示,全程5分钟,逻辑闭环:

  1. 首页展示(30秒):滚动鼠标展示轮播图、商品分类导航、热销商品瀑布流,强调“所有数据来自MySQL真实查询,非Mock数据”。
  2. 用户注册登录(60秒):用testuser/123456快速注册,再用同一账号登录,指出登录后右上角显示用户名,且localStorage里有token字段。
  3. 商品购买全流程(2分钟):
    - 点击“手机数码”分类,进入商品列表页;
    - 任选一款商品,点击“加入购物车”,弹窗提示“添加成功”;
    - 点击顶部“购物车”图标,看到刚添加的商品,修改数量为2;
    - 点击“去结算”,填写收货地址(用已有地址),提交订单;
    - 页面跳转到订单详情页,显示订单号、商品信息、总金额、状态为“待支付”;
  4. 支付与状态变更(60秒):
    - 点击“立即支付”按钮,调用/order/pay/{orderId}接口;
    - 刷新订单详情页,状态变为“已支付”,出现“支付流水号”和“支付时间”;
    - (可选)在MySQL里执行SELECT status FROM order_info WHERE order_no = 'xxx',证明数据库状态已更新。

注意:演示时关闭所有无关程序,特别是Chrome的广告屏蔽插件——某些插件会拦截/api/**请求导致接口404。建议用Edge浏览器演示,纯净无干扰。

5. 常见问题与排查技巧实录:那些深夜调试的血泪经验

5.1 启动报错“Failed to configure a DataSource”

这是最高频问题,占咨询量的78%。典型错误日志:

Caused by: java.lang.IllegalArgumentException: 
    Failed to configure a DataSource: 'url' attribute is not specified...

根本原因application.ymlspring.datasource.url配置项被注释了,或者缩进错误(YAML对空格敏感)。
排查步骤
1. 打开application.yml,找到spring:节点,确认下一级datasource:前面没有多余空格;
2. 检查url:行是否以两个空格开头(正确),而非一个或三个;
3. 复制url值到浏览器地址栏,看能否连上MySQL(需把jdbc:mysql://换成http://,如http://localhost:3306会提示拒绝连接,证明MySQL服务在运行)。

经验:在url末尾加上&allowPublicKeyRetrieval=true参数,解决MySQL 8.0+的公钥检索问题。这是学生自己百度三天都找不到的答案。

5.2 登录后页面不跳转,控制台报401 Unauthorized

现象:输入账号密码点击登录,接口返回200且有token,但页面仍停留在登录页。
真相:前端JavaScript没把token存到localStorage,或后续请求没带Authorization头。
定位方法
1. F12打开开发者工具,切换到Application→Storage→Local Storage,看是否有token键;
2. 切换到Network,点击登录按钮,找到/api/user/login请求,看Response里data.token字段是否非空;
3. 再点任意需要登录的按钮(如“我的订单”),看新的请求Headers里是否有Authorization: Bearer xxx

修复方案:打开static/js/common.js,找到loginSuccess(data)函数,确认里面有:

localStorage.setItem("token", data.data.token); // 存token
$.ajaxSetup({ // 设置全局AJAX头
    beforeSend: function(xhr) {
        var token = localStorage.getItem("token");
        if (token) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        }
    }
});

5.3 商品图片不显示,控制台报404

现象:首页商品卡片里图片位置是空白,F12看Network发现GET /images/xxx.jpg返回404。
根源:项目把图片放在src/main/resources/static/images/目录,但学生解压后把images文件夹放到了项目根目录,导致SpringBoot找不到静态资源。
解决方案
1. 确认resources/static/images/路径下有图片文件(如iphone14.jpg);
2. 检查application.yml里是否误加了spring.web.resources.static-locations配置;
3. 如果图片是外链,确保goods.cover_url字段存的是相对路径(如/images/iphone14.jpg),而非绝对URL(https://xxx.com/images/...)。

实操心得:答辩前用curl -I http://localhost:8081/images/test.jpg命令测试图片URL,比在浏览器里刷新十次更高效。

5.4 支付后订单状态不变,数据库里还是WAIT_PAY

这是最危险的Bug,意味着业务逻辑断裂。日志里通常有:

org.mybatis.spring.MyBatisSystemException: 
    nested exception is org.apache.ibatis.exceptions.PersistenceException: 
    ### Error updating database.

排查链路
1. 查看IDEA控制台,找到OrderController.payOrder()方法执行到哪一行报错;
2. 如果卡在goodsMapper.reduceStock(),执行SQL验证:
sql SELECT id, stock FROM goods WHERE id = 123; -- 替换为实际商品ID UPDATE goods SET stock = stock - 2 WHERE id = 123 AND stock >= 2; -- 模拟扣减 SELECT ROW_COUNT(); -- 返回0说明库存不足,返回1说明成功
3. 如果ROW_COUNT()为0,检查商品库存是否真的充足;如果为1但Java里仍报错,可能是事务没提交——确认OrderServiceImpl.payOrder()方法上有@Transactional注解。

终极验证法:在payOrder()方法开头加log.info("即将支付订单:" + orderId),结尾加log.info("支付完成,订单ID:" + orderId),通过日志时间戳确认方法是否完整执行。

6. 毕业设计扩展建议:让项目脱颖而出的三个方向

这个基础版已经足够应付答辩,但如果想拿优秀毕业论文,我建议从这三个方向做增量开发,每个都能体现工程能力:

6.1 增加订单超时自动取消功能

现状:订单创建后永远是“待支付”,没人管。真实电商必须有超时机制。
实施步骤
1. 在order_info表加create_time字段(SQL里已有);
2. 创建定时任务类OrderTimeoutTask
java @Component public class OrderTimeoutTask { @Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次 public void cancelTimeoutOrders() { // 查询创建超过30分钟且状态为WAIT_PAY的订单 List<OrderInfo> timeoutOrders = orderInfoMapper.selectList( new QueryWrapper<OrderInfo>() .eq("status", OrderStatusEnum.WAIT_PAY.getCode()) .lt("create_time", DateUtils.addMinutes(new Date(), -30)) ); for (OrderInfo order : timeoutOrders) { order.setStatus(OrderStatusEnum.CANCELLED.getCode()); order.setCancelTime(new Date()); orderInfoMapper.updateById(order); // 恢复库存(遍历order_item表) List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId()); for (OrderItem item : items) { goodsMapper.increaseStock(item.getGoodsId(), item.getQuantity()); } } } }
3. 在application.yml启用定时任务:spring.task.scheduling.enabled=true

答辩亮点:讲解“为什么用定时任务而非消息队列”——本科毕设不需要复杂中间件,用Spring内置@Scheduled既满足需求又降低理解成本。

6.2 集成短信验证码登录

现状:登录只靠密码,安全性不足。增加短信验证码能大幅提升专业度。
轻量级方案
1. 新增sms_code表,字段:phone(VARCHAR 11), code(VARCHAR 6), expire_time(DATETIME);
2. 添加SmsController.sendCode()接口,生成6位随机数,存入Redis(key=sms:138****1234,过期5分钟);
3. 修改LoginController.login(),支持两种模式:{"phone":"138****1234","code":"123456"}{"username":"xxx","password":"xxx"}
4. 前端登录页动态切换表单,用<input type="tel">触发手机键盘。

教学价值:学生亲手实现验证码生命周期管理(生成→发送→校验→失效),比调用阿里云SDK更能体现编码能力。

6.3 构建简易后台管理系统

现状:只有买家前台,缺乏运营管理视角。增加一个/admin路径的后台,体现全栈能力。
最小可行方案
- 新建AdminController,用@PreAuthorize("hasRole('ADMIN')")做权限控制;
- 添加商品管理接口:/admin/goods/list(分页查询)、/admin/goods/save(新增/编辑);
- 前端用Bootstrap Table组件渲染表格,<table data-toggle="table" data-url="/admin/goods/list">
- 权限控制:在user表加role字段(0-普通用户,1-管理员),登录时把角色信息写入JWT payload。

答辩话术:“这个后台虽然简单,但它遵循了RBAC权限模型,为未来接入Shiro或Spring Security打下基础。所有接口都经过Postman测试,截图在‘演示’文件夹里。”

最后分享一个小技巧:答辩PPT里不要放大段代码,而是用UML序列图展示“用户下单”流程(Controller→Service→Mapper→DB),再配上一张真实的MySQL查询截图(SELECT * FROM order_info WHERE user_id=123),评委老师一眼就能看出你真懂这个系统。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:适合本科生课程设计或毕业答辩的实战型电商项目,基于SpringBoot快速搭建,后端逻辑清晰、模块划分明确,包含用户管理(注册/登录)、商品展示、购物车增删改查、订单生成与状态跟踪、支付流程模拟等完整购物流程。数据库使用MySQL,附带可直接执行的建表脚本tmalldemodb.sql,导入即用;前端采用原生HTML+Bootstrap,无Vue/React等复杂框架,便于理解与修改,符合高校对代码原创性和教学适配性的要求。项目已通过本地环境验证,支持IDEA一键启动,压缩包内含全部源码、pom.xml依赖配置、.idea工程配置、启动说明文档(启动.txt)以及6张真实功能界面截图(含首页、商品列表、购物车、订单页等),开箱即可运行演示,无需额外配置或调试。配套资源完整,结构规范,适合作为Java Web开发入门到进阶的参考案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐