1. 项目背景与核心需求
去年帮学弟调试毕业设计时,遇到一个典型的场景:他的水果电商平台在促销活动期间频繁出现订单丢失。这个案例让我意识到,一个合格的毕设项目不仅需要基础功能完整,更要考虑真实业务场景中的各种边界情况。基于SpringBoot的水果电商平台,本质上是要解决三个核心问题:
- 商品信息的高效展示与管理(特别是水果这类易腐商品的时效性处理)
- 订单流转的可靠性保障(从购物车到支付的完整链路)
- 库存与促销的实时联动(避免超卖等典型电商问题)
这个项目的技术选型非常经典:SpringBoot提供快速开发能力,MySQL作为关系型数据库保证事务一致性,配合Thymeleaf或Freemarker实现前后端轻度耦合。但真正让项目出彩的,往往是对业务细节的处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计要点
2.1 分层架构实现
建议采用明确的分层结构(实际项目中我习惯用这种结构):
code复制com.fruitmall
├── config # 配置类
├── controller # 控制层
├── service # 服务层
│ ├── impl # 服务实现
├── dao # 数据访问层
├── entity # 实体类
├── dto # 数据传输对象
├── util # 工具类
└── exception # 异常处理
特别注意:很多同学会在controller直接调用dao,这会导致事务失效。正确的做法是通过service层组织业务逻辑,例如订单创建应该包含:
- 库存校验(@Transactional注解要加在service方法上)
- 订单主表生成
- 订单明细插入
- 库存扣减
2.2 数据库设计关键
水果电商的MySQL表设计有几个易错点:
- 商品表需要特殊字段:
sql复制CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '当前售价',
`original_price` decimal(10,2) DEFAULT NULL COMMENT '原价',
`stock` int NOT NULL DEFAULT '0' COMMENT '库存',
`weight_unit` varchar(10) DEFAULT 'g' COMMENT '重量单位',
`shelf_life` int DEFAULT NULL COMMENT '保质期(天)',
`is_hot` tinyint DEFAULT '0' COMMENT '是否热卖',
`is_seasonal` tinyint DEFAULT '0' COMMENT '是否时令水果',
`status` tinyint DEFAULT '1' COMMENT '上架状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 订单表必须考虑分库分表可能性(即使毕设不实现):
sql复制CREATE TABLE `order` (
`id` varchar(32) NOT NULL COMMENT '订单号(非自增)',
`user_id` bigint NOT NULL,
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总额',
`payment_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0未支付 1已支付 2已发货...',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现细节
3.1 购物车设计
避免直接操作数据库的常见错误方案:
java复制// 错误示范:每次增减都update数据库
public void addToCart(Long productId, Integer num) {
cartDao.updateQuantity(userId, productId, num);
}
推荐使用Redis+MySQL双写方案:
java复制// 正确做法:Redis缓存+异步持久化
public void addToCart(Long userId, Long productId, Integer num) {
String key = "cart:" + userId;
// 1. 先检查商品是否存在及库存
Product product = productService.getById(productId);
if(product == null || product.getStock() < num) {
throw new BusinessException("商品不存在或库存不足");
}
// 2. Redis操作
redisTemplate.opsForHash().increment(
key,
productId.toString(),
num
);
// 3. 异步记录操作日志(最终一致性)
cartLogService.asyncLog(userId, productId, num);
}
3.2 订单超时取消
用Spring Schedule实现比Quartz更轻量:
java复制@Component
public class OrderTimeoutTask {
@Autowired
private OrderService orderService;
// 每5分钟检查一次未支付订单
@Scheduled(cron = "0 */5 * * * ?")
public void cancelUnpaidOrders() {
List<Order> orders = orderService.getUnpaidOrders(30); // 30分钟未支付
orders.forEach(order -> {
try {
orderService.cancelOrder(order.getId(), "系统自动取消");
// 记得恢复库存
order.getItems().forEach(item -> {
productService.increaseStock(
item.getProductId(),
item.getQuantity()
);
});
} catch (Exception e) {
log.error("取消订单失败:{}", order.getId(), e);
}
});
}
}
4. 典型问题解决方案
4.1 库存超卖问题
错误做法(直接更新):
sql复制UPDATE product SET stock = stock - #{num} WHERE id = #{productId}
正确方案(乐观锁):
java复制// Product实体类增加version字段
@Version
private Integer version;
// Service层
@Transactional
public boolean reduceStock(Long productId, Integer num) {
// 先查询当前库存和版本
Product product = productDao.selectById(productId);
if (product.getStock() < num) {
return false;
}
// 带版本号更新
int affected = productDao.updateStock(
productId,
num,
product.getVersion()
);
return affected > 0;
}
4.2 支付回调处理
常见坑点:未做幂等处理导致重复发货
java复制@PostMapping("/pay/callback")
public String paymentCallback(@RequestBody CallbackData data) {
// 1. 签名验证(实际项目要用商户密钥)
if(!SignatureUtil.verify(data)) {
return "FAIL";
}
// 2. 检查订单是否存在
Order order = orderService.getById(data.getOrderId());
if(order == null) {
return "ORDER_NOT_EXIST";
}
// 3. 幂等处理(关键!)
if(order.getStatus() != OrderStatus.UNPAID) {
return "SUCCESS"; // 已处理过直接返回成功
}
// 4. 业务处理
boolean success = orderService.handlePaymentSuccess(
data.getOrderId(),
data.getPaymentTime()
);
return success ? "SUCCESS" : "FAIL";
}
5. 项目亮点打造建议
5.1 智能推荐实现
基于用户行为的简单推荐算法:
java复制public List<Product> recommendProducts(Long userId) {
// 1. 获取用户最近浏览
List<Long> viewHistory = redisTemplate.opsForList()
.range("user:view:" + userId, 0, 10);
// 2. 获取同类商品
Set<Long> similarProducts = new HashSet<>();
viewHistory.forEach(productId -> {
Product product = productService.getById(productId);
similarProducts.addAll(
productDao.selectSimilarProducts(
product.getCategoryId(),
5
)
);
});
// 3. 排除已购商品
List<Long> boughtProducts = orderDao.selectUserPurchased(userId);
similarProducts.removeAll(boughtProducts);
// 4. 按销量排序
return productService.listByIds(similarProducts).stream()
.sorted((p1, p2) -> p2.getSales() - p1.getSales())
.limit(8)
.collect(Collectors.toList());
}
5.2 日志收集优化
用AOP实现接口日志记录:
java复制@Aspect
@Component
@Slf4j
public class ApiLogAspect {
@Around("execution(* com.fruitmall.controller..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
try {
// 记录入参(敏感信息需脱敏)
log.info("API Start - {} args: {}",
methodName,
JsonUtils.toJson(args));
Object result = joinPoint.proceed();
// 记录耗时
long cost = System.currentTimeMillis() - startTime;
log.info("API End - {} cost: {}ms", methodName, cost);
return result;
} catch (Exception e) {
log.error("API Error - {}: {}", methodName, e.getMessage());
throw e;
}
}
}
6. 部署与性能优化
6.1 多环境配置
使用Spring Profiles区分环境:
yaml复制# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/fruitmall_dev
username: dev
password: dev123
# application-prod.yml
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db:3306/fruitmall
username: prod_user
password: ${DB_PASSWORD} # 从环境变量读取
启动时指定profile:
bash复制java -jar fruitmall.jar --spring.profiles.active=prod
6.2 缓存策略
商品详情缓存方案:
java复制@Service
@CacheConfig(cacheNames = "product")
public class ProductServiceImpl implements ProductService {
@Override
@Cacheable(key = "#id")
public Product getById(Long id) {
return productDao.selectById(id);
}
@Override
@CachePut(key = "#product.id")
public Product update(Product product) {
productDao.updateById(product);
return product;
}
@Override
@CacheEvict(key = "#id")
public void delete(Long id) {
productDao.deleteById(id);
}
}
7. 毕设答辩加分项
- 压测报告:用JMeter测试关键接口
xml复制<!-- 测试计划示例 -->
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="订单创建压测">
<intProp name="ThreadGroup.num_threads">100</intProp>
<intProp name="ThreadGroup.ramp_time">60</intProp>
<longProp name="ThreadGroup.duration">300</longProp>
</ThreadGroup>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="/order/create">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="productId" elementType="HTTPArgument">
<stringProp name="Argument.value">123</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.path">/order/create</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
</HTTPSamplerProxy>
- 数据库ER图:使用MySQL Workbench逆向生成
sql复制-- 导出ER图指令
mysqldump -u root -p --no-data fruitmall > schema.sql
- API文档:用Swagger UI展示
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.fruitmall.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("白果园水果电商API文档")
.description("毕业设计项目接口说明")
.version("1.0")
.build();
}
}
