1. 项目概述与设计思路
这个饰品商城系统采用前后端分离架构,前端基于Vue 3生态链,后端使用Node.js技术栈。作为典型的电商类应用,系统需要解决的核心问题包括:商品展示的高效渲染、购物流程的稳定性、支付环节的安全性以及后台管理的便捷性。
我在实际开发中发现,电商系统最关键的三个技术难点是:1) 高并发下的库存准确性 2) 复杂商品筛选的性能优化 3) 支付流程的可靠性。针对这些问题,我们的技术选型特别注重以下几个方面:
- 前端采用Vue 3的Composition API编写可复用业务逻辑
- 状态管理使用Pinia替代Vuex以获得更好的TypeScript支持
- 后端API设计遵循RESTful规范但针对电商场景做特殊优化
- 数据库事务处理确保库存和订单的一致性
2. 技术架构详解
2.1 前端技术栈选型
选择Vue 3而非React或Angular主要基于以下考量:
- 渐进式框架特性适合快速迭代的毕业设计项目
- Composition API更利于复杂业务逻辑的组织
- 与Element Plus组件库的完美兼容性
- Vite构建工具带来的极致开发体验
实际开发中,我特别推荐使用这些配置:
javascript复制// vite.config.js 优化配置
export default defineConfig({
plugins: [vue()],
build: {
chunkSizeWarningLimit: 1500, // 增大chunk大小警告阈值
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-libs': ['vue', 'vue-router', 'pinia']
}
}
}
}
})
2.2 后端技术决策
在Express和NestJS之间,我最终选择了Express,原因是:
- 学习曲线更平缓适合毕业设计周期
- 中间件生态丰富,JWT鉴权、文件上传等都有成熟方案
- 与MySQL/Redis的集成更简单直接
但需要注意几个关键点:
重要提示:Express需要自行组织项目结构,建议采用以下目录布局:
code复制/src
/config # 数据库和第三方配置
/controllers # 业务逻辑
/middlewares # 鉴权等中间件
/models # 数据模型
/routes # 路由定义
/services # 底层服务
/utils # 工具函数
3. 核心模块实现
3.1 商品模块深度优化
商品列表接口采用了三级缓存策略:
- 内存缓存:高频访问商品存储30秒
- Redis缓存:全量商品数据存储5分钟
- 数据库分页查询:最终数据源
javascript复制// 商品查询优化示例
router.get('/products', async (req, res) => {
const { page = 1, size = 10 } = req.query;
const cacheKey = `products:${page}:${size}`;
// 1. 检查内存缓存
if (memoryCache.has(cacheKey)) {
return res.json(memoryCache.get(cacheKey));
}
// 2. 检查Redis缓存
const cachedData = await redisClient.get(cacheKey);
if (cachedData) {
memoryCache.set(cacheKey, JSON.parse(cachedData), 30);
return res.json(JSON.parse(cachedData));
}
// 3. 数据库查询
const offset = (page - 1) * size;
const products = await Product.findAndCountAll({
offset,
limit: size,
attributes: ['id', 'name', 'price', 'cover_img']
});
// 设置缓存
redisClient.setEx(cacheKey, 300, JSON.stringify(products));
memoryCache.set(cacheKey, products, 30);
res.json(products);
});
3.2 购物车与订单流程
购物车设计采用了混合存储方案:
- 未登录用户:localStorage临时存储
- 已登录用户:服务端Redis存储
订单创建时的关键事务处理:
javascript复制// 订单创建事务示例
const createOrder = async (userId, items) => {
const t = await sequelize.transaction();
try {
// 1. 检查库存
for (const item of items) {
const product = await Product.findByPk(item.productId, {
lock: t.LOCK.UPDATE,
transaction: t
});
if (product.stock < item.quantity) {
throw new Error(`库存不足: ${product.name}`);
}
}
// 2. 扣减库存
await Promise.all(items.map(item =>
Product.decrement('stock', {
by: item.quantity,
where: { id: item.productId },
transaction: t
})
));
// 3. 创建订单
const order = await Order.create({
userId,
total: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
status: 'pending'
}, { transaction: t });
// 4. 添加订单项
await OrderItem.bulkCreate(
items.map(item => ({
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
price: item.price
})),
{ transaction: t }
);
await t.commit();
return order;
} catch (error) {
await t.rollback();
throw error;
}
};
4. 性能优化实战
4.1 图片处理方案
针对饰品商城大量图片展示的特点,我采用了以下优化组合:
- 前端使用
v-lazy实现懒加载 - 图片上传时自动生成WebP格式
- 使用七牛云CDN加速分发
- 为每张图片生成不同尺寸的缩略图
javascript复制// 图片上传处理中间件
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post('/upload', upload.single('image'), async (req, res) => {
const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}.webp`;
const sizes = [
{ suffix: 'lg', width: 800 },
{ suffix: 'md', width: 400 },
{ suffix: 'sm', width: 200 }
];
await Promise.all(
sizes.map(async ({ suffix, width }) => {
await sharp(req.file.buffer)
.resize(width)
.webp({ quality: 80 })
.toFile(`uploads/${filename.replace('.webp', `_${suffix}.webp`)}`);
})
);
res.json({
url: `/uploads/${filename.replace('.webp', '_lg.webp')}`,
thumbnail: `/uploads/${filename.replace('.webp', '_sm.webp')}`
});
});
4.2 数据库索引优化
经过压力测试后,我为以下字段添加了复合索引:
sql复制-- 商品表优化
ALTER TABLE products ADD INDEX idx_category_price (category_id, price);
ALTER TABLE products ADD FULLTEXT INDEX ft_name_desc (name, description);
-- 订单表优化
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
ALTER TABLE order_items ADD INDEX idx_order_product (order_id, product_id);
实测表明,在10万级商品数据下,搜索性能提升约15倍。
5. 部署与监控
5.1 Docker容器化部署
采用多阶段构建优化镜像大小:
dockerfile复制# 前端Dockerfile
FROM node:16 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
dockerfile复制# 后端Dockerfile
FROM node:16 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["node", "dist/server.js"]
5.2 性能监控配置
使用PM2的监控功能配合自定义指标:
javascript复制// 监控中间件
const responseTime = require('response-time');
const promClient = require('prom-client');
const collectDefaultMetrics = promClient.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'code'],
buckets: [0.1, 5, 15, 50, 100, 300, 500, 1000, 3000, 5000]
});
app.use(responseTime((req, res, time) => {
httpRequestDurationMicroseconds
.labels(req.method, req.route.path, res.statusCode)
.observe(time);
}));
// 暴露指标端点
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
6. 踩坑经验分享
- Vue 3响应式陷阱:在组合式函数中直接解构props会失去响应性,需要使用toRefs:
javascript复制// 错误做法
const { product } = defineProps(['product']);
const price = computed(() => product.price * discount); // 不会更新
// 正确做法
const props = defineProps(['product']);
const { product } = toRefs(props);
const price = computed(() => product.value.price * discount);
- MySQL连接池配置:开发初期经常遇到"Too many connections"错误,后来发现需要正确配置Sequelize:
javascript复制const sequelize = new Sequelize(/* ... */, {
pool: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000
}
});
- 支付回调验证:支付宝回调需要特别注意验证签名和订单金额,我封装了以下验证方法:
javascript复制const verifyAlipayCallback = async (params) => {
const sign = params.sign;
const signType = params.sign_type;
delete params.sign;
delete params.sign_type;
const sortedParams = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
const publicKey = await getAlipayPublicKey(params.app_id);
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(sortedParams);
if (!verifier.verify(publicKey, sign, 'base64')) {
throw new Error('签名验证失败');
}
const order = await Order.findByPk(params.out_trade_no);
if (Math.abs(order.total - parseFloat(params.total_amount)) > 0.01) {
throw new Error('金额不一致');
}
return true;
};
- Vite开发环境代理:解决跨域问题时,需要正确配置vite.config.js:
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}