在当今数字化浪潮中,电商系统已成为开发者必须掌握的核心技能之一。本文将带你从零开始,使用Spring Boot 2.x和Vue 3构建一个完整的咖啡商城系统,并集成支付宝沙箱支付功能。不同于简单的CRUD教程,我们将深入前后端分离架构的设计细节,解决实际开发中的各种挑战。
我们采用经典的前后端分离架构,后端提供RESTful API,前端通过axios消费这些API。系统主要分为三个层次:
技术栈对比表:
| 技术领域 | 选型方案 | 优势分析 |
|---|---|---|
| 前端框架 | Vue 3 + Composition API | 更好的TypeScript支持,性能提升40% |
| UI组件库 | Element Plus | 专为Vue 3优化,丰富的电商组件 |
| 后端框架 | Spring Boot 2.6.x | 稳定的企业级Java框架 |
| ORM工具 | MyBatis-Plus 3.5.x | 简化CRUD操作,内置分页插件 |
| 支付集成 | 支付宝沙箱环境 | 安全测试,免真实资金流动 |
确保你的开发环境包含以下组件:
bash复制# JDK安装验证
java -version
# 应输出类似:openjdk version "1.8.0_302"
# Node.js版本检查
node -v
# 推荐v16.x以上
# Maven版本验证
mvn -v
# 推荐3.6.x以上
数据库配置示例(application.yml):
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/coffee_shop?useSSL=false&serverTimezone=UTC
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
商品模块采用领域驱动设计(DDD)思想,核心实体关系如下:
java复制// 商品实体类示例
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private String description;
private String imageUrl;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
商品分页查询接口实现:
java复制@GetMapping("/products")
public R listProducts(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword) {
Page<Product> pageInfo = new Page<>(page, size);
LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(keyword)) {
queryWrapper.like(Product::getName, keyword);
}
return R.ok().put("data", productService.page(pageInfo, queryWrapper));
}
订单状态机设计:
mermaid复制stateDiagram
[*] --> PENDING
PENDING --> PAID: 支付成功
PENDING --> CANCELLED: 用户取消
PAID --> SHIPPED: 发货
SHIPPED --> DELIVERED: 签收
DELIVERED --> COMPLETED: 确认收货
订单创建核心逻辑:
java复制@Transactional
public Order createOrder(OrderDTO orderDTO, Long userId) {
// 1. 验证商品库存
List<OrderItem> items = validateStock(orderDTO.getItems());
// 2. 计算总金额
BigDecimal totalAmount = calculateTotal(items);
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
// 4. 保存订单项
items.forEach(item -> {
item.setOrderId(order.getId());
orderItemMapper.insert(item);
// 扣减库存
productMapper.deductStock(item.getProductId(), item.getQuantity());
});
return order;
}
使用Vite创建项目:
bash复制npm init vite@latest coffee-shop-frontend --template vue-ts
cd coffee-shop-frontend
npm install element-plus axios vue-router@4 pinia
路由配置示例(router/index.ts):
typescript复制const routes = [
{
path: '/',
component: () => import('@/views/Home.vue'),
children: [
{
path: '',
component: () => import('@/views/product/ProductList.vue')
},
{
path: 'product/:id',
component: () => import('@/views/product/ProductDetail.vue')
}
]
}
]
使用Composition API实现商品搜索:
vue复制<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getProducts } from '@/api/product'
const products = ref<Product[]>([])
const loading = ref(false)
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
const fetchProducts = async () => {
loading.value = true
try {
const res = await getProducts({
page: pagination.page,
size: pagination.size
})
products.value = res.data.records
pagination.total = res.data.total
} finally {
loading.value = false
}
}
onMounted(fetchProducts)
</script>
首先在支付宝开放平台申请沙箱账号:
后端配置示例:
properties复制# application-pay.properties
alipay.app-id=2021000122601234
alipay.gateway=https://openapi.alipaydev.com/gateway.do
alipay.merchant-private-key=你的应用私钥
alipay.alipay-public-key=支付宝公钥
alipay.notify-url=http://your-domain.com/api/pay/notify
支付创建接口:
java复制public String createPayOrder(Order order) {
AlipayClient alipayClient = new DefaultAlipayClient(
alipayProperties.getGateway(),
alipayProperties.getAppId(),
alipayProperties.getMerchantPrivateKey(),
"json",
"UTF-8",
alipayProperties.getAlipayPublicKey(),
"RSA2");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(alipayProperties.getReturnUrl());
request.setNotifyUrl(alipayProperties.getNotifyUrl());
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(order.getOrderNo());
model.setTotalAmount(order.getTotalAmount().toString());
model.setSubject("咖啡商城订单");
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
try {
return alipayClient.pageExecute(request).getBody();
} catch (AlipayApiException e) {
throw new RuntimeException("创建支付失败", e);
}
}
前端支付触发:
typescript复制const handlePay = async (orderId: string) => {
const { data } = await createPayOrder(orderId)
const div = document.createElement('div')
div.innerHTML = data
document.body.appendChild(div)
document.forms[0].submit()
}
支付结果回调处理:
java复制@PostMapping("/pay/notify")
public String handlePayNotify(HttpServletRequest request) {
Map<String, String> params = convertRequestToMap(request);
try {
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
alipayProperties.getAlipayPublicKey(),
"UTF-8",
"RSA2");
if (signVerified) {
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
if ("TRADE_SUCCESS".equals(tradeStatus)) {
orderService.handlePaySuccess(outTradeNo);
return "success";
}
}
} catch (AlipayApiException e) {
log.error("支付宝验签失败", e);
}
return "failure";
}
优化构建配置(vite.config.ts):
typescript复制export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0]
}
}
}
}
}
})
构建命令:
bash复制npm run build
# 输出位于dist目录,可直接部署到Nginx
添加缓存配置:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
商品查询缓存示例:
java复制@Cacheable(value = "product", key = "#id")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
数据库连接池优化(application.yml):
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
Spring Boot跨域配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
添加JWT认证过滤器:
java复制public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
// 将用户信息存入SecurityContext
} catch (Exception e) {
response.sendError(HttpStatus.UNAUTHORIZED.value());
return;
}
}
chain.doFilter(request, response);
}
}
支付宝沙箱测试注意事项:
支付状态检查接口:
java复制public boolean checkPayStatus(String orderNo) {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(orderNo);
request.setBizModel(model);
try {
AlipayTradeQueryResponse response = alipayClient.execute(request);
return response.isSuccess() &&
"TRADE_SUCCESS".equals(response.getTradeStatus());
} catch (AlipayApiException e) {
log.error("查询支付状态失败", e);
return false;
}
}
将单体应用拆分为微服务:
使用Spring Cloud Alibaba组件:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
使用Vant或NutUI构建移动端:
bash复制npm install vant@next
响应式布局示例:
vue复制<template>
<div class="container">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" v-for="product in products" :key="product.id">
<product-card :product="product" />
</el-col>
</el-row>
</div>
</template>
集成ELK日志分析系统:
yaml复制# logback-spring.xml
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>localhost:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
业务指标监控:
java复制@RestController
@RequestMapping("/metrics")
public class MetricsController {
@Autowired
private MeterRegistry meterRegistry;
@GetMapping("/order-count")
public String getOrderCount() {
Counter counter = meterRegistry.counter("order.create.count");
counter.increment();
return "当前订单总数: " + counter.count();
}
}
在实际项目开发中,最大的挑战往往不是技术实现,而是如何平衡开发速度与代码质量。特别是在支付集成环节,建议先充分理解支付宝的文档,再开始编码。我在处理异步通知时曾因未正确验证签名导致支付状态不同步,后来通过添加详细的日志记录和重试机制解决了这个问题。