毕设通关秘籍:基于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. 开发环境搭建(一步到位)
- JDK 1.8:配好JAVA_HOME环境变量
- Node.js 14+:装好npm
- MySQL 8.0:用Navicat可视化操作
- 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个加分技巧
- 演示要流畅:提前录好演示视频,按"用户浏览→加购物车→下单→支付"完整流程展示
- 突出亮点:重点讲"我解决了什么难题",比如"用Redis解决了秒杀超卖问题"
- 准备Q&A:提前想好导师可能问的问题:
- Q:为什么选MySQL不选MongoDB?
- A:电商系统事务要求高,MySQL事务支持更好
- Q:用户多了怎么优化?
- A:加Redis缓存、数据库读写分离、CDN加速静态资源
最后:一些真心话
以上是我做电商平台毕设的全部实战经验!毕设没那么可怕,一步步来都能搞定。
需要完整源码(带详细注释)、数据库脚本、部署教程的同学,可以在评论区留言;遇到具体技术问题也可以问我。
祝大家毕设顺利,答辩一次过!🎉
小贴士:记得每天备份代码!我当初硬盘坏了,差点重写所有代码😭。用Git托管到Gitee或GitHub,安全又方便!