别再只写CRUD了!用SpringBoot+Vue重构一个图书商城,我学到了这些架构设计经验
从CRUD到架构思维:图书商城实战中的技术决策复盘
当我在GitHub上看到第20个相似的图书商城项目时,突然意识到大多数教程都在教我们如何堆砌功能,却很少解释为什么选择这些技术方案。这促使我决定用SpringBoot+Vue重新设计一个图书商城时,把重点放在架构设计的思考过程上。本文将分享这个过程中关于技术选型、模块解耦和性能优化的真实决策逻辑,这些经验同样适用于其他电商类项目的开发。
1. 为什么传统三层架构不再够用
在最初的版本中,我严格按照Controller-Service-DAO的标准三层结构来组织代码。但当需求扩展到促销活动、库存预警和第三方支付时,代码很快变成了一个超过3000行的"上帝Service"。这让我开始重新思考分层架构的适用边界。
1.1 领域驱动设计的引入
通过分析图书商城的核心业务流,我识别出几个关键领域模型:
// 领域模型示例
public class Book {
private ISBN isbn; // 值对象
private Publisher publisher; // 实体
private List<Category> categories;
private Inventory inventory; // 库存聚合根
}
public class Order {
private OrderNo orderNo;
private List<OrderItem> items;
private Payment payment; // 支付值对象
private ShippingAddress address;
}
这种设计带来了两个明显优势:
- 业务逻辑集中在领域层,Service变得轻薄
- 聚合根明确了修改边界,解决了并发修改问题
1.2 模块化拆分实践
将单体项目拆分为多个Maven模块时,我遵循了这些原则:
| 模块类型 | 示例模块 | 依赖关系 |
|---|---|---|
| 核心领域模块 | book-domain | 不依赖其他业务模块 |
| 应用服务模块 | order-application | 依赖领域模块 |
| 基础设施模块 | payment-infrastructure | 实现领域层的接口 |
| 启动模块 | web-application | 聚合所有必要模块 ``` |
这种结构使得后续添加优惠券系统时,只需新增coupon-domain和coupon-application两个模块,完全不影响原有订单流程。
2. 前后端协作模式的演进
最初采用的传统AJAX交互方式很快暴露出接口文档不同步的问题。我们最终确立了基于OpenAPI的协作流程:
- 后端先定义API契约(使用SpringDoc OpenAPI)
- 前端根据yaml生成TypeScript客户端
- 双方基于Mock数据进行并行开发
- 集成测试时启用契约校验
# OpenAPI示例
paths:
/books/{isbn}:
get:
tags: [Book]
parameters:
- $ref: '#/components/parameters/isbn'
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/BookDetail'
components:
schemas:
BookDetail:
type: object
properties:
isbn: {type: string}
title: {type: string}
price: {type: number}
提示:在Vue中使用axios拦截器自动处理JWT刷新和错误码统一转换,可以大幅减少样板代码
3. 状态管理的边界划分
在Vuex和Pinia的选型上,我们最终选择了Pinia,因为它更好的TypeScript支持。但更重要的是我们制定了这些状态管理规则:
- 全局状态 :用户会话、购物车数据
- 模块状态 :图书分类树、搜索结果过滤条件
- 组件状态 :表单临时数据、UI控制状态
// Pinia store示例
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
version: 1 // 用于乐观锁
}),
actions: {
async syncWithServer() {
const { data } = await api.getCart()
this.items = data.items
},
// 乐观更新
async addItem(item: CartItem) {
const tempId = generateTempId()
this.items.push({ ...item, tempId })
try {
await api.addToCart(item)
this.syncWithServer()
} catch (error) {
this.items = this.items.filter(i => i.tempId !== tempId)
}
}
}
})
这种分层管理避免了将所有状态都塞进全局store的常见反模式。
4. 性能优化中的取舍艺术
在性能优化过程中,我学会了每个决策都需要权衡。以下是几个典型场景:
4.1 缓存策略矩阵
| 数据类型 | 缓存方案 | 更新机制 | 适用场景 |
|---|---|---|---|
| 图书基础信息 | Redis缓存 | 数据库触发器更新 | 读多写少 |
| 库存数据 | 本地Caffeine缓存 | 消息队列广播失效 | 需要快速响应 |
| 用户行为数据 | 不缓存 | 直接写入分析数据库 | 数据一致性要求低 |
4.2 并发控制方案对比
当实现库存扣减时,我们测试了三种方案:
// 方案1:数据库乐观锁
UPDATE inventory SET quantity = quantity - ?
WHERE book_id = ? AND quantity >= ? AND version = ?
// 方案2:Redis原子操作
redisTemplate.opsForValue().increment(key, -quantity)
// 方案3:分布式事务
try {
seataGlobalTransaction.begin();
inventoryService.reduceStock();
orderService.createOrder();
seataGlobalTransaction.commit();
} catch (Exception e) {
seataGlobalTransaction.rollback();
}
最终根据我们的业务特点(平均订单量小但并发高),选择了方案2结合异步对账的混合模式。
5. 错误处理的艺术
统一的错误处理可以显著提升系统可维护性。我们的错误处理架构包含:
- 领域异常 :业务规则违反(如库存不足)
- 基础设施异常 :数据库连接失败等
- 应用异常 :无效参数等用户输入问题
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DomainException.class)
public ResponseEntity<ErrorResponse> handleDomainException(
DomainException ex) {
return ResponseEntity.status(422)
.body(new ErrorResponse(ex.getErrorCode(), ex.getMessage()));
}
@ExceptionHandler(ConcurrentModificationException.class)
public ResponseEntity<ErrorResponse> handleOptimisticLocking(
ConcurrentModificationException ex) {
return ResponseEntity.status(409)
.body(new ErrorResponse("CONFLICT", "数据已被修改,请刷新重试"));
}
}
前端则根据错误类型采取不同策略:
apiClient.get('/books').catch(error => {
if (error.isBusinessError) {
showToast(error.message) // 展示友好错误提示
} else if (error.isConflict) {
refreshData() // 自动处理冲突
} else {
logError(error) // 记录未知错误
}
})
这种结构化的错误处理使系统在出现问题时仍能提供良好的用户体验。
更多推荐


所有评论(0)