1. 项目概述
1.1 项目背景
在当今数字化营销时代,企业需要通过创新的互动方式来吸引客户、增强品牌影响力并收集有价值的用户数据。二维码抽奖系统作为一种高效的市场营销工具,结合了移动互联网的便捷性和传统抽奖活动的趣味性,能够有效提升用户参与度,同时为企业提供精准的用户信息收集渠道。
本项目旨在开发一个完整的二维码抽奖系统,用户通过扫描二维码进入抽奖小程序,参与抽奖活动,在抽奖过程中完成微信登录,并在中奖后提交联系电话和地址等必要信息以便奖品发放。系统将涵盖前端用户界面、后端业务逻辑、数据库设计以及管理后台等多个模块。
1.2 系统功能需求
- 二维码生成与管理:系统能够为每次抽奖活动生成唯一二维码,并可对二维码进行有效期限、使用次数等参数设置。
- 用户抽奖流程:
- 扫描二维码进入小程序
- 参与抽奖(包括多种抽奖模式)
- 微信授权登录
- 中奖后填写联系信息
- 奖品管理:支持多种奖品类型设置,可配置中奖概率、库存等参数。
- 用户信息管理:安全存储用户提交的联系方式、地址等敏感信息。
- 数据统计与分析:提供参与人数、中奖率、用户地域分布等数据分析功能。
- 防作弊机制:防止同一用户多次抽奖、机器刷奖等作弊行为。
1.3 技术选型
基于项目需求和当前主流技术栈,我们选择以下技术方案:
前端技术栈:
- 微信小程序原生开发:使用WXML、WXSS、JavaScript
- 组件库:Vant Weapp或WeUI
- 图表库:wx-charts用于数据可视化
后端技术栈:
- 语言:Node.js(Express/Koa框架)或Java(Spring Boot)
- 数据库:MySQL(关系型数据)+ Redis(缓存和高并发处理)
- 微信接口:微信小程序服务端API、微信支付API(如需)
基础设施:
- 云服务:阿里云/腾讯云服务器
- CDN:用于静态资源加速
- 对象存储:OSS/COS用于图片等文件存储
开发工具:
- 代码管理:Git + GitHub/GitLab
- 接口管理:Swagger/YApi
- 持续集成:Jenkins/Docker
2. 系统架构设计
2.1 整体架构
系统采用典型的三层架构设计,分为表示层、业务逻辑层和数据访问层:
┌─────────────────────────────────────────────────┐
│ 表示层(Presentation) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 微信小程序 │ │ 管理后台 │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 业务逻辑层(Business Logic) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 抽奖服务 │ │ 用户服务 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 奖品服务 │ │ 微信接口服务│ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 数据访问层(Data Access) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ MySQL │ │ Redis │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────┘
2.2 微服务划分
为应对高并发场景和未来扩展需求,系统采用微服务架构设计:
- 用户服务:处理用户注册、登录、信息管理
- 抽奖服务:核心抽奖逻辑、概率计算、结果生成
- 奖品服务:奖品库存管理、发放记录
- 活动服务:抽奖活动配置、二维码生成
- 微信接口服务:封装与微信平台的交互逻辑
各服务通过RESTful API或gRPC进行通信,使用Eureka/Nacos作为服务注册与发现中心。
2.3 数据库设计
2.3.1 主要数据表结构
用户表(user)
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`openid` varchar(64) NOT NULL COMMENT '微信openid',
`unionid` varchar(64) DEFAULT NULL COMMENT '微信unionid',
`nickname` varchar(64) DEFAULT NULL COMMENT '微信昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
抽奖活动表(lottery_activity)
CREATE TABLE `lottery_activity` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '活动名称',
`description` text COMMENT '活动描述',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`rule_config` json DEFAULT NULL COMMENT '抽奖规则配置',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-未开始,1-进行中,2-已结束',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表';
奖品表(prize)
CREATE TABLE `prize` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`activity_id` bigint(20) NOT NULL COMMENT '关联活动ID',
`name` varchar(100) NOT NULL COMMENT '奖品名称',
`type` tinyint(4) NOT NULL COMMENT '奖品类型:1-实物,2-虚拟',
`image_url` varchar(255) DEFAULT NULL COMMENT '奖品图片',
`total_stock` int(11) NOT NULL COMMENT '总库存',
`remaining_stock` int(11) NOT NULL COMMENT '剩余库存',
`probability` decimal(5,4) NOT NULL COMMENT '中奖概率',
`position` int(11) DEFAULT NULL COMMENT '奖品位置(转盘类抽奖使用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_activity_id` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖品表';
用户抽奖记录表(user_lottery_record)
CREATE TABLE `user_lottery_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`activity_id` bigint(20) NOT NULL COMMENT '活动ID',
`prize_id` bigint(20) DEFAULT NULL COMMENT '奖品ID(未中奖为null)',
`is_win` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否中奖',
`lottery_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '抽奖时间',
`contact_info` json DEFAULT NULL COMMENT '联系信息(中奖后填写)',
`status` tinyint(4) DEFAULT '0' COMMENT '状态:0-未领取,1-已领取,2-已发货',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_activity` (`user_id`,`activity_id`),
KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表';
二维码表(qr_code)
CREATE TABLE `qr_code` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`activity_id` bigint(20) NOT NULL COMMENT '关联活动ID',
`code_key` varchar(64) NOT NULL COMMENT '二维码唯一标识',
`image_url` varchar(255) NOT NULL COMMENT '二维码图片URL',
`scan_limit` int(11) DEFAULT '-1' COMMENT '扫描次数限制(-1表示无限制)',
`scan_count` int(11) NOT NULL DEFAULT '0' COMMENT '已扫描次数',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code_key` (`code_key`),
KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动二维码表';
2.3.2 数据关系模型
- 一个抽奖活动(activity)可以包含多个奖品(prize)
- 一个用户可以有多条抽奖记录(record),每条记录关联一个活动和一个可选奖品
- 一个活动可以生成多个二维码(qr_code)
- 用户通过扫描二维码参与活动
2.4 API接口设计
系统主要API接口包括:
微信小程序端API
-
活动相关
- GET /api/activity/info - 获取活动基本信息
- GET /api/activity/prizes - 获取活动奖品列表
-
抽奖相关
- POST /api/lottery/draw - 执行抽奖
- GET /api/lottery/records - 获取用户抽奖记录
-
用户相关
- POST /api/user/login - 微信登录
- POST /api/user/contact - 提交联系信息
管理后台API
-
活动管理
- POST /api/admin/activity/create - 创建活动
- PUT /api/admin/activity/update - 更新活动
- GET /api/admin/activity/list - 活动列表
-
奖品管理
- POST /api/admin/prize/add - 添加奖品
- PUT /api/admin/prize/update - 更新奖品
-
二维码管理
- POST /api/admin/qrcode/generate - 生成二维码
- GET /api/admin/qrcode/list - 二维码列表
-
数据统计
- GET /api/admin/stat/participation - 参与数据统计
- GET /api/admin/stat/prize - 奖品发放统计
3. 核心功能实现
3.1 二维码生成与管理
3.1.1 二维码生成逻辑
二维码生成服务主要流程:
- 接收生成请求(包含活动ID、有效期等参数)
- 生成唯一code_key(UUID或雪花算法)
- 调用微信小程序码接口或自行生成二维码
- 存储二维码信息到数据库
- 返回二维码图片URL
public class QrCodeService {
@Autowired
private QrCodeMapper qrCodeMapper;
@Autowired
private WxMaService wxMaService;
public QrCode generateQrCode(Long activityId, QrCodeGenerateRequest request) {
// 生成唯一标识
String codeKey = UUID.randomUUID().toString().replace("-", "");
// 调用微信API生成小程序码
String page = "pages/lottery/index";
Map<String, Object> params = new HashMap<>();
params.put("code_key", codeKey);
WxMaQrcodeService wxMaQrcodeService = wxMaService.getQrcodeService();
File qrCodeFile = wxMaQrcodeService.createWxaCodeUnlimit(codeKey, page, params);
// 上传到OSS
String imageUrl = ossClient.upload(qrCodeFile);
// 保存到数据库
QrCode qrCode = new QrCode();
qrCode.setActivityId(activityId);
qrCode.setCodeKey(codeKey);
qrCode.setImageUrl(imageUrl);
qrCode.setScanLimit(request.getScanLimit());
qrCode.setExpireTime(request.getExpireTime());
qrCodeMapper.insert(qrCode);
return qrCode;
}
}
3.1.2 二维码扫描处理
当用户扫描二维码进入小程序时:
- 小程序获取场景值(scene参数中的code_key)
- 向服务端验证二维码有效性
- 返回关联的活动信息
// 小程序端代码
onLoad: function(options) {
// 获取场景值
const scene = decodeURIComponent(options.scene);
const codeKey = scene.split('=')[1];
// 调用接口验证二维码
wx.request({
url: 'https://api.example.com/activity/scan',
method: 'POST',
data: { code_key: codeKey },
success: (res) => {
if (res.data.code === 0) {
this.setData({
activityInfo: res.data.data
});
} else {
wx.showToast({
title: '二维码已过期或无效',
icon: 'none'
});
}
}
});
}
3.2 微信登录集成
3.2.1 登录流程设计
微信小程序登录流程:
- 前端调用wx.login获取临时code
- 将code发送到服务端
- 服务端使用code向微信接口服务获取session_key和openid
- 服务端生成自定义登录态(token)返回给前端
- 前端存储token用于后续接口验证
// 小程序登录代码
wx.login({
success: (res) => {
if (res.code) {
wx.request({
url: 'https://api.example.com/user/login',
method: 'POST',
data: { code: res.code },
success: (loginRes) => {
const token = loginRes.data.token;
wx.setStorageSync('token', token);
}
});
}
}
});
3.2.2 服务端登录实现
public class AuthController {
@Autowired
private WxMaService wxMaService;
@Autowired
private UserService userService;
@PostMapping("/user/login")
public Result login(@RequestBody LoginRequest request) {
try {
// 使用code换取session信息
WxMaJscode2SessionResult session =
wxMaService.getUserService().getSessionInfo(request.getCode());
// 查询或创建用户
User user = userService.findOrCreateUser(session.getOpenid(), session.getUnionid());
// 生成token
String token = JwtUtil.generateToken(user.getId(), user.getOpenid());
// 返回token和用户基本信息
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("userInfo", user);
return Result.success(data);
} catch (WxErrorException e) {
return Result.error("微信登录失败");
}
}
}
3.3 抽奖核心算法实现
3.3.1 概率算法设计
抽奖概率算法需要考虑:
- 每个奖品的中奖概率配置
- 奖品库存限制
- 防刷机制
- 公平性保证
采用基于概率区间的算法:
- 计算所有奖品的中奖概率总和
- 为每个奖品分配一个概率区间
- 生成随机数,判断落在哪个区间
public class LotteryService {
@Autowired
private PrizeService prizeService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public LotteryResult drawLottery(Long userId, Long activityId) {
// 1. 验证用户抽奖资格
if (!checkUserQualification(userId, activityId)) {
return LotteryResult.fail("您已参与过本次抽奖");
}
// 2. 获取活动奖品列表
List<Prize> prizes = prizeService.getActivityPrizes(activityId);
if (prizes.isEmpty()) {
return LotteryResult.fail("活动暂无奖品");
}
// 3. 计算总概率并校验
BigDecimal totalProbability = prizes.stream()
.map(Prize::getProbability)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalProbability.compareTo(BigDecimal.ONE) > 0) {
throw new RuntimeException("奖品总概率不能超过1");
}
// 4. 生成随机数
BigDecimal random = BigDecimal.valueOf(Math.random());
// 5. 概率区间判断
BigDecimal rangeStart = BigDecimal.ZERO;
for (Prize prize : prizes) {
BigDecimal rangeEnd = rangeStart.add(prize.getProbability());
if (random.compareTo(rangeStart) >= 0 && random.compareTo(rangeEnd) < 0) {
// 检查奖品库存
if (prize.getRemainingStock() <= 0) {
return LotteryResult.fail("奖品已发完");
}
// 扣减库存
boolean stockUpdated = prizeService.decrementPrizeStock(prize.getId());
if (!stockUpdated) {
return LotteryResult.fail("奖品库存不足");
}
// 保存中奖记录
saveLotteryRecord(userId, activityId, prize.getId(), true);
return LotteryResult.success(prize);
}
rangeStart = rangeEnd;
}
// 未中奖
saveLotteryRecord(userId, activityId, null, false);
return LotteryResult.fail("很遗憾,未中奖");
}
private boolean checkUserQualification(Long userId, Long activityId) {
// 使用Redis原子操作防止重复抽奖
String key = "lottery:qualification:" + activityId + ":" + userId;
return redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofHours(24));
}
}
3.3.2 转盘抽奖实现
对于转盘类抽奖,需要特殊处理:
- 前端定义转盘奖品位置
- 后端返回中奖奖品索引
- 前端根据索引执行转盘动画
// 前端转盘抽奖代码
startLottery: function() {
wx.request({
url: 'https://api.example.com/lottery/draw',
method: 'POST',
data: { activity_id: this.data.activityId },
success: (res) => {
if (res.data.code === 0) {
const result = res.data.data;
if (result.is_win) {
// 计算转盘停止位置
const prizeIndex = this.getPrizeIndex(result.prize_id);
this.rotateWheel(prizeIndex);
} else {
this.rotateWheel(-1); // 未中奖位置
}
}
}
});
},
// 转盘动画
rotateWheel: function(prizeIndex) {
const wheel = this.selectComponent('#wheel');
const rounds = 5; // 旋转圈数
const duration = 3000; // 旋转时间
if (prizeIndex >= 0) {
// 中奖,停在指定位置
const angle = 360 / 8 * prizeIndex; // 假设8个奖品
wheel.rotate(rounds * 360 + angle, duration);
} else {
// 未中奖,停在随机位置
const randomAngle = Math.floor(Math.random() * 360);
wheel.rotate(rounds * 360 + randomAngle, duration);
}
}
3.4 用户信息收集
3.4.1 联系信息表单设计
中奖后,用户需要填写以下信息:
- 姓名
- 手机号
- 收货地址(省市区+详细地址)
- 备注(可选)
表单验证规则:
- 姓名:2-20个字符
- 手机号:11位数字,符合手机号格式
- 地址:省市区必选,详细地址5-100字符
<!-- 小程序表单WXML -->
<view class="form-container" wx:if="{{isWin}}">
<form bindsubmit="submitContactInfo">
<view class="form-item">
<text class="label">姓名</text>
<input name="name" placeholder="请输入姓名" maxlength="20" />
</view>
<view class="form-item">
<text class="label">手机号</text>
<input name="phone" type="number" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="form-item">
<text class="label">省市区</text>
<picker mode="region" bindchange="regionChange">
<view class="picker">{{region.join(' ') || '请选择省市区'}}</view>
</picker>
</view>
<view class="form-item">
<text class="label">详细地址</text>
<input name="address" placeholder="请输入详细地址" maxlength="100" />
</view>
<button formType="submit" type="primary">提交</button>
</form>
</view>
3.4.2 信息提交与存储
// 小程序表单提交
submitContactInfo: function(e) {
const formData = e.detail.value;
const { region } = this.data;
// 表单验证
if (!formData.name || formData.name.length < 2) {
wx.showToast({ title: '请输入有效姓名', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
wx.showToast({ title: '请输入有效手机号', icon: 'none' });
return;
}
if (!region || region.length !== 3) {
wx.showToast({ title: '请选择省市区', icon: 'none' });
return;
}
if (!formData.address || formData.address.length < 5) {
wx.showToast({ title: '请输入详细地址', icon: 'none' });
return;
}
// 组装数据
const contactInfo = {
name: formData.name,
phone: formData.phone,
province: region[0],
city: region[1],
district: region[2],
detailAddress: formData.address
};
// 调用接口提交
wx.request({
url: 'https://api.example.com/user/contact',
method: 'POST',
data: {
record_id: this.data.recordId,
contact_info: contactInfo
},
success: (res) => {
if (res.data.code === 0) {
wx.showToast({ title: '提交成功' });
this.setData({ contactSubmitted: true });
}
}
});
}
服务端存储实现:
@PostMapping("/user/contact")
public Result submitContactInfo(@RequestBody ContactSubmitRequest request) {
// 验证记录是否存在且属于当前用户
LotteryRecord record = recordService.getById(request.getRecordId());
if (record == null || !record.getUserId().equals(getCurrentUserId())) {
return Result.error("无效的记录ID");
}
// 验证是否已中奖
if (!record.getIsWin()) {
return Result.error("未中奖记录无需提交信息");
}
// 验证是否已提交过
if (record.getContactInfo() != null) {
return Result.error("已提交过联系信息");
}
// 更新记录
record.setContactInfo(JSON.toJSONString(request.getContactInfo()));
record.setStatus(1); // 标记为已领取
recordService.updateById(record);
return Result.success();
}
4. 系统安全与性能优化
4.1 安全防护措施
4.1.1 防刷机制
- IP限制:同一IP在短时间内多次抽奖需验证码验证
- 设备指纹:采集设备信息生成唯一指纹识别重复设备
- 行为分析:检测异常抽奖行为(如间隔时间过短)
- Token验证:所有抽奖请求需携带有效登录token
public class AntiBrushFilter implements Filter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ip = getClientIp(httpRequest);
String path = httpRequest.getRequestURI();
// 抽奖接口防刷
if (path.contains("/lottery/draw")) {
String key = "anti:brush:" + ip + ":" + path;
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count != null && count == 1) {
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
}
if (count != null && count > 5) {
throw new RuntimeException("操作过于频繁,请稍后再试");
}
}
chain.doFilter(request, response);
}
}
4.1.2 数据加密
- 敏感信息加密:用户手机号等敏感信息数据库加密存储
- HTTPS传输:所有接口强制HTTPS协议
- 接口签名:重要接口增加签名验证防止篡改
public class DataEncryptor {
private static final String AES_KEY = "your-aes-256-bit-key";
// AES加密
public static String encrypt(String data) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
// AES解密
public static String decrypt(String encrypted) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(encrypted);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}
4.2 性能优化策略
4.2.1 缓存设计
- 活动信息缓存:活动基本信息、奖品列表等高读取数据缓存
- 奖品库存缓存:使用Redis原子操作保证库存准确性
- 用户抽奖记录缓存:用户是否已参与活动缓存
public class PrizeService {
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean decrementPrizeStock(Long prizeId) {
String lockKey = "prize:stock:lock:" + prizeId;
String stockKey = "prize:stock:" + prizeId;
// 分布式锁防止超卖
boolean locked = false;
try {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 先查缓存
Integer remaining = (Integer) redisTemplate.opsForValue().get(stockKey);
if (remaining == null) {
// 缓存未命中,查数据库
Prize prize = prizeMapper.selectById(prizeId);
remaining = prize.getRemainingStock();
redisTemplate.opsForValue().set(stockKey, remaining, 1, TimeUnit.HOURS);
}
if (remaining <= 0) {
return false;
}
// 原子减库存
long newStock = redisTemplate.opsForValue().decrement(stockKey);
if (newStock >= 0) {
// 异步更新数据库
asyncUpdateDbStock(prizeId, newStock);
return true;
} else {
// 库存不足,回滚
redisTemplate.opsForValue().increment(stockKey);
return false;
}
} finally {
if (locked) {
redisTemplate.delete(lockKey);
}
}
}
}
4.2.2 数据库优化
- 索引优化:为常用查询字段添加合适索引
- 读写分离:查询走从库,写入走主库
- 分表策略:抽奖记录表按用户ID或时间分表
4.2.3 高并发处理
- 限流措施:接口级别QPS限制
- 异步处理:非核心流程异步化(如记录日志)
- 队列削峰:使用消息队列缓冲瞬时高流量
public class LotteryController {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/lottery/draw")
public Result drawLottery(@RequestBody LotteryRequest request) {
// 基础验证
if (!validateRequest(request)) {
return Result.error("参数错误");
}
// 发送抽奖消息到队列
rabbitTemplate.convertAndSend("lottery.queue", request);
// 立即返回,前端轮询结果
return Result.success("抽奖处理中");
}
@RabbitListener(queues = "lottery.queue")
public void processLottery(LotteryRequest request) {
// 实际抽奖逻辑处理
LotteryResult result = lotteryService.drawLottery(request.getUserId(), request.getActivityId());
// 保存结果到缓存供前端查询
redisTemplate.opsForValue().set(
"lottery:result:" + request.getUserId() + ":" + request.getActivityId(),
result,
5, TimeUnit.MINUTES
);
}
}
5. 管理后台实现
5.1 活动管理
5.1.1 活动创建与配置
管理后台提供可视化界面配置抽奖活动:
- 基础信息:名称、时间、描述
- 抽奖规则:每人参与次数、中奖后是否可继续参与
- 奖品设置:添加多个奖品,设置概率和库存
- 样式配置:抽奖页面UI风格选择
<!-- 活动创建Vue组件 -->
<template>
<div class="activity-create">
<el-form :model="form" label-width="120px">
<el-form-item label="活动名称" required>
<el-input v-model="form.name" placeholder="请输入活动名称"></el-input>
</el-form-item>
<el-form-item label="活动时间" required>
<el-date-picker
v-model="form.timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="活动规则">
<el-input
type="textarea"
:rows="3"
v-model="form.description"
placeholder="请输入活动描述">
</el-input>
</el-form-item>
<el-form-item label="参与限制">
<el-radio-group v-model="form.limitType">
<el-radio :label="1">每人1次</el-radio>
<el-radio :label="2">每日1次</el-radio>
<el-radio :label="3">不限制</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="奖品设置">
<div class="prize-list">
<div class="prize-item" v-for="(prize, index) in form.prizes" :key="index">
<el-input v-model="prize.name" placeholder="奖品名称"></el-input>
<el-input-number v-model="prize.probability" :min="0" :max="1" :step="0.01" label="中奖概率"></el-input-number>
<el-input-number v-model="prize.stock" :min="1" label="库存"></el-input-number>
<el-button @click="removePrize(index)" type="danger">删除</el-button>
</div>
<el-button @click="addPrize" type="primary">添加奖品</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
name: '',
timeRange: [],
description: '',
limitType: 1,
prizes: [
{ name: '一等奖', probability: 0.01, stock: 1 },
{ name: '二等奖', probability: 0.05, stock: 5 },
{ name: '谢谢参与', probability: 0.94, stock: 10000 }
]
}
};
},
methods: {
addPrize() {
this.form.prizes.push({
name: '',
probability: 0,
stock: 1
});
},
removePrize(index) {
this.form.prizes.splice(index, 1);
},
submitForm() {
// 验证表单
if (!this.form.name) {
this.$message.error('请填写活动名称');
return;
}
if (!this.form.timeRange || this.form.timeRange.length !== 2) {
this.$message.error('请选择活动时间');
return;
}
// 计算总概率
const totalProb = this.form.prizes.reduce((sum, prize) => {
return sum + prize.probability;
}, 0);
if (Math.abs(totalProb - 1) > 0.0001) {
this.$message.error('奖品总概率必须等于1');
return;
}
// 提交数据
this.$axios.post('/admin/activity/create', {
name: this.form.name,
startTime: this.form.timeRange[0],
endTime: this.form.timeRange[1],
description: this.form.description,
limitType: this.form.limitType,
prizes: this.form.prizes
}).then(response => {
this.$message.success('创建成功');
this.$router.push('/activity/list');
});
}
}
};
</script>
5.2 数据统计与分析
5.2.1 参与数据统计
统计维度:
- 参与总人数
- 每日新增参与人数
- 用户地域分布
- 参与渠道分析(不同二维码)
@GetMapping("/admin/stat/participation")
public Result getParticipationStats(@RequestParam Long activityId,
@RequestParam(required = false) String groupBy) {
Map<String, Object> stats = new HashMap<>();
// 总参与人数
int totalParticipants = recordService.countParticipants(activityId);
stats.put("totalParticipants", totalParticipants);
// 按时间分组统计
if ("day".equals(groupBy)) {
List<Map<String, Object>> dailyStats = recordService.getDailyParticipation(activityId);
stats.put("dailyStats", dailyStats);
}
// 地域分布
if ("region".equals(groupBy)) {
List<Map<String, Object>> regionStats = recordService.getRegionDistribution(activityId);
stats.put("regionStats", regionStats);
}
return Result.success(stats);
}
5.2.2 奖品发放统计
统计内容:
- 各奖品发放数量
- 中奖率分析
- 奖品领取状态
<!-- 奖品统计Vue组件 -->
<template>
<div class="prize-stats">
<el-table :data="statsData" border style="width: 100%">
<el-table-column prop="prizeName" label="奖品名称"></el-table-column>
<el-table-column prop="totalStock" label="总库存"></el-table-column>
<el-table-column prop="awardedCount" label="已发放"></el-table-column>
<el-table-column prop="remainingStock" label="剩余库存"></el-table-column>
<el-table-column prop="probability" label="设定概率"></el-table-column>
<el-table-column prop="actualRate" label="实际中奖率"></el-table-column>
</el-table>
<div class="chart-container">
<div id="prizeChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
props: ['activityId'],
data() {
return {
statsData: []
};
},
mounted() {
this.loadStats();
},
methods: {
loadStats() {
this.$axios.get(`/admin/stat/prize?activity_id=${this.activityId}`).then(response => {
this.statsData = response.data.data;
this.renderChart();
});
},
renderChart() {
const chart = echarts.init(document.getElementById('prizeChart'));
const xData = this.statsData.map(item => item.prizeName);
const seriesData = this.statsData.map(item => item.awardedCount);
const option = {
title: {
text: '奖品发放统计'
},
tooltip: {},
xAxis: {
data: xData,
axisLabel: {
rotate: 45
}
},
yAxis: {},
series: [{
name: '发放数量',
type: 'bar',
data: seriesData,
itemStyle: {
color: function(params) {
const colorList = ['#c23531','#2f4554','#61a0a8','#d48265','#91c7ae'];
return colorList[params.dataIndex % colorList.length];
}
}
}]
};
chart.setOption(option);
window.addEventListener('resize', function() {
chart.resize();
});
}
}
};
</script>
6. 部署与运维
6.1 服务器部署方案
6.1.1 基础架构
┌─────────────────────────────────────────────────┐
│ 负载均衡(ALB/NLB) │
└─────────────────────────────────────────────────┘
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ Web服务器 │ │ Web服务器 │
│ (Node.js/Java) │ │ (Node.js/Java) │
└─────────────────┘ └─────────────────┘
↓ ↓
┌─────────────────────────────────────────────────┐
│ Redis集群 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ MySQL集群 │
│ (主从复制) │
└─────────────────────────────────────────────────┘
6.1.2 Docker部署示例
# Node.js服务Dockerfile示例
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
# docker-compose.yml示例
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
mysql:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=lottery
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
mysql-data:
redis-data:
6.2 监控与告警
6.2.1 监控指标
-
系统层面:
- CPU/Memory/Disk使用率
- 网络流量
- 服务响应时间
-
应用层面:
- 接口QPS/成功率
- 抽奖请求量
- 奖品发放速率
-
业务层面:
- 活动参与人数
- 中奖率
- 用户信息提交率
6.2.2 Prometheus + Grafana配置
# prometheus.yml 部分配置
scrape_configs:
- job_name: 'node_app'
static_configs:
- targets: ['app:3000']
- job_name: 'mysql'
static_configs:
- targets: ['mysql:9104']
- job_name: 'redis'
static_configs:
- targets: ['redis:9121']
6.3 应急预案
-
高并发应对:
- 自动扩容:根据CPU负载自动增加服务器实例
- 降级策略:非核心功能降级(如关闭数据分析)
-
数据库故障:
- 主从切换:主库故障自动切换到从库
- 缓存兜底:关键数据缓存,数据库不可用时从缓存读取
-
奖品超发处理:
- 库存预警:实时监控库存,接近耗尽时告警
- 补救措施:超发后联系用户更换等价奖品或补偿
7. 项目总结与展望
7.1 项目成果
本二维码抽奖系统实现了以下核心功能:
- 完整的抽奖业务流程:从二维码生成到奖品发放
- 安全可靠的微信登录集成
- 灵活可配置的抽奖概率算法
- 完善的用户信息收集机制
- 丰富的管理后台功能
- 健壮的安全防护和性能优化
7.2 技术亮点
- 高并发处理:通过Redis缓存、分布式锁、消息队列等技术应对高并发抽奖场景
- 防刷机制:多层次防护确保抽奖公平性
- 微服务架构:系统解耦,便于扩展和维护
- 数据安全:敏感信息加密存储,接口安全防护
- 可视化配置:管理后台提供灵活的活动配置界面
7.3 未来优化方向
- AI智能风控:引入机器学习模型识别异常抽奖行为
- 社交传播:增加分享助力等社交化功能扩大活动影响力
- 大数据分析:深度分析用户行为,优化营销策略
- 多平台支持:扩展至支付宝、抖音等多平台
- 区块链应用:抽奖结果上链,增强公信力
通过本项目的实施,企业可以获得一个功能完善、性能优异、安全可靠的数字化营销工具,有效提升用户参与度和品牌影响力,同时收集宝贵的用户数据为后续精准营销奠定基础。