兑换码生成算法

news2024/11/20 19:40:01

兑换码生成算法

  • 兑换码生成算法
  • 1.兑换码的需求
  • 2.算法分析
    • 2.重兑校验算法
    • 3.防刷校验算法
  • 3.算法实现

兑换码生成算法

兑换码生成通常涉及在特定场景下为用户提供特定产品或服务的权益或礼品,典型的应用场景包括优惠券、礼品卡、会员权益等。

1.兑换码的需求

在这里插入图片描述

要求如下:

  • 可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:
    • 长度不超过10个字符
    • 只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789
    • 不包含IO10 这些难以区分的字母和数字。
  • 数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量
  • 唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
  • 不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
  • 防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
  • 高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力

2.算法分析

要满足唯一性,很多人会想到以下技术:

  • UUID
  • Snowflake
  • 自增id

当然可以了,大家思考一下,假如我们将24个字母和8个数字放到数组中,如下:

我们的兑换码要求是24个大写字母和8个数字。而以上算法最终生成的结果都是数值类型,并不符合我们的需求!

有没有什么办法,可以把数字转为我们要求的格式呢?

1.Base32转码

角标0123456789101112131415
字符ABCDEFGHJKLMNPQR
角标16171819202122232425262728293031
字符STUVWXYZ23456789

这样,0 ~ 31的角标刚好对应了我们的32个字符!而2的5次幂刚好就是32,因此5位二进制数的范围就是0 ~ 31

那因此,只要我们让数字转为二进制的形式,然后每5个二进制位为一组,转10进制的结果是不是刚好对应一个角标,就能找到一个对应的字符呢?

这样是不是就把一个数字转为我们想要的字符个数了。这种把二进制数经过加密得到字符的算法就是Base32法,类似的还有Base64法。

举例:假如我们经过自增id计算出一个复杂数字,转为二进制,并每5位一组,结果如下:

01001 00010 01100 10010 01101 11000 01101 00010 11110 11010

此时,我们看看每一组的结果:

  • 01001转10进制是9,查数组得字符为:K
  • 00010转10进制是2,查数组得字符为:C
  • 01100转10进制是12,查数组得字符为:N
  • 10010转10进制是18,查数组得字符为:B
  • 01101转10进制是13,查数组得字符为:P
  • 11000转10进制是24,查数组得字符为:2

依此类推,最终那一串二进制数得到的结果就是KCNBP2PC84,刚好符合我们的需求。

但是思考一下,我们最终要求字符不能超过10位,而每个字符对应5个bit位,因此二进制数不能超过50个bit位

UUID和Snowflake算法得到的结果,一个是128位,一个是64位,都远远超出了我们的要求。

那自增id算法符合我们的需求呢?

自增id从1增加到Integer的最大值,可以达到40亿以上个数字,而占用的字节仅仅4个字节,也就是32个bit位,距离50个bit位的限制还有很大的剩余,符合要求!

综上,我们可以利用自增id作为兑换码,但是要利用Base32加密,转为我们要求的格式。此时就符合了我们的几个要求了:

  • 可读性好:可以转为要求的字母和数字的格式,长度还不超过10个字符
  • 数据量大:可以应对40亿以上的数据规模
  • 唯一性:自增id,绝对唯一

2.重兑校验算法

那重兑问题该如何判断呢?此处有两种方案:

  • 基于数据库:我们在设计数据库时有一个字段就是标示兑换码状态,每次兑换时可以到数据库查询状态,避免重兑。
    • 优点:简单
    • 缺点:对数据库压力大
  • 基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,是不是完美解决问题。而这种算法恰好就是BitMap的底层实现,而且Redis中的BitMap刚好能支持2^32个bit位。
    • 优点:简答、高效、性能好
    • 缺点:依赖于Redis

OK,重兑、高效的两个特性都满足了!

现在,就剩下防止爆刷了。我们的兑换码规律性不能太明显,否则很容易被人猜测到其它兑换码。但是,如果我们使用了自增id,那规律简直太明显了,岂不是很容易被人猜到其它兑换码?!

所以,我们采用自增id的同时,还需要利用某种校验算法对id做加密验证,避免他人找出规律,猜测到其它兑换码,甚至伪造、篡改兑换码。

那该采用哪种校验算法呢?

3.防刷校验算法

非常可惜,没有一种现成的算法能满足我们的需求,我们必须自己设计一种算法来实现这个功能。

不过大家不用害怕,我们可以模拟其它验签的常用算法。比如大家熟悉的JWT技术。我们知道JWT分为三部分组成:

  • Header:记录算法
  • Payload:记录用户信息
  • Verify Signature:验签,用于验证整个token
    JWT中的的Header和Payload采用的是Base64算法,与我们Base32类似,几乎算是明文传输,难道不怕其他人伪造、篡改token吗?
    为了解决这个问题,JWT中才有了第三部分,验证签名。这个签名是有一个秘钥,结合Header、Payload,利用MD5或者RSA算法生成的。因此:
  • 只要秘钥不泄露,其他人就无法伪造签名,也就无法伪造token。
  • 有人篡改了token,验签时会根据header和payload再次计算签名。数据被篡改,计算的到的签名肯定不一致,就是无效token

因此,我们也可以模拟这种思路:

  • 首先准备一个秘钥
  • 然后利用秘钥对自增id做加密,生成签名
  • 将签名、自增id利用Base32转码后生成兑换码
    只要秘钥不泄露,就没有人能伪造兑换码。只要兑换码被篡改,就会导致验签不通过。

当然,这里我们不能采用MD5和RSA算法来生成签名,因为这些算法得到的签名都太长了,一般都是128位以上,超出了长度限制。

因此,这里我们必须采用一种特殊的签名算法。由于我们的兑换码核心是自增id,也就是数字,因此这里我们打算采用按位加权的签名算法:

  • 将自增id(32位)每4位分为一组,共8组,都转为10进制
  • 每一组给不同权重
  • 把每一组数加权求和,得到的结果就是签名

举例:

在这里插入图片描述
最终的加权和就是:4 * 2 + 2 * 5 + 9 * 1 + 10 * 3 + 8 * 4 + 2 * 7 + 1 * 8 + 6 * 9 = 165

这里的权重数组就可以理解为加密的秘钥

当然,为了避免秘钥被人猜测出规律,我们可以准备16组秘钥。在兑换码自增id前拼接一个4位的新鲜值,可以是随机的。这个值是多少,就取第几组秘钥。

在这里插入图片描述
这样就进一步增加了兑换码的复杂度。

最后,把加权和,也就是签名也转二进制,拼接到最前面,最终的兑换码就是这样:

在这里插入图片描述

3.算法实现

提供了两个类其中:

  • Base32.java:是Base32工具类
  • CodeUtil.java:是签名工具

我们重点关注CodeUtil的实现,代码如下:

package com.promotion.utils;

//自定义的异常
import com.common.exceptions.BadRequestException;

/**
 * <h1 style='font-weight:500'>1.兑换码算法说明:</h1>
 * <p>兑换码分为明文和密文,明文是50位二进制数,密文是长度为10的Base32编码的字符串 </p>
 * <h1 style='font-weight:500'>2.兑换码的明文结构:</h1>
 * <p style='padding: 0 15px'>14(校验码) + 4 (新鲜值) + 32(序列号) </p>
 *   <ul style='padding: 0 15px'>
 *       <li>序列号:一个单调递增的数字,可以通过Redis来生成</li>
 *       <li>新鲜值:可以是优惠券id的最后4位,同一张优惠券的兑换码就会有一个相同标记</li>
 *       <li>载荷:将新鲜值(4位)拼接序列号(32位)得到载荷</li>
 *       <li>校验码:将载荷4位一组,每组乘以加权数,最后累加求和,然后对2^14求余得到</li>
 *   </ul>
 *  <h1 style='font-weight:500'>3.兑换码的加密过程:</h1>
 *     <ol type='a' style='padding: 0 15px'>
 *         <li>首先利用优惠券id计算新鲜值 f</li>
 *         <li>将f和序列号s拼接,得到载荷payload</li>
 *         <li>然后以f为角标,从提前准备好的16组加权码表中选一组</li>
 *         <li>对payload做加权计算,得到校验码 c  </li>
 *         <li>利用c的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
 *         <li>将payload与key做异或,作为新payload2</li>
 *         <li>然后拼接兑换码明文:f (4位) + payload2(36位)</li>
 *         <li>利用Base32对密文转码,生成兑换码</li>
 *     </ol>
 * <h1 style='font-weight:500'>4.兑换码的解密过程:</h1>
 * <ol type='a' style='padding: 0 15px'>
 *      <li>首先利用Base32解码兑换码,得到明文数值num</li>
 *      <li>取num的高14位得到c1,取num低36位得payload </li>
 *      <li>利用c1的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key</li>
 *      <li>将payload与key做异或,作为新payload2</li>
 *      <li>利用加密时的算法,用payload2和s1计算出新校验码c2,把c1和c2比较,一致则通过 </li>
 * </ol>
 */
public class CodeUtil {
	/**
     * 优惠券兑换码模板
     */
    private final static String COUPON_CODE_PATTERN = "^[23456789ABCDEFGHJKLMNPQRSTUVWXYZ]{8,10}$";

    /**
     * 异或密钥表,用于最后的数据混淆
     */
    private final static long[] XOR_TABLE = {
            45139281907L, 61261925523L, 58169127203L, 27031786219L,
            64169927199L, 46169126943L, 32731286209L, 52082227349L,
            59169127063L, 36169126987L, 52082200939L, 61261925739L,
            32731286563L, 27031786427L, 56169127077L, 34111865001L,
            52082216763L, 61261925663L, 56169127113L, 45139282119L,
            32731286479L, 64169927233L, 41390251661L, 59169127121L,
            64169927321L, 55139282179L, 34111864881L, 46169127031L,
            58169127221L, 61261925523L, 36169126943L, 64169927363L,
    };
    /**
     * fresh值的偏移位数
     */
    private final static int FRESH_BIT_OFFSET = 32;
    /**
     * 校验码的偏移位数
     */
    private final static int CHECK_CODE_BIT_OFFSET = 36;
    /**
     * fresh值的掩码,4位
     */
    private final static int FRESH_MASK = 0xF;
    /**
     * 验证码的掩码,14位
     */
    private final static int CHECK_CODE_MASK = 0b11111111111111;
    /**
     * 载荷的掩码,36位
     */
    private final static long PAYLOAD_MASK = 0xFFFFFFFFFL;
    /**
     * 序列号掩码,32位
     */
    private final static long SERIAL_NUM_MASK = 0xFFFFFFFFL;
    /**
     * 序列号加权运算的秘钥表
     */
    private final static int[][] PRIME_TABLE = {
            {23, 59, 241, 61, 607, 67, 977, 1217, 1289, 1601},
            {79, 83, 107, 439, 313, 619, 911, 1049, 1237},
            {173, 211, 499, 673, 823, 941, 1039, 1213, 1429, 1259},
            {31, 293, 311, 349, 431, 577, 757, 883, 1009, 1657},
            {353, 23, 367, 499, 599, 661, 719, 929, 1301, 1511},
            {103, 179, 353, 467, 577, 691, 811, 947, 1153, 1453},
            {213, 439, 257, 313, 571, 619, 743, 829, 983, 1103},
            {31, 151, 241, 349, 607, 677, 769, 823, 967, 1049},
            {61, 83, 109, 137, 151, 521, 701, 827, 1123},
            {23, 61, 199, 223, 479, 647, 739, 811, 947, 1019},
            {31, 109, 311, 467, 613, 743, 821, 881, 1031, 1171},
            {41, 173, 367, 401, 569, 683, 761, 883, 1009, 1181},
            {127, 283, 467, 577, 661, 773, 881, 967, 1097, 1289},
            {59, 137, 257, 347, 439, 547, 641, 839, 977, 1009},
            {61, 199, 313, 421, 613, 739, 827, 941, 1087, 1307},
            {19, 127, 241, 353, 499, 607, 811, 919, 1031, 1301}
    };

    /**
     * 生成兑换码
     *
     * @param serialNum 递增序列号
     * @return 兑换码
     */
    public static String generateCode(long serialNum, long fresh) {
        // 1.计算新鲜值
        fresh = fresh & FRESH_MASK;
        // 2.拼接payload,fresh(4位) + serialNum(32位)
        long payload = fresh << FRESH_BIT_OFFSET | serialNum;
        // 3.计算验证码
        long checkCode = calcCheckCode(payload, (int) fresh);
        // 4.payload做大质数异或运算,混淆数据
        payload ^= XOR_TABLE[(int) (checkCode & 0b11111)];
        // 5.拼接兑换码明文: 校验码(14位) + payload(36位)
        long code = checkCode << CHECK_CODE_BIT_OFFSET | payload;
        // 6.转码
        return Base32.encode(code);
    }

    private static long calcCheckCode(long payload, int fresh) {
        // 1.获取码表
        int[] table = PRIME_TABLE[fresh];
        // 2.生成校验码,payload每4位乘加权数,求和,取最后13位结果
        long sum = 0;
        int index = 0;
        while (payload > 0) {
            sum += (payload & 0xf) * table[index++];
            payload >>>= 4;
        }
        return sum & CHECK_CODE_MASK;
    }

    public static long parseCode(String code) {
        if (code == null || !code.matches(COUPON_CODE_PATTERN)) {
            // 兑换码格式错误
            throw new BadRequestException("无效兑换码");
        }
        // 1.Base32解码
        long num = Base32.decode(code);
        // 2.获取低36位,payload
        long payload = num & PAYLOAD_MASK;
        // 3.获取高14位,校验码
        int checkCode = (int) (num >>> CHECK_CODE_BIT_OFFSET);
        // 4.载荷异或大质数,解析出原来的payload
        payload ^= XOR_TABLE[(checkCode & 0b11111)];
        // 5.获取高4位,fresh
        int fresh = (int) (payload >>> FRESH_BIT_OFFSET & FRESH_MASK);
        // 6.验证格式:
        if (calcCheckCode(payload, fresh) != checkCode) {
            throw new BadRequestException("无效兑换码");
        }
        return payload & SERIAL_NUM_MASK;
    }
}

核心的两个方法:

  • generateCode(long serialNum, long fresh):根据自增id生成兑换码。两个参数
    • serialNum:兑换码序列号,也就是自增id
    • fresh:新鲜值,这里建议使用兑换码对应的优惠券id做新鲜值
  • parseCode(String code):验证并解析兑换码,返回的是兑换码的序列号,也就是自增id

Base32:

package com.promotion.utils;
//利用了外部工具包(pom依赖)
import cn.hutool.core.text.StrBuilder;

/**
 * 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
 */
public class Base32 {

    private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";

    public static String encode(long raw) {
        StrBuilder sb = new StrBuilder();
        while (raw != 0) {
            int i = (int) (raw & 0b11111);
            sb.append(baseChars.charAt(i));
            raw = raw >>> 5;
        }
        return sb.toString();
    }

    public static long decode(String code) {
        long r = 0;
        char[] chars = code.toCharArray();
        for (int i = chars.length - 1; i >= 0; i--) {
            long n = baseChars.indexOf(chars[i]);
            r = r | (n << (5*i));
        }
        return r;
    }

    public static String encode(byte[] raw) {
        StrBuilder sb = new StrBuilder();
        int size = 0;
        int temp = 0;
        for (byte b : raw) {
            if (size == 0) {
                // 取5个bit
                int index = (b >>> 3) & 0b11111;
                sb.append(baseChars.charAt(index));
                // 还剩下3位
                size = 3;
                temp = b & 0b111;
            } else {
                int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
                sb.append(baseChars.charAt(index));
                int left = 3 + size;
                size = 0;
                if(left >= 5){
                    index = b >>> (left - 5) & ((1 << 5) - 1);
                    sb.append(baseChars.charAt(index));
                    left = left - 5;
                }
                if(left == 0){
                    continue;
                }
                temp = b & ((1 << left) - 1);
                size = left;
            }
        }
        if(size > 0){
            sb.append(baseChars.charAt(temp));
        }
        return sb.toString();
    }

    public static byte[] decode2Byte(String code) {
        char[] chars = code.toCharArray();
        byte[] bytes = new byte[(code.length() * 5 )/ 8];
        byte tmp = 0;
        byte byteSize = 0;
        int index = 0;
        int i = 0;
        for (char c : chars) {
            byte n = (byte) baseChars.indexOf(c);
            i++;
            if (byteSize == 0) {
                tmp = n;
                byteSize = 5;
            } else {
                int left = Math.min(8 - byteSize, 5);
                if(i == chars.length){
                    bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
                    break;
                }
                tmp = (byte) (tmp << left | (n >>> (5 - left)));
                byteSize += left;
                if (byteSize >= 8) {
                    bytes[index++] = tmp;
                    byteSize = (byte) (5 - left);
                    if (byteSize == 0) {
                        tmp = 0;
                    } else {
                        tmp = (byte) (n & ((1 << byteSize) - 1));
                    }
                }
            }
        }
        return bytes;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1570186.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软考--软件设计师(软件工程总结2)

目录 1.测试方法 2.软件项目管理 3.软件容错技术 4.软件复杂性度量 5.结构化分析方法&#xff08;一种面向数据流的开发方法&#xff09; 6.数据流图 1.测试方法 软件测试&#xff1a;静态测试&#xff08;被测程序采用人工检测&#xff0c;计算机辅助静态分析的手段&…

MyBatis 解决上篇的参数绑定问题以及XML方式交互

前言 上文:MyBatis 初识简单操作-CSDN博客 上篇文章我们谈到的Spring中如何使用注解对Mysql进行交互 但是我们发现我们返回出来的数据明显有问题 我们发现后面三个字段的信息明显没有展示出来 下面我们来谈谈解决方案 解决方案 这里的原因本质上是因为mysql中和对象中的字段属性…

构建未来交通:香橙派OPI Airpro上的智能交通监管系统

引言&#xff1a; 随着城市化进程的加速&#xff0c;交通管理变得越来越复杂。 传统的交通监管系统往往无法有效应对日益增长的车辆数量和复杂的交通状况。因此&#xff0c;我们需要一种更加智能和自适应的解决方案来提高交通效率并减少事故发生率。 香橙派OPI Airpro以其强大的…

07 - D触发器

---- 整理自B站UP主 踌躇月光 的视频 1. 电路实现 将 D 接到 S 端&#xff0c;D 取反接到 R 端&#xff0c;两个输入合成一个输入&#xff0c;R 和 S 总是相反的&#xff0c;就构成了 D 触发器。再添加一个EN&#xff0c;作为锁存功能&#xff0c;EN0 时&#xff0c;R 和 S 都…

Mac删除软件,动一动手指,几秒就彻底删除 mac删除软件删不掉的解决方法 mac删除软件后怎么删除软件数据

当你入职新公司&#xff0c;接手前任员工使用的Mac电脑时&#xff0c;很可能会遇到一个非常普遍的问题&#xff1a;电脑中装有大量你不需要的软件。这些软件不仅占用宝贵的硬盘空间&#xff0c;还可能影响电脑的运行速度和效率。为了获得一个干净、清爽的使用体验&#xff0c;删…

Go协程池gopool源码解析

1、gopool简介 Repository&#xff1a;https://github.com/bytedance/gopkg/tree/develop/util/gopool gopool is a high-performance goroutine pool which aims to reuse goroutines and limit the number of goroutines. It is an alternative to the go keyword. gopool的…

【Java基础知识总结 | 第十篇】HashSet底层实现原理

文章目录 10.HashSet底层实现原理10.1HashSet特点10.2HashSet源码10.3 add流程10.4总结 10.HashSet底层实现原理 10.1HashSet特点 存储对象&#xff1a;HashSet 存储对象采用哈希表的方式&#xff0c;它不允许重复元素&#xff0c;即集合中不会包含相同的元素。当向 HashSet …

Linux_进程信号_7

文章目录 1.什么是信号2.信号列表3.信号处理常见方式4.信号的存储5.信号产生前-中-后1.信号产生前2.信号产生中 6产生信号1.signal2.kill3.raise4.abort5.alarm6.硬件异常 7.core dump8.信号产生中1. sigset_t&#xff08;数据类型&#xff09;2.信号集操作函数1.sigprocmask2.…

Vue3_2024_7天【回顾上篇watch常见的后两种场景】___续

Vue3中监听多条数据的两种使用 1.watch【使用上一章写法&#xff0c;监听两个属性&#xff0c;然后执行相应操作…】 2.watchEffect【相对于使用watch&#xff0c;watchEffect默认页面初始加载&#xff0c;有点类似加配置&#xff1a;立即执行 immediate】 代码&#xff1a; …

Python:百度AI开放平台——OCR图像文字识别应用

一、注册百度AI开放平台 使用百度AI服务的步骤为&#xff1a; 注册&#xff1a;注册成为百度AI开放平台开发者&#xff1b;创建AI应用&#xff1a;在百度API开放平台上创建相关类型的的AI应用&#xff0c;获得AppID、API Key和Secret Key&#xff1b;调用API&#xff1a;调用…

Word中插入Endnote参考文献时显示乱码

近期在写文章需要插入参考文献&#xff0c;使用Endnote插入时显示乱码&#xff0c;如下图所示&#xff1a; 文章末尾显示{ADDIN EN REFILIST } 解决方法 在网上找了诸多方法尝试也没有解决&#xff0c;最终找到一篇博客介绍了一种方法&#xff1a; word选项—高级&#xff1…

苍穹外卖07(缓存菜品,SpringCache,缓存套餐,添加购物车菜品和套餐多下单,查看购物车,清除购物车,删除购物车中一个商品)

目录 一、缓存菜品 1 问题说明 2 实现思路 3 代码开发&#xff1a;修改DishServiceImpl 4 功能测试 二、SpringCache 1. 介绍 2. 使用语法 1 起步依赖 2 使用要求 3 常用注解 4 SpEL表达式(了解备用) 5 步骤小结 3.入门案例 1 准备环境 2 使用入门 1 引导类上加…

四、MySQL读写分离之MyCAT

一、读写分离概述 1、什么是读写分离&#xff1a; 读写分离&#xff1a;就是将读写操作分发到不同的服务器&#xff0c;读操作分发到对应的服务器 &#xff08;slave&#xff09;&#xff0c;写操作分发到对应的服务器&#xff08;master&#xff09; ① M-S (主从) 架构下&…

【测试篇】测试用例

文章目录 前言具体设计测试用例等价类边界值场景设计法判定表&#xff08;因果图&#xff09;正交排列&#xff08;用的非常少&#xff09;错误猜测法 前言 什么是测试用例&#xff1f;&#xff1f; 测试用例是针对软件系统或应用程序的特定功能或场景编写的一组步骤&#xf…

记一次Cannot deploy POJO class [xxx$$EnhancerBySpringCGLIB$$xxx]的错误

最近项目上需要使用websocket做服务端&#xff0c;那好说啊&#xff0c;直接springboot集成的websocket 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId><versi…

开源推荐榜【Pear Admin Flask 用python来创建后台管理系统】

最新技术高效快速开发&#xff0c;前后端分离模式&#xff0c;开箱即用。 核心模块包括&#xff1a;用户、角色、职位、组织机构、菜单、字典、日志、多应用管理、文件管理、定时任务等功能。 代码量少、学习简单、功能强大、轻量级、易扩展&#xff0c;轻松开发从现在开始&…

【OpenCV】图像像素的遍历

1 前言 介绍两种遍历像素的方法&#xff08;非指针、指针&#xff09;。注意&#xff1a;.at() .ptr()的作用、用法。相关API&#xff1a; Mat对象.ptr() Mat对象.at() 2 代码及内容 #include "iostream" #include "opencv2/opencv.hpp"using namespac…

聊聊测试用例评审流程

测试人员将需求熟悉完成后&#xff0c;开始编写相应的测试用例&#xff0c;待测试用例编写完成后只是测试用例完成前的第一步&#xff0c;后边的流程需要组织线上或线下评审会议等等。 首先要了解测试用例评审的最终目的是什么&#xff1a;提高测试用例的质量和覆盖率&#xff…

notification+Android笔记

notification通知应用UI之外的消息并显示即推送&#xff1b; NotificationManager负责管理通知&#xff0c;例如显示取消&#xff0c;删除等&#xff1b; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager;…

从0开始搭建基于VUE的前端项目(三) Vuex的使用与配置

准备与版本 vuex 3.6.2(https://v3.vuex.vuejs.org/zh/)概念 vuex是什么? 是用作 【状态管理】的 流程图如下 state 数据状态,成员是个对象 mapState 组件使用this.$store.state.xxx获取state里面的数据 getters 成员是个函数,方便获取state里面的数据,也可以加工数据 ma…