毕设通关秘籍:基于Spring Boot的电商平台实战,从零到一避坑全攻略!

46 阅读12分钟

毕设通关秘籍:基于Spring Boot的电商平台实战,从零到一避坑全攻略!

家人们谁懂啊!做电商平台毕设时,光购物车表和订单表的库存并发问题就让我卡了整整5天——一开始没加事务锁,用户疯狂秒杀商品时库存直接变负数,导师看了直摇头说“这系统没法用”😫。后来熬夜改代码才总结出这套实战经验,今天把需求、技术、实现到测试的细节全公开,帮你轻松搞定毕设!

一、先搞懂“电商平台要啥”!需求分析是第一步

刚开始我直接写代码,花了两周做了个“商品分享朋友圈”功能,结果导师一句“电商核心是交易流程,不是社交功能”直接打回重做!后来才明白,需求分析要先抓住“谁用系统、要干啥”,这步做对了,后面能少走90%弯路。

1. 核心用户&功能拆解(踩坑总结版)

电商平台主要有三类用户:管理员商家普通用户(别乱加“物流员角色”!我当初加了后,发货流程全乱了,最后砍掉才顺):

  • 管理员端(核心功能):
    • 商家管理:审核商家资质、设置星级(用五星评分,别让管理员手动输数字)
    • 用户管理:查看用户列表、重置密码(支持手机号/姓名模糊查询)
    • 公告管理:发布促销活动、删除过期公告(加“生效时间”字段,自动上下架)
    • 数据统计:查看交易数据、导出报表(别漏“按时间段筛选”)
  • 商家端(重要功能):
    • 商品管理:新增商品、上传图片、管库存(支持批量导入,我当初没加,手动加100个商品到手酸)
    • 订单处理:查看订单、发货操作(用状态流:待付款→已付款→待发货→已发货→已完成)
    • 评价回复:查看用户评价、回复差评(加“回复时间戳”,避免纠纷)
  • 用户端(核心功能):
    • 商品浏览:按分类/价格/销量筛选、收藏商品(首页按“点击量”推荐热门)
    • 购物流程:加购物车、选收货地址、提交订单(满99元包邮,提升客单价)
    • 订单管理:查看订单状态、申请退款、评价商品(显示物流轨迹,提升体验)

2. 需求分析避坑指南(血泪教训!)

  • 别空想!找几个朋友模拟下单提意见:有朋友说“想批量删除购物车”,我才加了“购物车全选”功能
  • 一定要画用例图!用DrawIO画简洁版,标清“用户-下单”“商家-发货”,汇报时比光说“我有XX功能”直观多了
  • 写“需求文档”!不用复杂,把“功能点、约束条件”写清楚(比如“库存不能为负”“订单15分钟未付款自动取消”)

3. 可行性分析要写实!3点说清就过关

导师最爱问“你这系统可行吗”,别只说“技术可行”,从3个角度写:

  • 技术可行性:Spring Boot、MySQL、Vue都是成熟技术,学习资料多(别选太新的框架!)
  • 经济可行性:所有工具免费,开发成本几乎为0
  • 操作可行性:界面参考淘宝,用户上手快

二、技术选型要务实!这套组合稳得很

刚开始我用Spring Boot+React+Redis,结果“购物车缓存”配置出问题,用户数据总丢失😫。后来换成Spring Boot+Vue.js+MySQL,新手友好,调试简单!

1. 技术栈详细对比(附避坑提醒)

技术工具为啥选它避坑提醒!
Spring Boot 2.7配置简单,生态成熟别用3.x!兼容性还在完善
Vue.js 2.x学习曲线平缓,文档丰富按需引入组件,减小打包体积
MySQL 8.0性能稳定,事务支持好一定设utf8mb4编码!
Element-UI组件丰富,开发快注意版本兼容性

2. 开发环境搭建(一步到位)

  1. JDK 1.8:配好JAVA_HOME环境变量
  2. Node.js 14+:装好npm
  3. MySQL 8.0:用Navicat可视化操作
  4. IDEA:社区版就够用

3. 架构图要画!答辩加分

用DrawIO画前后端分离架构:Vue前端请求→Spring Boot后端处理→MySQL存数据。展示时评委都说“架构清晰”!

三、数据库设计:表关联别乱来

我当初没设计好“订单-商品”关联,查销售数据要写复杂SQL,调试到凌晨😫。按“实体-关系”设计才理顺。

1. 核心实体&属性(必做表)

  • 用户表(yonghu):id、username、password(MD5加密!)、yonghu_name、yonghu_phone
  • 商品表(goods):id、shangjia_id(关联商家)、goods_name、goods_photo(存路径)、goods_kucun_number(库存)
  • 订单表(goods_order):id、order_uuid_number(唯一订单号)、yonghu_id、goods_id、order_true_price(实付)

避坑:商品图片别存数据库!存路径就好。

2. 建表SQL示例(商品表)

CREATE TABLE `goods` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `shangjia_id` INT DEFAULT NULL COMMENT '商家ID',
  `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
  `goods_photo` VARCHAR(500) DEFAULT NULL COMMENT '图片路径',
  `goods_types` INT DEFAULT NULL COMMENT '分类:1食品/2服装/3数码',
  `goods_kucun_number` INT DEFAULT 0 COMMENT '库存',
  `goods_new_money` DECIMAL(10,2) DEFAULT 0.00 COMMENT '现价',
  `goods_clicknum` INT DEFAULT 0 COMMENT '点击量',
  `shangxia_types` INT DEFAULT 1 COMMENT '上架状态:1是/0否',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `fk_shangjia` (`shangjia_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3. 测试关联查询

SELECT u.yonghu_name, g.goods_name, o.order_uuid_number, o.order_true_price
FROM goods_order o
JOIN yonghu u ON o.yonghu_id = u.id
JOIN goods g ON o.goods_id = g.id
WHERE o.id = 1;

四、功能实现:核心模块代码

先搞定4个核心模块,答辩足够出彩。

1. 购物车模块(并发控制是重点!)

(1)Spring Boot后端
@Service
@Transactional
public class CartService {
    @Autowired
    private CartMapper cartMapper;
    @Autowired
    private GoodsMapper goodsMapper;
    
    // 添加购物车(带库存校验)
    public Result addCart(Integer userId, Integer goodsId, Integer buyNumber) {
        // 悲观锁:查询商品时加锁
        Goods goods = goodsMapper.selectForUpdate(goodsId);
        
        // 校验库存
        if (goods.getGoods_kucun_number() < buyNumber) {
            throw new RuntimeException("库存不足");
        }
        
        // 检查是否已在购物车
        Cart cart = cartMapper.findByUserAndGoods(userId, goodsId);
        if (cart != null) {
            cart.setBuyNumber(cart.getBuyNumber() + buyNumber);
            cartMapper.updateById(cart);
        } else {
            cart = new Cart();
            cart.setYonghuId(userId);
            cart.setGoodsId(goodsId);
            cart.setBuyNumber(buyNumber);
            cartMapper.insert(cart);
        }
        
        return Result.success("加入购物车成功");
    }
    
    // 批量删除购物车
    public Result batchDelete(List<Integer> cartIds) {
        if (cartIds == null || cartIds.isEmpty()) {
            return Result.error("请选择要删除的商品");
        }
        cartMapper.deleteBatchIds(cartIds);
        return Result.success("删除成功");
    }
}
(2)Vue前端购物车页面
<template>
  <div class="cart-page">
    <el-card>
      <!-- 全选操作 -->
      <div class="cart-header">
        <el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox>
        <el-button type="danger" @click="batchDelete" :disabled="selectedIds.length===0">
          批量删除
        </el-button>
      </div>
      
      <!-- 购物车列表 -->
      <div v-for="item in cartList" :key="item.id" class="cart-item">
        <el-checkbox v-model="item.selected"></el-checkbox>
        <img :src="item.goods_photo" class="goods-img">
        <div class="goods-info">
          <h4>{{ item.goods_name }}</h4>
          <p class="price">¥{{ item.goods_new_money }}</p>
        </div>
        <div class="quantity">
          <el-input-number 
            v-model="item.buy_number" 
            :min="1" 
            :max="item.goods_kucun_number"
            @change="updateQuantity(item)">
          </el-input-number>
        </div>
        <div class="subtotal">小计: ¥{{ (item.goods_new_money * item.buy_number).toFixed(2) }}</div>
        <el-button type="text" @click="deleteItem(item.id)">删除</el-button>
      </div>
      
      <!-- 结算栏 -->
      <div class="cart-footer">
        <div class="total">
          已选 {{ selectedCount }} 件商品,合计: <span class="total-price">¥{{ totalPrice }}</span>
        </div>
        <el-button type="danger" size="large" @click="checkout" :disabled="selectedCount===0">
          去结算
        </el-button>
      </div>
    </el-card>
  </div>
</template>

<script>
export default {
  data() {
    return {
      cartList: [],
      selectAll: false
    };
  },
  computed: {
    // 计算属性:选中的商品ID
    selectedIds() {
      return this.cartList
        .filter(item => item.selected)
        .map(item => item.id);
    },
    // 计算属性:选中商品数量
    selectedCount() {
      return this.selectedIds.length;
    },
    // 计算属性:总金额
    totalPrice() {
      return this.cartList
        .filter(item => item.selected)
        .reduce((sum, item) => {
          return sum + (item.goods_new_money * item.buy_number);
        }, 0)
        .toFixed(2);
    }
  },
  methods: {
    // 加载购物车数据
    async loadCartData() {
      const res = await this.$http.get('/api/cart/list');
      this.cartList = res.data.data.map(item => ({
        ...item,
        selected: false
      }));
    },
    
    // 全选/全不选
    handleSelectAll(val) {
      this.cartList.forEach(item => {
        item.selected = val;
      });
    },
    
    // 更新商品数量
    async updateQuantity(item) {
      try {
        await this.$http.post('/api/cart/update', {
          cartId: item.id,
          buyNumber: item.buy_number
        });
        this.$message.success('数量更新成功');
      } catch (error) {
        this.$message.error('更新失败');
        this.loadCartData(); // 重新加载数据
      }
    },
    
    // 批量删除
    async batchDelete() {
      try {
        await this.$confirm('确定删除选中商品吗?', '提示', {
          type: 'warning'
        });
        
        const res = await this.$http.post('/api/cart/batchDelete', {
          cartIds: this.selectedIds
        });
        
        if (res.data.code === 200) {
          this.$message.success('删除成功');
          this.loadCartData(); // 重新加载
        }
      } catch (error) {
        if (error !== 'cancel') {
          this.$message.error('删除失败');
        }
      }
    },
    
    // 单个删除
    async deleteItem(cartId) {
      try {
        await this.$confirm('确定删除该商品吗?', '提示', {
          type: 'warning'
        });
        
        const res = await this.$http.post('/api/cart/delete', { cartId });
        
        if (res.data.code === 200) {
          this.$message.success('删除成功');
          this.loadCartData();
        }
      } catch (error) {
        if (error !== 'cancel') {
          this.$message.error('删除失败');
        }
      }
    },
    
    // 去结算
    checkout() {
      // 跳转到订单确认页面,传递选中的购物车ID
      this.$router.push({
        path: '/order/confirm',
        query: { cartIds: this.selectedIds.join(',') }
      });
    }
  },
  mounted() {
    this.loadCartData();
  }
};
</script>

<style scoped>
.cart-page {
  max-width: 1200px;
  margin: 20px auto;
  padding: 20px;
}

.cart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.cart-item {
  display: flex;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #f5f5f5;
}

.cart-item:hover {
  background-color: #f9f9f9;
}

.goods-img {
  width: 80px;
  height: 80px;
  margin: 0 15px;
  object-fit: cover;
  border-radius: 4px;
}

.goods-info {
  flex: 1;
  margin: 0 15px;
}

.goods-info h4 {
  margin: 0 0 10px 0;
  font-size: 16px;
  color: #333;
}

.price {
  color: #ff5000;
  font-size: 18px;
  font-weight: bold;
}

.quantity {
  width: 120px;
  margin: 0 15px;
}

.subtotal {
  width: 150px;
  text-align: center;
  font-size: 16px;
  color: #333;
  font-weight: bold;
}

.cart-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 20px;
  padding: 20px;
  background-color: #f5f5f5;
  border-radius: 4px;
}

.total {
  font-size: 16px;
}

.total-price {
  color: #ff5000;
  font-size: 24px;
  font-weight: bold;
  margin-left: 10px;
}

::v-deep .el-checkbox {
  margin-right: 15px;
}

::v-deep .el-input-number {
  width: 120px;
}
</style>
(3)避坑提醒
  • 购物车商品去重逻辑:
    @Select("SELECT * FROM cart WHERE yonghu_id = #{userId} AND goods_id = #{goodsId}")
    Cart findByUserAndGoods(@Param("userId") Integer userId, 
                           @Param("goodsId") Integer goodsId);
    
  • 前端商品数量限制:
    // 商品数量不能超过库存
    if (newVal > item.goods_kucun_number) {
      this.$message.warning('不能超过库存数量');
      item.buy_number = item.goods_kucun_number;
    }
    

2. 订单提交模块(事务管理是重点!)

(1)下单Service(带完整事务)
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private CartMapper cartMapper;
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private AddressMapper addressMapper;
    
    // 提交订单(完整的事务管理)
    public Result submitOrder(OrderSubmitDTO dto, Integer userId) {
        // 1. 验证收货地址
        Address address = addressMapper.selectById(dto.getAddressId());
        if (address == null || !address.getYonghuId().equals(userId)) {
            return Result.error("收货地址不存在或不属于当前用户");
        }
        
        // 2. 验证购物车商品并锁定库存
        List<Cart> cartList = cartMapper.selectBatchIds(dto.getCartIds());
        if (cartList == null || cartList.isEmpty()) {
            return Result.error("购物车商品不存在");
        }
        
        // 3. 计算总金额并扣减库存
        BigDecimal totalAmount = BigDecimal.ZERO;
        List<Goods> goodsToUpdate = new ArrayList<>();
        
        for (Cart cart : cartList) {
            // 悲观锁查询商品
            Goods goods = goodsMapper.selectForUpdate(cart.getGoodsId());
            if (goods == null) {
                throw new RuntimeException("商品" + cart.getGoodsId() + "不存在");
            }
            if (goods.getGoods_kucun_number() < cart.getBuyNumber()) {
                throw new RuntimeException("商品" + goods.getGoods_name() + "库存不足");
            }
            
            // 扣减库存
            goods.setGoods_kucun_number(goods.getGoods_kucun_number() - cart.getBuyNumber());
            goodsToUpdate.add(goods);
            
            // 计算金额
            BigDecimal itemAmount = goods.getGoods_new_money()
                .multiply(new BigDecimal(cart.getBuyNumber()));
            totalAmount = totalAmount.add(itemAmount);
        }
        
        // 4. 批量更新库存
        for (Goods goods : goodsToUpdate) {
            goodsMapper.updateById(goods);
        }
        
        // 5. 生成订单
        String orderNumber = generateOrderNumber();
        GoodsOrder order = new GoodsOrder();
        order.setGoods_order_uuid_number(orderNumber);
        order.setYonghu_id(userId);
        order.setAddress_id(dto.getAddressId());
        order.setGoods_order_true_price(totalAmount);
        order.setGoods_order_types(0); // 0待付款
        order.setGoods_order_payment_types(dto.getPaymentType());
        orderMapper.insert(order);
        
        // 6. 生成订单商品明细
        for (Cart cart : cartList) {
            OrderItem item = new OrderItem();
            item.setOrderId(order.getId());
            item.setGoodsId(cart.getGoodsId());
            item.setBuyNumber(cart.getBuyNumber());
            orderItemMapper.insert(item);
        }
        
        // 7. 清空购物车
        cartMapper.deleteBatchIds(dto.getCartIds());
        
        // 8. 记录订单日志
        orderLogService.log(order.getId(), "订单创建", "用户提交订单");
        
        return Result.success("下单成功", orderNumber);
    }
    
    // 生成唯一订单号
    private String generateOrderNumber() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String timeStr = sdf.format(new Date());
        String randomStr = String.valueOf((int)(Math.random() * 9000) + 1000);
        return "DD" + timeStr + randomStr;
    }
}
(2)订单确认页面
<template>
  <div class="order-confirm">
    <!-- 收货地址 -->
    <el-card class="address-card">
      <div class="address-header">
        <h3>收货地址</h3>
        <el-button type="text" @click="showAddressDialog">选择其他地址</el-button>
      </div>
      <div v-if="selectedAddress" class="address-info">
        <p><strong>{{ selectedAddress.address_name }}</strong> {{ selectedAddress.address_phone }}</p>
        <p>{{ selectedAddress.address_dizhi }}</p>
      </div>
      <div v-else class="no-address">
        <el-button type="primary" @click="showAddressDialog">添加收货地址</el-button>
      </div>
    </el-card>
    
    <!-- 商品清单 -->
    <el-card class="goods-card">
      <h3>商品清单</h3>
      <div v-for="item in goodsList" :key="item.id" class="goods-item">
        <img :src="item.goods_photo">
        <div class="goods-detail">
          <h4>{{ item.goods_name }}</h4>
          <p class="price">¥{{ item.goods_new_money }}</p>
          <p class="quantity">x{{ item.buy_number }}</p>
        </div>
        <div class="item-total">¥{{ (item.goods_new_money * item.buy_number).toFixed(2) }}</div>
      </div>
    </el-card>
    
    <!-- 支付方式 -->
    <el-card class="payment-card">
      <h3>支付方式</h3>
      <el-radio-group v-model="paymentType">
        <el-radio :label="1">支付宝</el-radio>
        <el-radio :label="2">微信支付</el-radio>
        <el-radio :label="3">余额支付</el-radio>
      </el-radio-group>
    </el-card>
    
    <!-- 订单汇总 -->
    <el-card class="summary-card">
      <div class="summary-item">
        <span>商品总额</span>
        <span>¥{{ totalAmount }}</span>
      </div>
      <div class="summary-item">
        <span>运费</span>
        <span>¥{{ shippingFee }}</span>
      </div>
      <div class="summary-item total">
        <span>应付总额</span>
        <span class="total-price">¥{{ actualAmount }}</span>
      </div>
      <el-button type="danger" size="large" @click="submitOrder" :loading="submitting">
        提交订单
      </el-button>
    </el-card>
    
    <!-- 地址选择弹窗 -->
    <el-dialog title="选择收货地址" :visible.sync="addressDialogVisible">
      <div v-for="addr in addressList" :key="addr.id" 
           class="address-option" 
           :class="{ selected: selectedAddress && selectedAddress.id === addr.id }"
           @click="selectAddress(addr)">
        <p><strong>{{ addr.address_name }}</strong> {{ addr.address_phone }}</p>
        <p>{{ addr.address_dizhi }}</p>
        <span v-if="addr.isdefault_types === 1" class="default-tag">默认</span>
      </div>
      <div slot="footer">
        <el-button @click="addressDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmAddress">确定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedAddress: null,
      addressList: [],
      goodsList: [],
      paymentType: 1,
      shippingFee: 0,
      addressDialogVisible: false,
      submitting: false,
      tempAddress: null
    };
  },
  computed: {
    totalAmount() {
      return this.goodsList.reduce((sum, item) => {
        return sum + (item.goods_new_money * item.buy_number);
      }, 0).toFixed(2);
    },
    actualAmount() {
      return (parseFloat(this.totalAmount) + this.shippingFee).toFixed(2);
    }
  },
  methods: {
    async loadData() {
      // 加载收货地址
      const addrRes = await this.$http.get('/api/address/list');
      this.addressList = addrRes.data.data;
      this.selectedAddress = this.addressList.find(addr => addr.isdefault_types === 1);
      
      // 加载购物车商品
      const cartIds = this.$route.query.cartIds.split(',');
      const goodsRes = await this.$http.post('/api/cart/getGoodsInfo', { cartIds });
      this.goodsList = goodsRes.data.data;
      
      // 计算运费(满99包邮)
      if (parseFloat(this.totalAmount) >= 99) {
        this.shippingFee = 0;
      } else {
        this.shippingFee = 10;
      }
    },
    
    showAddressDialog() {
      this.tempAddress = this.selectedAddress;
      this.addressDialogVisible = true;
    },
    
    selectAddress(addr) {
      this.tempAddress = addr;
    },
    
    confirmAddress() {
      this.selectedAddress = this.tempAddress;
      this.addressDialogVisible = false;
    },
    
    async submitOrder() {
      if (!this.selectedAddress) {
        this.$message.error('请选择收货地址');
        return;
      }
      
      try {
        this.submitting = true;
        const cartIds = this.$route.query.cartIds.split(',').map(id => parseInt(id));
        
        const res = await this.$http.post('/api/order/submit', {
          addressId: this.selectedAddress.id,
          cartIds: cartIds,
          paymentType: this.paymentType
        });
        
        if (res.data.code === 200) {
          this.$message.success('下单成功');
          // 跳转到支付页面
          this.$router.push({
            path: '/order/pay',
            query: { orderNumber: res.data.data }
          });
        } else {
          this.$message.error(res.data.msg);
        }
      } catch (error) {
        this.$message.error('下单失败:' + error.message);
      } finally {
        this.submitting = false;
      }
    }
  },
  mounted() {
    this.loadData();
  }
};
</script>

<style scoped>
.order-confirm {
  max-width: 1000px;
  margin: 20px auto;
  padding: 20px;
}

.address-card, .goods-card, .payment-card, .summary-card {
  margin-bottom: 20px;
}

.address-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.address-info {
  padding: 15px;
  background-color: #f9f9f9;
  border-radius: 4px;
}

.no-address {
  padding: 30px;
  text-align: center;
}

.goods-item {
  display: flex;
  align-items: center;
  padding: 15px;
  border-bottom: 1px solid #f0f0f0;
}

.goods-item:last-child {
  border-bottom: none;
}

.goods-item img {
  width: 80px;
  height: 80px;
  margin-right: 15px;
}

.goods-detail {
  flex: 1;
}

.goods-detail h4 {
  margin: 0 0 10px 0;
}

.price {
  color: #ff5000;
  font-size: 16px;
  font-weight: bold;
}

.quantity {
  color: #666;
}

.item-total {
  width: 150px;
  text-align: right;
  font-size: 16px;
  font-weight: bold;
}

.summary-item {
  display: flex;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid #f0f0f0;
}

.summary-item.total {
  margin-top: 10px;
  padding-top: 20px;
  border-top: 2px solid #f0f0f0;
}

.total-price {
  color: #ff5000;
  font-size: 24px;
  font-weight: bold;
}

.address-option {
  padding: 15px;
  margin-bottom: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

.address-option:hover {
  border-color: #409eff;
}

.address-option.selected {
  border-color: #409eff;
  background-color: #f0f9ff;
}

.default-tag {
  display: inline-block;
  padding: 2px 8px;
  margin-left: 10px;
  background-color: #409eff;
  color: white;
  font-size: 12px;
  border-radius: 10px;
}

::v-deep .el-button--large {
  width: 200px;
  height: 50px;
  font-size: 18px;
  margin-top: 20px;
}
</style>
(3)避坑提醒
  • 订单号防重复:
    @Select("SELECT COUNT(*) FROM goods_order WHERE goods_order_uuid_number = #{orderNumber}")
    int checkOrderNumberExists(String orderNumber);
    
  • 订单超时取消(用定时任务):
    @Scheduled(fixedDelay = 60000) // 每分钟检查一次
    public void cancelTimeoutOrders() {
        List<GoodsOrder> timeoutOrders = orderMapper.selectTimeoutOrders();
        for (GoodsOrder order : timeoutOrders) {
            order.setGoods_order_types(4); // 4已取消
            orderMapper.updateById(order);
            // 恢复库存
            recoverStock(order.getId());
        }
    }
    

3. 商品秒杀模块(高并发处理)

(1)Redis秒杀方案
@Service
public class SeckillService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;
    
    // 秒杀下单
    @Transactional
    public Result seckill(Integer userId, Integer goodsId) {
        // 1. 校验是否重复抢购
        String seckillKey = "seckill:user:" + userId + ":goods:" + goodsId;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(seckillKey))) {
            return Result.error("不能重复抢购");
        }
        
        // 2. Redis预减库存
        String stockKey = "seckill:stock:" + goodsId;
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        if (stock == null || stock < 0) {
            redisTemplate.opsForValue().increment(stockKey); // 库存加回去
            return Result.error("库存不足");
        }
        
        try {
            // 3. 数据库下单
            GoodsOrder order = createOrder(userId, goodsId);
            
            // 4. 记录已抢购
            redisTemplate.opsForValue().set(seckillKey, "1", 1, TimeUnit.HOURS);
            
            return Result.success("抢购成功", order.getGoods_order_uuid_number());
        } catch (Exception e) {
            // 回滚Redis库存
            redisTemplate.opsForValue().increment(stockKey);
            throw e;
        }
    }
    
    // 初始化秒杀库存到Redis
    public void initSeckillStock(Integer goodsId, Integer stock) {
        String stockKey = "seckill:stock:" + goodsId;
        redisTemplate.opsForValue().set(stockKey, String.valueOf(stock));
    }
}
(2)秒杀前端限流
// 防重复点击
let seckillClicking = false;

async function handleSeckill(goodsId) {
    if (seckillClicking) return;
    
    seckillClicking = true;
    try {
        const res = await this.$http.post('/api/seckill/' + goodsId);
        if (res.data.code === 200) {
            this.$message.success('抢购成功!');
            // 跳转到订单页面
        } else {
            this.$message.error(res.data.msg);
        }
    } finally {
        setTimeout(() => {
            seckillClicking = false;
        }, 1000); // 1秒内只能点一次
    }
}

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

五、测试别偷懒!这3步让答辩稳过

1. 功能测试(重点测这3个)

表1:购物车功能测试
测试场景操作步骤预期结果
添加购物车商品A点"加入购物车"购物车数量+1
修改数量购物车商品数量从1改成5小计金额变成单价×5
批量删除勾选多个商品点"批量删除"选中商品从购物车消失
表2:订单流程测试
测试场景操作步骤预期结果
正常下单购物车选商品→提交订单生成订单号,库存减少
库存不足下单买库存为0的商品提示"库存不足"
重复下单短时间内重复提交同一购物车提示"请勿重复下单"
表3:支付测试
测试场景操作步骤预期结果
余额不足余额100买200的商品提示"余额不足"
支付成功余额充足支付订单订单状态变"已支付"

2. 性能测试(答辩加分项)

  • 并发测试:用JMeter模拟100用户同时秒杀
  • 压力测试:连续下单1000笔,看系统响应时间

3. 测试报告模板

## 测试报告

### 一、测试环境
- 后端:Spring Boot 2.7 + MySQL 8.0
- 前端:Vue 2.6 + Element-UI
- 测试工具:JMeter 5.4

### 二、测试结果
1. 功能测试:通过率98%
2. 性能测试:支持200并发,平均响应时间<500ms
3. 兼容性:Chrome/Firefox/Edge正常

### 三、发现的问题
1. 库存并发问题:已用Redis+数据库锁解决
2. 重复提交问题:前端防抖+后端幂等性处理

### 四、测试结论
系统满足毕业设计要求,可正常使用。

六、答辩准备:3个加分技巧

  1. 演示要流畅:提前录好演示视频,按"用户浏览→加购物车→下单→支付"完整流程展示
  2. 突出亮点:重点讲"我解决了什么难题",比如"用Redis解决了秒杀超卖问题"
  3. 准备Q&A:提前想好导师可能问的问题:
    • Q:为什么选MySQL不选MongoDB?
    • A:电商系统事务要求高,MySQL事务支持更好
    • Q:用户多了怎么优化?
    • A:加Redis缓存、数据库读写分离、CDN加速静态资源

最后:一些真心话

以上是我做电商平台毕设的全部实战经验!毕设没那么可怕,一步步来都能搞定。

需要完整源码(带详细注释)、数据库脚本部署教程的同学,可以在评论区留言;遇到具体技术问题也可以问我。

祝大家毕设顺利,答辩一次过!🎉


小贴士:记得每天备份代码!我当初硬盘坏了,差点重写所有代码😭。用Git托管到Gitee或GitHub,安全又方便!