基于Hashids的高效游戏礼包兑换码系统完整设计
所用生成器:Hashids - generate short unique ids from integers
系统优势:
- 生成的兑换码不实际入库,只在数据库中记录有效个数,数据库友好
- 兑换码使用时,无须查库校验有效性和加锁,在内存中确认有效后直接落库,由唯一索引进行数据库重复兑换校验,高效
- 兑换码仅绑定礼包id,可随时修改礼包内容,运营掌控力度大
- 兑换码长度和内容可随意设定(将影响可用数量),生成灵活
- 兑换码有加密,不能被轻易破解,安全保障
运营需求
- 运营配置游戏礼包,并为该礼包生成或指定礼包兑换码,玩家可使用兑换码兑换指定礼包
- 每个礼包玩家仅可兑换一次
- 礼包可设定以下属性并支持随时修改
- 生效时段
- 奖励内容
- 上下架状态
- 增加可兑换数量
- 礼包邮件内容
- 可兑换玩家限制(自行实现)
- 支持通用型礼包码,即单个礼包码可由大量玩家兑换,并可设定兑换数量
- 支持生成唯一兑换码,即单个礼包码仅可由单个玩家兑换,兑换后立刻失效
- 随时查询某兑换码是否处于生效状态,如已被兑换需查询何人何时兑换
- 随时查询某玩家已经兑换过的礼包
HashIds原理和使用
hashids的原理是将一个int数组转换为使用指定字符的字符串,并可混淆密码。如图所示:
由此,我们可以将礼包码和计数值放入数组,由Hashids生成兑换码,并在兑换时解析出礼包码和计数值,用于兑换。
具体步骤如下:
- 新建礼包,填好礼包的各项属性,入库,得到礼包的数据库id:1
- for循环遍历指定次数,将礼包id和遍历的计数值放入数组,并以此通过Hashids生成兑换码
- 更新礼包中的可兑换数量generatedCount,表示已经为这个礼包生成了generatedCount个兑换码
- 将生成的兑换码返回至运营后台
- 运营后台可随时继续生成兑换码,实际只是将遍历的初始计数值调整为generatedCount即可。
- 运营后台可随时查看已生成的兑换码,实际只需重新调用一次生成逻辑但不更新可兑换数量即可。
在玩家使用某个兑换码进行兑换时:
- 将该礼包码进行解码,如果无法解码出含有2个元素的int数组,该兑换码即为无效兑换码
- 查询解码出的礼包id,查库(建议缓存在内存中)得到该礼包信息,校验上下架/可兑换时间/自定义限制等。
- 使用解码出的计数值,对比礼包的可兑换数量,如已超过也无效
- 直接使用用户id、礼包id和计数值入库,入库时数据库唯一索引校验失败则表明该玩家已兑换了该礼包或该礼包码已被他人兑换。唯一索引见数据库设计。
- 所有校验通过并成功入库,向玩家发送奖励邮件
数据库设计
默认字段CreatedAt、UpdatedAt和DeletedAt
字段 | 类型 | 备注 |
---|---|---|
id | int | id |
name | string | 礼包名,仅运营后台中区分 |
type | int | 类型,通兑礼包或普通礼包 |
reward | string |
奖励内容 |
valid | bool | 兑换开关 |
code | string | 通兑码,仅通兑礼包有效 |
generatedCount | int | 已生成数量(最大可兑换数量) |
startTime | int | 开始时间 |
endTime | int | 结束时间 |
mailTemplateId | int | 邮件模板id(另行关联邮件表) |
otherLimit | string | 其他限制(自行实现) |
字段 | 类型 | 备注 | 索引 |
---|---|---|---|
id | int | id | 主键 |
packageId | int | 礼包id | uk_packageId_uid,uk_packageId_num |
num | int | 计数值 | uk_packageId_num。触发该索引即表示该兑换码已被他人兑换 |
uid | int |
兑换用户 |
uk_packageId_uid。触发该索引即表示已兑换过该礼包 |
工具类实现代码:
package cdkey_utilimport ("errors""github.com/speps/go-hashids""strings"
)type CdKeyUtil struct {hashID *hashids.HashIDcdKeyLen int
}func Init(secret, charSetStr string, cdKeyLen int) (CdKeyUtil, error) {upStr := strings.ToUpper(charSetStr)if strings.Compare(upStr, charSetStr) != 0 {return CdKeyUtil{}, errors.New("CDKEY必须使用大写字母:" + charSetStr)}mateData := &hashids.HashIDData{Alphabet: charSetStr,MinLength: cdKeyLen,Salt: secret,}HashId, err := hashids.NewWithData(mateData)if err != nil {return CdKeyUtil{}, err}return CdKeyUtil{hashID: HashId, cdKeyLen: cdKeyLen}, err
}// Generate 生成指定数量的cdKey,同参数生成的cdKey相同
// 例如:如针对礼包1生成100个,则使用 Generate(1,1,100)。生成礼包2,之前已经生成了100个,想继续生成100个,则 Generate(2,101,100)
func (c CdKeyUtil) Generate(contentId, numFrom int64, needCount int) (cdKeyList []string, err error) {if numFrom < 1 || needCount < 1 {err = errors.New("错误的生成调用")return}cdKeyList = make([]string, 0, needCount)for i := 0; i < needCount; i++ {s, e := c.hashID.EncodeInt64([]int64{contentId, numFrom})if e != nil {err = ereturn}cdKeyList = append(cdKeyList, s)numFrom++}return
}// Decode 解析cdKey,返回生成时所用的数据
func (c CdKeyUtil) Decode(cdKey string) (contentId, number int64) {if len(cdKey) < c.cdKeyLen {return}cdKey = strings.ToUpper(cdKey)d, err := c.hashID.DecodeInt64WithError(cdKey)if err != nil {return}if len(d) != 2 {return}return d[0], d[1]
}
生成代码:
func (s *CdKeyService) Generate(packageId int64, from int64, count int) (res []string, err error) {if packageId == 0 || from == 0 || count == 0 {err = errors.New("无效参数")return}// 数据库中检查该礼包是否存在p, err := s.DB.CdKeyPackageGet(packageId)if err != nil {return}if p.ID != packageId {return nil, errors.New("记录未找到")}// 调用生成器生成指定数量兑换码res, err = s.CdKeyUtil.Generate(packageId, from, count)if err != nil {return}// 更新数据库中可兑换礼包的数量nowMaxCount := from + int64(count) - 1if nowMaxCount > p.GenerateCount {err = s.RdsGame.CdKeyPackageUpdateCount(packageId, nowMaxCount)if err != nil {return}}return
}
兑换代码:
func (s *CdKeyService) Redeem(uid int64, code string) (err error) {// 校验阶段var cdkeyPackage dao.CdKeyPackagepackageId, number := s.CdKeyUtil.Decode(code)if packageId == 0 || number == 0 {// 解析失败return errors.New("兑换码无效")}// 查询礼包信息cdkeyPackage, err = s.DB.CdKeyPackageGet(packageId)if err != nil {return err}if cdkeyPackage.ID == 0 {return errors.New("兑换码无效")}if !cdkeyPackage.Valid {return errors.New("兑换尚未开启")}if number > cdkeyPackage.GenerateCount {return errors.New("兑换码无效")}now := time.Now().Unix()if cdkeyPackage.StartTime > now {return errors.New("兑换尚未开启")}if cdkeyPackage.EndTime < now {return errors.New("兑换已经结束")}user, err := s.DB.GetUser(uid)if err != nil {return errors.New("用户不存在")}// 实际兑换// 直接插入数据库repeatedCode, err := s.DB.CdKeyRecordCreate(uid, packageId, number)if err != nil {return err}if repeatedCode == 1 {return errors.New("礼包码无效:您已兑换过该礼包")}else if repeatedCode == 2{return errors.New("礼包码无效:该礼包码已被他人使用")}// 发送奖励err = s.sendReward(user, cdkeyPackage.MailTemplateId,cdkeyPackage.Reward)return err
}
其他闲话:
- 通过索引uk_packageId_uid可以快速查询玩家兑换过的所有礼包记录
- 查询某兑换码是否生效,可走一遍解析流程,解析出礼包码和计数值后查库即可
- 因为兑换码和礼包的关联仅为礼包id,所以礼包的各项属性可以随意修改,无须担心影响已生成的兑换码
- 建议使用邮件模板来发送奖励邮件,灵活性较高,具体设计后续会更新。
- 推荐使用
ABCDEFGHJKLMNPQRSTUVWXYZ23456789 作为可用字符,防止玩家看错,无此需求也可以加入小写字母和特殊字符等,可用字符越多,指定位数下可生成的兑换码数量就越多,安全性也越高。
通兑码的兑换流程是在上面的兑换代码中加入packageType的判断,num值使用redis计数自增即可
使用该方案需要注意兑换码上限问题,以使用2个元素的数组,使用上面的可用字符,生成8位兑换码,则当第一个元素<22时,第二个元素最大可达到5153631,超出后将溢出为9位兑换码。当第一个元素<22*22=484时,第二个元素可达5153631/22=234255,以此类推。建议使用10位兑换码,可生成数量更多。
- 如果有更多需求,可以使用三个元素的int数组,表达更多信息,但在限定了兑换码字符数量的情况下,可用的兑换码数量将会大幅减少。
- 在连续生成兑换码时,因底层数组相似,生成的兑换码也比较相似,如需将一批兑换码交给合作商等不安全人员,建议进行人为乱序,防止根据规律破解密码。
- 初始化Util时的密码非常重要,必须妥善保存,且终身不能修改(如必须修改需则可以在礼包中增加所用密码字段,同时使用老密码和新密码解析,如使用老密码解析出礼包码后与该字段对比即可)
基于Hashids的高效游戏礼包兑换码系统完整设计相关推荐
- 基于SSM的校园疫情防控系统的设计与实现
word完整版可点击如下下载>>>>>>>> 基于SSM的校园疫情防控系统的设计与实现.rar_基于ssm的疫-互联网文档类资源-CSDN下载内容包括详 ...
- 基于Vue和SpringBoot的论文检测系统的设计与实现
作者主页:Designer 小郑 作者简介:Java全栈软件工程师一枚,来自浙江宁波,负责开发管理公司OA项目,专注软件前后端开发(Vue.SpringBoot和微信小程序).系统定制.远程技术指导. ...
- 基于微信小程序的教务查询系统的设计与实现
目录 1 绪论 2 1.1 研究背景 2 1.2 教务查询系统的现状和发展前景 3 1.3 系统的技术架构 3 1.4 论文框架 4 2 系统需求分析 5 2.1 系统概述 5 2.2 系统功能需求 ...
- 基于qt和mysql点菜系统的优点_基于QT的电子点餐订餐系统的设计与实现(SQLite)
基于QT的电子点餐订餐系统的设计与实现(SQLite)(任务书,外文翻译,毕业论文20000字,程序代码,SQLite数据库,答辩PPT) 摘 要 在深入研究中小餐饮企业工作流程的基础上,分析制约餐 ...
- 车载DMI linux系统,基于嵌入式的CTCS3级车载DMI系统的设计与实现
基于嵌入式的CTCS3级车载DMI系统的设计与实现 本文根据CTCS3级列控系统仿真实验室的实际情况,设计了一款基于嵌入式Linux操作系统的模拟CTCS3级车载DMI系统.它通过无线通信实现了车载与 ...
- linux界面设计论文,毕业设计(论文)-基于linux的云校园桌面虚拟化系统的设计与实现.doc...
毕业设计(论文)-基于linux的云校园桌面虚拟化系统的设计与实现.doc 还剩 67页未读, 继续阅读 下载文档到电脑,马上远离加班熬夜! 亲,很抱歉,此页已超出免费预览范围啦! 如果喜欢就下载吧, ...
- java平台设计zhe_基于java平台的网上评教系统的设计与实现
基于java平台的网上评教系统的设计与实现 作者: 郭文占 摘要: 教师评价是高校教育管理的重要方面,也是促进教育发展和教师发展的重要手段.网上评教极大地减少了教务管理人员的工作量,正在被越来越多的高 ...
- linux虚拟化毕业设计,毕业设计(论文)-基于Linux的云校园桌面虚拟化系统的设计与实现(68页)-原创力文档...
毕业设计(论文) 题 目: 基于Linux的云校园桌面 虚拟化系统的设计与实现 学 生: 指导老师: 柯 院 别: 软件学院 专 业: 计算机科学与技术 班 级: 1301 学 号: 2015年5月 ...
- 基于c的语言开发,基于CC++等高级编程语言开发电子系统的设计自动化系统.doc
基于CC等高级编程语言开发电子系统的设计自动化系统 基于CC++等高级编程语言开发电子系统的设计自动化系统 摘 要:当前电子系统设计自动化技术已广泛地应用于各个领域,随着科技的发展,对电子系统设计自动 ...
最新文章
- 技术03期:自然语言处理NLP【分词篇】
- centos7下安装maven
- 写给用我的“新闻推荐项目”做毕设的同学们
- 【OpenCV3】cv::Mat的定义与初始化
- gulp + browsersync实现页面自动刷新
- 【渝粤教育】 国家开放大学2020年春季 2773特种动物养殖 参考试题
- jpa多表关联查询_Spring Boot 整合mybatis如何自定义 mapper 实现多表关联查询
- Docker快速搭建Taiga敏捷开发项目管理平台
- 字符串匹配(一)—— KMP / MP
- @ font-face 引入本地字体文件
- Android 音视频开发学习思路大纲
- 我是一只IT小小鸟(转载)
- MacOS 10.15 Catalina:13个问题和修复
- centos7.4更新安装ssh8.8
- java导出CSV文件
- Jmeter 压力测试 - Http2.0工具支持-【教学篇】
- html文本框的文字间距,word文本框中2行文字的间距为什么那么大
- terminate called after throwing an instance of ‘YAML::TypedBadConversion<int>‘ what(): bad conver
- 成功帮我拿3家大厂offer(阿里、美团、虾皮),这份Java面试宝典,简直神了
- 网络爬虫排除协议robots.txt介绍及写法详解.
热门文章
- 一个生日微信小程序 生日动画_生日当天发朋友圈的文案 生日快乐微信小句子...
- 分享28个前端优秀项目源码(React+Vue+Node)
- 互联网热点:“双十一”节奏提前,猿辅导、掌门教育积极转型素质教育
- LRC歌词原理和实现高仿Android网易云音乐
- IG541与七氟丙烷灭火系统到底有什么不一样呢?
- 搭建ruby + jekyll + github pages
- 微信小程序中的变量和作用域
- 信捷PLC中Y0用C语言怎么表示,信捷PLC
- NS10.1 产品技术规范
- 12岁的微博回港上市,还有新故事吗?