微信随机红包数详解和算法代码

1 需求

写一个固定红包 + 随机红包

  1. 固定红包就是每个红包金额一样,有多少个就发多少个固定红包金额就行。
  2. 随机红包的需求是。比如红包总金额5元,需要发10个红包。随机范围是 0.01到0.99;5元必需发完,金额需要有一定趋势的正态分布。(0.99可以任意指定,也可以是 avg * 2 - 0.01;比如avg = 5 / 10 = 0.5;(avg * 2 - 0.01 = 0.99))

2 需求分析

2.1 固定红包

如果是固定红包,则算法是一条直线。t就是固定红包的额度。如图。
f(x) = t;(1 <= x <= num)

image.png

2.2 随机红包

如果我们使用随机函数rand。rand(0.01,0.99);那么10次随机,如果最坏情况都是金额0.99,总金额就是9.9元。会超过5元。金额也会不正态分布。最后思考了一下借助与数学函数来当作随机红包的发生器,可以用抛物线,三角函数。最后选定了等腰三角线性函数。

1 算法原理

如果需要发红包总金额是totalMoney,红包个数是num个,金额范围是[min,max],线性方程如图。

image.png

三个点的坐标:
(x1,y1) = (1,min)
(x2,y2) = (num/2,max)
(x3,y3) = (num,min)

确定的线性方程:
$y = 1.0 * ($x - $x1) / ($x2 - $x1) * ($y2 - $y1) + $y1 ; (x1 <= x <= x2)
$y = 1.0 * ($x - $x2) / ($x3 - $x2) * ($y3 - $y2) + $y2; (x2 <= x <= x3)

修数据:
y(合) = y1 + y2 + y3 +...... ynum;
y(合)有可能 > totalMoney ,说明生成金额多了,需要修数据,则从(y1,y2,y3.....ynum)这些每次减少0.01。直到y(合) = totalMoney。
y(合)有可能 < totalMoney ,说明生成金额少了,需要修数据,则从(y1,y2,y3.....ynum)这些每次加上0.01。直到y(合) = totalMoney。

2 算法原理样例

如果需要发红包总金额是11470,红包个数是7400个,金额范围是[0.01,3.09],线性方程如图。

image.png

3 需求设计

3.1 类图设计

image.png

3.2 源码设计

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;  
        
        //中间点  
        $x2 = ceil($this->num /  1.0 / 2);  
        //我的峰值  
        $y2 = $this->maxMoney;  
  
        //最后点  
        $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);    
        }  

        // 如果还有余钱,则尝试加到小红包里,如果加不进去,则尝试下一个。  
        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->maxMoney)  
                {  
                    $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,10,100,100,'Equal');  
        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);  
        //print_r($data);  
  
        //随机红包[修数据]  
        $dto = OptionDTO::create(5,10,0.01,0.99,'RandTriangle');  
        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);  
        print_r($data);  
  
        //随机红包[不修数据]  
        $dto = OptionDTO::create(5,10,0.01,0.99,'RandTriangle','Can_Left');  
        $data = RedPackageBuilder::getInstance()->getRedPackageByDTO($dto);  
        //print_r($data);      
    }  
}

$argv = [];
Client::main($argv);

你可能感兴趣的:(微信随机红包数详解和算法代码)