golang使用protobuf协议进行交互,使用protojson进行序列化和反序列化解析复杂的proto协议操作案例讲解
2026/4/24 8:35:19 网站建设 项目流程

1.需求

前端通过post body的json方式把数据发生给api接口,通过protojson解析并进行数据校验,然后进行序列化保存到数据库,另一个接口获取数据并反序列化后返回

该需求中proto协议复杂, 嵌套众多, 条件判断复杂, 但只要理解了该需求, 其他需求的协议基本上都是小克斯

2.请求的json如下

{ "PlayBase": { "Priority": 779, "RepeatedTrigger": 3, "EffectiveTimeRange": { "From": "2026-03-29 00:00:00", "To": "2026-03-29 23:59:59" }, "Probability": 10000, "Remark": "一次性放分100-20000", "Creator": "xxx", "UserId": 2, "PlayStatus": 1, "PlayType": 0, "GroupId": 2, // 剧本分组id "Weight": 1, // 权重 "GroupPriority": 25, // 组优先级 "WinLoseValue": { // 输赢分:如果设置了输赢分,剧本达到输赢分跳出剧本; 这里 Type = 3 "Type": 3, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100 }, "ExeCount": { // 执行次数:如果设置了执行次数,剧本达到执行次数跳出剧本; 这里 Type = 3 "Type": 3, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100 } }, "ResultList": [ // 剧本多结果 { "Weight": 100, // 权重 "Result": { // 结果 "PlayResultType": 1, // 结果类型: 1 一次性赢分 "OneWinResult": { "GameIds": { // 剧本结果: 生效机台档位: 生效游戏id "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "GearLevel": { // 剧本结果: 生效机台档位: 档位 "IsExcept": false, // true-排除、false-包含 "Values": [ // 具体数据以 前端枚举为主,下面的只是举例 1, 2 ] }, "EffectCountRange": { // 生效次数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "Range": { // 赢分区间 "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "MultipleRange": { // 倍数区间(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "SetValueRange": { // 设定的值区间(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 } } } }, { "Weight": 100, // 权重 "Result": { // 结果 "PlayResultType": 3, // 结果类型: 3 获得一次FG "OneFGResult": { "GameIds": { // 剧本结果: 生效机台档位: 生效游戏id "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "GearLevel": { // 剧本结果: 生效机台档位: 档位 "IsExcept": false, // true-排除、false-包含 "Values": [ // 具体数据以 前端枚举为主,下面的只是举例 1, 2 ] }, "EffectCountRange": { // 生效次数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "Range": { // 赢分区间(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "MultipleRange": { // 倍数区间(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "SetValueRange": { // 设定的值区间(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 } } } }, { "Weight": 100, // 权重 "Result": { // 结果 "PlayResultType": 4, // 结果类型: 4 获得一次Bonus "OneBonusResult": { "GameIds": { // 剧本结果: 生效机台档位: 生效游戏id "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "GearLevel": { // 剧本结果: 生效机台档位: 档位 "IsExcept": false, // true-排除、false-包含 "Values": [ // 具体数据以 前端枚举为主,下面的只是举例 1, 2 ] }, "EffectCountRange": { // 生效次数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "Range": { // 倍数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 } } } }, { "Weight": 100, // 权重 "Result": { // 结果 "PlayResultType": 7, // 结果类型: 7 射击中奖概率 "ShootWinPropResult": { "PropRange": { // 中奖概率区间(百分之几)(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, // 第一个值,如果不是区间范围,只需要value即可 "ExtValue": 10000 // 区间末值 }, "Ground": { // 生效射击场(可多选): 2 2人射击场, 4 4人射击场 "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "Types": { // 生效射击种类(可多选) "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "GearLevel": { // 射击生效档位(可多选) "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] } }, } } ], "Condition": { // 条件 "WinLoseRange": { "Type": 1 }, "DayWinLoseRange": { "Type": 1 }, "UserIds": "", "BackDay": 0, "MallId": 0, "ShootGround": { // 判断射击场(可多选) "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "ShootTypes": { // 判断射击种类(可多选) "IsExcept": false, // true-排除、false-包含 "Values": [ 1, 2 ] }, "ShootTotalLaunchCountRange": { // 射击累计发射次数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, "ExtValue": 10000 }, "ShootDayLaunchCountRange": { // 射击当日发射次数(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100, "ExtValue": 10000 }, "ShootTotalRTPRange": { // 射击累计RTP(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100.11, "ExtValue": 10000.11 }, "ShootDayRTPRange": { // 射击当日发RTP(左闭右开) "Type": 8, // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 "Value": 100.11, "ExtValue": 10000.11 } } }

3.保存到数据库的json字符串如下:

该json不是步骤2中的数据, 只是类似的数据, 结构相同而已,这里仅举例说明

{ "PlayBase": { "PlayId": "440", "Priority": 1, "RepeatedTrigger": 3, "EffectiveTimeRange": { "From": "2026-04-21 00:00:00", "To": "2026-04-21 23:59:59" }, "Probability": 10000, "Remark": "", "Creator": "xxx", "UserId": "234", "PlayStatus": 3, "PlayType": 0, "GroupId": 0, "Weight": 0, "GroupPriority": 0, "WinLoseValue": { "Type": 8, "Value": "10000000", "ExtValue": "0" }, "ExeCount": { "Type": 8, "Value": "0", "ExtValue": "0" } }, "ResultList": [{ "Result": { "PlayResultType": 7, "FishWinPropResult": { "PropRange": { "Type": 8, "Value": "10000", "ExtValue": "100000" }, "Ground": { "IsExcept": false, "Values": [10011] }, "Types": { "IsExcept": false, "Values": [1] } } }, "Weight": 100 }], "Condition": { "WinLoseRange": { "Type": 1 }, "DayWinLoseRange": { "Type": 1 }, "UserIds": "", "BackDay": 0, "MallId": 0 } }

4.使用到的proto协议构成如下

//-------------枚举类型------------- // 触发频率: 1001 可重复触发(每天一次), 1002 不可重复触发(终身触发一次), 1003 可重复触发(满足条件) enum RepeatedTrigger { IDLE_TRIGGER = 0; // 空闲 ONCE_DAY_TRIGGER = 1001; // 可重复触发(每天一次) ONCE_LIFETIME_TRIGGER = 1002; // 不可重复触发(终身触发一次) CONDITON_TRIGGER = 1003; // 可重复触发(满足条件) } // 结果类型枚举 enum PlayResultType { RNADOM_RESULT = 0; // 随机 ONE_WIN_RESULT = 1001; // 一次性赢分 RTP_RESULT = 1002; // 特定RTP ONE_FG_RESULT = 1003; // 获得一次FG ONE_BONUS_RESULT = 1004; // 获得一次Bonus Shoot_WIN_PROP = 1007; // 射击中奖概率 } // 输赢类型枚举 enum WinOrLoseType { WINORLOSETYPE_DEFAULT = 0; WIN_TYPE = 1001; // 赢 LOST_TYPE = 1002; // 输 } // 状态枚举 enum PlayStatus { PlaySTATUS_INVALID = 0; // 未生效 PlaySTATUS_ACTIVE = 1001; // 生效中 PlaySTATUS_PAUSE = 1002; // 暂停 PlaySTATUS_FINISHED = 1003; // 已结束 PlaySTATUS_DELETED = 1004; // 已删除 } // 类型枚举, 参考 enum ControlRuleType enum PlayType { PlayTYPE_dial = 0; // dial } // 退出原因类型枚举 enum ReasonId { EXIT_REASON_ID_NORMAL = 0; //正常退出 EXIT_REEXIT_REASON_ID_BET_TOO_HIGH = 1001; //档位超出上限 EXIT_REXIT_REASON_ID_REWARD_TOO_HIGH = 1002; //下注档位过低 EXIT_EXIT_REASON_ID_BET_UNEXPECTLY = 1003; //异常退出 EXIT_REASON_ID_EXPIRED = 1004; //过期 EXIT_REASON_ID_PAUSE = 1005; //暂停 } //-------------协议交互------------ // 协议 编辑/创建/复制交互 message PlayParams { //-------------信息相关------------- // @gotags: json:"PlayBase" PlayBase PlayBase = 1; //-------------结果相关------------- // @gotags: json:"ResultList" repeated ResultList ResultList = 2; //-------------进入条件相关------------- Condition Condition = 3; } // 信息相关 message PlayBase { int64 PlayId = 1; // id // @gotags: json:"Priority" int32 Priority = 2; // 优先级 // @gotags: json:"RepeatedTrigger" uint32 RepeatedTrigger = 3; //触发频率: 1001 可重复触发(每天一次), 1002 不可重复触发(终身触发一次), 1003 可重复触发(满足条件) // @gotags: json:"EffectiveTimeRange" TimeStringParam EffectiveTimeRange = 4; // 开始/结束时间 // @gotags: json:"Probability" int32 Probability = 5; // 触发概率 : 限制输入1-10000,默认值10000 string Remark = 6; // 备注 string Creator = 7; // 创建人 int64 UserId = 8; // 创建人id uint32 PlayStatus = 9; // 状态:0-未生效 1001-生效中 1002-暂停 1003-已结束 1004-已删除 uint32 PlayType = 10; // 类型(默认为0): 0 dial int32 GroupId = 11; // 分组id int32 Weight = 12; // 权重 int32 GroupPriority = 13; // 组优先级 // @gotags: json:"WinLoseValue" ValueIntRange WinLoseValue = 14; // 输赢分:如果设置了输赢分,达到输赢分跳出; 这里 Type = 3 // @gotags: json:"ExeCount" ValueIntRange ExeCount = 15; // 执行次数:如果设置了执行次数,达到执行次数跳出; 这里 Type = 3 } //数值比较 message ValueIntRange { int32 Type = 1; // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 int64 Value = 2; // 第一个值,如果不是区间范围,只需要value即可 int64 ExtValue = 3; //区间末值 } //int多选值 message IntListRange { bool IsExcept = 1; repeated int32 Values = 2; } //数值比较Double型 message ValueDoubleRange { int32 Type = 1; // 1-等于、2-大于、3-大于等于、4-小于、5小于等于、6-区间 闭区间 7:区间 左开右闭 8:区间 左闭右开 9:区间 全开区间 10:不等于 11:包含 12:不包含 13:为空 14:非空 15:正则匹配 16: 正则不匹配 double Value = 2; // 第一个值,如果不是区间范围,只需要value即可 double ExtValue = 3; //区间末值 } message TimeStringParam { //开始时间 string From = 1 ; //结束时间 string To = 2 ; } // 结果相关 message ResultList { // @gotags: json:"Result" ResultItem Result = 1; // 结果 int32 Weight = 2; // 权重 } // 进入条件相关 message ConditionInfo { ValueIntRange RegisterRange = 1; // 进入条件 IntListRange CheckGameIds = 2; // 进入条件: 生效射击机台档位: 生效射击机台Id ValueIntRange OperCountRange = 3; // Oper次数(左闭右开) WinOrLose WinLoseRange = 4; // 输赢(左闭右开) WinOrLose DayWinLoseRange = 5; // 当日输赢(左闭右开) // WinOrLose WinLoseNumRange = 6; // 连续输赢局数(左闭右开) ValueIntRange MonetaryStockRange = 7; // 剩余金币(左闭右开) ValueIntRange DayOperCountRange = 8; // 当日Oper次数(左闭右开) ValueIntRange TotalChargeRange = 9; // 充值总金额(左闭右开) ValueIntRange DayTotalChargeRange = 10; // 当日充值金额(左闭右开) ValueDoubleRange RTPRange = 11; // dial累计RTP(左闭右开) ValueIntRange FGWinRange = 12; // FG赢N倍(左闭右开) ValueIntRange BounsWinRange = 13; // Bonus赢N倍(左闭右开) string UserIds = 14; // 玩家ID: 使用英文逗号分隔,需要校验输入格式仅有英文逗号和数字,且第一位字符和最后一位字符不能为英文逗号 IntListRange PrizeProbabilities = 15; // 返奖率 IntListRange VipLevel = 16; // vip等级 uint32 BackDay = 17; // 回流天数: >= BackDay天 uint32 MallId = 18; // 商品Id // 射击相关 IntListRange ShootGround = 19; // 判断射击场(可多选): 2 2人射击场, 4 4人射击场 // IntListRange ShootTypes = 20; // 判断射击种类(可多选) ValueIntRange ShootTotalLaunchCountRange = 21; // 射击累计发射次数(左闭右开) ValueIntRange ShootDayLaunchCountRange = 22; // 射击当日发射次数(左闭右开) ValueDoubleRange ShootTotalRTPRange = 23; // 射击累计RTP(左闭右开) ValueDoubleRange ShootDayRTPRange = 24; // 射击当日RTP(左闭右开) ValueDoubleRange PlatformTotalRTPRange = 25; // 平台累计RTP(左闭右开) } // 单个结果条目 message ResultItem { uint32 PlayResultType = 1; // 结果类型 oneof ResultData { // 结果 OneWinResult OneWinResult = 2; // 一次性赢分 RTPResult RTPResult = 3; // 特定RTP OneFGResult OneFGResult = 4; // 获得一次FG OneBonusResult OneBonusResult = 5; // 获得一次Bonus ShootWinPropResult ShootWinPropResult = 8; // 射击中奖概率 } } // 结果(类型7 射击中奖概率) message ShootWinPropResult { ValueIntRange PropRange = 1; // 中奖概率区间(百分之几)(左闭右开) IntListRange Ground = 2; // 生效射击场(可多选): 2 2人射击场, 4 4人射击场 IntListRange Types = 3; // 生效射击种类(可多选) IntListRange GearLevel = 4; // 射击生效档位(可多选) } // 结果(类型1 一次性赢分) message OneWinResult { ValueIntRange Range = 1; // 赢分区间 ValueIntRange MultipleRange = 2; // 倍数区间(左闭右开) // ValueIntRange SetValueRange = 3; // 设定的值区间(左闭右开) IntListRange GameIds = 3; // 结果: 生效射击机台档位: 生效射击机台Id IntListRange GearLevel = 4; // 结果: 生效射击机台档位: 档位 ValueIntRange EffectCountRange = 5; // 生效次数(左闭右开) } // 结果(类型2 特定RTP) message RTPResult { uint32 Value = 1; //轮轴表,默认值选101 WinOrLose OutWinLoseRange = 2; // 出输赢值(左闭右开) } // 结果(类型3 获得一次FG) message OneFGResult { ValueIntRange Range = 1; // 赢分区间(左闭右开) ValueIntRange MultipleRange = 2; // 倍数区间(左闭右开) // ValueIntRange SetValueRange = 3; // 设定的值区间(左闭右开) IntListRange GameIds = 3; // 结果: 生效射击机台档位: 生效射击机台Id IntListRange GearLevel = 4; // 结果: 生效射击机台档位: 档位 ValueIntRange EffectCountRange = 5; // 生效次数(左闭右开) } // 结果(类型4 获得一次Bonus) message OneBonusResult { ValueIntRange Range = 1; // 倍数区间(左闭右开) IntListRange GameIds = 2; // 结果: 生效射击机台档位: 生效射击机台Id IntListRange GearLevel = 3; // 结果: 生效射击机台档位: 档位 ValueIntRange EffectCountRange = 4; // 生效次数(左闭右开) } // 输赢 message WinOrLose { uint32 Type = 1; // 1 赢, 2 输 ValueIntRange Range = 2; // 输/赢范围 }

5.函数解析请求以及入库操作如下

// PlayParams 参数 type PlayParams struct { // 创建/复制时的请求参数 Param *backstage.PlayParams `json:"param"` } // 配置 protojson 序列化选项​: 在 Go 代码中强制序列化所有字段(包括默认值) func serialize(msg *backstage.PlayParams) ([]byte, error) { opts := protojson.MarshalOptions{ // EmitUnpopulated: true, // 强制序列化所有字段(含默认值) UseProtoNames: true, // 使用 Protobuf 原始字段名 EmitDefaultValues: true, // 指定是否输出默认 值的原始类型字段、空列表和空映射, 与 EmitUnpopulated相比,不会输出 "null" 值字段, 如果 EmitUnpopulated为 true,它会优先于 EmitDefaultValues } return opts.Marshal(msg) } // CreatePlay 创建 func (param *PlayParams) CreatePlay(ctx context.Context, userId uint64, req *backstage.PlayParams) error { if req != nil { param.Param = req } var ( err error dbClient = Mysql redisClient = Redises PlayStatus uint32 PlayList model.PlayStruct ) //判断优先级是否已存在 priority := dbClient.DB.WithContext(ctx).Debug(). Where(fmt.Sprintf("priority = %d and Play_status != %d and Play_type = %d", param.Param.PlayInfo.Priority, backstage.PlayStatus_PlaySTATUS_DELETED, backstage.PlayType_PlayTYPE_dial)).First(&PlayList) if priority.RowsAffected > 0 { return errors.New("优先级已存在,请重新创建!") } // 优先级不能大于10000 if param.Param.PlayInfo.Priority > 10000 { return errors.New("优先级不能大于10000!") } // 判断优先级是否在分组中已存在 if param.Param.PlayInfo.GroupId > 0 { PlayGroup, err := getPlayGroupById(ctx, uint64(param.Param.PlayInfo.GroupId)) if err != nil && err != gorm.ErrRecordNotFound { return err } if PlayGroup == nil || PlayGroup.Id == 0 { return errors.New("分组不存在!") } if PlayGroup.Priority == param.Param.PlayInfo.Priority { return fmt.Errorf(fmt.Sprintf("优先级和分组#%d的优先级%d重复, 请重新填写优先级! ", PlayGroup.Id, PlayGroup.Priority)) } } param.Param.PlayInfo.PlayId = -1 currentDate, _ := time.ParseInLocation(core.GoDayFormat, time.Now().Format(core.GoDayFormat), time.Local) PlayStartTime, _ := time.ParseInLocation(core.GoDayFormatSes, param.Param.PlayInfo.EffectiveTimeRange.From, time.Local) PlayEndTime, _ := time.ParseInLocation(core.GoDayFormatSes, param.Param.PlayInfo.EffectiveTimeRange.To, time.Local) if len(param.Param.ResultInfo) <= 0 { return errors.New("结果集不能为空") } // 是否需要校验 结果集数据 for _, result := range param.Param.ResultData { if result.Weight <= 0 { return errors.New("权重必须大于0") } if result.Result == nil { return errors.New("结果类型不能为空") } // 类型断言:确认 ResultData 是 OneWinResult(一次性赢分) 类型: 校验里面的数据 其他类型数据校验类似 if oneWinResult, ok := result.Result.ResultData.(*backstage.ResultItem_OneWinResult); ok { // 判断里面的数据是否存在 oneWinResultRange := oneWinResult.OneWinResult.Range oneWinResultMultiple := oneWinResult.OneWinResult.MultipleRange if oneWinResultRange == nil { return errors.New("一次赢分范围为必填项") } if oneWinResultMultiple == nil { return errors.New("一次赢分倍数为必填项") } } if RTPResult, ok := result.Result.ResultData.(*backstage.ResultItem_RTPResult); ok { // 特定RTP // 判断环境, 只有开发服和测试服才会有 特定RTP 结果集 appMode := appConfig.AppConfig.AppMode if appMode != "dev" && appMode != "test" { return errors.New("环境错误, 只有开发服和测试服才有 特定RTP 结果集") } value := RTPResult.RTPResult.Value outWinLoseRange := RTPResult.RTPResult.OutWinLoseRange if value == 0 { return errors.New("轮轴表必填项") } if outWinLoseRange == nil { return errors.New("出输赢值为必填项") } } if oneFGResult, ok := result.Result.ResultData.(*backstage.ResultItem_OneFGResult); ok { // 获得一次FG oneWinResultRange := oneFGResult.OneFGResult.Range oneWinResultMultiple := oneFGResult.OneFGResult.MultipleRange if oneWinResultRange == nil { return errors.New("一次赢分范围为必填项") } if oneWinResultMultiple == nil { return errors.New("一次赢分倍数为必填项") } } if oneBonusResult, ok := result.Result.ResultData.(*backstage.ResultItem_OneBonusResult); ok { // 获得一次Bonus oneWinResultRange := oneBonusResult.OneBonusResult.Range if oneWinResultRange == nil { return errors.New("倍数为必填项") } } if shootWinPropResult, ok := result.Result.ResultData.(*backstage.ResultItem_ShootWinPropResult); ok { // 捕鱼中奖概率 shootWinPropResultPropRange := shootWinPropResult.ShootWinPropResult.PropRange if shootWinPropResultPropRange == nil { return errors.New("捕鱼中奖概率范围为必填项") } } } //创建初始状态判断 if PlayStartTime.Unix() <= time.Now().Unix() { PlayStatus = uint32(backstage.PlayStatus_PlaySTATUS_ACTIVE) // 生效中 } if PlayEndTime.Unix() <= time.Now().Unix() { PlayStatus = uint32(backstage.PlayStatus_PlaySTATUS_FINISHED) // 已结束 } param.Param.PlayInfo.PlayStatus = PlayStatus // copy parma.Param, 后续会更新PlayId数据 copyParam := param.Param strParam, _ := serialize(param.Param) createModel := &model.PlayStruct{ CreatedAt: currentDate, Creator: param.Param.PlayInfo.Creator, CreatorId: int64(param.Param.PlayInfo.UserId), PlayResultCategory: 2, // 所有都是多结果类型 Priority: param.Param.PlayInfo.Priority, GroupId: int32(param.Param.PlayInfo.GroupId), GroupPriority: param.Param.PlayInfo.GroupPriority, PlayStartTime: param.Param.PlayInfo.EffectiveTimeRange.From, PlayEndTime: param.Param.PlayInfo.EffectiveTimeRange.To, PlayStatus: PlayStatus, EditParams: string(strParam), // 请求的数据都保存到该字段里面, 包括结果相关配置, 结果相关配置具体见backstage.PlayParams Remark: param.Param.PlayInfo.Remark, PlayType: uint32(backstage.PlayType_PlayTYPE_dial), } // 校验触发概率 if param.Param.PlayInfo.Probability > 10000 { // 运营的规则:万分之几 mylog.Infof("CreatePlay Error marshaling msg err: 触发概率错误, %v", param.Param.PlayInfo.Probability) return errors.New("所有结果触发概率不可大于100%") } tx := dbClient.DB.WithContext(ctx).Debug().Create(&createModel) if tx.Error != nil { mylog.Errorf("create插入失败:%v", tx.Error) return tx.Error } else { mylog.Infof("create插入成功,受影响的行数:%v", tx.RowsAffected) } // copy parma.Param, 后续会更新PlayId copyParam.PlayInfo.PlayId = int64(createModel.PlayID) strParam, _ = serialize(copyParam) tx = dbClient.DB.WithContext(ctx).Debug().Model(&createModel).UpdateColumn("edit_params", strParam) if tx.Error != nil { mylog.Errorf("create插入更新PlayId失败:%v", tx.Error) return tx.Error } else { mylog.Infof("create插入更新PlayId成功,受影响的行数:%v", tx.RowsAffected) } // 后续逻辑处理 return err }

6.通过id获取剧本数据,并解析结果返回给前端

// 通过Id获取PlayParams参数 func GetPlayParamsByPlayId(ctx context.Context, PlayId int64) (*backstage.PlayParams, error) { var ( dbClient = Mysqls PlayList model.PlayStuct ) // 查询 tx := dbClient.DB.WithContext(ctx).Debug().Where("play_id = ?", PlayId).First(&PlayList) if tx.Error != nil && tx.Error != gorm.ErrRecordNotFound { mylog.Errorf("通过Id获取PlayParams参数 查询失败:%v", tx.Error) return nil, tx.Error } // 判断是否存在 if PlayList.PlayID == 0 { // 不存在 mylog.Errorf("通过Id获取PlayParams参数 查询失败:不存在") return nil, nil } // 解析参数:为后续结果映射着准备 editParams := PlayList.EditParams // 配置解析选项 opts := protojson.UnmarshalOptions{ // 忽略未知字段: 如果设置为 true,JSON 中的未知字段(即 proto 消息中没有定义的字段)会被忽略,而不会报错,如果设置为 false(默认),遇到未知字段时会返回错误 // 当 JSON 数据可能包含额外的字段(例如,来自较新版本的 proto 定义),但希望兼容解析时,可以启用此选项 // DiscardUnknown: true, } var PlayParams backstage.PlayParams if err := opts.Unmarshal([]byte(editParams), &PlayParams); err != nil { return nil, fmt.Errorf("通过Id获取PlayParams参数 PlayId %v failed to unmarshal: %v", PlayId, err) } return &PlayParams, nil } // 获取详情 func PlayView(ctx context.Context, req *backstage.ViewPlayReq) (string, error) { // 获取PlayParams PlayParams, err := GetPlayParamsByPlayId(ctx, req.PlayId) if err != nil { return "", fmt.Errorf("通过[%v]获取PlayParams failed: %v", req.PlayId, err) } if PlayParams == nil { return "", fmt.Errorf("通过[%v]获取PlayParams failed: 不存在", req.PlayId) } // 1. 先转为 map 以便操作 // 手动序列化为 JSON(使用 protojson) marOpts := protojson.MarshalOptions{ // EmitUnpopulated: true, // 包含零值 UseProtoNames: true, // 使用原始字段名 EmitDefaultValues: true, } jsonBytes, err := marOpts.Marshal(PlayParams) if err != nil { return "", err } // 2. 将 JSON 字节转为 map[string]interface{} var dataMap map[string]interface{} if err := json.Unmarshal(jsonBytes, &dataMap); err != nil { return "", err } // 3. 删除 GroupId // 假设 PlayBase 在 map 中也是一个 map if PlayBaseMap, ok := dataMap["PlayBase"].(map[string]interface{}); ok { // 如果 GroupId 是 0,则删除 if val, exists := PlayInfoMap["GroupId"]; exists { // JSON 数字通常是 float64 if v, isFloat := val.(float64); isFloat && v == 0 { delete(PlayInfoMap, "GroupId") } } } if PlayBaseMap, ok := dataMap["PlayBase"].(map[string]interface{}); ok { // 如果 GroupPriority 是 0,则删除 if val, exists := PlayInfoMap["GroupPriority"]; exists { // JSON 数字通常是 float64 if v, isFloat := val.(float64); isFloat && v == 0 { delete(PlayInfoMap, "GroupPriority") } } } if PlayBaseMap, ok := dataMap["PlayBase"].(map[string]interface{}); ok { // 如果 Weight 是 0,则删除 if val, exists := PlayInfoMap["Weight"]; exists { // JSON 数字通常是 float64 if v, isFloat := val.(float64); isFloat && v == 0 { delete(PlayInfoMap, "Weight") } } } // 4. 再转回 JSON 字符串 finalJson, err := json.Marshal(dataMap) if err != nil { return "", err } return string(finalJson), nil }

至此, 整个操作完成!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询