PHP项目怎么实现刮刮乐?

wen java案例 2

PHP项目如何实现刮刮乐?从零搭建互动抽奖系统实战指南

目录导读

  1. 刮刮乐功能的核心技术逻辑
  2. 环境准备与数据库设计
  3. PHP后端实现刮奖逻辑
  4. 前端交互实现(HTML+CSS+JS)
  5. 安全与防作弊机制设计
  6. 常见问题与性能优化(问答环节)
  7. 总结与扩展思路

刮刮乐功能的核心技术逻辑

刮刮乐本质上是一种“按需揭晓结果”的交互式抽奖,用户在未刮开涂层前,结果已经由后端确定,但前端需要模拟物理刮擦的视觉效果,实现这一功能需要前后端紧密配合:

PHP项目怎么实现刮刮乐?

  • 后端职责:生成唯一的奖券ID,根据算法预置中奖状态,提供兑奖接口,防止重复刮奖。
  • 前端职责:渲染模拟涂层,通过Canvas或CSS实现刮开效果,刮开后显示结果,并向后端验证。

注意:结果必须在服务端预生成,不能依赖前端随机,否则极易被篡改。


环境准备与数据库设计

建议使用LNMP(Linux + Nginx + MySQL + PHP)或简单XAMPP环境,首先创建数据表:

CREATE TABLE `scratch_cards` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(32) NOT NULL COMMENT '唯一卡片编码',
  `prize_id` int(11) DEFAULT NULL COMMENT '中奖奖品ID',
  `status` tinyint(4) DEFAULT '0' COMMENT '0=未刮,1=已刮开,2=已兑换',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `scratched_at` datetime DEFAULT NULL,
  `user_ip` varchar(45) DEFAULT NULL COMMENT '用户IP',
  PRIMARY KEY (`id`),
  UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `prizes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `probability` decimal(5,2) NOT NULL COMMENT '概率百分比',
  `stock` int(11) DEFAULT '0' COMMENT '库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

设计要点code字段使用UUID或md5(uniqid)生成,确保唯一性;prize_id为NULL代表未中奖。


PHP后端实现刮奖逻辑

核心步骤:获取卡片 → 检查状态 → 返回结果。

1 生成卡片(管理员后台)

function createCard($prizeId = null) {
    $code = md5(uniqid(mt_rand(), true));
    $sql = "INSERT INTO scratch_cards (code, prize_id) VALUES ('$code', " . ($prizeId ? "'$prizeId'" : "NULL") . ")";
    // 执行插入
    return $code;
}

2 刮奖接口(前端请求)

header('Content-Type: application/json');
$code = $_POST['code'] ?? '';
if (empty($code)) {
    echo json_encode(['code' => -1, 'msg' => '参数错误']);
    exit;
}
// 检查卡片是否存在
$card = query("SELECT * FROM scratch_cards WHERE code='$code'");
if (!$card) {
    echo json_encode(['code' => -2, 'msg' => '卡片不存在']);
    exit;
}
// 检查是否已刮开
if ($card['status'] > 0) {
    echo json_encode(['code' => -3, 'msg' => '该卡片已刮开过']);
    exit;
}
// 更新状态为已刮开
$prizeName = $card['prize_id'] ? getPrizeName($card['prize_id']) : '未中奖';
update("UPDATE scratch_cards SET status=1, scratched_at=NOW(), user_ip='{$_SERVER['REMOTE_ADDR']}' WHERE id={$card['id']}");
echo json_encode([
    'code' => 0,
    'msg' => 'success',
    'data' => ['prize' => $prizeName]
]);

关键安全措施

  • 所有操作基于数据库状态判断,防止重复刮奖。
  • 记录用户IP,用于后期风控。
  • 使用预处理语句防止SQL注入(示例为了可读性简化)。

前端交互实现(HTML+CSS+JS)

使用Canvas模拟涂层效果,配合CSS3实现“刮开”视觉反馈。

1 HTML结构

<div class="scratch-card">
    <div class="card-result">
        <p id="prizeName">???</p>
        <p class="hint">使用鼠标刮开涂层</p>
    </div>
    <canvas id="scratchCanvas" width="300" height="200"></canvas>
</div>
<button id="verifyBtn" onclick="verifyPrize()">点击兑奖</button>
<script src="scratch.js"></script>

2 JavaScript刮擦逻辑(核心)

const canvas = document.getElementById('scratchCanvas');
const ctx = canvas.getContext('2d');
// 绘制灰色涂层
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '20px Arial';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText('刮开有奖', canvas.width/2, canvas.height/2);
let isDrawing = false;
let scratchPercent = 0;
canvas.addEventListener('mousedown', (e) => {
    isDrawing = true;
    e.preventDefault();
});
canvas.addEventListener('mousemove', (e) => {
    if (!isDrawing) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    // 用灰色擦除(模拟刮开)
    ctx.globalCompositeOperation = 'destination-out';
    ctx.beginPath();
    ctx.arc(x, y, 20, 0, Math.PI * 2);
    ctx.fill();
    // 计算刮开百分比
    checkScratchProgress();
});
canvas.addEventListener('mouseup', () => {
    isDrawing = false;
    if (scratchPercent > 50) {
        alert('刮开面积超过50%,自动显示结果!');
        showResult();
    }
});
function checkScratchProgress() {
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    let transparentPixels = 0;
    for (let i = 3; i < imageData.data.length; i += 4) {
        if (imageData.data[i] === 0) transparentPixels++;
    }
    scratchPercent = transparentPixels / (canvas.width * canvas.height) * 100;
}

安全与防作弊机制设计

这是刮刮乐系统的重中之重:

  1. 结果服务端预置:前端只负责显示,无法修改结果。
  2. 防刷机制
    • 限制单IP每小时请求次数(如Redis计数)。
    • 每个用户(根据Cookie或手机号)每天最多刮10张。
  3. 兑奖验证:兑奖时再次校验数据库status=1scratched_at时间合理。
  4. 日志记录:所有刮奖操作记录到日志表,便于事后审计。
  5. 防直接访问接口:前端请求携带一次性Token(CSRF防护)。

代码示例(限速中间件)

$ip = $_SERVER['REMOTE_ADDR'];
$key = "scratch_limit:{$ip}";
$count = $redis->get($key) ?: 0;
if ($count > 10) {
    die(json_encode(['code' => -5, 'msg' => '请求过于频繁']));
}
$redis->incr($key);
$redis->expire($key, 3600); // 1小时重置

常见问题与性能优化(问答环节)

Q:前端刮开涂层后,如何防止用户直接查看元素获取结果?
A:结果存放在数据库,前端只显示“???”,刮开后通过Ajax请求后端获取真实结果,涂层仅用于视觉交互,不包含任何数据。

Q:高并发时如何保证数据一致性?
A:使用MySQL事务或Redis原子操作,例如在更新卡片状态时使用UPDATE ... WHERE status=0,利用行级锁避免并发刮同一张卡。

Q:涂层刮开后手机端触摸事件为何不生效?
A:需要同时监听touchstarttouchmovetouchend事件,并阻止默认滚动行为,Canvas事件需适配触摸坐标。

Q:如何提高用户刮奖体验?
A:

  • 使用Web Workers在后台预加载下一张卡片信息。
  • 采用CSS动画过渡,避免刮开后瞬间闪烁。
  • 中奖时加入音效和弹窗特效(使用CDN资源避免拖慢加载)。

Q:如果用户刮开50%就自动显示结果,如何实现?
A:已在前面JS代码中实现checkScratchProgress(),当透明像素占比超过阈值时自动触发showResult()


总结与扩展思路

通过本文,你已掌握从数据库设计、PHP接口到前端Canvas实现的完整流程,核心要点在于前后端职责分离:后端控制逻辑与安全,前端只负责用户体验。

扩展方向

  • 动态奖池:根据库存实时调整中奖概率。
  • 多级奖品:增加“再来一次”或积分奖品。
  • 移动端优化:使用touch事件 + 自适应Canvas尺寸。
  • 数据看板:使用PHP生成统计报表,实时监控刮奖数据。

请根据业务需求调整限流阈值和奖品配置,并务必在生产环境开启PHP错误日志监控,如果遇到问题,欢迎在社区交流!

抱歉,评论功能暂时关闭!