Browse Source

会员优惠券

panda 3 months ago
parent
commit
e7ee7f013d

+ 1 - 1
application/admin/view/vip_config/add.html

@@ -47,7 +47,7 @@
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Coupon_ids')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-coupon_ids" data-rule="required" data-source="coupon/index" data-multiple="true" class="form-control selectpage" name="row[coupon_ids]" type="text" value="">
+            <input id="c-coupon_ids" data-rule="required" data-source="vip_coupon/index" data-multiple="true" class="form-control selectpage" name="row[coupon_ids]" type="text" value="">
         </div>
     </div>
     <div class="form-group">

+ 1 - 1
application/admin/view/vip_config/edit.html

@@ -47,7 +47,7 @@
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Coupon_ids')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-coupon_ids" data-rule="required" data-source="coupon/index" data-multiple="true" class="form-control selectpage" name="row[coupon_ids]" type="text" value="{$row.coupon_ids|htmlentities}">
+            <input id="c-coupon_ids" data-rule="required" data-source="vip_coupon/index" data-multiple="true" class="form-control selectpage" name="row[coupon_ids]" type="text" value="{$row.coupon_ids|htmlentities}">
         </div>
     </div>
     <div class="form-group">

+ 143 - 56
application/api/controller/Pay.php

@@ -3,6 +3,7 @@
 namespace app\api\controller;
 
 use app\common\controller\Api;
+use app\common\model\PayOrderModel;
 use app\common\model\Wallet;
 use app\utils\CurlUtil;
 use think\Db;
@@ -16,83 +17,169 @@ class Pay extends Api
     protected $noNeedLogin = [];
     protected $noNeedRight = ['*'];
 
-    //vip用的
+    // VIP套餐
     public function vip_config()
     {
-        $list                = Db::name('payvip_config')->order('weigh asc,id asc')->select();
-        $data['vipconfig']   = $list;
-        $data['vip_endtime'] = model('wallet')->getWallet($this->auth->id, 'vip_endtime');
-        $data['is_vip']      = $data['vip_endtime'] > time() ? 1 : 0;
-        $data['avatar']      = localpath_to_netpath($this->auth->avatar);
-        $this->success('success', $data);
+        $user_id = $this->auth->id;
+        $list = Db::name('vip_config')->field(['id','name','image','price','color','is_try','coupon_ids'])->where('status',1)->order('weigh desc,id asc')->select();
+
+        foreach ($list as $key => $val) {
+            $coupon_ids = explode(',',$val['coupon_ids']);
+            $list[$key]['image'] = cdnurl($val['image']);
+
+            // 如果是试用会员,则需要判断是否使用过了
+            if ($val['is_try'] == 1){
+                // 是否购买过,1已购买
+                $list[$key]['is_buy'] = Db::name('vip_order')->where(['user_id' => $user_id,'vip_id' => $val['id'],'status'=>1])->value('id') > 0 ? 1 : 0;
+            }
+
+            // 查询套餐信息
+            $coupon_list = Db::name('vip_coupon')->whereIn('id',$coupon_ids)->where('status',1)->select();
+            foreach ($coupon_list as $k => $v) {
+                $list[$key]['coupon_list'][$k] = [
+                    'id' => $v['id'],
+                    'image' => cdnurl($v['image']),
+                    'content' => "【{$v['name']}】【{$v['end_days']}日内使用有效】 \n {$v['info']}",
+                ];
+            }
+        }
+        $this->success('success', $list);
     }
 
     //vip用的,创建订单
     public function vip_recharge()
     {
+        $user_id = $this->auth->id;
+        $params = $this->request->param();
+        if (empty($params['vip_id'])) {
+            return $this->error('参数缺失');
+        }
+        if (empty($params['pay_type']) || empty($params['platform'])) {
+            return $this->error('请选择支付方式');
+        }
 
-        $rc_id    = input('rc_id', 0);
-        $pay_type = input('pay_type', 'wechat');
-        $platform = 'app';
-        $uid      = $this->auth->id;
+        if (!$vip_config = Db::name('vip_config')->where('id', $params['vip_id'])->where('status',1)->find()){
+            return $this->error('套餐不存在或已下架');
+        }
 
-        if (!$rc_id) {
-            $this->error('请选择会员套餐');
+        // 判断是否使用过
+        if ($vip_config['is_try'] == 1 && Db::name('vip_order')->where(['user_id' => $user_id,'vip_id' => $params['vip_id'],'status'=>1])->value('id') > 0){
+            return $this->error('您已经试用过了');
         }
 
-        if (!$this->user_auth_limit()) {
-            $this->error('请先完成实名认证');
+        if ($vip_config['is_try'] != 1 && $vip_config['price'] <= 0){
+            return $this->error('套餐价格不能为0');
         }
 
+        // 查询套餐信息
+        $coupon_ids = explode(',',$vip_config['coupon_ids']);
+        $coupon_list = Db::name('vip_coupon')->whereIn('id',$coupon_ids)->where('status',1)->select();
 
         //赋值money
-        $recharge_config = Db::name('payvip_config')->where('id', $rc_id)->find();
-        $money           = $recharge_config['money'];
-
-        if ($money <= 0) {
-            $this->error('支付金额必须大于0');
+        $pay_amount = $vip_config['is_try'] != 1 ? $vip_config['price'] : '0.00';
+        $nowTime    = time();
+        $data       = [
+            'user_id'     => $user_id,
+            'vip_id'      => $params['vip_id'],
+            'order_no'    => createUniqueNo('E', $user_id),
+            'pay_amount'  => $pay_amount,
+            'pay_time'    => $vip_config['is_try'] != 1 ? 0 : $nowTime,
+            'status'      => $vip_config['is_try'] != 1 ? 0 : 1,// 如果是试用会员 则无需支付
+            'create_time' => $nowTime
+        ];
+        Db::startTrans();
+        if (!$order_id = Db::name('vip_order')->insertGetId($data)) {
+            Db::rollback();
+            return $this->error('订单创建失败');
         }
-        if ($money > 10000) {
-            $this->error('支付金额太大');
+        $user_vip_coupon = [];
+        foreach ($coupon_list as $k => $v) {
+            for ($i = 0;$i < $v['num'];$i++){
+                $user_vip_coupon[] = [
+                    'coupon_id'           => $v['id'],
+                    'user_id'             => $user_id,
+                    'order_id'            => $order_id,
+                    'coupon_no'           => \app\utils\Common::createNo('C0',8),
+                    'type'                => $v['type'],
+                    'name'                => $v['name'],
+                    'info'                => $v['info'],
+                    'end_time'            => strtotime('+' . $v['end_days'] . ' day'),
+                    'use_frequency_day'   => $v['use_frequency_day'],
+                    'use_frequency_times' => $v['use_frequency_times'],
+                    'status' => $vip_config['is_try'] != 1 ? 0 : 1,
+                    'create_time' => $nowTime,
+                ];
+            }
         }
+        if (!Db::name('vip_coupon_user')->insertAll($user_vip_coupon)) {
+            Db::rollback();
+            return $this->error('订单创建失败');
+        }
+        Db::commit();
 
+        if ($vip_config['is_try'] != 1){
+            // 创建支付订单
+            $remark    = 'VIP套餐购买';
+            $orderData = [
+                'user_id'      => $user_id,
+                'out_trade_no' => $data['order_no'],
+                'order_amount' => $data['pay_amount'],
+                'pay_type'     => $params['pay_type'],
+                'platform'     => $params['platform'],
+                'table_name'   => 'university_event_apply',
+                'table_id'     => $order_id,
+                'createtime'   => time(),
+                'args'         => json_encode([
+                    'table_id' => $order_id,
+                    'remark'   => $remark
+                ], JSON_UNESCAPED_UNICODE),
+            ];
+            if (!Db::name('pay_order')->insert($orderData)) {
+                return $this->error('订单创建失败');
+            }
 
-        //创建订单
-        $data                 = [];
-        $data['user_id']      = $uid;
-        $data['out_trade_no'] = createUniqueNo('P', $uid); // 数据库订单号加密
-        $data['order_amount'] = $money;
-        $data['createtime']   = time();
-
-        $data['pay_type']     = $pay_type;
-        $data['platform']     = $platform;
-        $data['order_status'] = 0;
-        $data['table_name']   = 'vip_recharge';
-        $data['table_id']     = 0;
-        $data['args']         = json_encode(['days' => $recharge_config['days']]);
-
-        $orderid = Db::name('pay_order')->insertGetId($data);
-
-//        $openid = $this->auth->mini_openid;
-        //下单
-        $params = [
-            'type'      => $pay_type,
-            'orderid'   => $data['out_trade_no'],
-            'title'     => '支付订单',
-            'amount'    => $data['order_amount'],
-            'method'    => $platform,
-//            'openid'       => $openid,
-            'notifyurl' => config('pay_notify_url') . '/api/notify/vip_notify_base/paytype/' . $pay_type,
-            'returnurl' => '',
-        ];
+            // 拉起支付 余额支付
+            if ($params['pay_type'] == 'wallet') {
+                Db::startTrans();
+                //钱包更新
+                $walletService = new Wallet();
+                if (!$walletService->change($user_id, -$orderData['order_amount'], 'money', 20, $remark, $orderData['table_name'], $orderData['table_id'])) {
+                    Db::rollback();
+                    return $this->error($walletService->getMessage());
+                }
+                // 支付成功,更改支付金额
+                [$res,$msg] = PayOrderModel::vip($orderData['out_trade_no']);
+                if (!$res){
+                    Db::rollback();
+                    return $this->error($msg);
+                }
+                Db::commit();
+                return $this->success('支付成功');
+            }
 
-        $res = Service::submitOrder($params);
-
-        if ($pay_type == 'wechat') {
-            $this->success('success', json_decode($res, true));
-        } else {
-            $this->success('success', $res);
+            // 第三方支付下单
+            $params = [
+                'type'      => $orderData['pay_type'],
+                'orderid'   => $orderData['out_trade_no'],
+                'title'     => $remark,
+                'amount'    => $orderData['order_amount'],
+                'method'    => $orderData['platform'],
+                'notifyurl' => CurlUtil::getHttp("/api/notify/university_event_{$params['pay_type']}"),
+                'returnurl' => '',
+            ];
+            // 如果是小程序则需要添加 openid
+            if ($params['pay_type'] == 'wechat' && $params['platform'] == 'miniapp') {
+                $params['openid'] = $this->auth->mini_openid;
+            }
+            $res = Service::submitOrder($params);
+            if ($params['pay_type'] == 'wechat') {
+                $this->success('下单成功', json_decode($res, true));
+            } else {
+                $this->success('下单成功', $res);
+            }
         }
+
+        return $this->success('试用成功');
     }
 
     //人民币充值

+ 65 - 0
application/api/controller/Vip.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace app\api\controller;
+
+use app\common\controller\Api;
+use app\common\model\PayOrderModel;
+use app\common\model\Wallet;
+use app\utils\CurlUtil;
+use think\Db;
+use addons\epay\library\Service;
+
+/**
+ * 充值配置与充值订单
+ */
+class Vip extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = ['*'];
+
+    // VIP抵扣券
+    public function coupon()
+    {
+        $user_id = $this->auth->id;
+        $list = Db::name('vip_coupon_user')
+            ->field(['id','coupon_no','type','name','info','end_time','use_frequency_day','use_frequency_times','status','create_time'])
+            ->where('user_id',$user_id)
+            ->whereIn('status',[1,2])
+            ->order('id desc')
+            ->autopage()
+            ->select();
+
+        foreach ($list as $key=>$val){
+            $list[$key]['end_time_text'] = date('Y-m-d H:i',$val['end_time']);
+            $list[$key]['create_time_text'] = date('Y-m-d H:i',$val['create_time']);
+        }
+
+        $this->success('success', $list);
+    }
+
+    // 使用抵扣券
+    public function coupon_use()
+    {
+        $user_id = $this->auth->id;
+        $params = $this->request->param();
+        if (empty($params['coupon_id'])){
+            $this->error('请选择要使用的抵扣券');
+        }
+
+        $info = Db::name('vip_coupon_user')
+            ->where('id',$params['coupon_id'])
+            ->where('user_id',$user_id)
+            ->whereIn('status',[1,2])
+            ->find();
+        if (!$info){
+            $this->error('不存在的券');
+        }
+        if ($info['status'] != 1){
+            $this->error('券已使用');
+        }
+        if (!Db::name('vip_coupon_user')->where(['id'=>$params['coupon_id'],'status'=>1])->update(['status'=>2])){
+            $this->error('操作失败');
+        }
+        $this->error('使用成功');
+    }
+}

+ 33 - 0
application/common/model/PayOrderModel.php

@@ -86,4 +86,37 @@ class PayOrderModel extends Model
         Db::commit();
         return [true,'操作成功'];
     }
+
+    /**
+     * 老年大学活动支付回调
+     * @param $out_trade_no
+     * @return array
+     */
+    public static function vip($out_trade_no)
+    {
+        Db::startTrans();
+        if (!$info = self::where('out_trade_no',$out_trade_no)->lock(true)->find()){
+            Db::rollback();
+            return [false,'未找到订单'];
+        }
+        if ($info['order_status'] == 1){
+            Db::rollback();
+            return [true,'订单已支付'];
+        }
+        //更新订单状态
+        if (!self::where(['id' => $info['id']])->update(['status' => 1])) {
+            Db::rollback();
+            return [false,'订单状态更新失败'];
+        }
+        if (!VipOrderModel::where('id',$info['table_id'])->update(['status' => 1,'pay_time' => time()])) {
+            Db::rollback();
+            return [false,'VIP订单状态更新失败'];
+        }
+        if (!VipCouponUserModel::where('order_id',$info['table_id'])->update(['status' => 1,'update_time' => time()])) {
+            Db::rollback();
+            return [false,'VIP订单状态更新失败'];
+        }
+        Db::commit();
+        return [true,'操作成功'];
+    }
 }

+ 21 - 0
application/common/model/VipConfigModel.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\common\model;
+
+use think\Db;
+use think\Model;
+
+/**
+ * 群组
+ */
+class VipConfigModel extends Model
+{
+    // 表名
+    protected $name = 'vip_config';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+}

+ 21 - 0
application/common/model/VipCouponModel.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\common\model;
+
+use think\Db;
+use think\Model;
+
+/**
+ * 群组
+ */
+class VipCouponModel extends Model
+{
+    // 表名
+    protected $name = 'vip_coupon';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+}

+ 21 - 0
application/common/model/VipCouponUserModel.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\common\model;
+
+use think\Db;
+use think\Model;
+
+/**
+ * 群组
+ */
+class VipCouponUserModel extends Model
+{
+    // 表名
+    protected $name = 'vip_coupon_user';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+}

+ 21 - 0
application/common/model/VipOrderModel.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\common\model;
+
+use think\Db;
+use think\Model;
+
+/**
+ * 群组
+ */
+class VipOrderModel extends Model
+{
+    // 表名
+    protected $name = 'vip_order';
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = false;
+    // 定义时间戳字段名
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+}

+ 45 - 0
application/utils/Hashids.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\utils;
+
+use app\utils\Hashids\Hashids as HashidsBase;
+use think\Env;
+use think\Config;
+
+class Hashids
+{
+    private static $hashids;
+
+    /**
+     * 单列模型实例化
+     * @param $salt
+     * @param $hashLength
+     * @return HashidsBase
+     */
+    public static function getInstanceHashids($salt, $hashLength)
+    {
+        if (!self::$hashids instanceof HashidsBase) {
+            self::$hashids =  new HashidsBase($salt, $hashLength, Env::get('hashids.alphabet', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'));
+        }
+        return self::$hashids;
+    }
+
+    public static function encodeHex($str, $hashLength = 8)
+    {
+        $salt = Env::get('hashids.salt', Config::get('token.key'));
+
+        $hashids = self::getInstanceHashids($salt, $hashLength);
+
+        return $hashids->encodeHex($str);
+    }
+
+    public static function decodeHex($str, $hashLength = 8)
+    {
+        $salt = Env::get('hashids.salt', Config::get('token.key'));
+
+        $hashids = self::getInstanceHashids($salt, $hashLength);
+
+        return $hashids->decodeHex($str);
+    }
+
+}

+ 422 - 0
application/utils/Hashids/Hashids.php

@@ -0,0 +1,422 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids;
+
+use app\utils\Hashids\Math\Bc;
+use app\utils\Hashids\Math\Gmp;
+use app\utils\Hashids\Math\MathInterface;
+use RuntimeException;
+
+class Hashids implements HashidsInterface
+{
+    /**
+     * The seps divider.
+     *
+     * @var float
+     */
+    public const SEP_DIV = 3.5;
+
+    /**
+     * The guard divider.
+     *
+     * @var float
+     */
+    public const GUARD_DIV = 12;
+
+    /**
+     * The alphabet string.
+     *
+     * @var string
+     */
+    protected $alphabet;
+
+    /**
+     * Shuffled alphabets, referenced by alphabet and salt.
+     *
+     * @var array
+     */
+    protected $shuffledAlphabets;
+
+    /**
+     * The seps string.
+     *
+     * @var string
+     */
+    protected $seps = 'cfhistuCFHISTU';
+
+    /**
+     * The guards string.
+     *
+     * @var string
+     */
+    protected $guards;
+
+    /**
+     * The minimum hash length.
+     *
+     * @var int
+     */
+    protected $minHashLength;
+
+    /**
+     * The salt string.
+     *
+     * @var string
+     */
+    protected $salt;
+
+    /**
+     * The math class.
+     *
+     * @var \Hashids\Math\MathInterface
+     */
+    protected $math;
+
+    /**
+     * Create a new hashids instance.
+     *
+     * @param string $salt
+     * @param int $minHashLength
+     * @param string $alphabet
+     *
+     * @throws \Hashids\HashidsException
+     */
+    public function __construct($salt = '', $minHashLength = 0, $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
+    {
+        $this->salt = \mb_convert_encoding($salt, 'UTF-8', \mb_detect_encoding($salt));
+        $this->minHashLength = $minHashLength;
+        $alphabet = \mb_convert_encoding($alphabet, 'UTF-8', \mb_detect_encoding($alphabet));
+        $this->alphabet = \implode('', \array_unique($this->multiByteSplit($alphabet)));
+        $this->math = $this->getMathExtension();
+
+        if (\mb_strlen($this->alphabet) < 16) {
+            throw new HashidsException('Alphabet must contain at least 16 unique characters.');
+        }
+
+        if (false !== \mb_strpos($this->alphabet, ' ')) {
+            throw new HashidsException('Alphabet can\'t contain spaces.');
+        }
+
+        $alphabetArray = $this->multiByteSplit($this->alphabet);
+        $sepsArray = $this->multiByteSplit($this->seps);
+        $this->seps = \implode('', \array_intersect($sepsArray, $alphabetArray));
+        $this->alphabet = \implode('', \array_diff($alphabetArray, $sepsArray));
+        $this->seps = $this->shuffle($this->seps, $this->salt);
+
+        if (!$this->seps || (\mb_strlen($this->alphabet) / \mb_strlen($this->seps)) > self::SEP_DIV) {
+            $sepsLength = (int) \ceil(\mb_strlen($this->alphabet) / self::SEP_DIV);
+
+            if ($sepsLength > \mb_strlen($this->seps)) {
+                $diff = $sepsLength - \mb_strlen($this->seps);
+                $this->seps .= \mb_substr($this->alphabet, 0, $diff);
+                $this->alphabet = \mb_substr($this->alphabet, $diff);
+            }
+        }
+
+        $this->alphabet = $this->shuffle($this->alphabet, $this->salt);
+        $guardCount = (int) \ceil(\mb_strlen($this->alphabet) / self::GUARD_DIV);
+
+        if (\mb_strlen($this->alphabet) < 3) {
+            $this->guards = \mb_substr($this->seps, 0, $guardCount);
+            $this->seps = \mb_substr($this->seps, $guardCount);
+        } else {
+            $this->guards = \mb_substr($this->alphabet, 0, $guardCount);
+            $this->alphabet = \mb_substr($this->alphabet, $guardCount);
+        }
+    }
+
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers): string
+    {
+        $ret = '';
+
+        if (1 === \count($numbers) && \is_array($numbers[0])) {
+            $numbers = $numbers[0];
+        }
+
+        if (!$numbers) {
+            return $ret;
+        }
+
+        foreach ($numbers as $number) {
+            $isNumber = \ctype_digit((string) $number);
+
+            if (!$isNumber) {
+                return $ret;
+            }
+        }
+
+        $alphabet = $this->alphabet;
+        $numbersSize = \count($numbers);
+        $numbersHashInt = 0;
+
+        foreach ($numbers as $i => $number) {
+            $numbersHashInt += $this->math->intval($this->math->mod($number, $i + 100));
+        }
+
+        $lottery = $ret = \mb_substr($alphabet, $numbersHashInt % \mb_strlen($alphabet), 1);
+        foreach ($numbers as $i => $number) {
+            $alphabet = $this->shuffle($alphabet, \mb_substr($lottery . $this->salt . $alphabet, 0, \mb_strlen($alphabet)));
+            $ret .= $last = $this->hash($number, $alphabet);
+
+            if ($i + 1 < $numbersSize) {
+                $number %= (\mb_ord($last, 'UTF-8') + $i);
+                $sepsIndex = $this->math->intval($this->math->mod($number, \mb_strlen($this->seps)));
+                $ret .= \mb_substr($this->seps, $sepsIndex, 1);
+            }
+        }
+
+        if (\mb_strlen($ret) < $this->minHashLength) {
+            $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 0, 1), 'UTF-8')) % \mb_strlen($this->guards);
+
+            $guard = \mb_substr($this->guards, $guardIndex, 1);
+            $ret = $guard . $ret;
+
+            if (\mb_strlen($ret) < $this->minHashLength) {
+                $guardIndex = ($numbersHashInt + \mb_ord(\mb_substr($ret, 2, 1), 'UTF-8')) % \mb_strlen($this->guards);
+                $guard = \mb_substr($this->guards, $guardIndex, 1);
+
+                $ret .= $guard;
+            }
+        }
+
+        $halfLength = (int) (\mb_strlen($alphabet) / 2);
+        while (\mb_strlen($ret) < $this->minHashLength) {
+            $alphabet = $this->shuffle($alphabet, $alphabet);
+            $ret = \mb_substr($alphabet, $halfLength) . $ret . \mb_substr($alphabet, 0, $halfLength);
+
+            $excess = \mb_strlen($ret) - $this->minHashLength;
+            if ($excess > 0) {
+                $ret = \mb_substr($ret, (int) ($excess / 2), $this->minHashLength);
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash): array
+    {
+        $ret = [];
+
+        if (!\is_string($hash) || !($hash = \trim($hash))) {
+            return $ret;
+        }
+
+        $alphabet = $this->alphabet;
+
+        $hashBreakdown = \str_replace($this->multiByteSplit($this->guards), ' ', $hash);
+        $hashArray = \explode(' ', $hashBreakdown);
+
+        $i = 3 === \count($hashArray) || 2 === \count($hashArray) ? 1 : 0;
+
+        $hashBreakdown = $hashArray[$i];
+
+        if ('' !== $hashBreakdown) {
+            $lottery = \mb_substr($hashBreakdown, 0, 1);
+            $hashBreakdown = \mb_substr($hashBreakdown, 1);
+
+            $hashBreakdown = \str_replace($this->multiByteSplit($this->seps), ' ', $hashBreakdown);
+            $hashArray = \explode(' ', $hashBreakdown);
+
+            foreach ($hashArray as $subHash) {
+                $alphabet = $this->shuffle($alphabet, \mb_substr($lottery . $this->salt . $alphabet, 0, \mb_strlen($alphabet)));
+                $result = $this->unhash($subHash, $alphabet);
+                if ($this->math->greaterThan($result, PHP_INT_MAX)) {
+                    $ret[] = $this->math->strval($result);
+                } else {
+                    $ret[] = $this->math->intval($result);
+                }
+            }
+
+            if ($this->encode($ret) !== $hash) {
+                $ret = [];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str): string
+    {
+        if (!\ctype_xdigit((string) $str)) {
+            return '';
+        }
+
+        $numbers = \trim(chunk_split($str, 12, ' '));
+        $numbers = \explode(' ', $numbers);
+
+        foreach ($numbers as $i => $number) {
+            $numbers[$i] = \hexdec('1' . $number);
+        }
+
+        return $this->encode(...$numbers);
+    }
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash): string
+    {
+        $ret = '';
+        $numbers = $this->decode($hash);
+
+        foreach ($numbers as $i => $number) {
+            $ret .= \mb_substr(dechex($number), 1);
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Shuffle alphabet by given salt.
+     *
+     * @param string $alphabet
+     * @param string $salt
+     *
+     * @return string
+     */
+    protected function shuffle($alphabet, $salt): string
+    {
+        $key = $alphabet . ' ' . $salt;
+
+        if (isset($this->shuffledAlphabets[$key])) {
+            return $this->shuffledAlphabets[$key];
+        }
+
+        $saltLength = \mb_strlen($salt);
+        $saltArray = $this->multiByteSplit($salt);
+        if (!$saltLength) {
+            return $alphabet;
+        }
+        $alphabetArray = $this->multiByteSplit($alphabet);
+        for ($i = \mb_strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
+            $v %= $saltLength;
+            $p += $int = \mb_ord($saltArray[$v], 'UTF-8');
+            $j = ($int + $v + $p) % $i;
+
+            $temp = $alphabetArray[$j];
+            $alphabetArray[$j] = $alphabetArray[$i];
+            $alphabetArray[$i] = $temp;
+        }
+        $alphabet = \implode('', $alphabetArray);
+        $this->shuffledAlphabets[$key] = $alphabet;
+
+        return $alphabet;
+    }
+
+    /**
+     * Hash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return string
+     */
+    protected function hash($input, $alphabet): string
+    {
+        $hash = '';
+        $alphabetLength = \mb_strlen($alphabet);
+
+        do {
+            $hash = \mb_substr($alphabet, $this->math->intval($this->math->mod($input, $alphabetLength)), 1) . $hash;
+
+            $input = $this->math->divide($input, $alphabetLength);
+        } while ($this->math->greaterThan($input, 0));
+
+        return $hash;
+    }
+
+    /**
+     * Unhash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return int
+     */
+    protected function unhash($input, $alphabet)
+    {
+        $number = 0;
+        $inputLength = \mb_strlen($input);
+
+        if ($inputLength && $alphabet) {
+            $alphabetLength = \mb_strlen($alphabet);
+            $inputChars = $this->multiByteSplit($input);
+
+            foreach ($inputChars as $char) {
+                $position = \mb_strpos($alphabet, $char);
+                $number = $this->math->multiply($number, $alphabetLength);
+                $number = $this->math->add($number, $position);
+            }
+        }
+
+        return $number;
+    }
+
+    /**
+     * Get BC Math or GMP extension.
+     *
+     * @codeCoverageIgnore
+     *
+     * @throws \RuntimeException
+     *
+     * @return \Hashids\Math\MathInterface
+     */
+    protected function getMathExtension(): MathInterface
+    {
+        if (\extension_loaded('gmp')) {
+            return new Gmp();
+        }
+
+        if (\extension_loaded('bcmath')) {
+            return new Bc();
+        }
+
+        throw new RuntimeException('Missing BC Math or GMP extension.');
+    }
+
+    /**
+     * Replace simple use of $this->multiByteSplit with multi byte string.
+     *
+     * @param string $string
+     *
+     * @return array<int, string>
+     */
+    protected function multiByteSplit($string): array
+    {
+        return \preg_split('/(?!^)(?=.)/u', $string) ?: [];
+    }
+}

+ 19 - 0
application/utils/Hashids/HashidsException.php

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids;
+
+use InvalidArgumentException;
+
+class HashidsException extends InvalidArgumentException
+{
+    //
+}

+ 51 - 0
application/utils/Hashids/HashidsInterface.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids;
+
+interface HashidsInterface
+{
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers);
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash);
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str);
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash);
+}

+ 116 - 0
application/utils/Hashids/Math/Bc.php

@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids\Math;
+
+class Bc implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return \bcadd($a, $b, 0);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return \bcmul($a, $b, 0);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return \bcdiv($a, $b, 0);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return \bcmod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return \bccomp($a, $b, 0) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return \intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return $a;
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return $a;
+    }
+}

+ 116 - 0
application/utils/Hashids/Math/Gmp.php

@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids\Math;
+
+class Gmp implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return \gmp_add($a, $b);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return \gmp_mul($a, $b);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return \gmp_div_q($a, $b);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return \gmp_mod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return \gmp_cmp($a, $b) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return \gmp_intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return \gmp_strval($a);
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return \gmp_init($a);
+    }
+}

+ 92 - 0
application/utils/Hashids/Math/MathInterface.php

@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * Copyright (c) Ivan Akimov.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @see https://github.com/vinkla/hashids
+ */
+
+namespace app\utils\Hashids\Math;
+
+interface MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b);
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b);
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b);
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d);
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b);
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a);
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a);
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a);
+}