系统优化与实践经验:从开发到生产的完整指南
本文将分享WiFi扫码连接系统在生产环境中的优化实践和经验总结,包括性能优化、错误处理、监控告警、安全加固等方面的宝贵经验。
前言
一个系统从开发到上线,再到稳定运行,需要经历大量的优化和调优工作。本文将分享我们在实际项目中遇到的挑战和解决方案,希望能为读者提供有价值的参考。
一、性能优化
1.1 数据库优化
索引优化
合理的索引设计是数据库性能的基础。我们为常用查询字段建立了复合索引:
— 广告曝光日统计表的索引
CREATE INDEX idx_merchant_ad_date ON ad_exposure_daily(merchant_id, ad_id, exposure_date);
CREATE INDEX idx_exposure_date ON ad_exposure_daily(exposure_date);
— 防刷记录表的索引
CREATE INDEX idx_openid_ad_created ON ad_exposure_anti_brush(openid, ad_id, created_at);
CREATE INDEX idx_device_ad_created ON ad_exposure_anti_brush(device_id, ad_id, created_at);
经验总结:
- 为WHERE、JOIN、ORDER BY字段建立索引
- 复合索引遵循最左前缀原则
- 避免过度索引,影响写入性能
- 定期分析慢查询,优化索引
查询优化
// ❌ 不好的做法:N+1查询
const qrcodes = await prisma.qrcode.findMany();
for (const qrcode of qrcodes) {
const merchant = await prisma.user.findUnique({
where: { id: qrcode.bindUserId },
});
}
// ✅ 好的做法:使用include预加载
const qrcodes = await prisma.qrcode.findMany({
include: {
bindUser: {
select: {
id: true,
email: true,
role: true,
},
},
},
});
经验总结:
- 使用include预加载关联数据,避免N+1查询
- 只查询需要的字段,使用select限制返回字段
- 大数据量查询使用分页,避免一次性加载过多数据
- 使用游标分页替代offset分页,性能更好
批量操作优化
// ❌ 不好的做法:循环插入
for (const item of items) {
await prisma.qrcode.create({ data: item });
}
// ✅ 好的做法:批量插入
await prisma.qrcode.createMany({
data: items,
skipDuplicates: true, // 跳过重复项
});
// ✅ 更好的做法:使用事务批量插入
await prisma.$transaction(
items.map(item => prisma.qrcode.create({ data: item }))
);
1.2 缓存策略
Redis缓存
// utils/cache.ts
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
});
export default {
// 获取缓存
async get(key: string): Promise<any> {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
},
// 设置缓存
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await redis.setex(key, ttl, JSON.stringify(value));
},
// 删除缓存
async del(key: string): Promise<void> {
await redis.del(key);
},
// 批量删除(支持通配符)
async delPattern(pattern: string): Promise<void> {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(…keys);
}
},
};
缓存应用场景
// 1. 广告配置缓存
async getAdConfig(adId: string) {
const cacheKey = `ad:config:${adId}`;
// 先查缓存
let config = await cache.get(cacheKey);
if (config) {
return config;
}
// 缓存未命中,查数据库
config = await prisma.adConfig.findUnique({
where: { adId },
});
// 写入缓存(TTL: 1小时)
if (config) {
await cache.set(cacheKey, config, 3600);
}
return config;
}
// 2. 统计数据缓存
async getDashboardStats(userId: string) {
const cacheKey = `dashboard:stats:${userId}`;
let stats = await cache.get(cacheKey);
if (stats) {
return stats;
}
stats = await this.calculateDashboardStats(userId);
// 统计数据缓存5分钟
await cache.set(cacheKey, stats, 300);
return stats;
}
经验总结:
- 热点数据使用缓存,减少数据库压力
- 设置合理的TTL,平衡数据新鲜度和性能
- 缓存更新时使用删除策略,避免数据不一致
- 使用缓存预热,提前加载热点数据
1.3 接口优化
懒加载优化
// 二维码图片懒加载
async getQrcode(qrcodeId: string) {
const qrcode = await prisma.qrcode.findUnique({
where: { id: qrcodeId },
});
// 不直接返回Base64图片,而是返回URL
// 图片按需生成,减少响应大小
const imagePath = this.getQrcodeImagePath(qrcodeId);
const imageUrl = fs.existsSync(imagePath)
? this.getQrcodeImageUrl(qrcodeId)
: null;
return {
…qrcode,
qrcodeImageUrl: imageUrl, // 返回URL而非Base64
};
}
响应压缩
// config/config.default.ts
import compress from 'koa-compress';
export default {
middleware: ['compress', 'errorHandler', 'auth', 'requestLog'],
compress: {
threshold: 1024, // 超过1KB才压缩
gzip: {
flush: require('zlib').constants.Z_SYNC_FLUSH,
},
},
};
接口限流
// app/middleware/rateLimit.ts
import rateLimit from 'koa-ratelimit';
import Redis from 'ioredis';
const redis = new Redis();
export default function rateLimitMiddleware() {
return rateLimit({
driver: 'redis',
db: redis,
duration: 60000, // 时间窗口:1分钟
max: 100, // 最大请求数
id: (ctx) => ctx.ip, // 基于IP限流
errorMessage: '请求过于频繁,请稍后再试',
});
}
二、错误处理
2.1 统一错误处理
// app/middleware/errorHandler.ts
export default function errorHandler() {
return async (ctx: Context, next: () => Promise<any>) => {
try {
await next();
} catch (error: any) {
// 记录错误日志
ctx.logger.error('请求处理失败:', {
error: error.message,
stack: error.stack,
url: ctx.url,
method: ctx.method,
userId: ctx.state.user?.id,
requestId: ctx.state.requestId,
});
// 根据错误类型返回不同的状态码
if (error.name === 'ValidationError') {
ctx.status = 400;
ctx.body = {
success: false,
message: error.message || '参数验证失败',
code: 'VALIDATION_ERROR',
};
} else if (error.message === '未认证' || error.message.includes('Token')) {
ctx.status = 401;
ctx.body = {
success: false,
message: '未认证或Token已过期',
code: 'UNAUTHORIZED',
};
} else if (error.message === '无权限') {
ctx.status = 403;
ctx.body = {
success: false,
message: '无权限访问',
code: 'FORBIDDEN',
};
} else if (error.code === 'P2002') {
// Prisma唯一约束冲突
ctx.status = 409;
ctx.body = {
success: false,
message: '数据已存在',
code: 'DUPLICATE_ENTRY',
};
} else {
// 未知错误,不暴露详细信息
ctx.status = 500;
ctx.body = {
success: false,
message: process.env.NODE_ENV === 'production'
? '服务器内部错误'
: error.message,
code: 'INTERNAL_ERROR',
};
}
}
};
}
2.2 业务异常处理
// utils/errors.ts
export class BusinessError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 400
) {
super(message);
this.name = 'BusinessError';
}
}
// 使用示例
async createQrcode(data: any) {
// 检查商户是否存在
const merchant = await prisma.user.findUnique({
where: { id: data.merchantId },
});
if (!merchant) {
throw new BusinessError('商户不存在', 'MERCHANT_NOT_FOUND', 404);
}
if (merchant.status !== 1) {
throw new BusinessError('商户已被禁用', 'MERCHANT_DISABLED', 403);
}
// 业务逻辑…
}
2.3 前端错误处理
// utils/request.ts
request.interceptors.response.use(
(response) => {
const res = response.data;
// 业务错误
if (!res.success) {
const errorMessage = res.message || '请求失败';
// 根据错误码处理
if (res.code === 'UNAUTHORIZED') {
// Token过期,跳转登录
const userStore = useUserStore();
userStore.logout();
window.location.href = '/login';
return Promise.reject(new Error('未登录或Token已过期'));
} else if (res.code === 'FORBIDDEN') {
// 无权限
message.error('无权限访问');
return Promise.reject(new Error(errorMessage));
} else {
// 其他业务错误
message.error(errorMessage);
return Promise.reject(new Error(errorMessage));
}
}
return res;
},
(error) => {
// 网络错误
if (error.response) {
// 服务器返回了错误状态码
const status = error.response.status;
if (status >= 500) {
message.error('服务器错误,请稍后重试');
} else if (status === 404) {
message.error('请求的资源不存在');
} else {
message.error('请求失败,请稍后重试');
}
} else if (error.request) {
// 请求已发出但没有收到响应
message.error('网络连接失败,请检查网络');
} else {
// 请求配置错误
message.error('请求配置错误');
}
return Promise.reject(error);
}
);
三、监控与告警
3.1 日志系统
// config/config.default.ts
export default {
logger: {
level: 'INFO',
consoleLevel: 'DEBUG',
outputJSON: true, // 生产环境输出JSON格式
appLogName: 'egg-web.log',
coreLogName: 'egg-web.log',
agentLogName: 'egg-agent.log',
errorLogName: 'common-error.log',
},
};
结构化日志
// 记录结构化日志
ctx.logger.info('广告曝光上报', {
qrcodeId,
adId,
merchantId,
deviceId,
openid: openid ? `${openid.substring(0, 8)}…` : null,
timestamp: Date.now(),
requestId: ctx.state.requestId,
});
3.2 性能监控
// app/middleware/performance.ts
export default function performanceMonitor() {
return async (ctx: Context, next: () => Promise<any>) => {
const startTime = Date.now();
await next();
const duration = Date.now() – startTime;
// 记录慢请求
if (duration > 1000) {
ctx.logger.warn('慢请求检测', {
url: ctx.url,
method: ctx.method,
duration,
requestId: ctx.state.requestId,
});
}
// 设置响应头
ctx.set('X-Response-Time', `${duration}ms`);
};
}
3.3 健康检查
// app/controller/health.ts
export default class HealthController extends Controller {
async index() {
const { ctx } = this;
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
database: await this.checkDatabase(),
redis: await this.checkRedis(),
};
ctx.body = health;
}
async checkDatabase() {
try {
await prisma.$queryRaw`SELECT 1`;
return { status: 'ok' };
} catch (error) {
return { status: 'error', error: error.message };
}
}
async checkRedis() {
try {
const redis = require('../utils/cache').default;
await redis.ping();
return { status: 'ok' };
} catch (error) {
return { status: 'error', error: error.message };
}
}
}
四、安全加固
4.1 参数验证
// app/middleware/validator.ts
import Joi from 'joi';
export function validate(schema: Joi.Schema) {
return async (ctx: Context, next: () => Promise<any>) => {
const { error, value } = schema.validate(ctx.request.body, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
ctx.status = 400;
ctx.body = {
success: false,
message: '参数验证失败',
errors: error.details.map(d => d.message),
};
return;
}
ctx.request.body = value;
await next();
};
}
// 使用示例
const createQrcodeSchema = Joi.object({
count: Joi.number().integer().min(1).max(100).required(),
type: Joi.string().valid('merchant', 'agent').required(),
});
router.post('/api/qrcode/batch-generate',
authMiddleware(),
validate(createQrcodeSchema),
controller.qrcode.batchGenerate
);
4.2 SQL注入防护
使用ORM的参数化查询,避免SQL注入:
// ✅ 好的做法:使用ORM参数化查询
const user = await prisma.user.findUnique({
where: { id: userId },
});
// ❌ 不好的做法:直接拼接SQL
const user = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE id = '${userId}'`
);
4.3 XSS防护
// 使用xss库过滤用户输入
import xss from 'xss';
const cleanHtml = xss(userInput, {
whiteList: {}, // 白名单为空,过滤所有HTML标签
stripIgnoreTag: true, // 过滤不在白名单的标签
});
4.4 CSRF防护
// config/config.default.ts
export default {
security: {
csrf: {
enable: true,
ignore: ['/api/public/*'], // 公开接口忽略CSRF检查
},
},
};
五、部署与运维
5.1 PM2进程管理
// ecosystem.config.js
module.exports = {
apps: [{
name: 'wifi-qrcode-backend',
script: './dist/app.js',
instances: 'max', // 使用所有CPU核心
exec_mode: 'cluster', // 集群模式
env: {
NODE_ENV: 'production',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
max_memory_restart: '500M', // 内存超过500M自动重启
watch: false,
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
}],
};
5.2 Nginx配置
upstream backend {
server 127.0.0.1:7001;
server 127.0.0.1:7002;
keepalive 64;
}
server {
listen 80;
server_name api.example.com;
# 请求日志
access_log /var/log/nginx/api.access.log;
error_log /var/log/nginx/api.error.log;
# 请求体大小限制
client_max_body_size 10M;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源
location /public/ {
alias /path/to/app/public/;
expires 7d;
add_header Cache-Control "public, immutable";
}
}
5.3 数据库备份
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backup/mysql"
DB_NAME="wifi_qrcode"
# 创建备份目录
mkdir -p $BACKUP_DIR
# 备份数据库
mysqldump -u root -p$MYSQL_PASSWORD $DB_NAME > $BACKUP_DIR/${DB_NAME}_${DATE}.sql
# 压缩备份文件
gzip $BACKUP_DIR/${DB_NAME}_${DATE}.sql
# 删除7天前的备份
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
echo "备份完成: ${DB_NAME}_${DATE}.sql.gz"
六、问题排查
6.1 常见问题
问题1:收益计算不准确
原因分析:
- 曝光数据写入延迟
- 收益计算时读取到旧数据
- 并发更新导致数据不一致
解决方案:
// 使用轮询确保数据已写入
async waitForExposureData(merchantId: string, adId: string, date: Date) {
const maxRetries = 10;
const retryInterval = 1000; // 1秒
for (let i = 0; i < maxRetries; i++) {
const exposure = await prisma.adExposureDaily.findUnique({
where: {
merchantId_adId_exposureDate: {
merchantId,
adId,
exposureDate: date,
},
},
});
if (exposure && exposure.exposureCount > 0) {
return exposure;
}
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
throw new Error('等待曝光数据超时');
}
问题2:二维码图片生成失败
原因分析:
- 微信API调用失败
- 文件系统权限问题
- 磁盘空间不足
解决方案:
// 添加重试机制
async generateMiniprogramQrcode(path: string, retryCount: number = 3) {
for (let i = 0; i < retryCount; i++) {
try {
const buffer = await generateMiniprogramQRCode(path);
return buffer;
} catch (error) {
if (i === retryCount – 1) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
6.2 调试技巧
// 1. 开启SQL日志
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
],
});
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Params: ' + e.params);
console.log('Duration: ' + e.duration + 'ms');
});
// 2. 请求追踪
ctx.state.requestId = generateUUID();
ctx.logger.info('请求开始', {
requestId: ctx.state.requestId,
url: ctx.url,
method: ctx.method,
});
总结
本文分享了系统优化与实践经验:
这些实践经验来自于真实的生产环境,希望能为读者提供有价值的参考。
技术要点回顾:
- 数据库索引优化和查询优化
- Redis缓存策略的应用
- 接口懒加载和响应压缩
- 统一错误处理机制
- 结构化日志和性能监控
- 安全防护的多层机制
- PM2和Nginx的配置优化
下期预告: 下一篇我们将对整个系统进行总结,回顾技术架构、功能实现和优化经验,并展望未来的发展方向。




