BSN区块链服务网络开发入门

直播打卡合约详细介绍

小韦云科技-区块链+小程序+公众号+商城+分销+直播+企业官网+外贸电商-为您提供优质的开发服务-电话/微信联系:18123611282

接口通用方法

直播打卡系统介绍见我们精心准备的PPT文档:小韦云--直播打卡挑战赛系统(详细设计说明文档)

为了代码的通用性和可读性,我们有代码文件结构是与bsn给出的示例是一致的,并且只修改了bsnchaincode(以Fabric为例)下的base.go和chaincode.go两个业务相关的文件,其它地方并没作修改。

为提高合约代码的简约性,把一些接口通用的代码封装成通用方法,并且放到bsnchaincode/base.go文件中,主要有以下这些

字符串转数字

由于参数传进来的都是字符串格式,而有些字段是数字类型的,因此需要转换下

func intval(str string) int {}
func intval8(str string) int8 {}
func intval64(str string) int64 {}

一共提供了三个方法,分别返回int,int8,int64三种格式。
一般int是用于金钱或计数字段,int8用于状态字段,int64用于时间戳字段

转账

给某个用户转账时使用的方法,只需要转入转换的用户ID和要转账的金额就行,当金额为负时表示支付扣款,方法内会自动判断余额是否足够支付,不足时返回错误

func transferMoney(userId string, money int) peer.Response {}

获取某个用户数据

只需要给定用户ID,就可以返回用户数据的结构体,结构体内的json数据会自动解析好,在调用时直接使用即可

func getUser(userId string) (User, peer.Response) {}

获取某个活动数据

只需要给定活动ID,就可以返回活动数据的结构体,结构体内的json数据会自动解析好,在调用时直接使用即可

func getEvent(eventId string) (Event, peer.Response) {}

获取某个key对应的value值(重点)

类似示例中的get方法,但它能直接返回string字符串格式,并且自动判断给定的key是否加前缀,没有的话会自动加上

func GetValue(mainKey string) (string, peer.Response) {}

由于保存是以json格式保存,因此它会自动把json转能string返回,调用时不再需要json转换

设置key-value值(重点)

类似示例中的set和update两个方法的结合,并且自动判断给定的key是否加前缀,没有的话会自动加上

func SetValue(mainKey string, valueObj interface{}, canUpdate bool) peer.Response {}

mainKey 是要设置的key值
valueObj 是要设置的value值,支持多种类型的数据,保存前会自动转成json格式保存
canUpdate 当key已经存在时,是否允许更新,当为true时就相当于update操作

接口1:新增用户

用户的结构体定义如下

type User struct {
    Money        int                 //用户余额,单位为分
    JoinEventIds map[string]int8     //用户已经参与的活动ID集,格式:["活动ID"=>"支付方式"...],其中参与金额的支付方式:0 余额支付 1 微信支付
    UserKeywords map[string][]string //用户打卡成功的口令,格式:["活动1的ID"=>["口令1","口令2"],"活动2的ID"=>["口令3"]...]"
}

可以看到三个参数都不需要在注册时赋值,因此新增用户的实现代码如下

func UserReg(strings []string) peer.Response {
    data := User{0, make(map[string]int8), make(map[string][]string)}
    return SetValue(strings[0], data, false)
}

可以看到只需要传入一个用户ID就可以,其它参数使用默认值。

通过nodejs的SDK调用示例

contract.submitTransaction("UserReg", "user_123");

其中第一个参数为接口方法名,从第二个参数开始为方法的输入参数,下标从0开始,如在golang合约中通过strings[0]可得到user_123这个值,下面下接口都是这种格式输入,不再累赘。

接口2:充值/提现

充值或提现,当money为正数时表示充值,为负数时表示提现

func Recharge(strings []string) peer.Response {
    userId := strings[0]
    money := intval(strings[1])
    return transferMoney(userId, money)
}

需要转入两个参数,用户ID和金额

通过nodejs的SDK调用示例

contract.submitTransaction("Recharge", "user_123","100");

接口3:新增活动

活动的结构体定义

type Event struct {
    Uid           string              //发布活动的用户ID
    PrizeMoney    int                 //主播(发布活动的用户)追加的奖金
    PrizeCount    int                 //主播追加的奖金的分配方式 0 为全部平分 >0 表示只奖励打卡成功的前n名
    JoinMoney     int                 //参与活动用户需要支付的金额,单位为分
    JoinUserIds   []string            //成功参与活动的用户ID集,格式:"[用户1的ID,用户3的ID,...]"
    Eventkeywords map[string][]string //活动的口令设置,格式:"[口令1=>[口令1的开始时间戳,口令1的结束时间戳],口令2=>[口令2的开始时间戳,口令2的结束时间戳]...]"
    StartTime     int64               //活动的开始时间戳
    EndTime       int64               //活动的结束时间戳
    IsEnd         int8                //活动状态:0 未结束 1 已结束
    AdminPercent  int                 //平台奖金分成比例,如10%时,值为10
    AnchorPercent int                 //主播奖金分成比例,如20%时,值为20
}

由于活动的参数比较多,有10多个参数,其中JoinUserIds不需要传入,因此通过nodejs的SDK调用示例如下

var EventId = "event_123";
var Uid = "user_123";
var PrizeMoney = "10000";
var PrizeCount = "0";
var JoinMoney = "100";
var Eventkeywords = JSON.stringify(['小韦云':['1591670276','1591673276']]);
var StartTime = "1591670276";
var EndTime = "1591673276";
var IsEnd = "0";
var AdminPercent = "10";
var AnchorPercent = "20";

contract.submitTransaction("EventReg", EventId, Uid, PrizeMoney,PrizeCount, JoinMoney, Eventkeywords, StartTime, EndTime,IsEnd, AdminPercent, AnchorPercent);

注意数字都需要加引号当作字符串输入,否则golang得到的参数格式不对

接口4:报名参与活动

报名参与活动的实现代码

func UserJoinEvent(strings []string) peer.Response {
    //参数检查
    if len(strings) < 3 {
        return shim.Error("参数信息不正确")
    }
    userId := strings[0]
    eventId := strings[1]
    payType := intval8(strings[2])
    //其它业务代码省...
}

可以看到需要输入三个参数:用户ID,活动ID和支付方式

通过nodejs的SDK调用示例

contract.submitTransaction("UserJoinEvent", "user_123","event_123","0");

其中支付方式默认为0,表示余额支付,后续可扩展其它支付方式

接口5:用户打卡

打卡的实现代码

func UserClockIn(strings []string) peer.Response {
    //参数检查
    if len(strings) < 4 {
        return shim.Error("参数信息不正确")
    }
    userId := strings[0]
    eventId := strings[1]
    keyword := strings[2]
    nowTime := intval64(strings[3])
    //其它业务代码省...
}

可以看到需要输入四个参数:用户ID,活动ID和口令和打卡时间,由于区块链上是多个节点执行合约,每个节点的服务器时间可能都不一样,因此需要由业务端传入统一的打卡时间,以方便比较这个打卡时间是否是活动设置的打卡有效期内。

通过nodejs的SDK调用示例

contract.submitTransaction("UserJoinEvent", "user_123","event_123","小韦云","1591673270");

其中支付方式默认为0,表示余额支付,后续可扩展其它支付方式

接口6:奖金分配

通过定时器检查活动是否已经结束,结束的活动要完成奖金分配,这是本合约关键功能,全部实现代码如下

func CronEndEvent(strings []string) peer.Response {
    //参数检查
    if len(strings) < 1 {
        return shim.Error("参数信息不正确")
    }
    //遍历所有未结束的活动的key值
    fmt.Println("定时器任务开始:" + LiveEventKey)
    //获取value值
    value, err := GetValue(LiveEventKey)
    if err.GetStatus() != 200 {
        return err
    }
    if len(value) == 0 {
        return shim.Success([]byte("没有需要处理的活动"))
    }

    var eventIds []string
    jsonErr := json.Unmarshal([]byte (value), &eventIds)
    if jsonErr != nil {
        return shim.Error(fmt.Sprintf("Json 转换失败:%s", err))
    }
    if len(eventIds) == 0 {
        return shim.Success([]byte("真的没有需要处理的活动"))
    }

    nowTime := intval64(strings[0])

    for index, eventId := range eventIds {
        eventData, err2 := getEvent(eventId)
        if err2.GetStatus() != 200 {
            return err2
        }
        //活动结束后3分钟再结算奖金,防止活动结束那一秒还有人打卡
        if eventData.IsEnd == 0 && (nowTime-eventData.EndTime) > 180 {
            //获取活动的口令设置
            needClockInCount := len(eventData.Eventkeywords)
            fmt.Printf("需要打卡的次数:%d \n", needClockInCount)

            //初始化变量
            successUserIds := make([]string, 0) //成功打卡的用户ID
            failCount := 0
            successCount := 0
            //奖金分配记录,格式:[uid=>money]
            moneyArr := map[string]int{}

            if len(eventData.JoinUserIds) == 0 {
                fmt.Printf("没用户参与,只扣除平台费用,主播奖金退回到余额 \n")

                //没用户参与,只扣除平台费用,主播奖金退回到余额
                adminMoney := eventData.PrizeMoney * eventData.AdminPercent / 100
                fmt.Printf("计算平台的费用为:%d \n", adminMoney)
                transferMoney("user_1", adminMoney)

                AnchorMoney := eventData.PrizeMoney - adminMoney
                fmt.Printf("计算主播的退款为:%d \n", AnchorMoney)
                transferMoney(eventData.Uid, AnchorMoney)
            } else {
                fmt.Println("有用户参数,开始循环活动中的参与用户ID集:")
                fmt.Println(eventData.JoinUserIds)
                for _, uid := range eventData.JoinUserIds {
                    fmt.Printf("获取用户ID为 %s 的用户信息: \n", uid)
                    //获取用户的打卡信息
                    userData, err3 := getUser(uid)
                    if err3.GetStatus() != 200 {
                        return err3
                    }
                    fmt.Println(userData)

                    userCount := len(userData.UserKeywords[eventId])
                    fmt.Printf("获取该用户的打卡次数: %d \n", userCount)

                    fmt.Printf("比较用户的打卡次数与活动需要打卡的次数是否一致: %d == %d ? \n", userCount, needClockInCount)
                    if userCount != needClockInCount {
                        //用户打卡失败
                        failCount += 1
                        fmt.Printf("该用户打卡失败,失败用户数加1:%d \n", failCount)
                    } else {
                        //用户打卡成功
                        successCount += 1
                        fmt.Printf("该用户打卡成功,成功数加1:%d \n", successCount)
                        successUserIds = append(successUserIds, uid)
                        fmt.Printf("该用户打卡成功,用户ID加入到待颁奖的名单中 \n")
                        fmt.Println(successUserIds)
                    }
                }

                //计算总奖金 = 主播追加的奖金 + 用户打卡失败部分的累计奖金
                totalMoney := eventData.PrizeMoney + eventData.JoinMoney*failCount
                fmt.Printf("总奖金:%d \n", totalMoney)

                /***
                 * 资金分配方案
                 * 没有主播奖金的活动,只有打卡失败的累计奖金,10%归平台,20%归主播,70%打卡成功者平分(实际比例在创建活动时提交)
                 * 有主播奖金的活动,主播奖金部分按主播设置的颁奖方式90%分配给打卡成功者,10%归平台,打卡失败的累计奖金部分分配方式同上
                 **/

                //没有人打卡成功,奖金就平台和主播分
                if successCount == 0 {
                    fmt.Println("====没有人打卡成功,奖金就平台和主播分===")
                    adminMoney := totalMoney * eventData.AdminPercent / 100
                    fmt.Printf("计算平台的费用为:%d \n", adminMoney)
                    transferMoney(AdminId, adminMoney)

                    AnchorMoney := totalMoney - adminMoney
                    fmt.Printf("计算主播的收益为:%d \n", AnchorMoney)
                    transferMoney(eventData.Uid, AnchorMoney)
                } else {
                    //先分配主播奖金,但如果主播设置全部平分奖金,或者成功打卡人数比设置的人数还少,那就直接在后面平均分配,不需要单独分配
                    if eventData.PrizeMoney > 0 && eventData.PrizeCount > 0 && eventData.PrizeCount > successCount {
                        fmt.Println("====进入了单独分配主播奖金部分===")
                        totalMoney -= eventData.PrizeMoney //
                        fmt.Printf("后面平分的奖金要减少先分配的主播奖金,减后的总奖金为:%d \n", totalMoney)

                        adminMoney := eventData.PrizeMoney * eventData.AdminPercent / 100
                        fmt.Printf("计算平台的费用为:%d \n", adminMoney)
                        transferMoney(AdminId, adminMoney)

                        rewardMoney := eventData.PrizeMoney - adminMoney
                        fmt.Printf("计算主播剩余的奖金为:%d \n", rewardMoney)
                        reward(rewardMoney, eventData.PrizeCount, successUserIds, moneyArr)
                    }
                    fmt.Println("====进入了平均分配总奖金部分===")
                    //平均分配剩余奖金
                    adminMoney := totalMoney * eventData.AdminPercent / 100
                    fmt.Printf("计算平台的费用为:%d \n", adminMoney)
                    transferMoney(AdminId, adminMoney)

                    AnchorMoney := totalMoney * eventData.AnchorPercent / 100
                    fmt.Printf("计算主播的收益为:%d \n", AnchorMoney)
                    transferMoney(eventData.Uid, AnchorMoney)

                    UserMoney := totalMoney - adminMoney - AnchorMoney
                    fmt.Printf("计算用户的总奖金为:%d \n", UserMoney)
                    reward(UserMoney, successCount, successUserIds, moneyArr)
                    fmt.Println("计算出待分配的名单和奖金清单")
                    fmt.Println(moneyArr)

                    //算完钱后开始真正发钱了
                    for userId, money := range moneyArr {
                        transferMoney(userId, money)
                    }
                }
            }

            //把已经处理完毕的活动从待处理活动列表中去掉
            eventIds = append(eventIds[:index], eventIds[index+1:]...)

            return SetValue(LiveEventKey, eventIds, true)

            //一次只处理一个活动
            break
        }

    }
    return shim.Success([]byte("处理任务完毕"))
}

代码中增加详细注释,不再累赘。

这个接口由业务定时(可设置5分钟一次)执行,只需要传入一个当前时间即可。

通过nodejs的SDK调用示例

contract.submitTransaction("CronEndEvent", "1591673270");

其实理论上可以让合约通过定时器自动执行,这样就可以做到完全无人工干涉,自动执行的智能合约。但一方面时执行合约的节点时间可能不一致,要做到一致的话合约的复杂度就大大增加,另一方面增加定时器也会增加了合约的复杂度。因此目前先由接口触发为主。

本文由小韦云原创,转载请注明出处:https://bctos.cn/doc/10/1856,否则追究其法律责任

关键词:

广告位招商