欢迪迈手机商城是一个典型的B2C电商平台解决方案,采用当前主流的前后端分离架构。这个项目特别值得关注的点在于其技术栈的选型组合:后端基于SpringBoot 2.x构建,前端采用Vue 3的组合式API风格,数据持久层使用MyBatis-Plus增强工具,数据库则选择了MySQL 8.0的最新特性。整套系统源码附带完整开发文档,非常适合作为企业级电商项目的学习范本。
我在实际电商系统开发中发现,这种技术组合能够很好地平衡开发效率与系统性能。SpringBoot的约定优于配置原则大幅减少了XML配置,Vue 3的Composition API让前端组件逻辑组织更清晰,MyBatis-Plus的ActiveRecord模式简化了CRUD操作,而MySQL 8.0的窗口函数和CTE特性则大大提升了复杂查询的效率。
SpringBoot 2.7.x作为基础框架,采用了经典的MVC分层架构:
特别值得注意的是项目中采用的MyBatis-Plus 3.5.x版本,它通过Lambda表达式实现了类型安全的SQL构建。例如商品查询的Service层实现:
java复制public Page<Product> searchProducts(ProductQuery query) {
return productMapper.selectPage(new Page<>(query.getPage(), query.getSize()),
Wrappers.<Product>lambdaQuery()
.like(StringUtils.isNotBlank(query.getKeyword()), Product::getName, query.getKeyword())
.eq(query.getCategoryId() != null, Product::getCategoryId, query.getCategoryId())
.between(query.getMinPrice() != null && query.getMaxPrice() != null,
Product::getPrice, query.getMinPrice(), query.getMaxPrice())
);
}
Vue 3的组合式API让代码组织更加灵活。项目中使用的主要技术点包括:
一个典型的商品列表组件实现:
javascript复制<script setup>
import { ref, onMounted } from 'vue'
import { useProductStore } from '@/stores/product'
const productStore = useProductStore()
const loading = ref(false)
onMounted(async () => {
loading.value = true
await productStore.fetchProducts()
loading.value = false
})
</script>
MySQL 8.0的表设计充分考虑了电商场景特点:
sql复制CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` decimal(10,2) NOT NULL COMMENT '售价',
`stock` int NOT NULL DEFAULT '0' COMMENT '库存',
`category_id` bigint NOT NULL COMMENT '分类ID',
`specs` json DEFAULT NULL COMMENT '商品规格JSON',
`sales` int DEFAULT '0' COMMENT '销量',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_sales` (`sales`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
特别使用了JSON类型存储商品规格,避免了传统的关系型设计需要多表关联的复杂性。
商品系统采用SPU+SKU模型:
库存管理实现了两种方案:
java复制@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1
WHERE id = #{productId} AND version = #{version}")
int deductStockWithVersion(@Param("productId") Long productId,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
java复制public boolean deductStock(Long productId, Integer quantity) {
String key = "product:stock:" + productId;
return redisTemplate.execute(new DefaultRedisScript<Boolean>(
"if tonumber(redis.call('get', KEYS[1])) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" return true " +
"else " +
" return false " +
"end",
Boolean.class
), Collections.singletonList(key), quantity.toString());
}
订单状态机设计:
java复制public enum OrderStatus {
PENDING_PAYMENT(1, "待支付"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消"),
REFUNDED(6, "已退款");
// 状态流转校验逻辑
public static boolean canChangeTo(OrderStatus current, OrderStatus target) {
switch (current) {
case PENDING_PAYMENT:
return target == PAID || target == CANCELLED;
case PAID:
return target == SHIPPED || target == REFUNDED;
// 其他状态流转规则...
}
}
}
分布式事务处理采用本地消息表方案:
对接了支付宝和微信支付双通道,采用策略模式实现支付方式切换:
java复制public interface PaymentStrategy {
PaymentResult pay(Order order);
boolean refund(Order order);
}
@Service
@RequiredArgsConstructor
public class PaymentService {
private final Map<String, PaymentStrategy> strategyMap;
public PaymentResult pay(String paymentType, Order order) {
PaymentStrategy strategy = strategyMap.get(paymentType + "Strategy");
if (strategy == null) {
throw new IllegalArgumentException("Unsupported payment type");
}
return strategy.pay(order);
}
}
支付结果异步通知处理:
java复制@PostMapping("/notify/alipay")
public String handleAlipayNotify(@RequestBody String notifyData) {
// 1. 验证签名
if (!alipaySignature.verify(notifyData)) {
return "failure";
}
// 2. 解析通知参数
Map<String, String> params = parseNotifyData(notifyData);
// 3. 处理业务逻辑
orderService.processPayment(
params.get("out_trade_no"),
params.get("trade_no"),
new BigDecimal(params.get("total_amount"))
);
return "success";
}
采用多级缓存架构:
热点商品缓存方案:
java复制@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
@CachePut(value = "product", key = "#product.id")
public Product updateProduct(Product product) {
productMapper.updateById(product);
return product;
}
@CacheEvict(value = "product", key = "#id")
public void deleteProduct(Long id) {
productMapper.deleteById(id);
}
MySQL 8.0特定优化:
sql复制SELECT * FROM (
SELECT p.*, ROW_NUMBER() OVER(ORDER BY sales DESC) AS rn
FROM product p WHERE category_id = 1
) t WHERE rn BETWEEN 11 AND 20;
sql复制WITH discounted_products AS (
SELECT * FROM product WHERE discount_rate > 0.2
),
hot_products AS (
SELECT * FROM product WHERE sales > 1000
)
SELECT d.id, d.name, h.sales
FROM discounted_products d
JOIN hot_products h ON d.id = h.id;
Vue 3特有的优化手段:
javascript复制const ProductDetail = defineAsyncComponent(() =>
import('./views/ProductDetail.vue')
)
html复制<RecycleScroller
class="product-list"
:items="products"
:item-size="200"
key-field="id"
>
<template #default="{ item }">
<ProductCard :product="item" />
</template>
</RecycleScroller>
html复制<img v-lazy="product.imageUrl" alt="product image">
java复制@Bean
public HttpMessageConverters customConverters() {
StringHttpMessageConverter converter = new StringHttpMessageConverter();
converter.setDefaultCharset(StandardCharsets.UTF_8);
converter.setWriteAcceptCharset(false);
return new HttpMessageConverters(false, Collections.singleton(converter));
}
java复制@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
java复制public String maskMobile(String mobile) {
if (StringUtils.isBlank(mobile) || mobile.length() != 11) {
return mobile;
}
return mobile.substring(0, 3) + "****" + mobile.substring(7);
}
java复制@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
Docker Compose编排方案:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: mall
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
redis_data:
properties复制management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics(
@Value("${spring.application.name}") String appName) {
return registry -> registry.config().commonTags("application", appName);
}
xml复制<appender name="ELK" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"mall-backend","env":"${spring.profiles.active}"}</customFields>
</encoder>
</appender>
java复制public class Result<T> {
private int code;
private String message;
private T data;
private long timestamp = System.currentTimeMillis();
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
}
typescript复制const OrderStatus = {
1: '待支付',
2: '已支付',
// ...
} as const;
java复制@Test
public void testBatchInsert() {
List<User> users = generateTestUsers(10000);
// 错误做法:循环单条插入
// users.forEach(userMapper::insert);
// 正确做法:使用批量插入
SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH);
UserMapper batchMapper = session.getMapper(UserMapper.class);
users.forEach(batchMapper::insert);
session.commit();
session.clearCache();
}
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
java复制@Bean
public Snowflake snowflake() {
return new Snowflake(1, 1); // 实际部署时应从配置中心获取
}
java复制public Product getProductWithCache(Long id) {
// 1. 先查缓存
Product product = cacheService.getProduct(id);
if (product != null) {
return product;
}
// 2. 加分布式锁
String lockKey = "product:lock:" + id;
try {
if (redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
// 3. 二次检查缓存
product = cacheService.getProduct(id);
if (product != null) {
return product;
}
// 4. 查数据库
product = productMapper.selectById(id);
if (product == null) {
// 防止缓存穿透,设置空值
cacheService.setProductNull(id);
return null;
}
// 5. 写入缓存
cacheService.setProduct(id, product);
return product;
}
} finally {
redisLock.unlock(lockKey);
}
// 6. 锁获取失败时降级处理
return productMapper.selectById(id);
}
这套手机商城系统在开发过程中积累的这些经验,对于构建同类电商平台具有很高的参考价值。特别是在高并发场景下的库存管理、分布式事务处理等方面,项目提供的解决方案经过了实战检验。