1. 项目概述与核心架构设计
作为一个完整的图书电商平台,这个系统采用了当前主流的前后端分离架构。后端基于SpringBoot框架构建,提供RESTful API服务;前端使用Vue3实现用户交互界面;数据持久层采用MyBatis操作MySQL数据库。这种架构选择充分考虑了现代Web应用的开发效率和性能需求。
在实际开发中,我们特别注重三个核心原则:
- 前后端职责分离:前端专注UI展示和用户交互,后端专注业务逻辑和数据处理
- 组件化开发:前后端都采用模块化设计,便于功能扩展和维护
- 接口契约先行:先定义好API接口规范,再并行开发,提高团队协作效率
提示:选择SpringBoot+Vue3+MyBatis这套技术栈时,我们特别考虑了团队技术储备和社区生态。SpringBoot的自动配置特性大幅减少了XML配置,Vue3的Composition API让前端逻辑组织更灵活,而MyBatis在复杂SQL处理上比JPA更有优势。
2. 数据库设计与核心表结构
2.1 用户管理系统设计
用户信息表(user_info)是整套系统的认证基础,采用以下安全设计:
- password_hash字段存储BCrypt加密后的密码,而非明文
- account_status实现软删除功能,避免直接删除用户记录
- 建立了username和email的唯一索引,确保账号唯一性
sql复制CREATE TABLE `user_info` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8mb4_bin NOT NULL,
`password_hash` varchar(100) COLLATE utf8mb4_bin NOT NULL,
`email` varchar(100) COLLATE utf8mb4_bin NOT NULL,
`phone_number` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`account_status` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2.2 图书商品模型设计
图书信息表(book_details)的设计考虑了电商平台的典型需求:
- 价格使用DECIMAL(10,2)确保精确计算
- stock_quantity实时更新,配合事务保证库存准确
- 建立了category_id索引优化分类查询性能
- publish_date和shelf_time帮助运营分析商品生命周期
java复制// 对应的MyBatis实体类设计
public class Book {
private Long bookId;
private String title;
private String author;
private BigDecimal price;
private Integer stockQuantity;
private Date publishDate;
private Long categoryId;
private Date shelfTime;
// 省略getter/setter
}
2.3 订单系统设计
订单信息表(order_records)是交易核心,设计要点包括:
- 使用payment_status和order_status两个字段分别跟踪支付和物流状态
- total_amount在创建订单时计算并固化,不受后续价格变动影响
- 建立了user_id索引优化用户订单查询
- delivery_address存储快照,与用户档案地址解耦
注意:订单表设计最容易犯的错误是把关联商品信息直接嵌入。我们采用独立的订单项表(order_items)来记录订单中的商品明细,这样既保持范式化设计,又能准确记录下单时的商品状态。
3. 后端核心模块实现
3.1 SpringBoot应用架构
我们采用典型的三层架构:
code复制com.example.bookstore
├── config # 配置类
├── controller # REST API接口
├── service # 业务逻辑
├── repository # 数据访问
├── model # 实体类
└── exception # 异常处理
安全配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
3.2 MyBatis优化实践
- 动态SQL处理:
xml复制<select id="searchBooks" resultType="Book">
SELECT * FROM book_details
<where>
<if test="title != null">
title LIKE CONCAT('%', #{title}, '%')
</if>
<if test="author != null">
AND author LIKE CONCAT('%', #{author}, '%')
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
</where>
ORDER BY shelf_time DESC
</select>
- 批量插入优化:
java复制@Insert("<script>" +
"INSERT INTO order_items (order_id, book_id, quantity, unit_price) VALUES " +
"<foreach collection='items' item='item' separator=','>" +
"(#{item.orderId}, #{item.bookId}, #{item.quantity}, #{item.unitPrice})" +
"</foreach>" +
"</script>")
void batchInsert(@Param("items") List<OrderItem> items);
3.3 事务管理关键点
图书下单的典型事务处理:
java复制@Service
@RequiredArgsConstructor
public class OrderService {
private final BookRepository bookRepository;
private final OrderRepository orderRepository;
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 检查库存
List<Book> books = bookRepository.findAllById(
request.getItems().stream()
.map(OrderItemRequest::getBookId)
.collect(Collectors.toList())
);
// 2. 扣减库存
for (Book book : books) {
int ordered = request.getItems().stream()
.filter(i -> i.getBookId().equals(book.getBookId()))
.findFirst()
.map(OrderItemRequest::getQuantity)
.orElse(0);
if (book.getStockQuantity() < ordered) {
throw new InsufficientStockException(book.getTitle());
}
book.setStockQuantity(book.getStockQuantity() - ordered);
}
// 3. 创建订单
Order order = new Order();
// 省略订单构建逻辑...
return orderRepository.save(order);
}
}
经验:在事务方法中,所有数据库操作应该使用相同的Hibernate Session或MyBatis SqlSession。我们通过在Service层添加@Transactional注解确保这一点,避免出现LazyInitializationException。
4. 前端Vue3实现要点
4.1 前端项目结构
采用Vue3 + TypeScript + Pinia状态管理:
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── types/ # TS类型定义
└── views/ # 页面组件
4.2 典型页面实现
图书列表页的关键逻辑:
vue复制<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useBookStore } from '@/stores/book'
const bookStore = useBookStore()
const loading = ref(false)
const searchParams = reactive({
keyword: '',
categoryId: null as number | null,
page: 1,
size: 10
})
const loadBooks = async () => {
loading.value = true
try {
await bookStore.fetchBooks(searchParams)
} finally {
loading.value = false
}
}
onMounted(loadBooks)
</script>
<template>
<div class="book-list">
<el-input v-model="searchParams.keyword" placeholder="搜索书名/作者" />
<el-select v-model="searchParams.categoryId" placeholder="选择分类">
<!-- 分类选项 -->
</el-select>
<div v-if="loading">加载中...</div>
<div v-else>
<book-card
v-for="book in bookStore.books"
:key="book.bookId"
:book="book"
/>
<el-pagination
v-model:current-page="searchParams.page"
:page-size="searchParams.size"
:total="bookStore.total"
@current-change="loadBooks"
/>
</div>
</div>
</template>
4.3 状态管理实践
使用Pinia管理购物车状态:
ts复制// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => state.items.reduce(
(sum, item) => sum + item.quantity * item.book.price, 0
),
},
actions: {
async addItem(book: Book, quantity = 1) {
const existing = this.items.find(i => i.book.bookId === book.bookId)
if (existing) {
existing.quantity += quantity
} else {
this.items.push({ book, quantity })
}
},
async removeItem(bookId: number) {
this.items = this.items.filter(i => i.book.bookId !== bookId)
},
async clearCart() {
this.items = []
}
},
persist: true // 启用本地持久化
})
5. 系统部署与性能优化
5.1 后端性能调优
- 缓存策略实现:
java复制@Service
@CacheConfig(cacheNames = "books")
@RequiredArgsConstructor
public class BookServiceImpl implements BookService {
private final BookRepository bookRepository;
@Override
@Cacheable(key = "#bookId")
public Book getBookById(Long bookId) {
return bookRepository.findById(bookId)
.orElseThrow(() -> new BookNotFoundException(bookId));
}
@Override
@CacheEvict(key = "#book.bookId")
public Book updateBook(Book book) {
return bookRepository.save(book);
}
}
- 接口响应优化:
java复制@GetMapping("/api/books")
public ResponseEntity<PageResponse<BookDTO>> listBooks(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@PageableDefault(sort = "shelfTime", direction = DESC) Pageable pageable) {
// 使用DTO投影减少数据传输量
Page<BookDTO> page = bookRepository.findBySearchCriteria(
keyword, categoryId, pageable
).map(bookMapper::toDTO);
return ResponseEntity.ok(PageResponse.of(page));
}
5.2 前端性能优化
- 路由懒加载:
js复制const routes = [
{
path: '/',
component: () => import('@/views/HomeView.vue')
},
{
path: '/books/:id',
component: () => import('@/views/BookDetail.vue')
}
]
- API请求防抖:
ts复制import { debounce } from 'lodash-es'
const searchBooks = debounce(async (keyword: string) => {
const res = await bookApi.search({ keyword })
books.value = res.data
}, 500)
- 图片懒加载:
vue复制<template>
<img v-lazy="book.coverUrl" alt="图书封面">
</template>
<script setup>
import { VueLazyload } from 'vue-lazyload'
app.use(VueLazyload, {
preLoad: 1.3,
error: require('@/assets/default-book.png'),
loading: require('@/assets/loading.gif'),
attempt: 3
})
</script>
6. 常见问题与解决方案
6.1 跨域问题处理
SpringBoot后端配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
Vue3前端代理配置(vite.config.ts):
ts复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
6.2 身份认证实现
JWT认证流程实现:
- 后端生成Token:
java复制public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
- 前端存储Token:
ts复制// composables/useAuth.ts
export const useAuth = () => {
const token = useCookie('token')
const login = async (credentials: LoginForm) => {
const res = await authApi.login(credentials)
token.value = res.data.token
}
const logout = () => {
token.value = null
}
return { token, login, logout }
}
- Axios请求拦截:
ts复制// api/client.ts
const client = axios.create({
baseURL: '/api'
})
client.interceptors.request.use(config => {
const token = useCookie('token').value
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
client.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
navigateTo('/login')
}
return Promise.reject(error)
}
)
6.3 支付流程实现
模拟支付接口实现:
java复制@RestController
@RequestMapping("/api/payment")
@RequiredArgsConstructor
public class PaymentController {
private final OrderService orderService;
@PostMapping("/create")
public ResponseEntity<PaymentResponse> createPayment(
@RequestBody PaymentRequest request) {
Order order = orderService.getOrder(request.getOrderId());
if (order.getPaymentStatus() == PAID) {
throw new PaymentException("订单已支付");
}
// 模拟支付成功
orderService.updatePaymentStatus(order.getOrderId(), PAID);
return ResponseEntity.ok(new PaymentResponse(
"PAY-" + System.currentTimeMillis(),
"支付成功",
order.getTotalAmount()
));
}
}
前端支付组件:
vue复制<script setup lang="ts">
const props = defineProps<{
orderId: number
amount: number
}>()
const pay = async () => {
const res = await paymentApi.create({
orderId: props.orderId,
amount: props.amount
})
if (res.success) {
// 跳转到支付成功页面
}
}
</script>
<template>
<el-button type="primary" @click="pay">
立即支付 ¥{{ amount.toFixed(2) }}
</el-button>
</template>
7. 项目扩展方向
7.1 推荐系统集成
基于用户行为的简单推荐实现:
java复制@Service
@RequiredArgsConstructor
public class RecommendationService {
private final BookRepository bookRepository;
private final UserBehaviorRepository behaviorRepository;
public List<Book> recommendBooks(Long userId) {
// 1. 获取用户最近浏览的图书分类
List<Long> viewedCategories = behaviorRepository
.findRecentViewedCategories(userId, 5);
// 2. 获取同分类的热销图书
return bookRepository.findTop10ByCategoryIdInOrderBySalesDesc(
viewedCategories
);
}
}
7.2 数据分析模块
使用Spring Batch处理每日销售统计:
java复制@Configuration
@RequiredArgsConstructor
public class SalesAnalysisJobConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job dailySalesJob() {
return jobBuilderFactory.get("dailySalesJob")
.start(analysisStep())
.build();
}
@Bean
public Step analysisStep() {
return stepBuilderFactory.get("analysisStep")
.<Order, SalesStat>chunk(100)
.reader(orderReader())
.processor(salesProcessor())
.writer(statWriter())
.build();
}
// 省略reader/processor/writer定义
}
7.3 微服务改造
将单体应用拆分为微服务:
- 用户服务:处理认证和用户信息
- 商品服务:管理图书和分类
- 订单服务:处理交易流程
- 支付服务:集成支付渠道
- 推荐服务:实现个性化推荐
使用Spring Cloud实现服务通信:
java复制// 商品服务Feign客户端
@FeignClient(name = "product-service", path = "/api/products")
public interface ProductClient {
@GetMapping("/{bookId}")
BookDTO getBook(@PathVariable Long bookId);
@PostMapping("/stock/decrease")
void decreaseStock(@RequestBody StockDecreaseRequest request);
}
在实际开发中,我们遇到最大的挑战是库存并发控制。最初使用乐观锁版本号控制,但在高并发场景下重试率太高。后来改为Redis分布式锁+数据库悲观锁的组合方案,既保证了性能又确保了数据一致性。具体实现中,我们使用Redisson的RLock实现分布式锁,关键代码片段如下:
java复制public boolean purchaseWithLock(Long bookId, int quantity) {
RLock lock = redissonClient.getLock("stock_lock:" + bookId);
try {
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}
Book book = bookRepository.findById(bookId)
.orElseThrow(() -> new BookNotFoundException(bookId));
if (book.getStockQuantity() < quantity) {
throw new InsufficientStockException(book.getTitle());
}
book.setStockQuantity(book.getStockQuantity() - quantity);
bookRepository.save(book);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("操作被中断");
} finally {
lock.unlock();
}
}
另一个值得分享的技巧是使用Hibernate的@DynamicUpdate注解优化更新操作。当只需要更新部分字段时,这个注解可以让Hibernate只生成变更字段的SQL,减少数据库压力。我们在用户信息更新等场景中应用这个优化,性能提升了约30%。