2018-12-21php红包分发算法

<?php

/**

* 随机红包+固定红包算法[策略模式]

* copyright (c) 2016 http://blog.csdn.net/CleverCode

*/

//配置传输数据DTO

class OptionDTO

{/*{{{*/

    //红包总金额

    public $totalMoney;

    //红包数量

    public $num;

    //范围开始

    public $rangeStart;

    //范围结算

    public $rangeEnd;

    //生成红包策略

    public $builderStrategy;

    //随机红包剩余规则

    public $randFormatType; //Can_Left:不修数据,可以有剩余;No_Left:不能有剩余

    public static function create($totalMoney,$num,$rangeStart,$rangEnd,

        $builderStrategy,$randFormatType = 'No_Left')

    {/*{{{*/

        $self = new self();

        $self->num = $num;

        $self->rangeStart = $rangeStart;

        $self->rangeEnd = $rangEnd;

        $self->totalMoney = $totalMoney;

        $self->builderStrategy = $builderStrategy;

        $self->randFormatType = $randFormatType;

        return $self;

    }/*}}}*/

}/*}}}*/

//红包生成器接口

interface IBuilderStrategy

{/*{{{*/

    //创建红包

    public function create();   

    //设置配置

    public function setOption(OptionDTO $option);

    //是否可以生成红包

    public function isCanBuilder();

    //生成红包函数

    public function fx($x);

}/*}}}*/

//固定等额红包策略

class EqualPackageStrategy implements IBuilderStrategy

{/*{{{*/

    //单个红包金额

    public $oneMoney;

    //数量

    public $num;

    public function __construct($option = null)

    {

        if($option instanceof OptionDTO)

        {

            $this->setOption($option);

        }

    }

    public function setOption(OptionDTO $option)

    {

        $this->oneMoney = $option->rangeStart;

        $this->num = $option->num;

    }

    public function create()

    {/*{{{*/

        $data = array();

        if(false == $this->isCanBuilder())

        {

            return $data;   

        }

        $data = array();

        if(false == is_int($this->num) || $this->num <= 0)

        {

            return $data;   

        }

        for($i = 1;$i <= $this->num;$i++)

        {

            $data[$i] = $this->fx($i);

        }

        return $data;

    }/*}}}*/


    /**

    * 等额红包的方程是一条直线

    *

    * @param mixed $x

    * @access public

    * @return void

    */

    public function fx($x)

    {/*{{{*/

        return $this->oneMoney;

    }/*}}}*/

    /**

    * 是否能固定红包

    *

    * @access public

    * @return void

    */

    public function isCanBuilder()

    {/*{{{*/

        if(false == is_int($this->num) || $this->num <= 0)

        {

            return false;   

        }

        if(false ==  is_numeric($this->oneMoney) || $this->oneMoney <= 0)

        {

            return false;

        }

        //单个红包小于1分

        if($this->oneMoney < 0.01)

        {

            return false;

        }


        return true;

    }/*}}}*/

}/*}}}*/

//随机红包策略(三角形)

class RandTrianglePackageStrategy implements IBuilderStrategy

{/*{{{*/

    //总额

    public $totalMoney;

    //红包数量

    public $num;

    //随机红包最小值

    public $minMoney;

    //随机红包最大值

    public $maxMoney;

    //修数据方式:NO_LEFT: 红包总额 = 预算总额;CAN_LEFT: 红包总额 <= 预算总额

    public $formatType;

    //预算剩余金额

    public $leftMoney;

    public function __construct($option = null)

    {/*{{{*/

        if($option instanceof OptionDTO)

        {

            $this->setOption($option);

        }

    }/*}}}*/

    public function setOption(OptionDTO $option)

    {/*{{{*/

        $this->totalMoney = $option->totalMoney;

        $this->num = $option->num;

        $this->formatType = $option->randFormatType;

        $this->minMoney = $option->rangeStart;

        $this->maxMoney = $option->rangeEnd;

        $this->leftMoney = $this->totalMoney;

    }/*}}}*/

    /**

    * 创建随机红包

    *

    * @access public

    * @return void

    */

    public function create()

    {/*{{{*/


        $data = array();

        if(false == $this->isCanBuilder())

        {

            return $data;   

        }


        $leftMoney = $this->leftMoney;

        for($i = 1;$i <= $this->num;$i++)

        {

            $data[$i] = $this->fx($i);

            $leftMoney = $leftMoney - $data[$i];

        }

        //修数据

        list($okLeftMoney,$okData) = $this->format($leftMoney,$data);

        //随机排序

        shuffle($okData);

        $this->leftMoney = $okLeftMoney;

        return $okData;

    }/*}}}*/

    /**

    * 是否能够发随机红包

    *

    * @access public

    * @return void

    */

    public function isCanBuilder()

    {/*{{{*/

        if(false == is_int($this->num) || $this->num <= 0)

        {

            return false;   

        }

        if(false ==  is_numeric($this->totalMoney) || $this->totalMoney <= 0)

        {

            return false;

        }

        //均值

        $avgMoney = $this->totalMoney / 1.0 / $this->num;


        //均值小于最小值

        if($avgMoney < $this->minMoney )

        {

            return false;

        }


        return true;

    }/*}}}*/

    /**

    * 获取剩余金额

    *

    * @access public

    * @return void

    */

    public function getLeftMoney()

    {/*{{{*/

        return $this->leftMoney;

    }/*}}}*/

    /**

    * 随机红包生成函数。三角函数。[(1,0.01),($num/2,$avgMoney),($num,0.01)]

    *

    * @param mixed $x,1 <= $x <= $this->num;

    * @access public

    * @return void

    */

    public function fx($x)

    {/*{{{*/


        if(false == $this->isCanBuilder())

        {

            return 0;

        }

        if($x < 1 || $x > $this->num)

        {

            return 0;

        }


        $x1 = 1;

        $y1 = $this->minMoney;


        //我的峰值

        $y2 = $this->maxMoney;

        //中间点

        $x2 = ceil($this->num /  1.0 / 2);

        //最后点

        $x3 = $this->num;

        $y3 = $this->minMoney; 

        //当x1,x2,x3都是1的时候(竖线)

        if($x1 == $x2 && $x2 == $x3)

        {

            return $y2;

        }

        // '/_\'三角形状的线性方程

        //'/'部分

        if($x1 != $x2 && $x >= $x1 && $x <= $x2)

        {

            $y = 1.0 * ($x - $x1) / ($x2 - $x1) * ($y2 - $y1) + $y1; 

            return number_format($y, 2, '.', '');

        }

        //'\'形状

        if($x2 != $x3 && $x >= $x2 && $x <= $x3)

        {

            $y = 1.0 * ($x - $x2) / ($x3 - $x2) * ($y3 - $y2) + $y2; 

            return number_format($y, 2, '.', '');

        }


        return 0;

    }/*}}}*/

    /**

    * 格式化修红包数据

    *

    * @param mixed $leftMoney

    * @param array $data

    * @access public

    * @return void

    */

    private function format($leftMoney,array $data)

    {/*{{{*/

        //不能发随机红包

        if(false == $this->isCanBuilder())

        {

            return array($leftMoney,$data); 

        }


        //红包剩余是0

        if(0 == $leftMoney)

        {

            return array($leftMoney,$data); 

        }

        //数组为空

        if(count($data) < 1)

        {

            return array($leftMoney,$data); 

        }

        //如果是可以有剩余,并且$leftMoney > 0

        if('Can_Left' == $this->formatType

          && $leftMoney > 0)

        {

            return array($leftMoney,$data); 

        }

        //我的峰值

        $myMax = $this->maxMoney;

        // 如果还有余钱,则尝试加到小红包里,如果加不进去,则尝试下一个。

        while($leftMoney > 0)

        {

            $found = 0;

            foreach($data as $key => $val)

            {

                //减少循环优化

                if($leftMoney <= 0)

                {

                    break;

                }

                //预判

                $afterLeftMoney =  (double)$leftMoney - 0.01;

                $afterVal = (double)$val + 0.01;

                if( $afterLeftMoney >= 0  && $afterVal <= $myMax)

                {

                    $found = 1;

                    $data[$key] = number_format($afterVal,2,'.','');

                    $leftMoney = $afterLeftMoney;

                    //精度

                    $leftMoney = number_format($leftMoney,2,'.','');

                }

            }

            //如果没有可以加的红包,需要结束,否则死循环

            if($found == 0)

            {

                break;

            }

        }

        //如果$leftMoney < 0 ,说明生成的红包超过预算了,需要减少部分红包金额

        while($leftMoney < 0)

        {

            $found = 0;

            foreach($data as $key => $val)

            {

                if($leftMoney >= 0)

                {

                    break;

                }

                //预判


                $afterLeftMoney =  (double)$leftMoney + 0.01;

                $afterVal = (double)$val - 0.01;

                if( $afterLeftMoney <= 0 && $afterVal >= $this->minMoney)

                {

                    $found = 1;

                    $data[$key] = number_format($afterVal,2,'.','');

                    $leftMoney = $afterLeftMoney;

                    $leftMoney = number_format($leftMoney,2,'.','');

                }

            }


            //如果一个减少的红包都没有的话,需要结束,否则死循环

            if($found == 0)

            {

                break;

            }

        }

        return array($leftMoney,$data); 

    }/*}}}*/

}/*}}}*/

//维护策略的环境类

class RedPackageBuilder

{/*{{{*/

    // 实例 

    protected static $_instance = null; 

    /**

    * Singleton instance(获取自己的实例)

    *

    * @return MemcacheOperate

    */ 

    public static function getInstance()

    {  /*{{{*/

        if (null === self::$_instance)

        { 

            self::$_instance = new self(); 

        } 

        return self::$_instance; 

    }  /*}}}*/

    /**

    * 获取策略【使用反射】

    *

    * @param string $type 类型

    * @return void

    */ 

    public function getBuilderStrategy($type)

    {  /*{{{*/

        $class = $type.'PackageStrategy';

        if(class_exists($class))

        {

            return new $class(); 

        }

        else

        {

            throw new Exception("{$class} 类不存在!");

        }

    }  /*}}}*/

    public function getRedPackageByDTO(OptionDTO $optionDTO)

    {/*{{{*/

        //获取策略

        $builderStrategy = $this->getBuilderStrategy($optionDTO->builderStrategy);

        //设置参数

        $builderStrategy->setOption($optionDTO);

        return $builderStrategy->create();

    }/*}}}*/


}/*}}}*/

class Client

{/*{{{*/

    public static function main($argv)

    {

        //固定红包,开始金额需要自己平均算出

        $dto = OptionDTO::create(1000,3,333.33,333.33,'Equal');

        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);

        print_r($data);

        echo array_sum($data);

        //随机红包[修数据]

        $dto = OptionDTO::create(800,53,0.1,30.17,'RandTriangle');

        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);

        // print_r($data);

        // echo array_sum($data);

        //随机红包[不修数据]

        $dto = OptionDTO::create(800,53,0.1,30.17,'RandTriangle','Can_Left');

        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);

        // print_r($data);

        // echo array_sum($data);

    }

}/*}}}*/

Client::main($argv);

//随机红包,金额最大最小范围算法,以五元发10个红包为例:avg = 5 / 10 = 0.5;(avg * 2 - 0.01 = 0.99)

//这样的结果符合正态分布图

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容