1. 项目背景与技术选型
这个SSM球鞋商城交易平台全Vue项目,是我去年为一个运动品牌电商平台做的全栈开发实战。当时客户需要一个既能快速上线又具备长期维护性的球鞋交易系统,经过技术评估后,我选择了SSM+Vue的全家桶方案。
SSM框架(Spring+SpringMVC+MyBatis)作为后端主力有几个明显优势:Spring的IoC容器让依赖管理变得优雅,AOP支持方便实现日志、事务等横切关注点;SpringMVC的注解驱动开发模式与RESTful风格天然契合;MyBatis的SQL与Java代码分离特性,特别适合需要复杂查询的电商系统。实测中,这套组合在QPS 3000的压力测试下平均响应时间保持在80ms左右。
前端选择Vue全家桶则是因为:
- 组件化开发模式完美适配电商平台的多页面复用需求(比如商品卡片在列表页、详情页、推荐位都要出现)
- Vuex的状态管理解决了跨组件数据同步难题(购物车数据需要全局共享)
- 基于Virtual DOM的渲染机制,在商品瀑布流等动态内容场景下比传统jQuery方案性能提升40%
- CLI工具链和丰富的UI库(用了Element-UI)能快速搭建标准化界面
技术选型避坑提示:曾考虑过React+Redux方案,但学习曲线较陡不利于团队快速上手;也测试过Thymeleaf服务端渲染,发现前后端耦合度过高不利于独立部署。最终Vue的渐进式特性成为决定性因素。
2. 工程架构设计与环境搭建
2.1 前后端分离架构
项目采用经典的前后端分离模式:
code复制project-root/
├── sneaker-api/ # SSM后端工程
│ ├── src/main/java/com/example/
│ │ ├── config/ # Spring配置类
│ │ ├── controller/ # 7个RESTful控制器
│ │ ├── service/ # 业务服务层
│ │ ├── dao/ # MyBatis Mapper接口
│ │ └── entity/ # 15个领域对象
│ └── resources/
│ ├── mapper/ # XML映射文件
│ └── application.yml
└── sneaker-web/ # Vue前端工程
├── public/ # 静态资源
├── src/
│ ├── api/ # 封装Axios请求
│ ├── assets/ # 样式与图片
│ ├── components/ # 32个可复用组件
│ ├── router/ # Vue路由配置
│ ├── store/ # Vuex模块化状态
│ └── views/ # 18个页面级组件
2.2 关键环境配置
后端环境:
xml复制<!-- pom.xml核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<!-- 数据库连接池配置示例 -->
spring:
datasource:
url: jdbc:mysql://localhost:3306/sneaker_db?useSSL=false
username: root
password: 123456
hikari:
maximum-pool-size: 20
前端环境:
bash复制# 推荐使用Vue CLI 5.x
npm install -g @vue/cli
vue create sneaker-web
# 必须安装的核心依赖
npm install axios vuex vue-router element-ui --save
# 开发时解决跨域问题
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
3. 核心功能模块实现
3.1 商品展示系统
商品模块采用三级分类设计,后端Mapper实现:
xml复制<!-- CategoryMapper.xml -->
<resultMap id="categoryTree" type="Category">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="children" ofType="Category"
select="findByParentId" column="id"/>
</resultMap>
<select id="findByParentId" resultMap="categoryTree">
SELECT * FROM category WHERE parent_id = #{pid}
</select>
前端使用递归组件渲染分类树:
vue复制<template>
<ul>
<li v-for="item in categories" :key="item.id">
{{ item.name }}
<category-tree
v-if="item.children"
:categories="item.children"/>
</li>
</ul>
</template>
<script>
export default {
name: 'CategoryTree',
props: ['categories']
}
</script>
3.2 购物车与订单系统
Vuex状态管理设计:
javascript复制// store/modules/cart.js
const state = {
items: JSON.parse(localStorage.getItem('cart')) || []
}
const mutations = {
ADD_ITEM(state, product) {
const existing = state.items.find(i => i.id === product.id)
existing ? existing.quantity++ : state.items.push({...product, quantity: 1})
localStorage.setItem('cart', JSON.stringify(state.items))
},
// 其他操作方法...
}
// 订单创建后端逻辑
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(
@RequestBody OrderDTO orderDTO,
@RequestHeader("Authorization") String token) {
String username = JwtUtil.parseToken(token);
Order order = orderService.create(orderDTO, username);
return ResponseEntity.ok(order);
}
3.3 支付系统集成
支付宝沙箱环境集成关键代码:
java复制// AlipayConfig.java
@Configuration
public class AlipayConfig {
@Value("${alipay.appId}")
private String appId;
@Bean
public AlipayClient alipayClient() {
return new DefaultAlipayClient(
"https://openapi.alipaydev.com/gateway.do",
appId,
privateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2");
}
}
// 前端支付页面处理
async handlePayment() {
const { data } = await this.$axios.post('/api/payments', {
orderId: this.order.id
})
const div = document.createElement('div')
div.innerHTML = data
document.body.appendChild(div)
document.forms[0].submit()
}
4. 性能优化实战
4.1 数据库优化
商品表索引设计示例:
sql复制CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int NOT NULL,
`category_id` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_price` (`price`),
FULLTEXT KEY `ft_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- MyBatis二级缓存配置
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
4.2 前端性能提升
按需加载路由配置:
javascript复制const ProductDetail = () => import(/* webpackChunkName: "product" */ '@/views/ProductDetail.vue')
const routes = [
{
path: '/product/:id',
component: ProductDetail,
meta: { keepAlive: true }
}
]
图片懒加载指令:
javascript复制// directives/lazyLoad.js
export default {
inserted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
}
}
// 使用示例
<img v-lazy-load="product.imageUrl" alt="球鞋图片">
5. 安全防护方案
5.1 认证与授权
JWT令牌实现:
java复制// JwtUtil.java
public class JwtUtil {
private static final String SECRET = "your-256-bit-secret";
private static final long EXPIRATION = 864000000L; // 10天
public static String generateToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
public static String parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
前端Axios拦截器:
javascript复制// api/interceptors.js
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
service.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
router.push('/login')
}
return Promise.reject(error)
}
)
5.2 XSS防护
后端统一处理:
java复制@ControllerAdvice
public class XssProtectionAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return XssUtils.cleanXSS(body);
}
}
前端使用DOMPurify:
javascript复制import DOMPurify from 'dompurify'
// 在显示富文本内容时
<div v-html="DOMPurify.sanitize(product.description)"></div>
6. 部署与监控
6.1 多环境部署
Spring Profile配置:
yaml复制# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/sneaker_prod
redis:
host: redis-master
password: ${DB_PASSWORD}
# Dockerfile示例
FROM openjdk:11-jre
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Dspring.profiles.active=prod","-jar","/app.jar"]
前端环境变量管理:
javascript复制// .env.production
VUE_APP_API_BASE=https://api.sneaker.com
VUE_APP_GA_ID=UA-XXXXX-X
// axios配置
baseURL: process.env.VUE_APP_API_BASE
6.2 监控方案
Spring Boot Actuator集成:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.yml配置
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
前端错误监控(Sentry示例):
javascript复制import * as Sentry from '@sentry/vue'
Sentry.init({
dsn: 'your-dsn',
integrations: [new Sentry.BrowserTracing()],
tracesSampleRate: 0.2
})
// Vue错误捕获
Vue.config.errorHandler = (err, vm, info) => {
Sentry.captureException(err)
}
7. 典型问题解决方案
7.1 跨域会话保持
解决方案:采用JWT+HttpOnly Cookie双认证
java复制// 登录接口返回Token和Cookie
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginDTO dto,
HttpServletResponse response) {
User user = userService.authenticate(dto);
String token = JwtUtil.generateToken(user);
ResponseCookie cookie = ResponseCookie.from("token", token)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(Duration.ofDays(7))
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok().body(Map.of(
"username", user.getUsername(),
"roles", user.getRoles()
));
}
7.2 高并发库存扣减
使用Redis分布式锁:
java复制public boolean reduceStock(Long productId, int quantity) {
String lockKey = "lock:product:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
Product product = productMapper.selectById(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
return productMapper.updateById(product) > 0;
}
return false;
}
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
return false;
}
7.3 大文件上传优化
前端分片上传实现:
vue复制<template>
<input type="file" @change="handleUpload">
</template>
<script>
export default {
methods: {
async handleUpload(e) {
const file = e.target.files[0]
const chunkSize = 2 * 1024 * 1024 // 2MB
const chunks = Math.ceil(file.size / chunkSize)
const fileMd5 = await this.calculateMd5(file)
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumber', i)
formData.append('totalChunks', chunks)
formData.append('identifier', fileMd5)
await this.$axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 通知合并文件
await this.$axios.post('/api/merge', {
filename: file.name,
identifier: fileMd5
})
}
}
}
</script>
8. 项目演进方向
8.1 微服务改造
当单体架构遇到性能瓶颈时,可考虑拆分为:
- 用户服务(account-service)
- 商品服务(product-service)
- 订单服务(order-service)
- 支付服务(payment-service)
- 推荐服务(recommend-service)
使用Spring Cloud Alibaba组件:
yaml复制# Nacos服务发现配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: localhost:8080
# OpenFeign声明式调用示例
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/products/{id}")
Product getById(@PathVariable Long id);
}
8.2 前端架构升级
渐进式迁移到Vue 3方案:
- 使用Vue 2.7版本(支持Composition API)
- 新组件用
<script setup>语法开发 - 逐步替换Vuex为Pinia
- 使用Vite替代Webpack提升构建速度
javascript复制// 示例Pinia store
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(product) {
const existing = this.items.find(i => i.id === product.id)
existing ? existing.quantity++ : this.items.push({...product, quantity: 1})
}
}
})
// Vite配置示例
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
8.3 可视化数据分析
集成ECharts实现销售看板:
vue复制<template>
<div ref="chart" style="width:600px;height:400px"></div>
</template>
<script>
import * as echarts from 'echarts'
export default {
mounted() {
this.initChart()
},
methods: {
async initChart() {
const { data } = await this.$axios.get('/api/sales/stats')
const chart = echarts.init(this.$refs.chart)
const option = {
tooltip: { trigger: 'axis' },
xAxis: { data: data.months },
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: data.amounts
}]
}
chart.setOption(option)
window.addEventListener('resize', chart.resize)
}
}
}
</script>
在项目后期维护阶段,我们接入了ELK日志系统,通过Kibana面板发现某个商品详情页的API响应时间偶尔会飙升到2秒以上。经过排查发现是MyBatis的N+1查询问题,通过添加@Transactional注解和调整fetchType为EAGER后,性能回归到正常水平。这种持续优化的过程,正是全栈项目最有价值的部分。
