正文
项目开发中碰到一个需求:企业端发起一条生成企业专用购物券申请,内容包含生成多少张、每张多少元的兑换券; 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