高性能(无需判重)批量生成优惠券码方案,优惠券券码生成规则

4747 834 2023-02-20

本文转载自网络公开信息

本文讲述了高性能(无需判重)批量生成优惠券码方案,优惠券券码生成规则。

高性能(无需判重)批量生成优惠券码方案,优惠券券码生成规则

UUID方案:将uuid分成等份,转成16进制即可。(代码里有11位和8位数的券码代码参考)

话不多说,直接上代码,欢迎各位大佬留言指正

优惠券码生成工具类CouponCodeUtil2(基于uuid,测试使用,注意修改):

import java.util.HashSet;

import java.util.Random;

import java.util.Set;

public class CouponCodeUtil2 {

    private static final char[] FIX_STR = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M',

            'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',

            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',

            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',

            '2', '3', '4', '5', '6', '7', '8', '9'};

    private static final String STR = new String(FIX_STR);

    private static String strToSplitHex16(String uuid) {

        StringBuilder shortBuffer = new StringBuilder();

        //我们这里想要保证券码为11位,所以32位uuid加了一位随机数,再分成11等份;(如果是8位券码,则32位uuid分成8等份进行计算即可)

        for (int i = 0; i < 11; i++) {

            String str = uuid.substring(i * 3, i * 3 + 3);

            int x = Integer.parseInt(str, 16);

            shortBuffer.append(FIX_STR[x % FIX_STR.length]);

        }

        return shortBuffer.toString();

    }

    private static String strToSplitHex16V2(String uuid) {

        StringBuilder shortBuffer = new StringBuilder();

        //8位券码,则32位uuid分成8等份进行计算即可

        for (int i = 0; i < 8; i++) {

            String str = uuid.substring(i * 4, i * 4 + 4);

            int x = Integer.parseInt(str, 16);

            shortBuffer.append(FIX_STR[x % FIX_STR.length]);

        }

        return shortBuffer.toString();

    }

    /**

     * 产生优惠券编码的方法

     *

     * @return

     */

    public static String generateCouponCode() {

        String uuid = UUIDGenerator.get() + new Random().nextInt(10);

        return strToSplitHex16(uuid);

    }

    public static String generateCouponCodeV2() {

        String uuid = UUIDGenerator.get();

        return strToSplitHex16V2(uuid);

    }

    public static void main(String[] args) {

        System.out.println("---------------- 分割线 -----------------------");

        /*基于uuid生成:因为基于id产生的券码,被猜的可能性较大*/

        Set<String> hashSet = new HashSet<>(10000000);

        for (int i = 0; i < 10000000; i++) {

            //产生的券码

            String key = generateCouponCodeV2();

            if (hashSet.contains(key)) {

                System.out.println(key);

                System.out.println("我重复了-------------====================-----------------");

            } else {

                hashSet.add(key);

            }

        }

        System.out.println("---------------- 分割线 -----------------------");

    }

优惠券码生成工具类CouponCodeUtil1(:

import java.util.HashSet;

import java.util.Set;

public class CouponCodeUtil1 {

    private static final char[] FIX_STR = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M',

            'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',

            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',

            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',

            '2', '3', '4', '5', '6', '7', '8', '9'};

    private static final String STR = new String(FIX_STR);

    public static String fFToStr(long i) {

        StringBuilder sb = new StringBuilder();

        //55进制

        while (i >= 55) {

            long a = i % 55;

            i /= 55;

            sb.append(FIX_STR[Math.toIntExact(a)]);

        }

        sb.append(FIX_STR[Math.toIntExact(i)]);

        return sb.reverse().toString();

    }

    public static long strToFF(String str) {

        long r = 0L;

        char[] chars = str.toCharArray();

        for (int i = chars.length - 1, j = 0; i >= 0; i--, j++) {

            r += STR.indexOf(chars[i]) * Math.pow(55, j);

        }

        //todo 此处有坑,会有精度丢失问题,有时候转换出来的long值貌似会和原值不一样。。

        return r;

    }

    /**

     * 产生券码的方法

     *

     * @return

     */

    public static String generateCouponCode() {

        return fFToStr(SnowFlakeUtils.nextId());

    }

    public static void main(String[] args) {

        System.out.println("---------------- 分割线 -----------------------");

        Set<String> hashSet = new HashSet<>(10);

        for (int i = 0; i < 10; i++) {

            //产生的券码

            String key = generateCouponCode();

            if (hashSet.contains(key)) {

                System.out.println(key);

                System.out.println("我重复了-------------====================-----------------");

            } else {

                hashSet.add(key);

            }

        }

        System.out.println("---------------- 分割线 -----------------------");

        /*long nextId = SnowFlakeUtils.nextId();

        System.out.println(nextId);

        //转成55进制后的字符(即我们需要的券码)

        System.out.println(fFToStr(nextId));

        //55进制字符转换成10进制的值 todo 此处有坑,会有精度丢失问题,有时候转换出来的long值貌似会和原值不一样。。

        System.out.println(strToFF(fFToStr(nextId)));

        //理论上说,一个id为20位的10进制数值,用55进制不会超过12位字符。

        System.out.println(Math.pow(10, 20) >= Math.pow(55, 12));*/

    }

}

代码

代码仓库:地址

代码分支:master

博客:地址

简介

优惠券是常见的营销工具,每逢佳节必有促销活动,有活动就会有优惠券。线下活动通常限量提供优惠券,因此不需要特殊设计,但是互联网环境下,线上活动开展频繁,而且线上活动用户体量要比线下活动大很多,通常以万为单位进行发放,因此需要优化优惠券存储,降低空间成本(互联网活动通常采用广撒网的方式)。这里需要特别注意本文针对的优惠券需求,有以下几个特殊点:

预先生成,在活动正式开始前生成优惠券(生成指的是需要预先知道优惠券活动Id、优惠券面值、优惠券序号,例如活动id为1,面值100,满1000减100,优惠券序号1~1,000,000)

优惠券体量大,以万为单位,通常在10万级别以上

在互联网电商中的优惠券通常不需要预先生成,只需要在用户领取时分配优惠券信息即可,在线下和线上结合使用场景中,预生成方式很实用,在预生成的模式下,如果全量存储优惠券将浪费大量空间,因为优惠券不会100%被使用(瓶装饮料经常会出现输入优惠编码兑换奖品活动)。

需求

线上线下营销需要优惠券功能来支撑,传统的方式是生成优惠券方案,提供以下内容:

活动id

活动名称

优惠券面值

优惠券使用条件

发放条件

发放数量

创建优惠券方案后不会立即生成优惠券,只有当用户领取的时候才会生成用户优惠券记录,这是主流方案,可以避免生成大量优惠券记录,浪费存储空间,但是没办法满足需要预生成的要求。我们需要设置一套优惠券编码方案,既可以满足预生成要求,同时要避免浪费存储空间。

设计思路

预生成的优惠券号通常是一段无规则的字符串,类似于:yuIkJGGS,用户输入优惠编号领取对应的优惠券,yuIkJGGS经过解析可以得到活动id信息,

通过上述分析可以知道,优惠券通过活动id就可以知道优惠券详细信息,为了防止重复使用,我们可以为每一张优惠券设置一个唯一编号,同时为了验证优惠券的有效性,需要添加校验码信息,因此一个优惠券通常有以下字段组成:

活动id

优惠券编号

校验码

我们可以设置一个功能函数:H活动Id,优惠券编号) => code,输入活动Id、优惠券编号参数,输出code,我们将code设置为校验码,H可以是md5、sha256等一系列单项函数。

得到活动Id、优惠券编号、校验码之后我们通过对其进行编码得到优惠券号信息,我们假设编码函数为E,那么转换函数可以写成如下形式:

E(活动Id、优惠券编号, H(活动Id、优惠券编号)) => yuIkJGGS    假设输出是yuIkJGGS

那么我们只需要设计实现H、E两个函数即可。

H函数分析

H可以理解为一个签名函数,我们使用这个函数生成的code来鉴别真伪,方式他人仿照,那么这里有很多种实现方案,例如,我们可以参照区块链签名方案(注意这里指示为了讲解方便,本文不是按照这个方式实现)。

secp256k1(keccak256(活动Id + 优惠券编码), privateKey) => 签名数据

签名数据长度不满足要求,我们可以截取前[n, n+m]位(bit)作为校验码,

本文使用13bit来表示检验码,计算方式如下所示

前6位表示(活动Id + 优惠编码)组成数字中的二进制中1的个数,例如,活动id为2,二进制为10,优惠编码是5,二进制为101,那么(活动Id + 优惠编码)二进制是10101,1的个数为3

后7位表示活动Id+优惠编码组成的数字进行取模运算,具体看代码

伪代码如下所示:

/// couponSchemeId表示活动id, redeemSerialNum表示优惠券序号

/// COUPON_ID_BIT_LEN 表示活动Id二进制位数

redeemSerialNum = redeemSerialNum << COUPON_ID_BIT_LEN;

/// r 表示 活动id + 优惠券编号

long r = couponSchemeId | redeemSerialNum;

/// 计算二进制表示形式中的1的个数(前6位值)

long n = numOfOne(r);

/// 取模运算

long re = r % DIVISOR;

/// 加入后7位值

n = (n << REMAINDER_BIT_LEN) | re;

return n;

E函数分析

E函数将输入的参数进行编码生成无规则字符串,同时能够对编号后的字符串进行还原,因此不能使用hash这类单向函数。这里我们参考进制转换,例如,二进制只有两个元素(0,1),16进制有16个元素(0-9,a-f),16进制中的某一位可以有16种选择,而二进制只有2种选择,类似的我们要先寻找一个编码空间。

我们使用a-z,A-Z,数字0-9元素,同时去除了容易混淆的大写O、大写I,(60个元素):

abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789 

我们来回顾一下10进制转二进制运算规则:

十进制转换成二进制

    17

    17/2 = 8 ... 1

    8/2  = 4 ... 0

    4/2  = 2 ... 0

    2/2  = 1 ... 0

    1/2  = 0 ... 1

十进制转换成n进制类似

转换代码如下所示:

private static final char[] r =

            new char[]{'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', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',

                    'X', 'Z', 'Y', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}

private static final int l = r.length;

public static String enRedeemCode(long redeemNum) {

  /// buf存储余数信息,这里需要保证余数位数小于buf长度

  char[] buf = new char[32];

  int charPos = 32;

  while ((redeemNum / l) > 0) {

       /// 得到余数

       int ind = (int) (redeemNum % l);

       /// 将余数映射到r字符空间中,buf存储映射后的字符

       buf[--charPos] = r[ind];

       redeemNum /= l;

  }

  buf[--charPos] = r[(int) (redeemNum % l)];

  /// 将存储的字符数组转化为字符串

  String str = new String(buf, charPos, (32 - charPos));

  return str;

}

上述代码中的redeemNum由活动Id、优惠券编号、校验码组成,组成结构如下:

-------------------------------------------------------------

|30位优惠券编号 |    15位活动id    |     13位校验位(前6 + 后7) |

---------------|------------------|--------------------------|

- 优惠券编号:30位bit位可表示范围:1073741824(10亿优惠券)

- 活动Id:15位表示,可以表示范围:32768(考虑到运营活动的频率,15位足够,365天每天有运营活动,可以使用89年)

伪代码如下所示:

/**

     * 生成优惠券编码

     * -------------------------------------------------------

     * 30位优惠券编号         15位活动id      13位校验位(前6 + 后7)

     * -------------------|--------------|--------------------|

     *

     * @param couponSchemeId 活动Id

     * @param redeemSerialNum 优惠券编号

     * @return

     */

public static long enRedeemNum(long couponSchemeId, long redeemSerialNum) {

   /// COUPON_ID_BIT_LEN表示活动Id二进制位数,这里是15

   redeemSerialNum = redeemSerialNum << COUPON_ID_BIT_LEN;

   /// 使用或运算将活动id和优惠券编号组合在一起

   long r = couponSchemeId | redeemSerialNum;

   /// 下面整合校验码到编码中

   long n = numOfOne(r);

   long re = r % DIVISOR;

   r = (r << NUMBER_OF_ONE_BIT_LEN) | n;

   r = (r << REMAINDER_BIT_LEN) | re;

   return r;

}

生成优惠券编码后,在利用进制转换函数将Long类型转换为字符串。

校验函数分析

完成编码操作后得到优惠券编码信息,例如获取活动id为1,优惠券序号为10,那么得到编码为:dBhLzM

当用户输入上述编码后,需要对编码的正确性进行校验,并且需要解码出活动id和优惠券编号,逆向操作上述编码可以很容易实现,校验代码如下:

/// 将优惠券编码转化为10进制表示形式

public static long deRedeemCode(String redeemCode) {

   char chs[] = redeemCode.toCharArray();

   long res = 0L;

   for (int i = 0; i < chs.length; i++) {

      int ind = -1;

      for (int j = 0; j < l; j++) {

         if (chs[i] == r[j]) {

             ind = j;

             break;

          }

       }

        if (ind == -1) {

          return -1;

        }

        if (i > 0) {

          res = res * l + ind;

        } else {

           res = ind;

        }

   }

   return res;

}

 校验10进制表示的优惠券编码

 public static boolean checkVaild(long redeemNum) {

   if (redeemNum > 0) {

     /// 先获取校验码后7位

     long checkSum = redeemNum & REMAINDER_MASK;

     /// 得到校验码前6位

     long n = (redeemNum & NUMBER_OF_ONE_MASK) >> REMAINDER_BIT_LEN;

     /// 获取 优惠券编号 + 活动Id数值信息

     long r = redeemNum >> CHECK_SUM_BIT_LEN;

     /// 校验前6位信息是否一致

     if (numOfOne(r) == n) {

        /// 校验后7位数值是否一致

        if (r % DIVISOR == checkSum) {

            return Boolean.TRUE;

          }

        }

   }

   return Boolean.FALSE;

}

至此完成了无存储式优惠券编码方案

总结

主要实现了如下功能:

选取字符空间

设置活动Id、优惠券编号、校验码结构(二进制位数以及二进制内容组建方式)

将10进制转化为N进制(本文N表示60)

将转化后的N进制映射到字符空间中,然后转化为字符串输出

设计校验函数

解码函数

上文就是小编为大家整理的高性能(无需判重)批量生成优惠券码方案,优惠券券码生成规则。

国内(北京、上海、广州、深圳、成都、重庆、杭州、西安、武汉、苏州、郑州、南京、天津、长沙、东莞、宁波、佛山、合肥、青岛)班牛客服系统分析、比较及推荐。

本站部分文章、图片属于网络上可搜索到的公开信息,均用于学习和交流用途,不能代表班牛的观点、立场或意见。我们接受网民的监督,如发现任何违法内容或侵犯了您的权益,请第一时间联系小编邮箱jiasou666@gmail.com 处理。
上一篇:提升DSR动态评分的五个实用技巧,DSR提升方法
下一篇:关于vuejs客服系统的信息
相关文章

 发表评论

暂时没有评论,来抢沙发吧~