背景

  • 日常的网站开发中,会遇到网站的促销活动,就有涉及到邀请好礼的功能
  • 成功邀请好友,则获取相应奖励,这时候,就有邀请码的需求
  • 邀请码要求每个用户唯一

方法一. 可根据用户的uid生成邀请码

方法二. 邀请码可根据某个初始化id生成,用户主动请求,生成code,绑定uid

  • 方法二,这种方式,需额外记录uid和code关系
  • 方法一,根据uid生成,也可根据code反推出uid,不用额外查询,比较方便

实现

  • 记录方法一的实现
  • 由长数字转换为特定长度的code,首先需确定code的字符范围
  • 可转换为 0-9A-Z 36进制数,或者更多字符可添加小写字符
  • 本次实现 转换为 32进制数
  • 去掉0 1 和 o 容易混淆的字符和补位字符F,剩余32字符

代码

php实现

/**


Class ShareCodeUtils

 


 
邀请码生成器,基本原理

 

1)参数用户ID

 

2)使用自定义进制转换之后为:V

 

3)最小code长度为6位,若不足则在后面添加分隔字符'F':VF

 

4)在VF后面再随机补足4位,得到形如 VFAADD

 

5)反向转换时以'F'为分界线,'F'后面的不再解析

*/

class ShareCodeUtils {
// 32个进制字符(0,1 没加入,容易和 o l 混淆,O 未加入,F 未加入,用于补位)

// 顺序可进行调整, 增加反推难度

private static $base = ['H', 'V', 'E', '8', 'S', '2', 'D', 'Z', 'X', '9', 'C', '7', 'P','5', 'I', 'K', '3', 'M', 'J', 'U', 'A', 'R', '4', 'W', 'Y', 'L', 'T', 'N', '6', 'B', 'G', 'Q'];
// F为补位字符,不能和上述字符重复

private static $pad = "F";
// 进制长度

private static $decimal_len = 32;
// 生成code最小长度

private static $code_min_len = 6;
/**

id转为code
 
相除去模法
 


 @param $id
 
@return string

*/

public static function idToCode($id)

{
\(result = "";
 while (floor(\)id / static::$decimal_len) > 0){

$index = \(id % static::\)decimal_len;
\(result.= static::\)base[$index];
\(id = floor(\)id / static::$decimal_len);

}

$index =  \(id % static::\)decimal_len;
\(result.= static::\)base[$index];

// code长度不足,则随机补全
\(code_len = strlen(\)result);

if (\(code_len < static::\)code_min_len) {
\(result .= static::\)pad;

for ($i = 0; \(i < static::\)code_min_len - $code_len - 1; $i ++) {
\(result .= static::\)base[rand(0, static::$decimal_len -1)];

}

}

return $result;

}
 

/**

code转为id
 
根据code获取对应的下标
 
在进行进制转换
 
eg: N8FASR, F为分隔符, 后面不在处理
 
N ---> 27
 
8 ---> 3
 
进制转换 2732(0) + 332(1) = 123
 
32(0) ---> 32的0次方
 
32(1) ---> 32的1次方
 


 @param $code
 
@return string

*/

public static function codeToId($code)

{

$result = 0;
\(base_flip_map = array_flip(static::\)base);
\(is_pad = strpos(\)code, static::\(pad);
 if (!empty(\)is_pad)) {

$len_real = $is_pad;

} else {
\(len_real = strlen(\)code);

}

for ($i = 0; $i < $len_real; $i ++) {

$str = \(code[\)i];

$index = \(base_flip_map[\)str] ?? '';

if ($index === '') {

break;

}
\(result += pow(static::\)decimal_len, $i) * $index;

}

return \(result;
}
}
\)num = "123";

var_dump(ShareCodeUtils::idToCode(\(num));
\)code = "N8FMJ3";

var_dump(ShareCodeUtils::codeToId($code));

go实现

package main
import (

"errors"

"fmt"

"math/rand"

"strings"

"time"

)
type code struct {

base string // 进制的包含字符, string类型

decimal uint64 // 进制长度

pad string // 补位字符,若生成的code小于最小长度,则补位+随机字符, 补位字符不能在进制字符中

len int // code最小长度

}
// id转code

func (c *code) idToCode (id uint64) string {

mod := uint64(0)

res := ""

for id!=0 {

mod = id % c.decimal

id = id / c.decimal

res += string(c.base[mod])

}

resLen := len(res)

if resLen < c.len {

res += c.pad

for i:=0; i< c.len - resLen - 1; i++ {

rand.Seed(time.Now().UnixNano())

res += string(c.base[rand.Intn(int(c.decimal))])

}

}

return res

}
// code转id

func (c *code) codeToId (code string) uint64 {

res:=uint64(0)

lenCode:=len(code)
//var baseArr [] byte = []byte(c.base)
baseArr := [] byte (c.base) // 字符串进制转换为byte数组
baseRev := make(map[byte] int) // 进制数据键值转换为map
for k, v := range baseArr {
	baseRev[v] = k
}

// 查找补位字符的位置
isPad := strings.Index(code, c.pad)
if isPad != -1 {
	lenCode = isPad
}

r := 0
for i:=0; i< lenCode; i++ {
	// 补充字符直接跳过
	if string(code[i]) == c.pad {
		continue
	}
	index := baseRev[code[i]]
	b := uint64(1)
	for j:=0; j < r; j ++ {
		b *= c.decimal
	}
	// pow 类型为 float64 , 类型转换太麻烦, 所以自己循环实现pow的功能
	//res += float64(index) * math.Pow(float64(32), float64(2))
	res += uint64(index) * b
	r ++
}
return res

}
// 初始化检查

func (c *code) initCheck () (bool, error) {

lenBase := len(c.base)

// 检查进制字符

if c.base == "" {

return false, errors.New("base string is nil or empty")

}

// 检查长度是否符合

if uint64(lenBase) != c.decimal {

return false, errors.New("base length and len not match")

}

return true, errors.New("")

}
func main() {

inviteCode := code{

base: "HVE8S2DZX9C7P5IK3MJUAR4WYLTN6BGQ",

decimal: 32,

pad: "F",

len: 6,

}

// 初始化检查

if res, err := inviteCode.initCheck(); !res {

fmt.Println(err)

return

}

id := uint64(5509767398598656)

code := inviteCode.idToCode(id)

fmt.Printf("id=%v, code=%v\n", id, code)
code = "HHC59YC8U6S"
id = inviteCode.codeToId(code)
fmt.Printf("code=%v, id=%v\n", code, id)

}

//var baseArr [] byte = []byte(c.base)
baseArr := [] byte (c.base) // 字符串进制转换为byte数组
baseRev := make(map[byte] int) // 进制数据键值转换为map
for k, v := range baseArr {
	baseRev[v] = k
}

// 查找补位字符的位置
isPad := strings.Index(code, c.pad)
if isPad != -1 {
	lenCode = isPad
}

r := 0
for i:=0; i< lenCode; i++ {
	// 补充字符直接跳过
	if string(code[i]) == c.pad {
		continue
	}
	index := baseRev[code[i]]
	b := uint64(1)
	for j:=0; j < r; j ++ {
		b *= c.decimal
	}
	// pow 类型为 float64 , 类型转换太麻烦, 所以自己循环实现pow的功能
	//res += float64(index) * math.Pow(float64(32), float64(2))
	res += uint64(index) * b
	r ++
}
return res

code = "HHC59YC8U6S"
id = inviteCode.codeToId(code)
fmt.Printf("code=%v, id=%v\n", code, id)

go实现2

package main
import(

"container/list"

"errors"

"fmt"

)
var baseStr string = "HVE8S2DZX9C7P5IK3MJUAR4WYLTN6BGQ"

var base [] byte = []byte(baseStr)

var baseMap map[byte] int
func InitBaseMap(){

baseMap = make(map[byte]int)

for i, v := range base {

baseMap[v] = i

}

}

func Base34(n uint64)([]byte){

quotient := n

mod := uint64(0)

l := list.New()

for quotient != 0 {
//fmt.Println("---quotient:", quotient)

mod = quotient%32

quotient = quotient/32

l.PushFront(base[int(mod)])

//res = append(res, base[int(mod)])
//fmt.Printf("---mod:%d, base:%s\n", mod, string(base[int(mod)]))

}

listLen := l.Len()
if listLen >= 6 {
	res := make([]byte,0,listLen)
	for i := l.Front(); i != nil ; i = i.Next(){
		res = append(res, i.Value.(byte))
	}
	return res
} else {
	res := make([]byte,0,6)
	for i := 0; i < 6; i++ {
		if i < 6-listLen {
			res = append(res, base[0])
		} else {
			res = append(res, l.Front().Value.(byte))
			l.Remove(l.Front())
		}

	}
	return res
}

}
func Base34ToNum(str []byte)(uint64, error){

if baseMap == nil {

return 0, errors.New("no init base map")

}

if str == nil || len(str) == 0 {

return 0, errors.New("parameter is nil or empty")

}

var res uint64 = 0

var r uint64 = 0

for i:=len(str)-1; i>=0; i-- {

v, ok := baseMap[str[i]]

if !ok {

fmt.Printf("")

return 0, errors.New("character is not base")

}

var b uint64 = 1

for j:=uint64(0); j<r; j++ {

b = 32

}

res +=  buint64(v)

r++

}

return res, nil

}
func main() {

InitBaseMap()

fmt.Printf("len(baseStr):%d, len(base):%d\n", len(baseStr), len(base))

res := Base34(1544804416)

fmt.Printf("=base:1544804416->%s, %d\n", string(res), len(res))

str := "VIVZ4EH"

num, err := Base34ToNum([]byte(str))

if err == nil {

fmt.Printf("=base:%s->%d\n", str, num)

} else {

fmt.Printf("===============err:%s\n", err.Error())

}

}

if listLen >= 6 {
	res := make([]byte,0,listLen)
	for i := l.Front(); i != nil ; i = i.Next(){
		res = append(res, i.Value.(byte))
	}
	return res
} else {
	res := make([]byte,0,6)
	for i := 0; i < 6; i++ {
		if i < 6-listLen {
			res = append(res, base[0])
		} else {
			res = append(res, l.Front().Value.(byte))
			l.Remove(l.Front())
		}

	}
	return res
}

总结

  • 本次实现由 php 和 go 两种语言实现
  • 最大的心得就是 go中的 类型转换是比较麻烦的,因为都是强类型
  • 和php 还不太一样
  • 但也算是进一步熟悉了go的语法代码