PHP 围绕平均值生成随机红包

围绕剩余平均值随机分配红包,自动计算单份上限,并支持平均分配与分币尾差处理。

开发思路

先计算剩余金额的平均值,再取可行上下限距离平均值的较小值,作为对称浮动范围。每次围绕平均值生成随机金额,最后打乱红包顺序。这是对称区间内随机取值,不是正态分布。

kind=0 为随机红包,kind=1 为平均红包。随机模式下,max=0 或上限不符合校验条件时,自动回退为总金额的三分之一;仅当份数或分币精度使该上限无法分完时,提高到能完成分配的最小上限。

金额、数量和模式支持数字字符串。数量为零或金额不足时返回 false,平均分配处理分币尾差,随机分配在每一步保留两位小数。每份至少 0.01 元,返回两位小数的金额字符串。

PHP 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php

class RedPacket
{
    /**
     * @param float|string $rmb 红包总金额(元)
     * @param int|string $num 红包数量
     * @param int|string $kind 0 随机,1 平均
     * @param float|string $max 单份上限,0 自动计算
     * @return array|false
     */
    public static function create($rmb, $num, $kind = 0, $max = 0)
    {
        if (!is_numeric($rmb) || !is_numeric($num) || !is_numeric($max)
            || $num < 1 || $num > PHP_INT_MAX || $num != (int) $num) {
            return false;
        }
        $rmb = round((float) $rmb, 2);
        $num = (int) $num;
        $max = (float) $max;
        if (!is_finite($rmb) || !is_finite($max)
            || $rmb <= 0 || $rmb >= PHP_INT_MAX / 100) {
            return false;
        }
        if ($num < 2) $kind = 1;
        $min = 0.01;
        $packets = [];
        if ($min * $num > $rmb + 1e-9) {
            return false;
        }
        switch ($kind) {
            case 1:
                // 以分处理平均分配尾差,保证总金额不变。
                $total = (int) round($rmb * 100);
                $base = (int) floor($total / $num);
                $extra = $total % $num;
                for ($i = 0; $i < $num; $i++) {
                    $packets[] = sprintf('%.2f', ($base + ($i < $extra ? 1 : 0)) / 100);
                }
                return $packets;
            default:
                // 上限不满足分配条件时自动回退。
                if ($num * $max < $rmb - 1e-9 || $max + $num * $min > $rmb + 1e-9) {
                    $max = $rmb / 3;
                }
                $max = floor($max * 100 + 1e-9) / 100;
                // 两份红包或分币尾差可能使三分之一上限不可行。
                $requiredMax = ceil(round($rmb * 100) / $num) / 100;
                $max = max($max, $requiredMax);

                while ($num >= 1) {
                    $num--;
                    $kMin = round(max($min, $rmb - $num * $max), 2);
                    $kMax = round(min($max, $rmb - $num * $min), 2);
                    $kAvg = $rmb / ($num + 1);
                    $kDis = max(0, min($kAvg - $kMin, $kMax - $kAvg));
                    $r = ((float) (rand(1, 10000) / 10000) - 0.5) * $kDis * 2;
                    $k = $num === 0 ? $rmb : min($kMax, max($kMin, round($kAvg + $r, 2)));
                    $rmb = round($rmb - $k, 2);
                    $packets[] = sprintf('%.2f', $k);
                }
                shuffle($packets);
                return $packets;
        }
    }
}

示例

1
2
3
4
5
6
7
8
// 自动计算单份上限,并围绕剩余平均值随机分配。
$random = RedPacket::create('10.00', '5');

// kind 可以使用数字字符串;三份金额之和为 1.00 元。
$average = RedPacket::create('1.00', '3', '1');

// 数量为零,或总金额不足以让每份至少 0.01 元时返回 false。
$invalid = RedPacket::create('1.00', 0);
使用 Hugo 构建
主题 Stack 由 Jimmy 设计