记录:简易CD-Key算法

说明

工作业务需求,生成一批随机的CD-Key,要求如下

  • 随机长度10-15字符(大写英文字母+数字)
  • 带一个自定义前缀用来区分 CD-Key 的批次
  • 一个批次内不重复
  • 一个批次的生成以万为数量级

其实是个很简单uuid就好,用语言的set/hashset特性去重,实际写的时候用了觉得会更快的方式

实现

基于洗牌算法

  • 因为考虑一些字符:1,L,0,O 显示区分上有点烦就考虑去掉这些字符(虽然没有这个需求,后面策划觉得一般都是复制粘贴使用的,这个需求也不重要)
  • 这样相当于在一些字符里面随机
    1
    2
    3
    4
    source_list = ['2', '3', '4', '5', '6', '7', '8', '9',
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R',
    'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
  • 然后第一个想到的就是洗牌了
  • 具体实现:比如要生成10长度的串,极端情况这个串里都是相同的字符,也就是初始随机库里每个字符先重复10次,得到新的source_list
    1
    2
    3
    4
    rand_list = []
    for src in source_list:
    for n in range(length):
    rand_list.append(src)
  • 洗牌一次就可以按照实际长度取出结果了
    • 洗牌一次可以取出的结果个数: len(source_list)
    • 这里没有把 source_list 当成一个环队列处理,当成环的话理论上可以取出更多个结果
    • 当然,这里的结果还是要依赖set/hashset来去除重复的
  • python 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    def __gen_alg(self, prefix, length, count):

    self._message = ""

    if length <= 0 or count <= 0:
    self._message = "!!!! [length] and [count] must > 0 !!!!"
    return

    self._random_set.clear()
    self._random_prefix = prefix
    source_list = ['2', '3', '4', '5', '6', '7', '8', '9',
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R',
    'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    merge_len = length
    rand_list = []
    for src in source_list:
    for n in range(merge_len):
    rand_list.append(src)
    total_len = len(rand_list)
    while len(self._random_set) < count:
    random.shuffle(rand_list)
    index = 0
    while index + merge_len < total_len:
    result_str = ''.join(rand_list[index:merge_len + index])
    index += 1
    self._random_set.add(prefix + '-' + result_str)

    while len(self._random_set) > count:
    self._random_set.pop()
  • 实际测下来,30W结果大概需要300ms,达到工具的使用

基于Hash

  • 英文字符+数字的组合,这符合大部分Hash算法的特征
  • 直接拷贝库里面hash算法来处理
  • 局限性是长度最大值,当然也可以用其他办法处理掉
  • cpp 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    #include <iostream>

    #include <string>
    #include <vector>
    #include <set>
    #include <algorithm>
    #include <thread>
    #include <time.h>
    #include <chrono>

    using namespace std::chrono_literals;

    std::vector<char> shows;
    std::vector<std::vector<std::string>> res;

    static std::string hash(const std::string& key) {
    thread_local std::string ret;
    ret.clear();

    constexpr static uint64_t _FNV_offset_basis = 14695981039346656037ULL;
    constexpr static uint64_t _FNV_prime = 1099511628211ULL;
    uint64_t _Vals[3] = { _FNV_offset_basis , _FNV_offset_basis , _FNV_offset_basis };
    for (auto iter = key.begin(); iter != key.end();)
    {
    auto index = rand() % 3;
    _Vals[index] ^= static_cast<uint64_t>(*iter);
    _Vals[index] *= _FNV_prime;
    iter++;
    }
    static auto show_size = shows.size();
    ret.append(1, shows[(_Vals[0] & 0x3FF) % show_size]);
    ret.append(1, shows[((_Vals[0] & (0x3FFull << 10)) >> 10) % show_size]);
    ret.append(1, shows[((_Vals[0] & (0x3FFull << 20)) >> 20) % show_size]);
    ret.append(1, shows[((_Vals[0] & (0x3FFull << 30)) >> 30) % show_size]);
    ret.append(1, shows[((_Vals[0] & (0x3FFull << 40)) >> 40) % show_size]);
    ret.append(1, shows[((_Vals[0] & (0x3FFull << 50)) >> 50) % show_size]);
    ret.append(1, shows[(((_Vals[0] & (0xFull << 60)) >> 60) | (_Vals[1] & 0x3F)) % show_size]);
    ret.append(1, shows[((_Vals[1] & (0x3FFull << 6)) >> 6) % show_size]);
    ret.append(1, shows[((_Vals[1] & (0x3FFull << 16)) >> 16) % show_size]);
    ret.append(1, shows[((_Vals[1] & (0x3FFull << 26)) >> 26) % show_size]);
    ret.append(1, shows[((_Vals[1] & (0x3FFull << 36)) >> 36) % show_size]);
    ret.append(1, shows[((_Vals[1] & (0x3FFull << 46)) >> 46) % show_size]);
    ret.append(1, shows[(((_Vals[1] & (0xFFull << 56)) >> 56) | (_Vals[2] & 0x3)) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 2)) >> 2) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 12)) >> 12) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 22)) >> 22) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 32)) >> 32) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 42)) >> 42) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 52)) >> 52) % show_size]);
    ret.append(1, shows[((_Vals[2] & (0x3FFull << 54)) >> 54) % show_size]); // 不足10bit,特殊处理下

    return ret;
    }

    int main() {
    for (char i = 'A'; i <= 'Z'; ++i)
    {
    shows.emplace_back(i);
    }
    for (char i = '2'; i <= '9'; ++i)
    {
    shows.emplace_back(i);
    }
    auto iter = std::remove_if(shows.begin(), shows.end(), [](char ch) {
    return ch == 'i'
    || ch == 'l'
    || ch == 'I'
    || ch == 'o'
    || ch == 'O'
    || ch == '0';
    });


    shows.erase(iter, shows.end());
    auto t0 = time(0);
    srand(t0);

    auto before = std::chrono::system_clock::now();
    uint64_t thread_count = 10;
    uint64_t count_per_thread = 30000;
    std::vector<std::thread> threads;
    res.resize(thread_count);
    for (uint64_t i = 0; i < thread_count; ++i)
    {
    threads.emplace_back([i, count_per_thread]() {
    for (int k = count_per_thread * i; k < count_per_thread *(i + 1); ++k)
    {
    res[i].emplace_back(hash(std::to_string(k)));
    }
    });
    }
    for (auto&& t : threads)
    {
    t.join();
    }
    auto after = std::chrono::system_clock::now();
    std::cout << "total: " << count_per_thread * thread_count << " got: " << res.size() * count_per_thread << " cost: " << std::chrono::duration_cast<std::chrono::milliseconds>(after - before).count() << "ms";
    return 0;
    }

end

  • 最后还是选择了理解上更简单的洗牌方式
  • 结果组织上写入数据库的SQL语句
  • 操作上用dearPyGui
    • 虽然是基于imgui,但是C++里面是循环进入函数刷新的,而这个是类似把控件推入栈的方式,也许还有其他方式暂时没有太多研究
  • 完整代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    import random

    import dearpygui.core as core
    import dearpygui.simple as simple

    class CDKeyApp:

    window_name = "CD-Key Generate: "
    cdKey_prefix_src = "cdKey-prefix"
    cdKey_length_src = "cdKey-length"
    cdKey_count_src = "cdKey-count"
    cdKey_length_combo = ['5', '6', '7', '8', '9', '10', '11', '12']

    _random_set = set()
    _random_prefix = ""
    _message = ""
    copy_cdKeys = ""
    copy_cdKeys_mongo = ""

    def __init__(self):
    core.add_additional_font('C:\\Windows\\Fonts\\simyou.ttf', 13, glyph_ranges = 'chinese_full')
    return

    def __gen_txt(self):
    self.copy_cdKeys = ""
    self.copy_cdKeys_mongo = ""

    self.copy_cdKeys_mongo += 'try {\n'
    self.copy_cdKeys_mongo += 'db.DiceCDKey.insertMany([\n'

    for one in self._random_set:
    self.copy_cdKeys += one + '\n'
    self.copy_cdKeys_mongo += '{_id:"' + one + '", keys:"' + self._random_prefix + '"},\n'

    self.copy_cdKeys_mongo += ']);} catch(e) { print(e); }'


    def __gen_alg(self, prefix, length, count):

    self._message = ""

    if length <= 0 or count <= 0:
    self._message = "!!!! [length] and [count] must > 0 !!!!"
    return

    self._random_set.clear()
    self._random_prefix = prefix
    source_list = ['2', '3', '4', '5', '6', '7', '8', '9',
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U',
    'V', 'W', 'X', 'Y', 'Z']
    merge_len = length
    rand_list = []
    for src in source_list:
    for n in range(merge_len):
    rand_list.append(src)
    total_len = len(rand_list)
    while len(self._random_set) < count:
    random.shuffle(rand_list)
    index = 0
    while index + merge_len < total_len:
    result_str = ''.join(rand_list[index:merge_len + index])
    index += 1
    self._random_set.add(prefix + '-' + result_str)

    while len(self._random_set) > count:
    self._random_set.pop()

    def __gen(self):
    prefix = core.get_value(self.cdKey_prefix_src)
    length = int('0'+core.get_value(self.cdKey_length_src))
    count = int('0'+core.get_value(self.cdKey_count_src))

    self.__gen_alg(prefix, length, count)
    self.__gen_txt()

    static_item_1 = "static_item_1"
    static_item_2 = "static_item_2"
    static_item_3 = "static_item_3"
    static_item_4 = "static_item_4"

    if core.does_item_exist("##"+static_item_1):
    core.delete_item("##"+static_item_1)
    core.delete_item("##"+static_item_2)
    core.delete_item("##"+static_item_3)
    core.delete_item("##"+static_item_4)

    core.add_spacing(name="##"+static_item_1, count=2, parent=self.window_name)
    core.add_input_text(name="##"+static_item_2, parent=self.window_name,
    default_value=self.copy_cdKeys,
    multiline=True, readonly=True, width=200, height=250)
    core.add_same_line(name="##"+static_item_3, spacing=20, parent=self.window_name)
    core.add_input_text(name="##"+static_item_4, parent=self.window_name,
    default_value=self.copy_cdKeys_mongo,
    multiline=True, readonly=True, width=300, height=250)

    if len(self._message) > 0:
    core.add_separator(parent=self.window_name)
    core.add_text("ERROR: " + self._message, parent=self.window_name, color=[255, 0, 0])
    core.add_separator(parent=self.window_name)


    def show(self):
    with simple.window(self.window_name):
    core.set_main_window_size(550, 550)
    core.set_main_window_resizable(False)
    core.set_main_window_title(self.window_name)

    core.add_spacing()
    core.add_separator()

    core.add_text("CD-Key Generate")
    core.add_text("生成随机兑换码对应的 key 文本", bullet=True, color=[255, 0, 0])
    core.add_text("生成随机兑换码对应的 mongoDB insert 代码", bullet=True)
    core.add_text("mongoDB 代码记得提交给运维执行导入到数据库", bullet=True, color=[255, 0, 0])

    core.add_spacing()
    core.add_separator()

    core.add_spacing(count=8)
    core.add_input_text(name="Prefix", source=self.cdKey_prefix_src, width=100, hexadecimal=True)
    core.add_same_line(spacing=20)
    core.add_combo("Length", items=self.cdKey_length_combo, source=self.cdKey_length_src, default_value='10', width=80)
    core.add_same_line(spacing=20)
    core.add_input_text(name="Count", source=self.cdKey_count_src, default_value='10', width=100, decimal=True)
    core.add_spacing(count=2)
    core.add_button("Gen", callback=self.__gen, width=500)

    core.add_spacing(count=2)
    core.add_separator()

    # self.__render()
    # core.set_render_callback(self.render)
    core.start_dearpygui(primary_window=self.window_name)


    if __name__ == '__main__':
    cdkeyApp = CDKeyApp()
    cdkeyApp.show()
------ 本文结束 ------
------ 版权声明:转载请注明出处 ------