文章目录
-
- 引言
- 一、技术架构总览
-
- 1.1 系统整体架构
- 1.2 核心技术栈
- 二、微信官方接口深度解析
-
- 2.1 小程序码生成接口对比
- 2.2 接口调用流程详解
- 2.3 Access Token管理策略
- 三、数据库设计
-
- 3.1 核心数据表设计
- 3.2 数据关系图
- 四、后端实现(Node.js + Express)
-
- 4.1 项目结构
- 4.2 核心服务层实现
- 4.3 控制器层实现
- 4.4 路由配置
- 五、前端小程序实现
-
- 5.1 小程序页面结构
- 5.2 扫码验证页面实现
- 5.3 WXML页面结构
- 5.4 WXSS样式文件
- 六、安全防护策略
-
- 6.1 防伪验证算法
- 6.2 风控策略
- 七、性能优化
-
- 7.1 缓存策略
- 7.2 数据库优化
- 八、部署与监控
-
- 8.1 Docker部署配置
- 8.2 监控告警配置
- 九、测试方案
-
- 9.1 单元测试
- 9.2 压力测试
- 十、总结与展望
-
- 10.1 核心优势
- 10.2 业务价值
- 10.3 未来扩展方向
引言
在数字化营销和产品管理中,“一物一码”已成为企业实现产品数字化、精准营销和防伪溯源的核心技术。通过微信小程序实现一物一码,不仅能为企业提供强大的营销工具,还能构建完整的用户互动和数据收集系统。本文将深入探讨如何基于微信小程序官方能力,构建完整的一物一码解决方案。
一、技术架构总览
1.1 系统整体架构
┌─────────────────────────────────────────┐
│ 前端小程序 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 扫码页面 │ │ 商品详情 │ │ 营销活动 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ 微信服务端 │
│ ┌─────────────────────────────┐ │
│ │ 小程序码生成接口 │ │
│ └─────────────────────────────┘ │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ 业务服务器 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │商品数据库│ │用户数据库│ │扫码记录库│ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
1.2 核心技术栈
- 前端:微信小程序原生开发
- 后端:Node.js(Express/Koa)或 Java(Spring Boot)
- 数据库:MySQL + Redis缓存
- 存储:COS对象存储(存放生成的二维码图片)
二、微信官方接口深度解析
2.1 小程序码生成接口对比
微信官方提供了三种二维码生成接口,适用于不同场景:
| 小程序码(wxacode) | 10万 | 128字节 | 固定场景码 |
| 小程序码(unlimited) | 无限制 | 32字符 | 一物一码 |
| 二维码(createQRCode) | 10万 | 128字节 | 简单场景 |
推荐使用 getwxacodeunlimit(无数量限制小程序码)实现一物一码
2.2 接口调用流程详解
/**
* 微信小程序码生成完整流程
* 参考:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
*/
async function generateWXACodeUnlimited(params) {
// 步骤1:获取access_token
const accessToken = await getAccessToken();
// 步骤2:构造请求参数
const requestData = {
scene: params.scene, // 最大32个字符
page: params.page, // 必须是已经发布的小程序页面
width: params.width || 430, // 默认430px
auto_color: false, // 自动配置线条颜色
line_color: { // auto_color 为 false 时生效
"r": "0",
"g": "0",
"b": "0"
},
is_hyaline: false, // 是否需要透明底色
check_path: true, // 检查page是否存在
env_version: 'release' // 体验版为'trial', 开发版为'develop'
};
// 步骤3:调用微信接口
const response = await axios({
method: 'POST',
url: `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`,
data: requestData,
responseType: 'arraybuffer' // 重要:接收二进制数据
});
// 步骤4:处理响应
const contentType = response.headers['content-type'];
if (contentType.includes('application/json')) {
// 返回的是JSON,说明出错
const error = JSON.parse(response.data.toString());
throw new Error(`微信接口错误: ${error.errmsg}`);
} else {
// 返回的是图片
return response.data;
}
}
2.3 Access Token管理策略
// Redis缓存access_token的完整实现
class AccessTokenManager {
constructor() {
this.redisClient = redis.createClient();
}
async getAccessToken() {
// 从Redis获取缓存的token
const cachedToken = await this.redisClient.get('wx_access_token');
if (cachedToken) {
return cachedToken;
}
// 重新获取token
const newToken = await this.fetchNewAccessToken();
// 缓存7100秒(微信返回7200秒,提前100秒刷新)
await this.redisClient.setex(
'wx_access_token',
7100,
newToken
);
return newToken;
}
async fetchNewAccessToken() {
const response = await axios.get(
'https://api.weixin.qq.com/cgi-bin/token',
{
params: {
grant_type: 'client_credential',
appid: process.env.WX_APPID,
secret: process.env.WX_APPSECRET
}
}
);
if (response.data.errcode) {
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
}
return response.data.access_token;
}
}
三、数据库设计
3.1 核心数据表设计
— 商品主表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',
product_no VARCHAR(50) UNIQUE NOT NULL COMMENT '商品编号',
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
product_type VARCHAR(20) COMMENT '商品类型',
batch_no VARCHAR(50) COMMENT '生产批次',
production_date DATE COMMENT '生产日期',
expiration_date DATE COMMENT '过期日期',
status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-下架',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_no (product_no),
INDEX idx_batch_no (batch_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
— 二维码唯一码表(一物一码核心表)
CREATE TABLE qr_codes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
qr_id VARCHAR(32) UNIQUE NOT NULL COMMENT '二维码唯一ID',
product_id BIGINT NOT NULL COMMENT '关联商品ID',
serial_no VARCHAR(50) NOT NULL COMMENT '序列号',
secret_key VARCHAR(64) NOT NULL COMMENT '加密密钥,用于防伪验证',
status TINYINT DEFAULT 0 COMMENT '状态:0-未激活,1-已激活,2-已扫描,3-已作废',
activate_time DATETIME COMMENT '激活时间',
first_scan_time DATETIME COMMENT '首次扫码时间',
scan_count INT DEFAULT 0 COMMENT '扫码次数',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_qr_id (qr_id),
INDEX idx_product_serial (product_id, serial_no),
INDEX idx_secret_key (secret_key),
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='二维码表';
— 扫码记录表
CREATE TABLE scan_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
qr_id VARCHAR(32) NOT NULL COMMENT '二维码ID',
openid VARCHAR(100) COMMENT '用户openid',
user_id BIGINT COMMENT '用户ID',
scan_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '扫码时间',
ip_address VARCHAR(45) COMMENT 'IP地址',
user_agent TEXT COMMENT '用户代理',
location VARCHAR(100) COMMENT '地理位置',
device_info JSON COMMENT '设备信息',
scene VARCHAR(50) COMMENT '扫码场景',
result_code INT DEFAULT 0 COMMENT '扫码结果:0-成功,1-重复扫码,2-假码',
INDEX idx_qr_id_time (qr_id, scan_time),
INDEX idx_openid_time (openid, scan_time),
INDEX idx_scan_time (scan_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='扫码记录表';
— 用户表(扫码用户)
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
openid VARCHAR(100) UNIQUE NOT NULL COMMENT '微信openid',
unionid VARCHAR(100) COMMENT '微信unionid',
nickname VARCHAR(100) COMMENT '用户昵称',
avatar_url VARCHAR(500) COMMENT '头像',
phone VARCHAR(20) COMMENT '手机号',
gender TINYINT COMMENT '性别',
province VARCHAR(50) COMMENT '省份',
city VARCHAR(50) COMMENT '城市',
country VARCHAR(50) COMMENT '国家',
is_subscribed TINYINT DEFAULT 0 COMMENT '是否关注公众号',
total_scans INT DEFAULT 0 COMMENT '总扫码次数',
last_scan_time DATETIME COMMENT '最后扫码时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_openid (openid),
INDEX idx_unionid (unionid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
— 防伪验证日志表
CREATE TABLE anti_fake_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
qr_id VARCHAR(32) NOT NULL COMMENT '二维码ID',
verify_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '验证时间',
verify_method VARCHAR(20) COMMENT '验证方式:scan-扫码,manual-手动',
verify_result TINYINT COMMENT '验证结果:1-真品,0-假货',
verify_ip VARCHAR(45) COMMENT '验证IP',
verify_user_agent TEXT COMMENT '验证用户代理',
verify_code VARCHAR(50) COMMENT '验证码(手动输入时)',
INDEX idx_qr_id_time (qr_id, verify_time),
INDEX idx_verify_time (verify_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='防伪验证日志表';
3.2 数据关系图
┌───────────┐ 1:n ┌──────────┐ 1:1 ┌─────────────┐
│ Products │───────────>│ QR_Codes │<──────────│ ScanRecords │
└───────────┘ └──────────┘ └─────────────┘
│ │
│ 1:n │ n:1
▼ ▼
┌─────────────┐ ┌──────────┐
│ AntiFakeLogs│ │ Users │
└─────────────┘ └──────────┘
四、后端实现(Node.js + Express)
4.1 项目结构
src/
├── config/ # 配置文件
│ ├── database.js
│ ├── redis.js
│ └── wechat.js
├── controllers/ # 控制器
│ ├── qrCodeController.js
│ ├── productController.js
│ └── scanController.js
├── services/ # 业务逻辑层
│ ├── wechatService.js
│ ├── qrCodeService.js
│ └── antiFakeService.js
├── models/ # 数据模型
│ ├── Product.js
│ ├── QRCode.js
│ └── ScanRecord.js
├── utils/ # 工具函数
│ ├── crypto.js
│ ├── validator.js
│ └── logger.js
├── middleware/ # 中间件
│ ├── auth.js
│ ├── rateLimit.js
│ └── logger.js
├── routes/ # 路由
│ ├── qrCodeRoutes.js
│ ├── productRoutes.js
│ └── scanRoutes.js
└── app.js # 应用入口
4.2 核心服务层实现
// services/qrCodeService.js
const QRCode = require('../models/QRCode');
const Product = require('../models/Product');
const WechatService = require('./wechatService');
const CryptoUtil = require('../utils/crypto');
class QRCodeService {
constructor() {
this.wechatService = new WechatService();
}
/**
* 批量生成一物一码
* @param {Object} params 生成参数
* @param {number} count 生成数量
*/
async generateBatchQRCode(params, count = 1) {
const { productId, batchNo, prefix = '' } = params;
// 验证商品是否存在
const product = await Product.findById(productId);
if (!product) {
throw new Error('商品不存在');
}
const qrCodes = [];
for (let i = 0; i < count; i++) {
// 生成唯一序列号
const serialNo = this.generateSerialNo(prefix, i + 1);
// 生成加密密钥
const secretKey = CryptoUtil.generateSecretKey();
// 创建二维码记录
const qrCode = await QRCode.create({
qr_id: this.generateQRId(),
product_id: productId,
serial_no: serialNo,
secret_key: secretKey,
batch_no: batchNo,
status: 0 // 未激活
});
// 生成微信小程序码
const scene = this.buildSceneString({
qid: qrCode.qr_id,
pid: productId,
sn: serialNo
});
const qrCodeImage = await this.wechatService.generateWXACode({
scene: scene,
page: 'pages/product/verify/index',
width: 600, // 高质量图片
line_color: { r: '25', g: '118', b: '210' } // 品牌色
});
// 上传到COS存储
const imageUrl = await this.uploadToCOS(qrCodeImage, qrCode.qr_id);
qrCodes.push({
id: qrCode.id,
qr_id: qrCode.qr_id,
serial_no: serialNo,
qr_code_url: imageUrl,
secret_key: secretKey,
product_info: {
name: product.product_name,
batch_no: product.batch_no
}
});
}
return qrCodes;
}
/**
* 处理扫码请求
* @param {Object} scanData 扫码数据
*/
async handleScan(scanData) {
const { scene, openid, userInfo, deviceInfo } = scanData;
// 解析scene参数
const sceneParams = this.parseScene(scene);
const { qid, pid, sn } = sceneParams;
// 查询二维码信息
const qrCode = await QRCode.findOne({
where: { qr_id: qid },
include: [{ model: Product }]
});
if (!qrCode) {
return {
success: false,
code: 'QR_NOT_FOUND',
message: '二维码不存在'
};
}
// 防伪验证
const verifyResult = await this.verifyAntiFake(qrCode, sceneParams);
if (!verifyResult.success) {
// 记录假码
await this.recordFakeScan(qrCode, scanData);
return verifyResult;
}
// 检查是否首次扫码
const isFirstScan = qrCode.status === 0;
if (isFirstScan) {
// 首次扫码,激活二维码
await qrCode.update({
status: 1,
activate_time: new Date(),
first_scan_time: new Date()
});
}
// 更新扫码次数
await qrCode.increment('scan_count');
// 记录扫码日志
await this.recordScanLog({
qr_id: qid,
openid: openid,
user_info: userInfo,
device_info: deviceInfo,
is_first_scan: isFirstScan,
verify_result: true
});
// 更新用户信息
await this.updateUserInfo(openid, userInfo);
// 返回结果
return {
success: true,
data: {
is_first_scan: isFirstScan,
product_info: qrCode.Product,
scan_count: qrCode.scan_count + 1,
scan_time: new Date(),
anti_fake_result: verifyResult.data
}
};
}
/**
* 防伪验证逻辑
*/
async verifyAntiFake(qrCode, sceneParams) {
// 1. 基础验证
if (qrCode.status === 3) {
return { success: false, code: 'QR_INVALID', message: '二维码已作废' };
}
// 2. 密钥验证
const expectedHash = CryptoUtil.hmacSHA256(
`${qrCode.qr_id}${qrCode.serial_no}`,
qrCode.secret_key
);
const providedHash = sceneParams.hash;
if (expectedHash !== providedHash) {
return { success: false, code: 'FAKE_CODE', message: '防伪验证失败' };
}
// 3. 地理位置验证(可选)
if (qrCode.activate_location) {
const distance = this.calculateDistance(
scanLocation,
qrCode.activate_location
);
if (distance > 100000) { // 100公里以外
return {
success: false,
code: 'LOCATION_MISMATCH',
message: '扫码位置异常'
};
}
}
// 4. 扫码频率验证
const recentScans = await this.getRecentScans(qrCode.qr_id, 5); // 最近5次扫码
if (recentScans.length >= 5) {
const timeDiff = Date.now() – recentScans[0].scan_time;
if (timeDiff < 60000) { // 1分钟内超过5次
return {
success: false,
code: 'SCAN_TOO_FREQUENT',
message: '扫码过于频繁'
};
}
}
return {
success: true,
data: {
level: 'GENUINE',
confidence: 0.99,
verification_id: this.generateVerificationId()
}
};
}
// 生成序列号
generateSerialNo(prefix, index) {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substr(2, 6);
return `${prefix}${timestamp}${random}${index.toString().padStart(6, '0')}`;
}
// 生成二维码ID
generateQRId() {
return `QR${Date.now()}${Math.random().toString(36).substr(2, 9)}`;
}
// 构建scene字符串
buildSceneString(params) {
// 添加签名
const signature = CryptoUtil.hmacSHA256(
JSON.stringify(params),
process.env.SCENE_SECRET
);
const finalParams = {
…params,
sig: signature.substr(0, 8), // 取前8位作为签名
t: Date.now() // 时间戳防重放
};
// 转换为URL安全格式
return Object.entries(finalParams)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
}
// 解析scene参数
parseScene(sceneStr) {
const params = {};
const pairs = sceneStr.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value) {
params[key] = decodeURIComponent(value);
}
}
// 验证签名
const { sig, t, …dataParams } = params;
const expectedSig = CryptoUtil.hmacSHA256(
JSON.stringify(dataParams),
process.env.SCENE_SECRET
).substr(0, 8);
if (sig !== expectedSig) {
throw new Error('签名验证失败');
}
// 防重放攻击(5分钟内有效)
if (Date.now() – parseInt(t) > 5 * 60 * 1000) {
throw new Error('参数已过期');
}
return dataParams;
}
}
module.exports = QRCodeService;
4.3 控制器层实现
// controllers/scanController.js
const QRCodeService = require('../services/qrCodeService');
const logger = require('../utils/logger');
class ScanController {
constructor() {
this.qrCodeService = new QRCodeService();
}
/**
* 处理小程序扫码进入
*/
async handleMiniProgramScan(req, res) {
try {
const {
scene,
openid,
userInfo,
deviceInfo,
location
} = req.body;
logger.info('收到扫码请求', {
scene: scene.substr(0, 50),
openid,
location
});
// 验证请求参数
if (!scene || !openid) {
return res.status(400).json({
code: 400,
message: '缺少必要参数'
});
}
// 调用服务处理扫码
const result = await this.qrCodeService.handleScan({
scene,
openid,
userInfo,
deviceInfo,
location
});
// 记录到日志
logger.info('扫码处理完成', {
success: result.success,
qid: scene.includes('qid=') ? scene.split('qid=')[1].split('&')[0] : 'unknown',
openid
});
// 返回结果
if (result.success) {
res.json({
code: 200,
message: '扫码成功',
data: result.data
});
} else {
res.status(400).json({
code: 400,
message: result.message,
data: {
error_code: result.code
}
});
}
} catch (error) {
logger.error('扫码处理异常', {
error: error.message,
stack: error.stack
});
res.status(500).json({
code: 500,
message: '服务器内部错误',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
}
/**
* 获取扫码统计数据
*/
async getScanStatistics(req, res) {
try {
const { productId, startDate, endDate, groupBy = 'day' } = req.query;
const statistics = await this.qrCodeService.getScanStatistics({
productId,
startDate,
endDate,
groupBy
});
res.json({
code: 200,
data: statistics
});
} catch (error) {
res.status(500).json({
code: 500,
message: error.message
});
}
}
/**
* 导出扫码记录
*/
async exportScanRecords(req, res) {
try {
const { productId, batchNo, startDate, endDate } = req.query;
const records = await this.qrCodeService.exportScanRecords({
productId,
batchNo,
startDate,
endDate
});
// 生成Excel文件
const workbook = await this.generateExcel(records);
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=scan_records.xlsx');
// 发送文件
await workbook.xlsx.write(res);
res.end();
} catch (error) {
res.status(500).json({
code: 500,
message: error.message
});
}
}
}
module.exports = ScanController;
4.4 路由配置
// routes/scanRoutes.js
const express = require('express');
const router = express.Router();
const ScanController = require('../controllers/scanController');
const authMiddleware = require('../middleware/auth');
const rateLimitMiddleware = require('../middleware/rateLimit');
const scanController = new ScanController();
// 小程序扫码回调接口(需要身份验证)
router.post(
'/scan/callback',
authMiddleware.verifyToken, // 验证访问令牌
rateLimitMiddleware.scanLimit, // 扫码频率限制
scanController.handleMiniProgramScan.bind(scanController)
);
// 手动输入验证接口
router.post(
'/verify/manual',
rateLimitMiddleware.verifyLimit,
scanController.handleManualVerify.bind(scanController)
);
// 数据统计接口(需要管理员权限)
router.get(
'/statistics',
authMiddleware.verifyAdmin,
scanController.getScanStatistics.bind(scanController)
);
// 数据导出接口
router.get(
'/export',
authMiddleware.verifyAdmin,
scanController.exportScanRecords.bind(scanController)
);
// 防伪验证结果查询
router.get(
'/result/:verificationId',
scanController.getVerifyResult.bind(scanController)
);
module.exports = router;
五、前端小程序实现
5.1 小程序页面结构
pages/
├── product/
│ ├── verify/ # 扫码验证页面
│ │ ├── index.js
│ │ ├── index.wxml
│ │ ├── index.wxss
│ │ └── index.json
│ ├── detail/ # 商品详情页面
│ └── list/ # 商品列表页面
├── user/
│ ├── center/ # 用户中心
│ └── history/ # 扫码历史
├── marketing/
│ ├── lottery/ # 扫码抽奖
│ └── points/ # 积分商城
└── admin/
├── dashboard/ # 数据看板
└── qrcode/ # 二维码管理
5.2 扫码验证页面实现
// pages/product/verify/index.js
const app = getApp();
Page({
data: {
// 页面状态
status: 'scanning', // scanning, loading, success, error
// 商品信息
productInfo: null,
// 扫码结果
scanResult: null,
// 用户信息
userInfo: null,
// 防伪信息
antiFakeInfo: {
level: '',
confidence: 0,
tips: ''
},
// 营销活动
marketingActivity: null
},
onLoad(options) {
// 获取扫码进入的参数
if (options.scene) {
this.handleSceneParams(options.scene);
} else {
// 如果没有scene参数,启用摄像头扫码
this.startCameraScan();
}
// 获取用户信息
this.getUserInfo();
},
/**
* 处理scene参数
*/
handleSceneParams(sceneStr) {
this.setData({ status: 'loading' });
// 解析并处理scene参数
const sceneParams = this.parseScene(sceneStr);
// 调用后端验证接口
this.verifyQRCode(sceneParams);
},
/**
* 启动摄像头扫码
*/
startCameraScan() {
const ctx = wx.createCameraContext();
// 监听扫码事件
ctx.onScanCode((res) => {
if (res.result && !this.data.scanResult) {
this.handleScanResult(res.result);
}
});
this.setData({
showCamera: true,
status: 'scanning'
});
},
/**
* 处理扫码结果
*/
async handleScanResult(scanResult) {
this.setData({ status: 'loading' });
try {
// 调用后端验证接口
const result = await this.verifyQRCode(scanResult);
if (result.success) {
this.handleScanSuccess(result.data);
} else {
this.handleScanError(result);
}
} catch (error) {
this.handleScanError({
code: 'NETWORK_ERROR',
message: '网络连接失败,请重试'
});
}
},
/**
* 验证二维码
*/
async verifyQRCode(sceneParams) {
// 获取用户登录态
const { code } = await wx.login();
// 获取用户信息
const userInfo = this.data.userInfo || {};
// 获取设备信息
const systemInfo = wx.getSystemInfoSync();
// 调用后端接口
return await wx.request({
url: `${app.globalData.baseUrl}/api/scan/verify`,
method: 'POST',
data: {
scene: sceneParams,
code: code,
userInfo: userInfo,
deviceInfo: {
model: systemInfo.model,
platform: systemInfo.platform,
system: systemInfo.system,
version: systemInfo.version
}
},
header: {
'content-type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`
}
});
},
/**
* 处理扫码成功
*/
handleScanSuccess(result) {
const { product_info, is_first_scan, anti_fake_result } = result;
// 更新页面数据
this.setData({
status: 'success',
productInfo: product_info,
scanResult: result,
antiFakeInfo: {
level: anti_fake_result.level,
confidence: anti_fake_result.confidence,
tips: this.getAntiFakeTips(anti_fake_result.level)
}
});
// 记录扫码历史
this.recordScanHistory(result);
// 首次扫码特殊处理
if (is_first_scan) {
this.handleFirstScanReward(product_info);
}
// 检查营销活动
this.checkMarketingActivity(product_info);
// 播放成功音效
this.playSuccessSound();
// 显示成功动画
this.showSuccessAnimation();
},
/**
* 处理扫码错误
*/
handleScanError(error) {
this.setData({
status: 'error',
errorMessage: error.message,
errorCode: error.code
});
// 记录错误日志
wx.reportAnalytics('scan_error', {
error_code: error.code,
scene: this.data.sceneParams ? 'scene' : 'camera'
});
// 显示错误提示
wx.showToast({
title: error.message,
icon: 'none',
duration: 3000
});
},
/**
* 获取用户信息
*/
getUserInfo() {
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
this.setData({
userInfo: res.userInfo
});
// 保存用户信息
wx.setStorageSync('userInfo', res.userInfo);
},
fail: () => {
// 用户拒绝授权,使用默认信息
this.setData({
userInfo: {
nickName: '微信用户',
avatarUrl: '/images/default-avatar.png'
}
});
}
});
},
/**
* 处理首次扫码奖励
*/
handleFirstScanReward(productInfo) {
// 显示奖励弹窗
wx.showModal({
title: '首次扫码奖励',
content: `恭喜您获得${productInfo.brand}的会员积分!`,
showCancel: false,
confirmText: '查看奖励',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/user/rewards/index'
});
}
}
});
// 上报分析
wx.reportAnalytics('first_scan', {
product_id: productInfo.id,
brand: productInfo.brand
});
},
/**
* 显示成功动画
*/
showSuccessAnimation() {
this.animate('.success-icon', [
{ scale: [0, 0], opacity: 0 },
{ scale: [1.2, 1.2], opacity: 1 },
{ scale: [1, 1], opacity: 1 }
], 1000, () => {
// 动画完成
});
},
/**
* 重新扫码
*/
rescan() {
this.setData({
status: 'scanning',
scanResult: null,
productInfo: null
});
this.startCameraScan();
},
/**
* 查看商品详情
*/
viewProductDetail() {
if (this.data.productInfo) {
wx.navigateTo({
url: `/pages/product/detail/index?id=${this.data.productInfo.id}`
});
}
},
/**
* 分享扫码结果
*/
onShareAppMessage() {
const { productInfo, antiFakeInfo } = this.data;
return {
title: `我验证了${productInfo.name}是正品!`,
path: `/pages/product/verify/index?scene=${this.data.sceneParams}`,
imageUrl: productInfo.imageUrl
};
}
});
5.3 WXML页面结构
<!– pages/product/verify/index.wxml –>
<view class="container">
<!– 扫码界面 –>
<view wx:if="{{status === 'scanning'}}" class="scan-container">
<view class="scan-header">
<text class="title">扫码验证真伪</text>
<text class="subtitle">将二维码放入框内,即可快速验证</text>
</view>
<view class="camera-container">
<camera
device-position="back"
flash="off"
binderror="cameraError"
class="camera"
></camera>
<view class="scan-frame">
<view class="frame-line top-left"></view>
<view class="frame-line top-right"></view>
<view class="frame-line bottom-left"></view>
<view class="frame-line bottom-right"></view>
<view class="scan-line"></view>
</view>
</view>
<view class="scan-tips">
<text>对准商品上的二维码,距离10-20厘米</text>
<text>保持光线充足,避免反光</text>
</view>
<button class="manual-input-btn" bindtap="goToManualInput">
手动输入验证码
</button>
</view>
<!– 加载中 –>
<view wx:if="{{status === 'loading'}}" class="loading-container">
<view class="loading-icon">
<view class="spinner"></view>
</view>
<text class="loading-text">正在验证中…</text>
</view>
<!– 验证成功 –>
<view wx:if="{{status === 'success'}}" class="success-container">
<view class="success-header">
<view class="success-icon">
<image src="/images/success-icon.png" mode="aspectFit"></image>
</view>
<text class="success-title">验证成功</text>
<text class="success-subtitle">此商品为正品</text>
</view>
<!– 商品信息 –>
<view class="product-card">
<image class="product-image" src="{{productInfo.imageUrl}}" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{productInfo.name}}</text>
<text class="product-brand">{{productInfo.brand}}</text>
<text class="product-batch">批次号:{{productInfo.batchNo}}</text>
<text class="product-date">生产日期:{{productInfo.productionDate}}</text>
</view>
</view>
<!– 防伪信息 –>
<view class="anti-fake-card">
<view class="card-header">
<text class="card-title">防伪验证结果</text>
<view class="confidence-level">
<text class="level-badge {{antiFakeInfo.level}}">
{{antiFakeInfo.level === 'GENUINE' ? '正品' : '可疑'}}
</text>
<text class="confidence-text">
可信度:{{(antiFakeInfo.confidence * 100).toFixed(1)}}%
</text>
</view>
</view>
<text class="anti-fake-tips">{{antiFakeInfo.tips}}</text>
<view class="verification-details">
<view class="detail-item">
<text>验证时间:{{scanResult.scanTime}}</text>
</view>
<view class="detail-item">
<text>验证编号:{{scanResult.verificationId}}</text>
</view>
<view class="detail-item">
<text>扫码次数:{{scanResult.scanCount}}</text>
</view>
</view>
</view>
<!– 营销活动 –>
<view wx:if="{{marketingActivity}}" class="marketing-card">
<view class="activity-header">
<text class="activity-title">{{marketingActivity.title}}</text>
</view>
<view class="activity-content">
<text>{{marketingActivity.description}}</text>
</view>
<button class="activity-btn" bindtap="joinActivity">
{{marketingActivity.buttonText}}
</button>
</view>
<!– 操作按钮 –>
<view class="action-buttons">
<button class="btn rescan-btn" bindtap="rescan">再次扫码</button>
<button class="btn detail-btn" bindtap="viewProductDetail">查看详情</button>
<button class="btn share-btn" open-type="share">分享结果</button>
</view>
</view>
<!– 验证失败 –>
<view wx:if="{{status === 'error'}}" class="error-container">
<view class="error-icon">
<image src="/images/error-icon.png" mode="aspectFit"></image>
</view>
<text class="error-title">验证失败</text>
<text class="error-message">{{errorMessage}}</text>
<view class="error-suggestions">
<text wx:if="{{errorCode === 'FAKE_CODE'}}">建议:</text>
<text wx:if="{{errorCode === 'FAKE_CODE'}}">1. 请通过正规渠道购买</text>
<text wx:if="{{errorCode === 'FAKE_CODE'}}">2. 联系客服举报假货</text>
<text wx:if="{{errorCode === 'NETWORK_ERROR'}}">请检查网络连接后重试</text>
</view>
<button class="btn retry-btn" bindtap="rescan">重新扫码</button>
<button class="btn report-btn" bindtap="reportFake">举报假货</button>
</view>
</view>
5.4 WXSS样式文件
/* pages/product/verify/index.wxss */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* 扫码界面样式 */
.scan-container {
padding: 40rpx;
color: white;
}
.scan-header {
text-align: center;
margin-bottom: 60rpx;
}
.scan-header .title {
font-size: 48rpx;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.scan-header .subtitle {
font-size: 28rpx;
opacity: 0.9;
}
.camera-container {
position: relative;
width: 600rpx;
height: 600rpx;
margin: 0 auto 40rpx;
border-radius: 20rpx;
overflow: hidden;
}
.camera {
width: 100%;
height: 100%;
}
.scan-frame {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400rpx;
height: 400rpx;
border: 4rpx solid rgba(255, 255, 255, 0.8);
}
.frame-line {
position: absolute;
width: 60rpx;
height: 60rpx;
border-color: #09BB07;
border-style: solid;
}
.frame-line.top-left {
top: 0;
left: 0;
border-width: 8rpx 0 0 8rpx;
border-top-left-radius: 20rpx;
}
.frame-line.top-right {
top: 0;
right: 0;
border-width: 8rpx 8rpx 0 0;
border-top-right-radius: 20rpx;
}
.frame-line.bottom-left {
bottom: 0;
left: 0;
border-width: 0 0 8rpx 8rpx;
border-bottom-left-radius: 20rpx;
}
.frame-line.bottom-right {
bottom: 0;
right: 0;
border-width: 0 8rpx 8rpx 0;
border-bottom-right-radius: 20rpx;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4rpx;
background: linear-gradient(90deg,
transparent 0%,
#09BB07 50%,
transparent 100%);
animation: scan 2s linear infinite;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
/* 验证成功样式 */
.success-container {
padding: 40rpx;
background: white;
min-height: 100vh;
border-radius: 40rpx 40rpx 0 0;
margin-top: 100rpx;
}
.success-header {
text-align: center;
margin-bottom: 60rpx;
}
.success-icon {
width: 120rpx;
height: 120rpx;
margin: 0 auto 30rpx;
}
.success-icon image {
width: 100%;
height: 100%;
}
.success-title {
font-size: 48rpx;
font-weight: bold;
color: #09BB07;
display: block;
margin-bottom: 20rpx;
}
.success-subtitle {
font-size: 32rpx;
color: #666;
}
.product-card {
display: flex;
background: #f8f9fa;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 40rpx;
}
.product-image {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
margin-right: 30rpx;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 15rpx;
}
.product-brand {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.product-batch,
.product-date {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 5rpx;
}
/* 防伪卡片样式 */
.anti-fake-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 40rpx;
color: white;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 36rpx;
font-weight: bold;
}
.confidence-level {
display: flex;
align-items: center;
}
.level-badge {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
margin-right: 20rpx;
}
.level-badge.GENUINE {
background: #09BB07;
}
.level-badge.SUSPICIOUS {
background: #FF6B6B;
}
.confidence-text {
font-size: 24rpx;
opacity: 0.9;
}
.anti-fake-tips {
font-size: 28rpx;
line-height: 1.5;
margin-bottom: 30rpx;
display: block;
}
.verification-details {
background: rgba(255, 255, 255, 0.1);
border-radius: 10rpx;
padding: 20rpx;
}
.detail-item {
font-size: 24rpx;
margin-bottom: 10rpx;
}
.detail-item:last-child {
margin-bottom: 0;
}
/* 按钮样式 */
.action-buttons {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
.rescan-btn {
background: #f0f0f0;
color: #333;
}
.detail-btn {
background: #09BB07;
color: white;
}
.share-btn {
background: #007AFF;
color: white;
}
/* 错误界面样式 */
.error-container {
padding: 100rpx 40rpx;
text-align: center;
color: white;
}
.error-icon {
width: 150rpx;
height: 150rpx;
margin: 0 auto 40rpx;
}
.error-icon image {
width: 100%;
height: 100%;
}
.error-title {
font-size: 48rpx;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.error-message {
font-size: 32rpx;
opacity: 0.9;
display: block;
margin-bottom: 40rpx;
}
.error-suggestions {
background: rgba(255, 255, 255, 0.1);
border-radius: 10rpx;
padding: 30rpx;
margin-bottom: 40rpx;
text-align: left;
}
.error-suggestions text {
display: block;
font-size: 28rpx;
margin-bottom: 10rpx;
}
.retry-btn {
background: white;
color: #667eea;
margin-bottom: 20rpx;
}
.report-btn {
background: transparent;
color: white;
border: 2rpx solid white;
}
六、安全防护策略
6.1 防伪验证算法
// utils/antiFake.js
const crypto = require('crypto');
class AntiFakeService {
constructor() {
this.secretKey = process.env.ANTI_FAKE_SECRET;
}
/**
* 生成防伪签名
*/
generateSignature(data) {
const sortedData = this.sortObject(data);
const stringToSign = this.stringifyParams(sortedData);
// 使用HMAC-SHA256生成签名
const hmac = crypto.createHmac('sha256', this.secretKey);
hmac.update(stringToSign);
return hmac.digest('hex');
}
/**
* 验证签名
*/
verifySignature(data, signature) {
const expectedSignature = this.generateSignature(data);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
/**
* 生成二维码加密数据
*/
encryptQRData(qrData) {
const { qrId, productId, serialNo } = qrData;
// 生成随机盐值
const salt = crypto.randomBytes(16).toString('hex');
// 构建签名数据
const signData = {
qid: qrId,
pid: productId,
sn: serialNo,
salt: salt,
t: Date.now()
};
// 生成签名
const signature = this.generateSignature(signData);
// 返回加密数据
return {
…signData,
sig: signature.substring(0, 12) // 取前12位作为验证码
};
}
/**
* 多重验证策略
*/
async multiLayerVerification(qrCode, scanData) {
const verifications = [
this.verifySignature(qrCode),
this.verifyScanFrequency(qrCode),
this.verifyGeolocation(qrCode, scanData),
this.verifyDeviceFingerprint(qrCode, scanData),
this.verifyTimeWindow(qrCode)
];
const results = await Promise.all(verifications);
const passed = results.filter(r => r.passed).length;
const total = results.length;
return {
passed: passed >= Math.ceil(total * 0.8), // 80%通过率
details: results,
confidence: passed / total
};
}
/**
* 基于机器学习的异常检测
*/
async detectAnomaly(scanPattern) {
// 收集特征
const features = {
scan_time_diff: scanPattern.timeDiff,
scan_location_change: scanPattern.locationDiff,
device_changes: scanPattern.deviceChanges,
scan_interval_variance: scanPattern.intervalVariance
};
// 调用机器学习模型(可集成TensorFlow.js)
const anomalyScore = await this.mlModel.predict(features);
return anomalyScore > 0.7; // 阈值
}
}
6.2 风控策略
// services/riskControlService.js
class RiskControlService {
constructor() {
this.riskRules = [
{
name: '高频扫描',
check: async (qrCode) => {
const recentScans = await this.getRecentScans(qrCode.id, 10);
return recentScans.length >= 10;
},
action: 'block_1h',
score: 30
},
{
name: '地理位置跳跃',
check: async (qrCode, currentLocation) => {
const lastScan = await this.getLastScan(qrCode.id);
if (!lastScan || !lastScan.location) return false;
const distance = this.calculateDistance(
lastScan.location,
currentLocation
);
return distance > 100000; // 100公里
},
action: 'verify_sms',
score: 50
},
{
name: '设备指纹异常',
check: async (qrCode, deviceFingerprint) => {
const historicalDevices = await this.getDeviceHistory(qrCode.id);
if (historicalDevices.length < 3) return false;
return !historicalDevices.includes(deviceFingerprint);
},
action: 'captcha',
score: 40
}
];
}
/**
* 风险评估
*/
async assessRisk(qrCode, scanData) {
let totalScore = 0;
const triggeredRules = [];
for (const rule of this.riskRules) {
const triggered = await rule.check(qrCode, scanData);
if (triggered) {
totalScore += rule.score;
triggeredRules.push({
rule: rule.name,
action: rule.action,
score: rule.score
});
}
}
return {
riskLevel: this.getRiskLevel(totalScore),
totalScore,
triggeredRules,
actions: this.getRequiredActions(triggeredRules)
};
}
getRiskLevel(score) {
if (score >= 80) return 'HIGH';
if (score >= 50) return 'MEDIUM';
return 'LOW';
}
getRequiredActions(triggeredRules) {
const actions = new Set();
triggeredRules.forEach(rule => {
actions.add(rule.action);
});
return Array.from(actions);
}
}
七、性能优化
7.1 缓存策略
// utils/cacheManager.js
const NodeCache = require('node-cache');
const Redis = require('ioredis');
class CacheManager {
constructor() {
// 内存缓存(快速访问)
this.memoryCache = new NodeCache({
stdTTL: 300, // 5分钟
checkperiod: 60
});
// Redis缓存(分布式)
this.redisClient = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
}
/**
* 三级缓存策略
*/
async getWithCache(key, fetchFunc, options = {}) {
const { ttl = 300, forceRefresh = false } = options;
// 1. 检查内存缓存
if (!forceRefresh) {
const memoryData = this.memoryCache.get(key);
if (memoryData) {
this.updateAccessStats(key, 'memory');
return memoryData;
}
}
// 2. 检查Redis缓存
if (!forceRefresh) {
const redisData = await this.redisClient.get(key);
if (redisData) {
const parsedData = JSON.parse(redisData);
this.memoryCache.set(key, parsedData, ttl);
this.updateAccessStats(key, 'redis');
return parsedData;
}
}
// 3. 从数据源获取
try {
const freshData = await fetchFunc();
// 异步更新缓存
this.updateCacheAsync(key, freshData, ttl);
return freshData;
} catch (error) {
// 降级:尝试获取过期数据
const staleData = await this.getStaleData(key);
if (staleData) {
return staleData;
}
throw error;
}
}
/**
* 异步更新缓存
*/
async updateCacheAsync(key, data, ttl) {
try {
// 更新内存缓存
this.memoryCache.set(key, data, ttl);
// 更新Redis缓存
await this.redisClient.setex(
key,
ttl,
JSON.stringify(data)
);
// 记录缓存命中率
this.recordCacheHit(key);
} catch (error) {
console.error('缓存更新失败:', error);
}
}
/**
* 批量缓存预热
*/
async warmUpCache(productIds) {
const batchSize = 50;
const batches = [];
for (let i = 0; i < productIds.length; i += batchSize) {
batches.push(productIds.slice(i, i + batchSize));
}
for (const batch of batches) {
await Promise.all(
batch.map(async (productId) => {
const cacheKey = `product:${productId}`;
const data = await this.fetchProductData(productId);
await this.updateCacheAsync(cacheKey, data, 3600);
})
);
}
}
}
7.2 数据库优化
— 创建优化索引
CREATE INDEX idx_qr_scans_composite ON scan_records(qr_id, scan_time DESC, result_code);
CREATE INDEX idx_user_activity ON scan_records(openid, scan_time DESC);
— 创建分区表(按时间分区)
CREATE TABLE scan_records_partitioned (
— 字段定义同上
) PARTITION BY RANGE (YEAR(scan_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026)
);
— 使用物化视图加速统计查询
CREATE MATERIALIZED VIEW daily_scan_stats AS
SELECT
DATE(scan_time) as scan_date,
product_id,
COUNT(*) as total_scans,
COUNT(DISTINCT openid) as unique_users,
SUM(CASE WHEN result_code = 0 THEN 1 ELSE 0 END) as success_scans
FROM scan_records sr
JOIN qr_codes qc ON sr.qr_id = qc.qr_id
WHERE scan_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(scan_time), product_id;
— 定期刷新物化视图
CREATE EVENT refresh_daily_stats
ON SCHEDULE EVERY 1 HOUR
DO
REFRESH MATERIALIZED VIEW daily_scan_stats;
八、部署与监控
8.1 Docker部署配置
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# 安装依赖
COPY package*.json ./
RUN npm ci –only=production
# 复制源码
COPY . .
# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000
# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# 更改文件权限
RUN chown -R nodejs:nodejs /app
USER nodejs
# 暴露端口
EXPOSE 3000
# 启动命令
CMD ["node", "app.js"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
– "3000:3000"
environment:
– NODE_ENV=production
– DATABASE_URL=mysql://user:pass@mysql:3306/qrcode_db
– REDIS_URL=redis://redis:6379
depends_on:
– mysql
– redis
restart: unless–stopped
networks:
– app–network
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: qrcode_db
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
– mysql–data:/var/lib/mysql
ports:
– "3306:3306"
networks:
– app–network
redis:
image: redis:7–alpine
command: redis–server ––requirepass ${REDIS_PASSWORD}
volumes:
– redis–data:/data
ports:
– "6379:6379"
networks:
– app–network
nginx:
image: nginx:alpine
ports:
– "80:80"
– "443:443"
volumes:
– ./nginx.conf:/etc/nginx/nginx.conf
– ./ssl:/etc/nginx/ssl
depends_on:
– app
networks:
– app–network
volumes:
mysql-data:
redis-data:
networks:
app-network:
driver: bridge
8.2 监控告警配置
// utils/monitor.js
const prometheus = require('prom-client');
class Monitor {
constructor() {
// 初始化指标
this.register = new prometheus.Registry();
// 定义指标
this.scanCounter = new prometheus.Counter({
name: 'scans_total',
help: 'Total number of QR code scans',
labelNames: ['product_id', 'result']
});
this.responseTimeHistogram = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
this.qrGenerationGauge = new prometheus.Gauge({
name: 'qr_codes_generated',
help: 'Number of QR codes generated'
});
// 注册指标
this.register.registerMetric(this.scanCounter);
this.register.registerMetric(this.responseTimeHistogram);
this.register.registerMetric(this.qrGenerationGauge);
}
/**
* 记录扫码事件
*/
recordScan(productId, result) {
this.scanCounter.labels(productId, result).inc();
// 实时告警逻辑
if (result === 'FAKE_CODE') {
this.sendAlert({
type: 'FAKE_CODE_DETECTED',
productId,
severity: 'HIGH',
timestamp: new Date().toISOString()
});
}
}
/**
* 性能监控中间件
*/
monitorMiddleware(req, res, next) {
const start = Date.now();
// 监控响应时间
res.on('finish', () => {
const duration = Date.now() – start;
this.responseTimeHistogram
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration / 1000);
// 慢查询告警
if (duration > 5000) {
this.sendAlert({
type: 'SLOW_RESPONSE',
route: req.path,
duration,
threshold: 5000
});
}
});
next();
}
/**
* 发送告警
*/
async sendAlert(alertData) {
// 发送到钉钉/企业微信/邮件
const webhookUrl = process.env.ALERT_WEBHOOK;
await axios.post(webhookUrl, {
msgtype: 'markdown',
markdown: {
title: '系统告警',
text: `**${alertData.type}**\\n\\n` +
`时间:${alertData.timestamp}\\n` +
`详情:${JSON.stringify(alertData, null, 2)}`
}
});
}
/**
* 健康检查
*/
async healthCheck() {
const checks = {
database: await this.checkDatabase(),
redis: await this.checkRedis(),
wechat_api: await this.checkWeChatAPI(),
storage: await this.checkStorage()
};
const allHealthy = Object.values(checks).every(c => c.healthy);
return {
status: allHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
checks
};
}
}
九、测试方案
9.1 单元测试
// tests/qrCodeService.test.js
const QRCodeService = require('../services/qrCodeService');
const QRCode = require('../models/QRCode');
describe('QRCodeService', () => {
let qrCodeService;
beforeEach(() => {
qrCodeService = new QRCodeService();
jest.clearAllMocks();
});
test('should generate unique serial numbers', () => {
const serial1 = qrCodeService.generateSerialNo('TEST', 1);
const serial2 = qrCodeService.generateSerialNo('TEST', 2);
expect(serial1).not.toBe(serial2);
expect(serial1.startsWith('TEST')).toBe(true);
});
test('should build valid scene string', () => {
const params = {
qid: 'test123',
pid: 'product456',
sn: 'serial789'
};
const scene = qrCodeService.buildSceneString(params);
expect(scene).toContain('qid=test123');
expect(scene).toContain('pid=product456');
expect(scene).toContain('sn=serial789');
expect(scene).toContain('sig=');
expect(scene).toContain('t=');
});
test('should handle valid scan', async () => {
// Mock数据库查询
jest.spyOn(QRCode, 'findOne').mockResolvedValue({
qr_id: 'test_qr',
product_id: 1,
serial_no: 'serial123',
secret_key: 'test_key',
status: 0,
Product: { name: 'Test Product' },
increment: jest.fn(),
update: jest.fn()
});
const scanData = {
scene: 'qid=test_qr&pid=1&sn=serial123&sig=abc123&t=1234567890',
openid: 'test_openid',
userInfo: { nickName: 'Test User' }
};
const result = await qrCodeService.handleScan(scanData);
expect(result.success).toBe(true);
expect(result.data.is_first_scan).toBe(true);
});
test('should detect fake code', async () => {
// Mock假码场景
jest.spyOn(QRCode, 'findOne').mockResolvedValue(null);
const scanData = {
scene: 'qid=fake_qr',
openid: 'test_openid'
};
const result = await qrCodeService.handleScan(scanData);
expect(result.success).toBe(false);
expect(result.code).toBe('QR_NOT_FOUND');
});
});
9.2 压力测试
// tests/loadTest.js
const autocannon = require('autocannon');
async function runLoadTest() {
const instance = autocannon({
url: 'http://localhost:3000',
connections: 100, // 并发连接数
duration: 30, // 测试持续时间
pipelining: 1,
requests: [
{
method: 'POST',
path: '/api/scan/verify',
headers: {
'content-type': 'application/json',
'Authorization': 'Bearer test_token'
},
body: JSON.stringify({
scene: 'qid=test_qr_${id}&pid=1&sn=test_sn&sig=test_sig&t=1234567890',
openid: 'test_openid_${id}',
userInfo: { nickName: 'Test User' }
})
}
]
}, (err, result) => {
if (err) {
console.error('Load test failed:', err);
} else {
console.log('Load test results:', {
requests: result.requests.total,
latency: result.latency,
throughput: result.throughput.total,
errors: result.errors
});
}
});
// 实时显示进度
autocannon.track(instance, { renderProgressBar: true });
}
runLoadTest();
十、总结与展望
通过本文的完整实现方案,我们可以构建一个稳定、安全、高性能的微信小程序一物一码系统。系统的主要特点包括:
10.1 核心优势
10.2 业务价值
10.3 未来扩展方向
本方案已在多个实际项目中验证,能够满足不同规模企业的需求。随着技术的不断发展,一物一码系统将在产品数字化、营销智能化方面发挥越来越重要的作用。





