二维码抽奖系统设计与实现

1. 项目概述

1.1 项目背景

在当今数字化营销时代,企业需要通过创新的互动方式来吸引客户、增强品牌影响力并收集有价值的用户数据。二维码抽奖系统作为一种高效的市场营销工具,结合了移动互联网的便捷性和传统抽奖活动的趣味性,能够有效提升用户参与度,同时为企业提供精准的用户信息收集渠道。

本项目旨在开发一个完整的二维码抽奖系统,用户通过扫描二维码进入抽奖小程序,参与抽奖活动,在抽奖过程中完成微信登录,并在中奖后提交联系电话和地址等必要信息以便奖品发放。系统将涵盖前端用户界面、后端业务逻辑、数据库设计以及管理后台等多个模块。

1.2 系统功能需求

  1. 二维码生成与管理:系统能够为每次抽奖活动生成唯一二维码,并可对二维码进行有效期限、使用次数等参数设置。
  2. 用户抽奖流程
    • 扫描二维码进入小程序
    • 参与抽奖(包括多种抽奖模式)
    • 微信授权登录
    • 中奖后填写联系信息
  3. 奖品管理:支持多种奖品类型设置,可配置中奖概率、库存等参数。
  4. 用户信息管理:安全存储用户提交的联系方式、地址等敏感信息。
  5. 数据统计与分析:提供参与人数、中奖率、用户地域分布等数据分析功能。
  6. 防作弊机制:防止同一用户多次抽奖、机器刷奖等作弊行为。

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 微服务划分

为应对高并发场景和未来扩展需求,系统采用微服务架构设计:

  1. 用户服务:处理用户注册、登录、信息管理
  2. 抽奖服务:核心抽奖逻辑、概率计算、结果生成
  3. 奖品服务:奖品库存管理、发放记录
  4. 活动服务:抽奖活动配置、二维码生成
  5. 微信接口服务:封装与微信平台的交互逻辑

各服务通过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
  1. 活动相关

    • GET /api/activity/info - 获取活动基本信息
    • GET /api/activity/prizes - 获取活动奖品列表
  2. 抽奖相关

    • POST /api/lottery/draw - 执行抽奖
    • GET /api/lottery/records - 获取用户抽奖记录
  3. 用户相关

    • POST /api/user/login - 微信登录
    • POST /api/user/contact - 提交联系信息
管理后台API
  1. 活动管理

    • POST /api/admin/activity/create - 创建活动
    • PUT /api/admin/activity/update - 更新活动
    • GET /api/admin/activity/list - 活动列表
  2. 奖品管理

    • POST /api/admin/prize/add - 添加奖品
    • PUT /api/admin/prize/update - 更新奖品
  3. 二维码管理

    • POST /api/admin/qrcode/generate - 生成二维码
    • GET /api/admin/qrcode/list - 二维码列表
  4. 数据统计

    • GET /api/admin/stat/participation - 参与数据统计
    • GET /api/admin/stat/prize - 奖品发放统计

3. 核心功能实现

3.1 二维码生成与管理

3.1.1 二维码生成逻辑

二维码生成服务主要流程:

  1. 接收生成请求(包含活动ID、有效期等参数)
  2. 生成唯一code_key(UUID或雪花算法)
  3. 调用微信小程序码接口或自行生成二维码
  4. 存储二维码信息到数据库
  5. 返回二维码图片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 二维码扫描处理

当用户扫描二维码进入小程序时:

  1. 小程序获取场景值(scene参数中的code_key)
  2. 向服务端验证二维码有效性
  3. 返回关联的活动信息
// 小程序端代码
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 登录流程设计

微信小程序登录流程:

  1. 前端调用wx.login获取临时code
  2. 将code发送到服务端
  3. 服务端使用code向微信接口服务获取session_key和openid
  4. 服务端生成自定义登录态(token)返回给前端
  5. 前端存储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 概率算法设计

抽奖概率算法需要考虑:

  • 每个奖品的中奖概率配置
  • 奖品库存限制
  • 防刷机制
  • 公平性保证

采用基于概率区间的算法:

  1. 计算所有奖品的中奖概率总和
  2. 为每个奖品分配一个概率区间
  3. 生成随机数,判断落在哪个区间
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 转盘抽奖实现

对于转盘类抽奖,需要特殊处理:

  1. 前端定义转盘奖品位置
  2. 后端返回中奖奖品索引
  3. 前端根据索引执行转盘动画
// 前端转盘抽奖代码
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 防刷机制
  1. IP限制:同一IP在短时间内多次抽奖需验证码验证
  2. 设备指纹:采集设备信息生成唯一指纹识别重复设备
  3. 行为分析:检测异常抽奖行为(如间隔时间过短)
  4. 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 数据加密
  1. 敏感信息加密:用户手机号等敏感信息数据库加密存储
  2. HTTPS传输:所有接口强制HTTPS协议
  3. 接口签名:重要接口增加签名验证防止篡改
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 缓存设计
  1. 活动信息缓存:活动基本信息、奖品列表等高读取数据缓存
  2. 奖品库存缓存:使用Redis原子操作保证库存准确性
  3. 用户抽奖记录缓存:用户是否已参与活动缓存
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 数据库优化
  1. 索引优化:为常用查询字段添加合适索引
  2. 读写分离:查询走从库,写入走主库
  3. 分表策略:抽奖记录表按用户ID或时间分表
4.2.3 高并发处理
  1. 限流措施:接口级别QPS限制
  2. 异步处理:非核心流程异步化(如记录日志)
  3. 队列削峰:使用消息队列缓冲瞬时高流量
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 活动创建与配置

管理后台提供可视化界面配置抽奖活动:

  1. 基础信息:名称、时间、描述
  2. 抽奖规则:每人参与次数、中奖后是否可继续参与
  3. 奖品设置:添加多个奖品,设置概率和库存
  4. 样式配置:抽奖页面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 监控指标
  1. 系统层面

    • CPU/Memory/Disk使用率
    • 网络流量
    • 服务响应时间
  2. 应用层面

    • 接口QPS/成功率
    • 抽奖请求量
    • 奖品发放速率
  3. 业务层面

    • 活动参与人数
    • 中奖率
    • 用户信息提交率
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 应急预案

  1. 高并发应对

    • 自动扩容:根据CPU负载自动增加服务器实例
    • 降级策略:非核心功能降级(如关闭数据分析)
  2. 数据库故障

    • 主从切换:主库故障自动切换到从库
    • 缓存兜底:关键数据缓存,数据库不可用时从缓存读取
  3. 奖品超发处理

    • 库存预警:实时监控库存,接近耗尽时告警
    • 补救措施:超发后联系用户更换等价奖品或补偿

7. 项目总结与展望

7.1 项目成果

本二维码抽奖系统实现了以下核心功能:

  1. 完整的抽奖业务流程:从二维码生成到奖品发放
  2. 安全可靠的微信登录集成
  3. 灵活可配置的抽奖概率算法
  4. 完善的用户信息收集机制
  5. 丰富的管理后台功能
  6. 健壮的安全防护和性能优化

7.2 技术亮点

  1. 高并发处理:通过Redis缓存、分布式锁、消息队列等技术应对高并发抽奖场景
  2. 防刷机制:多层次防护确保抽奖公平性
  3. 微服务架构:系统解耦,便于扩展和维护
  4. 数据安全:敏感信息加密存储,接口安全防护
  5. 可视化配置:管理后台提供灵活的活动配置界面

7.3 未来优化方向

  1. AI智能风控:引入机器学习模型识别异常抽奖行为
  2. 社交传播:增加分享助力等社交化功能扩大活动影响力
  3. 大数据分析:深度分析用户行为,优化营销策略
  4. 多平台支持:扩展至支付宝、抖音等多平台
  5. 区块链应用:抽奖结果上链,增强公信力

通过本项目的实施,企业可以获得一个功能完善、性能优异、安全可靠的数字化营销工具,有效提升用户参与度和品牌影响力,同时收集宝贵的用户数据为后续精准营销奠定基础。

评论 76
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百锦再@新空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值