PHP红包算法怎么设计

wen PHP项目 4

本文目录导读:

PHP红包算法怎么设计

  1. 基础红包算法(随机分配)
  2. 二倍均值法(微信红包算法)
  3. 高级红包算法(可配置参数)
  4. 手气红包(有运气成分)
  5. 完整示例和测试
  6. 完整的生产环境代码
  7. 算法对比表
  8. 使用建议

我来设计一个完整的PHP红包算法,包含多种实现方案和完整示例代码。

基础红包算法(随机分配)

普通随机算法(可能不够均匀)

<?php
/**
 * 简单随机红包算法
 * @param int $totalAmount 总金额(分)
 * @param int $num 红包个数
 * @return array 每个红包金额(分)
 */
function simpleRedPacket($totalAmount, $num) {
    $result = [];
    $remaining = $totalAmount;
    for ($i = 0; $i < $num - 1; $i++) {
        // 随机分配,但确保每个人至少1分钱
        $max = $remaining - ($num - $i - 1); // 预留其他人的最少金额
        $amount = rand(1, $max);
        $result[] = $amount;
        $remaining -= $amount;
    }
    // 最后一个红包拿剩下的
    $result[] = $remaining;
    return $result;
}

二倍均值法(微信红包算法)

<?php
/**
 * 二倍均值法红包算法(更均匀)
 * @param int $totalAmount 总金额(分)
 * @param int $num 红包个数
 * @return array 每个红包金额(分)
 */
function averageRedPacket($totalAmount, $num) {
    $result = [];
    $remaining = $totalAmount;
    $remainingNum = $num;
    for ($i = 0; $i < $num; $i++) {
        if ($remainingNum == 1) {
            $amount = $remaining;
        } else {
            // 最大金额 = 剩余金额 / 剩余个数 * 2
            $max = (int)($remaining / $remainingNum * 2);
            // 随机取 [1, max] 范围
            $amount = rand(1, $max);
        }
        $result[] = $amount;
        $remaining -= $amount;
        $remainingNum--;
    }
    return $result;
}

高级红包算法(可配置参数)

<?php
class RedPacket {
    private $totalAmount;     // 总金额(分)
    private $num;            // 红包个数
    private $minAmount;      // 最小金额(分)
    private $maxAmount;      // 最大金额(分)
    public function __construct($totalAmount, $num, $minAmount = 1, $maxAmount = null) {
        $this->totalAmount = $totalAmount;
        $this->num = $num;
        $this->minAmount = $minAmount;
        $this->maxAmount = $maxAmount ?? $totalAmount;
        // 验证参数
        if ($totalAmount < $num * $minAmount) {
            throw new Exception("金额不足以分配");
        }
    }
    /**
     * 生成红包
     * @return array
     */
    public function generate() {
        $result = [];
        $remaining = $this->totalAmount;
        $remainingNum = $this->num;
        for ($i = 0; $i < $this->num; $i++) {
            if ($remainingNum == 1) {
                $amount = $remaining;
            } else {
                // 计算最大可分配金额
                $maxAllocatable = $remaining - ($remainingNum - 1) * $this->minAmount;
                $maxAmount = min($maxAllocatable, 
                               (int)($remaining / $remainingNum * 2), 
                               $this->maxAmount);
                // 计算最小可分配金额
                $minAmount = max($this->minAmount, 
                               $remaining - ($remainingNum - 1) * $this->maxAmount);
                if ($maxAmount <= $minAmount) {
                    $amount = $minAmount;
                } else {
                    $amount = rand($minAmount, $maxAmount);
                }
            }
            $result[] = $amount;
            $remaining -= $amount;
            $remainingNum--;
        }
        shuffle($result); // 打乱顺序
        return $result;
    }
}

手气红包(有运气成分)

<?php
/**
 * 手气红包算法(有些人会拿到大的)
 * @param int $totalAmount 总金额(分)
 * @param int $num 红包个数
 * @param float $luckFactor 运气因子(0.5-2)
 * @return array
 */
function luckyRedPacket($totalAmount, $num, $luckFactor = 1.5) {
    $result = [];
    $remaining = $totalAmount;
    for ($i = 0; $i < $num; $i++) {
        if ($i == $num - 1) {
            $result[] = $remaining;
            break;
        }
        // 动态调整系数,让后面的红包更难拿大的
        $factor = $luckFactor * (1 - $i / $num * 0.5);
        $max = (int)($remaining / ($num - $i) * $factor);
        // 确保每个红包至少1分
        $min = max(1, $remaining - ($num - $i - 1));
        $amount = rand($min, min($max, $remaining - ($num - $i - 1)));
        $result[] = $amount;
        $remaining -= $amount;
    }
    return $result;
}

完整示例和测试

<?php
// 测试代码
function testRedPacket() {
    // 配置
    $totalAmount = 10000;  // 100元(以分为单位)
    $num = 10;             // 10个红包
    echo "=== 测试各种红包算法 ===\n";
    echo "总金额:{$totalAmount}分(" . ($totalAmount/100) . "元)\n";
    echo "红包数:{$num}个\n\n";
    // 测试二倍均值法
    $result1 = averageRedPacket($totalAmount, $num);
    echo "二倍均值法结果:\n";
    printResult($result1);
    // 测试高级算法
    $redPacket = new RedPacket($totalAmount, $num, 1, 2000);
    $result2 = $redPacket->generate();
    echo "高级算法结果(限制最高20元):\n";
    printResult($result2);
    // 测试手气红包
    $result3 = luckyRedPacket($totalAmount, $num, 1.8);
    echo "手气红包结果(运气因子1.8):\n";
    printResult($result3);
}
function printResult($result) {
    $total = 0;
    foreach ($result as $i => $amount) {
        $yuan = $amount / 100;
        echo "第" . ($i+1) . "个:{$yuan}元\n";
        $total += $amount;
    }
    echo "总计:{$total}分(" . ($total/100) . "元)\n\n";
}
// 运行测试
testRedPacket();

完整的生产环境代码

<?php
class RedPacketService {
    private $db; // 数据库连接
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 发红包
     * @param int $userId 用户ID
     * @param float $amount 金额(元)
     * @param int $num 红包个数
     * @param string $message 祝福语
     * @return array
     */
    public function sendRedPacket($userId, $amount, $num, $message = '恭喜发财') {
        // 参数验证
        if ($amount <= 0 || $num <= 0) {
            throw new Exception("参数错误");
        }
        // 转换为分
        $amountInCents = (int)round($amount * 100);
        // 生成红包
        $packetId = uniqid('rp_', true);
        $amounts = $this->generateAmounts($amountInCents, $num);
        // 保存到数据库(事务)
        $this->db->beginTransaction();
        try {
            // 插入红包记录
            $this->db->insert('red_packets', [
                'id' => $packetId,
                'user_id' => $userId,
                'total_amount' => $amountInCents,
                'remaining_amount' => $amountInCents,
                'num' => $num,
                'remaining_num' => $num,
                'message' => $message,
                'created_at' => date('Y-m-d H:i:s')
            ]);
            // 插入红包详情
            foreach ($amounts as $i => $amount) {
                $this->db->insert('red_packet_items', [
                    'packet_id' => $packetId,
                    'amount' => $amount,
                    'order_no' => $i,
                    'status' => 0  // 未领取
                ]);
            }
            $this->db->commit();
            return [
                'success' => true,
                'packet_id' => $packetId,
                'message' => '红包发送成功'
            ];
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
    /**
     * 领取红包
     */
    public function receiveRedPacket($packetId, $userId) {
        // 开启事务
        $this->db->beginTransaction();
        try {
            // 获取红包信息
            $packet = $this->db->selectOne("SELECT * FROM red_packets WHERE id = ? FOR UPDATE", [$packetId]);
            if (!$packet) {
                throw new Exception("红包不存在");
            }
            if ($packet['remaining_num'] <= 0) {
                throw new Exception("红包已被抢完");
            }
            // 检查是否已经领取过
            $received = $this->db->selectOne("SELECT * FROM red_packet_records WHERE packet_id = ? AND user_id = ?", 
                [$packetId, $userId]);
            if ($received) {
                throw new Exception("您已经领过了");
            }
            // 获取一个未领取的红包
            $item = $this->db->selectOne(
                "SELECT * FROM red_packet_items WHERE packet_id = ? AND status = 0 ORDER BY RAND() LIMIT 1 FOR UPDATE", 
                [$packetId]
            );
            if (!$item) {
                throw new Exception("红包内部错误");
            }
            // 更新红包状态
            $this->db->update("UPDATE red_packets SET remaining_num = remaining_num - 1 WHERE id = ?", [$packetId]);
            $this->db->update("UPDATE red_packet_items SET status = 1, receive_user_id = ?, receive_time = ? WHERE id = ?", 
                [$userId, date('Y-m-d H:i:s'), $item['id']]);
            // 记录领取日志
            $this->db->insert('red_packet_records', [
                'packet_id' => $packetId,
                'user_id' => $userId,
                'amount' => $item['amount'],
                'received_at' => date('Y-m-d H:i:s')
            ]);
            // 增加用户余额
            $this->db->update("UPDATE users SET balance = balance + ? WHERE id = ?", 
                [$item['amount'] / 100, $userId]);
            $this->db->commit();
            return [
                'success' => true,
                'amount' => $item['amount'] / 100,
                'message' => '领取成功'
            ];
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
    /**
     * 生成红包金额
     */
    private function generateAmounts($totalAmount, $num) {
        $result = [];
        $remaining = $totalAmount;
        // 确保最小金额为1分
        $minAmount = 1;
        for ($i = 0; $i < $num; $i++) {
            if ($i == $num - 1) {
                $result[] = $remaining;
                break;
            }
            // 二倍均值法
            $max = (int)($remaining / ($num - $i) * 2);
            $min = max($minAmount, $remaining - ($num - $i - 1));
            $amount = rand($min, min($max, $remaining - ($num - $i - 1)));
            $result[] = $amount;
            $remaining -= $amount;
        }
        // 打乱顺序
        shuffle($result);
        return $result;
    }
}

算法对比表

算法 优点 缺点 适用场景
随机算法 简单快速 可能太均匀 小场景
二倍均值法 较均匀 可能过于平均 微信红包
高级配置算法 灵活性高 参数配置复杂 需要控制的场景
手气红包 有惊喜感 可能差异太大 促销活动

使用建议

  1. 金额单位:建议使用分(整数)避免浮点数精度问题
  2. 数据库事务:复杂的红包操作必须使用事务
  3. 并发处理:使用数据库锁或Redis解决并发抢红包
  4. 日志记录:所有操作都要记录日志便于审计
  5. 性能优化:可以使用Redis预生成红包队列

这个设计已经包含了算法、并发处理、数据库操作等生产环境需要的要素。

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