[聚合文章] 65行写个微信红包

JavaScript 2017-12-01 10 阅读

65行写个微信红包

陶文陶文

docstore是一种创新的文档数据库,帮助开发者更快地完成复杂的业务需求。这个例子里,我们来看这样的一个需求:

  • 发红包时设置可以抢到的人数
  • 多个人可以并发的抢,然后知道自己抢到没有,抢到了多少
  • 可以查看红包当前被抢的情况,都有谁抢到了

下面我们来看如何用不到65行代码实现一个微信红包

最后完成的代码:v2pro/quokka

业务逻辑的定义

docstore.Entity("Hongbao", `struct taken {	1: i64 taken_at	2: i64 taken_amount // unit fen}struct Doc {	1: i64 total_count	2: i64 total_amount // unit fen	3: i64 remaining_amount // unit fen	4: map<string, taken> takens}`).Command("create", `function handle(doc, req) {	doc.total_count = req.count;	doc.total_amount = req.amount;	doc.remaining_amount = req.amount;	doc.takens = {};	return {};}`, `struct Request {	1: float64 amount	2: i64 count}struct Response {}`).Command("take", `function handle(doc, req) {	if (doc.takens[req.username]) {		throw 'one user can not take twice'	}	var takens_count = Object.keys(doc.takens).length;	if (takens_count == doc.total_count) {		throw 'nothing left'	}	if (takens_count == doc.total_count - 1) {		// last one, take all		var taken_amount = doc.remaining_amount;		doc.remaining_amount = 0;		doc.takens[req.username] = {taken_at: Date.now(), taken_amount: taken_amount};		return {taken_amount: taken_amount};	}	var taken_amount = calcRandomAmount(doc.remaining_amount, doc.total_amount, doc.total_count);	doc.remaining_amount -= taken_amount;	doc.takens[req.username] = {		taken_at: Date.now(),		taken_amount: taken_amount	};	return {taken_amount: taken_amount};}function calcRandomAmount(remaining_amount, total_amount, total_count) {	var cap_amount = 2 * total_amount / total_count;	if (remaining_amount < cap_amount) {		cap_amount = remaining_amount;	}	return Math.floor(randBetween(1, cap_amount));}function randBetween(min, max) {	return Math.random() * (max - min) + min;}`, `struct Request {	1: string username}struct Response {	1: float64 taken_amount}`)

可以数数,一共就65行。这里先定义了一个了业务对象(entity)叫“Hongbao”。

struct taken {	1: i64 taken_at	2: i64 taken_amount // unit fen}struct Doc {	1: i64 total_count	2: i64 total_amount // unit fen	3: i64 remaining_amount // unit fen	4: map<string, taken> takens}

其中的“taken”对象的含义是一次红包的领取。map<string, taken>的key是领取人,value是领取的详情,包括什么时候领取的,抢到了多少钱。

然后定义了两个命令,create和take。先来看create

function handle(doc, req) {	doc.total_count = req.count;	doc.total_amount = req.amount;	doc.remaining_amount = req.amount;	doc.takens = {};	return {};}

虽然docstore框架是用golang实现的,但是命令的handler是拿javascript写的。这个命令很简单,就是初始化一下Hongbao。

然后是take命令

function handle(doc, req) {	if (doc.takens[req.username]) {		throw 'one user can not take twice'	}	var takens_count = Object.keys(doc.takens).length;	if (takens_count == doc.total_count) {		throw 'nothing left'	}	if (takens_count == doc.total_count - 1) {		// last one, take all		var taken_amount = doc.remaining_amount;		doc.remaining_amount = 0;		doc.takens[req.username] = {taken_at: Date.now(), taken_amount: taken_amount};		return {taken_amount: taken_amount};	}	var taken_amount = calcRandomAmount(doc.remaining_amount, doc.total_amount, doc.total_count);	doc.remaining_amount -= taken_amount;	doc.takens[req.username] = {		taken_at: Date.now(),		taken_amount: taken_amount	};	return {taken_amount: taken_amount};}

这里实现的是抢红包的主流程,分别对应了这么几条规则

  • 抢过了不能再抢
  • 抢完了就没得抢了
  • 最后一次领取拿走全部的红包余额
  • 其余情况用随机的算法计算这次能领到多少

具体的随机算法实现在了calcRandomAmount的子函数里:

function calcRandomAmount(remaining_amount, total_amount, total_count) {	var cap_amount = 2 * total_amount / total_count;	if (remaining_amount < cap_amount) {		cap_amount = remaining_amount;	}	return Math.floor(randBetween(1, cap_amount));}function randBetween(min, max) {	return Math.random() * (max - min) + min;}

注意的是计算是用分作为单位,用整数计算的,保证了不会出零头的问题。

然后我们来看一下这个红包后台怎么被调用:

func Test_take_hongbao(t *testing.T) {	should := require.New(t)	execAndExpectSuccess(t, "/Hongbao/create",		"EntityId", "123", "CommandRequest", runtime.NewObject("amount", 1314, "count", 3))	resp := execAndExpectSuccess(t, "/Hongbao/take",		"EntityId", "123", "CommandRequest", runtime.NewObject("username", "tom"))	taken1 := jsoniter.Get(resp, "data", "taken_amount").ToInt()	resp = execAndExpectError(t, "/Hongbao/take", docstore.ErrBusinessRuleViolated,		"EntityId", "123", "CommandRequest", runtime.NewObject("username", "tom"))	should.Equal("one user can not take twice", jsoniter.Get(resp, "errmsg").ToString())	resp = execAndExpectSuccess(t, "/Hongbao/take",		"EntityId", "123", "CommandRequest", runtime.NewObject("username", "jerry"))	taken2 := jsoniter.Get(resp, "data", "taken_amount").ToInt()	resp = execAndExpectSuccess(t, "/Hongbao/take",		"EntityId", "123", "CommandRequest", runtime.NewObject("username", "donald"))	taken3 := jsoniter.Get(resp, "data", "taken_amount").ToInt()	should.Equal(1314, taken1+taken2+taken3)	resp = execAndExpectError(t, "/Hongbao/take", docstore.ErrBusinessRuleViolated,		"EntityId", "123", "CommandRequest", runtime.NewObject("username", "lily"))	should.Equal("nothing left", jsoniter.Get(resp, "errmsg").ToString())	resp = queryAndExpectSuccess(t, "/entities/Hongbao/123")	fmt.Println(string(resp))}

这个测试调用的就是这么三个url

最后一条println打印出来的json

{   "errno":0,   "entity_version":6,   "data":{      "remaining_amount":0,      "takens":{         "tom":{            "taken_at":1512130112473,            "taken_amount":302         },         "jerry":{            "taken_at":1512130112473,            "taken_amount":143         },         "donald":{            "taken_amount":869,            "taken_at":1512130112473         }      },      "total_count":3,      "total_amount":1314   }}

那么docstore除了把javascript翻译成了go进行编译执行之外,提供了什么好处呢?

  • 并发控制:命令是串行处理的。在javascript代码里完全就按照没并发的情况来写
  • 性能:整个计算都是在内存里进行的,每次落盘只是插入一条event log。因为计算前就进行了命令的排队(先确保发到同一个master节点,然后master节点内部用go channel进行了串行化),所以没有并发写入带来的锁的成本。
  • 强类型:虽然是动态类型的,但是所有对象(doc,request,response)都是有schema定义的。

参见:创新的主存储方案

参见:我们需要什么样的数据库