PHP 批量生成兑换码实例


PHP 批量生成兑换码实例


正文

项目开发中碰到一个需求:企业端发起一条生成企业专用购物券申请,内容包含生成多少张、每张多少元的兑换券; Admin管理端对该申请进行审核,审核通过后,企业端可以看到这一条企业专用购物券申请中的大量兑换券码,然后把券码分发给每个人。 收到券码的人登录项目对外平台,然后用券码兑换企业专用购物券。兑换成功后,点击该企业专用购物券详细使用说明,点击使用, 开始在企业商品页面进行下单。

Admin管理端对申请列表进行一条记录审核那里,审核通过时推送队列,队列消费者来生成该条申请记录的一批兑换券码。

先看下队列消费者中的代码实现:

<?php
// 获取企业模型
$EnterpriseCoucherModel = EnterpriseCoucher::where('enterprise_id', 101)->find();

// 企业ID的二进制数据,企业ID最大为 9999999
$bin_enterprise_id = str_pad(decbin($EnterpriseCoucherModel->enterprise_id), 24, "0", STR_PAD_LEFT);
// 企业的第几次申请的二进制数据,最大为 99999
$bin_enterprise_apply_times = str_pad(decbin($EnterpriseCoucherModel->apply_times), 17, "0", STR_PAD_LEFT);

// 一批兑换券码数组
$coucher_codes = [];
// 兑换券码数组的键名数组
$array_keys = [];
// 循环生成指定次数的兑换券码
for ($i = 0; $i < $EnterpriseCoucherModel->apply_num; $i++) {
    // 如果兑换券吗未生成,或者生成了但在可兑换券码数组中已存在,则继续生成
    while (empty($code) || (!empty($code) && in_array($code, $array_keys))) {
        // 头部随机数
        $num_header = random_int(1, 99999);
        // 尾部验证数字
        $num_tail = substr($num_header, -1);

        // 补齐长度
        $bin_header = str_pad(decbin($num_header), 17, "0", STR_PAD_LEFT);
        $bin_tail = str_pad(decbin($num_tail), 4, "0", STR_PAD_LEFT);

        // 凑成64位长度二进制数据
        $bin_code = '0' .$bin_header  .$bin_enterprise_id .$bin_enterprise_apply_times . $bin_tail . '0';

        // 转化为32位进制的数据,并转为大写
        $code = strtoupper(base_convert($bin_code, 2, 32));
    }

    $coucher_codes[$code] = 0;  // 默认该券码未兑换
    array_push($array_keys, $code);
}

$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();

// 保存
$EnterpriseCoucherModel->save();
?>

生成的券码如:4MHQ00108008B,为13长度的字符串。

兑换逻辑:

<?php
/* 验证 */
// 兑换码
if (!isset($post["code"]) || empty($post["code"])) {
    $this->error('请输入兑换码');
}
$code = $post["code"];
$code_lower = strtolower($code);
$bin_code = str_pad(base_convert($code_lower, 32, 2), 64, "0", STR_PAD_LEFT);

$bin_header = substr($bin_code, 1, 17);
$bin_enterprise_id = substr($bin_code, 18, 24);
$bin_enterprise_apply_times = substr($bin_code, 42, 17);
$bin_tail = substr($bin_code, 59, 4);

$dec_header = bindec($bin_header);
$dec_enterprise_id = bindec($bin_enterprise_id);
$dec_enterprise_apply_times = bindec($bin_enterprise_apply_times);
$dec_tail = bindec($bin_tail);

if (substr($dec_header, -1) != $dec_tail) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}

$EnterpriseCoucherModel = EnterpriseCoucher::where([
    'enterprise_id' => $dec_enterprise_id,
    'apply_times' => $dec_enterprise_apply_times,
    'audit_status' => 20
])->find();
if (empty($EnterpriseCoucherModel)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}

$coucher_codes = json_decode($EnterpriseCoucherModel->coucher_codes, true);
$array_keys = array_keys($coucher_codes);
if (!in_array($code, $array_keys)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}
if ($coucher_codes[$code] == 1) {
    $this->error('该券码已被兑换');
}

/* 写入 */
$coucher_codes[$code] = 1;
$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();
// 保存
$EnterpriseCoucherModel->save();
?>

优化一

上面逻辑运行后发现一个问题,就是输入相似 对券码 可以兑换成功,如 4MHQ00108008B ,我输入 4MHQ00108009B 也能兑换成功, 原因是碰巧到了有第二条申请记录的存在,所以兑换成功。

解决办法是对该 对券码 加密,然后跟上几位加密后的 字符。如:

队列消费者中的代码实现:

<?php
// 获取企业模型
$EnterpriseCoucherModel = EnterpriseCoucher::where('enterprise_id', 101)->find();

// 企业ID的二进制数据,企业ID最大为 9999999
$bin_enterprise_id = str_pad(decbin($EnterpriseCoucherModel->enterprise_id), 24, "0", STR_PAD_LEFT);
// 企业的第几次申请的二进制数据,最大为 99999
$bin_enterprise_apply_times = str_pad(decbin($EnterpriseCoucherModel->apply_times), 17, "0", STR_PAD_LEFT);

// 密匙
$key = '123abcdef';
// 一批兑换券码数组
$coucher_codes = [];
// 兑换券码数组的键名数组
$array_keys = [];
// 循环生成指定次数的兑换券码
for ($i = 0; $i < $EnterpriseCoucherModel->apply_num; $i++) {
    // 如果兑换券吗未生成,或者生成了但在可兑换券码数组中已存在,则继续生成
    while (empty($code) || (!empty($code) && in_array($code, $array_keys))) {
        // 头部随机数
        $num_header = random_int(1, 99999);
        // 尾部验证数字
        $num_tail = substr($num_header, -1);

        // 补齐长度
        $bin_header = str_pad(decbin($num_header), 17, "0", STR_PAD_LEFT);
        $bin_tail = str_pad(decbin($num_tail), 4, "0", STR_PAD_LEFT);

        // 凑成64位长度二进制数据
        $bin_code = '0' .$bin_header  .$bin_enterprise_id .$bin_enterprise_apply_times . $bin_tail . '0';
        
        $code_lower = base_convert($bin_code, 2, 32);
        // 加密
        $code_ept = hash_hmac('sha256', $code_lower, $key);
        $code_lower .= mb_substr($code_ept, -3);

        // 转化为32位进制的数据,并转为大写
        $code = strtoupper($code_lower);
    }

    $coucher_codes[$code] = 0;  // 默认该券码未兑换
    array_push($array_keys, $code);
}

$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();

// 保存
$EnterpriseCoucherModel->save();
?>

生成的券码如:4MHQ00108008B0FO,为16长度的字符串。

兑换逻辑:

<?php
/* 验证 */
// 兑换码
if (!isset($post["code"]) || empty($post["code"])) {
    $this->error('请输入兑换码');
}
$code = $post["code"];
$code_lower = strtolower($code);
$code_str = mb_substr($code_lower, 0, -3);
$code_ept = mb_substr($code_lower, -3);

// 加密验证
$key = '123abcdef';
$hsah_string = hash_hmac('sha256', $code_str, $key);
$hsah_ept = mb_substr($hsah_string, -3);
if ($code_ept != $hsah_ept) {
    $this->error('输入错误,请仔细核对券码');
}

 // 64位验证
$bin_code = str_pad(base_convert($code_str, 32, 2), 64, "0", STR_PAD_LEFT);

$bin_header = substr($bin_code, 1, 17);
$bin_enterprise_id = substr($bin_code, 18, 24);
$bin_enterprise_apply_times = substr($bin_code, 42, 17);
$bin_tail = substr($bin_code, 59, 4);

$dec_header = bindec($bin_header);
$dec_enterprise_id = bindec($bin_enterprise_id);
$dec_enterprise_apply_times = bindec($bin_enterprise_apply_times);
$dec_tail = bindec($bin_tail);

if (substr($dec_header, -1) != $dec_tail) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}

$EnterpriseCoucherModel = EnterpriseCoucher::where([
    'enterprise_id' => $dec_enterprise_id,
    'apply_times' => $dec_enterprise_apply_times,
    'audit_status' => 20
])->find();
if (empty($EnterpriseCoucherModel)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}

$coucher_codes = json_decode($EnterpriseCoucherModel->coucher_codes, true);
$array_keys = array_keys($coucher_codes);
if (!in_array($code, $array_keys)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}
if ($coucher_codes[$code] == 1) {
    $this->error('该券码已被兑换');
}

/* 写入 */
$coucher_codes[$code] = 1;
$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();
// 保存
$EnterpriseCoucherModel->save();
?>

优化二

上面生成的 对券码,具体如:4MHQ00108008B0FO,发现存在 0 / O / D 不分,还有 8 / B ,1 / I,我们可以把 B、D、I、O 去掉; 小写的有 0 / o , 1 / l / i , 8 / a ,我们可以把 a, i, l, o 这几个去掉。

突然想到了 GeoHash 地理位置算法,可以参考 https://ibaiyang.github.io/blog/it技术/2020/07/30/LBS经纬度编码搜索实现.html , 里面用到了自己定义的 32位 替换法,受到启发,为什么这里不可以这样使用呢? 所以这里我们用自己定义的替换方法来实现,类比于 编码转换,但不同于编码转换。

实现方式是:前面6位为自定义随机字符串,中间为数据ID,后面4位为自定义随机字符串。

公式:6位字符 + 数据ID + 4位字符。

前面6位字符适用于 万级别 的券码数量。中间的数据ID放开了对长度的限制。 后面4位字符类似于加密,其实加密不就是生成只有我们自己知道的字符串吗,我们这里把字符串存在数据库中。

下面具体看下实例。

队列消费者中的代码实现:

<?php
// 获取企业模型
$EnterpriseCoucherModel = EnterpriseCoucher::where('enterprise_id', 101)->find();

//字符组合
$source_str = 'ACEFGHJKLMNPQRSTUVWXYZ123456789';
$source_len = strlen($source_str) - 1;

// 一批兑换券码数组
$coucher_codes = [];
// 兑换券码数组的键名数组
$array_keys = [];
// 循环生成指定次数的兑换券码
for ($i = 0; $i < $EnterpriseCoucherModel->apply_num; $i++) {
    // 如果兑换券吗未生成,或者生成了但在可兑换券码数组中已存在,则继续生成
    while (empty($code) || (!empty($code) && in_array($code, $array_keys))) {
        $code_str = '';
        for ($j = 0; $j < 6; $j++) {
            $rand_key = mt_rand(0, $source_len);
            $code_str .= $source_str[$rand_key];
        }

        $code_str .= $yq_shop_coucher_id;

        for ($j = 0; $j < 4; $j++) {
            $rand_key = mt_rand(0, $source_len);
            $code_str .= $source_str[$rand_key];
        }
    }

    $coucher_codes[$code] = 0;  // 默认该券码未兑换
    array_push($array_keys, $code);
}

$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();

// 保存
$EnterpriseCoucherModel->save();
?>

生成的券码如:5624VW32V472,为12长度的字符串。

兑换逻辑:

<?php
/* 验证 */
// 兑换码
if (!isset($post["code"]) || empty($post["code"])) {
    $this->error('请输入兑换码');
}
$code = $post["code"];

$str_len = mb_strlen($code);
if ($str_len <= (6 + 4)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}
$coucher_id = mb_substr($code, 6, ($str_len - 6 - 4));

$EnterpriseCoucherModel = EnterpriseCoucher::where([
    'id' => $coucher_id,
    'audit_status' => EnterpriseCoucher::AUDIT_STATUS_SUCC  // 审核成功
])->find();
if (empty($EnterpriseCoucherModel)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}

$coucher_codes = json_decode($EnterpriseCoucherModel->coucher_codes, true);
$array_keys = array_keys($coucher_codes);
if (!in_array($code, $array_keys)) {
    $this->error('输入错误,错误5次账号将被锁定,请仔细核对券码');
}
if ($coucher_codes[$code] == 1) {
    $this->error('该券码已被兑换');
}

/* 写入 */
$coucher_codes[$code] = 1;
$EnterpriseCoucherModel->coucher_codes = json_encode($coucher_codes);
$EnterpriseCoucherModel->update_time = time();
// 保存
$EnterpriseCoucherModel->save();
?>

附录

随机字符串

php生成随机数/生成随机字符串的方法。

第一种:mt_rand()

<?php
echo mt_rand(10,100);
?>

第二种:array_rand()数组

<?php
function make_password($length)
{
    // 密码字符集,可任意添加你需要的字符
    $str = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 
    'i', 'j', 'k', 'l','m', 'n', 'o', 'p', 'q', 'r', 's', 
    't', 'u', 'v', 'w', 'x', 'y','z', 'A', 'B', 'C', 'D', 
    'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L','M', 'N', 'O', 
    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y','Z', 
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
    // 在 $str 中随机取 $length 个数组元素键名
    $keys = array_rand($str, $length); 
    $password = '';
    for($i = 0; $i < $length; $i++)
    {
        // 将 $length 个数组元素连接成字符串
        $password .= $str[$keys[$i]];
    }
    return $password;
}

echo make_password(6);
?>

第三种:把字符串打乱,然后返回其中的一小截

<?php
function getrandstr($length){
    $str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
    $randStr = str_shuffle($str);//打乱字符串
    $rands= substr($randStr,0,$length);//substr(string,start,length);返回字符串的一部分
    return $rands;
}

echo getrandstr(6);
?>

第四种:返回任意随机数

<?php
//返回1000-9999其中的一个随机数
echo rand(1000,9999);
?>

第五种:对时间戳进行MD5加密,截取其中一部分

<?php
function token($length){
    $str = md5(time());
    $token = substr($str,5,$length);
    return $token;
}

echo token(6);
?>

还有多个字符串进行拼接,最后做md5加密或SHA1加密,然后返回字符串,这种比较普遍用于token验证或签名验证。






参考资料

php生成随机数/生成随机字符串的方法小结【5种方法】 https://www.jb51.net/article/187452.htm


返回