diff --git a/api/task/task.go b/api/task/task.go new file mode 100644 index 0000000..61b625b --- /dev/null +++ b/api/task/task.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package task + +import ( + "context" + + "server/api/task/v1" +) + +type ITaskV1 interface { + Ranking(ctx context.Context, req *v1.RankingReq) (res *v1.RankingRes, err error) +} diff --git a/api/task/v1/task.go b/api/task/v1/task.go index 6a8735a..c40dd19 100644 --- a/api/task/v1/task.go +++ b/api/task/v1/task.go @@ -2,14 +2,17 @@ package v1 import "github.com/gogf/gf/v2/frame/g" -type TaskListReq struct { -} -type TaskRankingReq struct { +type RankingReq struct { g.Meta `path:"/task/ranking" method:"get" tags:"Task" summary:"任务排行榜"` StoreId int `json:"storeId" v:"required#请选择店铺" dc:"门店id"` Page int `json:"page" dc:"页数"` Size int `json:"size" dc:"条数"` } -type TaskRankingRes struct { +type RankingRes struct { +} + +type ListReq struct { +} +type ListRes struct { } diff --git a/api/user/user.go b/api/user/user.go index c513c2a..beac2c3 100644 --- a/api/user/user.go +++ b/api/user/user.go @@ -16,4 +16,7 @@ type IUserV1 interface { Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) BindPhone(ctx context.Context, req *v1.BindPhoneReq) (res *v1.BindPhoneRes, err error) GetPhoneCode(ctx context.Context, req *v1.GetPhoneCodeReq) (res *v1.GetPhoneCodeRes, err error) + GetUserBoundInfo(ctx context.Context, req *v1.GetUserBoundInfoReq) (res *v1.GetUserBoundInfoRes, err error) + GetBoundUrl(ctx context.Context, req *v1.GetBoundUrlReq) (res *v1.GetBoundUrlRes, err error) + GetUnboundUrl(ctx context.Context, req *v1.GetUnboundUrlReq) (res *v1.GetUnboundUrlRes, err error) } diff --git a/api/user/v1/user.go b/api/user/v1/user.go index bc27ead..3167695 100644 --- a/api/user/v1/user.go +++ b/api/user/v1/user.go @@ -58,20 +58,31 @@ type GetPhoneCodeReq struct { type GetPhoneCodeRes struct { Success bool `json:"success" dc:"是否成功"` } -type GetGameLifeBondReq struct { - g.Meta `path:"/user/getGameLifeBond" method:"post" tags:"User" summary:"获取GameLife绑定情况"` +type GetUserBoundInfoReq struct { + g.Meta `path:"/user/boundInfo" method:"get" tags:"User" summary:"获取用户绑定信息"` + PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` } -type GetGameLifeBondRes struct { +type GetUserBoundInfoRes struct { + IsBound bool `json:"isBound" dc:"是否已绑定"` } -type BundleGameLifeReq struct { - g.Meta `path:"/user/bindGameLife" method:"post" tags:"User" summary:"绑定GameLife账号"` + +type GetBoundUrlReq struct { + g.Meta `path:"/user/boundUrl" method:"get" tags:"User" summary:"获取用户绑定url"` + PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` + BindType int `json:"bindType" v:"required|in:1,2#请选择绑定方式,只能为1或2" dc:"绑定方式,默认值1,1: qq; 2: wx" default:"1"` + AppName string `json:"appName" v:"required#请选择游戏" dc:"游戏名称"` } -type BundleGameLifeRes struct { - Url string `json:"url" dc:"绑定游戏人生的 h5 页面 url"` +type GetBoundUrlRes struct { + Url string `json:"url" dc:"绑定的 h5 页面 url"` } -type UnbundleGameLifeReq struct { - g.Meta `path:"/user/unbindGameLife" method:"post" tags:"User" summary:"解绑GameLife账号"` + +type GetUnboundUrlReq struct { + g.Meta `path:"/user/unBoundUrl" method:"get" tags:"User" summary:"获取用户解绑url"` + PopenId string `json:"popenId" v:"required#popenId不能为空" dc:"用户详情接口返回的 wxPopenId 或者是 qqPopenId"` + BindType int `json:"bindType" v:"required|in:1,2#请选择绑定方式,只能为1或2" dc:"绑定方式,默认值1,1: qq; 2: wx" default:"1"` + AppName string `json:"appName" v:"required#请选择游戏" dc:"游戏名称"` + Nickname string `json:"nickname" v:"required#昵称不能为空" dc:"昵称"` } -type UnbundleGameLifeRes struct { - Url string `json:"url" dc:"解绑游戏人生的 h5 页面 url"` +type GetUnboundUrlRes struct { + Url string `json:"url" dc:"解绑的 h5 页面 url"` } diff --git a/internal/consts/gamelife.go b/internal/consts/gamelife.go new file mode 100644 index 0000000..59f3868 --- /dev/null +++ b/internal/consts/gamelife.go @@ -0,0 +1,10 @@ +package consts + +const ( + GamelifeExtplatType = "bindcode" + GamelifeExtplatExtraMobile = "mobile" + GamelifeExtplatExtraPc = "pc" + GamelifeExtplatBoundTypeQQ = "qq" + GamelifeExtplatBoundTypeWX = "wx" + GamelifeMiniProgramBand = "1" +) diff --git a/internal/consts/redis.go b/internal/consts/redis.go index 2d3383b..6d96d21 100644 --- a/internal/consts/redis.go +++ b/internal/consts/redis.go @@ -5,3 +5,8 @@ const ( UserBindPhoneKey = "user:bindPhone:%d" UserCodeExpire = 5 * 60 ) + +const ( + GameLifeUserKey = "gamelife:user:%s" + GameLifeUserExpire = 2 * 60 * 60 +) diff --git a/internal/controller/task/task.go b/internal/controller/task/task.go new file mode 100644 index 0000000..4399401 --- /dev/null +++ b/internal/controller/task/task.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package task diff --git a/internal/controller/task/task_new.go b/internal/controller/task/task_new.go new file mode 100644 index 0000000..be0124d --- /dev/null +++ b/internal/controller/task/task_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package task + +import ( + "server/api/task" +) + +type ControllerV1 struct{} + +func NewV1() task.ITaskV1 { + return &ControllerV1{} +} diff --git a/internal/controller/task/task_v1_ranking.go b/internal/controller/task/task_v1_ranking.go new file mode 100644 index 0000000..b91825a --- /dev/null +++ b/internal/controller/task/task_v1_ranking.go @@ -0,0 +1,14 @@ +package task + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "server/api/task/v1" +) + +func (c *ControllerV1) Ranking(ctx context.Context, req *v1.RankingReq) (res *v1.RankingRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/controller/user/user_v1_get_bound_url.go b/internal/controller/user/user_v1_get_bound_url.go new file mode 100644 index 0000000..b578a97 --- /dev/null +++ b/internal/controller/user/user_v1_get_bound_url.go @@ -0,0 +1,17 @@ +package user + +import ( + "context" + "server/internal/model" + "server/internal/service" + + "server/api/user/v1" +) + +func (c *ControllerV1) GetBoundUrl(ctx context.Context, req *v1.GetBoundUrlReq) (res *v1.GetBoundUrlRes, err error) { + out, err := service.User().BoundUrl(ctx, &model.UserBoundUrlIn{PopenId: req.PopenId, AppName: req.AppName, BindType: req.BindType, IsBound: true}) + if err != nil { + return nil, err + } + return &v1.GetBoundUrlRes{Url: out.Url}, nil +} diff --git a/internal/controller/user/user_v1_get_unbound_url.go b/internal/controller/user/user_v1_get_unbound_url.go new file mode 100644 index 0000000..68e9738 --- /dev/null +++ b/internal/controller/user/user_v1_get_unbound_url.go @@ -0,0 +1,17 @@ +package user + +import ( + "context" + "server/internal/model" + "server/internal/service" + + "server/api/user/v1" +) + +func (c *ControllerV1) GetUnboundUrl(ctx context.Context, req *v1.GetUnboundUrlReq) (res *v1.GetUnboundUrlRes, err error) { + out, err := service.User().UnBoundUrl(ctx, &model.UserBoundUrlIn{PopenId: req.PopenId, AppName: req.AppName, BindType: req.BindType, IsBound: false, Nickname: req.Nickname}) + if err != nil { + return nil, err + } + return &v1.GetUnboundUrlRes{Url: out.Url}, nil +} diff --git a/internal/controller/user/user_v1_get_user_bound_info.go b/internal/controller/user/user_v1_get_user_bound_info.go new file mode 100644 index 0000000..f9e3c56 --- /dev/null +++ b/internal/controller/user/user_v1_get_user_bound_info.go @@ -0,0 +1,17 @@ +package user + +import ( + "context" + "server/internal/model" + "server/internal/service" + + "server/api/user/v1" +) + +func (c *ControllerV1) GetUserBoundInfo(ctx context.Context, req *v1.GetUserBoundInfoReq) (res *v1.GetUserBoundInfoRes, err error) { + out, err := service.User().BoundInfo(ctx, &model.UserBoundInfoIn{PopenId: req.PopenId}) + if err != nil { + return nil, err + } + return &v1.GetUserBoundInfoRes{IsBound: out.IsBound}, nil +} diff --git a/internal/logic/admin/admin.go b/internal/logic/admin/admin.go index 59eafeb..da414f7 100644 --- a/internal/logic/admin/admin.go +++ b/internal/logic/admin/admin.go @@ -11,7 +11,7 @@ import ( "server/internal/model/entity" "server/internal/service" "server/utility/ecode" - utility "server/utility/encrypt" + "server/utility/encrypt" "server/utility/jwt" ) @@ -54,7 +54,7 @@ func checkAdmin() { return err } if !exist { - password, err := utility.EncryptPassword("Aa123456") + password, err := encrypt.EncryptPassword("Aa123456") if err != nil { return err } @@ -91,7 +91,7 @@ func (s *sAdmin) Login(ctx context.Context, in *model.AdminLoginIn) (out *model. if err := dao.Admins.Ctx(ctx).Where(do.Admins{Username: in.Username}).Scan(&admin); err != nil { return nil, ecode.Fail.Sub("查询管理员失败") } - if !utility.ComparePassword(admin.PasswordHash, in.Password) { + if !encrypt.ComparePassword(admin.PasswordHash, in.Password) { return nil, ecode.Auth } value, err := dao.Roles.Ctx(ctx).WherePri(admin.RoleId).Fields(dao.Roles.Columns().Code).Value() diff --git a/internal/logic/merchantAdmin/merchantAdmin.go b/internal/logic/merchantAdmin/merchantAdmin.go index ceb9e9a..a0b0013 100644 --- a/internal/logic/merchantAdmin/merchantAdmin.go +++ b/internal/logic/merchantAdmin/merchantAdmin.go @@ -15,7 +15,7 @@ import ( "server/internal/model/entity" "server/internal/service" "server/utility/ecode" - utility "server/utility/encrypt" + "server/utility/encrypt" "server/utility/jwt" "server/utility/snowid" ) @@ -65,7 +65,7 @@ func (s *sMerchantAdmin) Login(ctx context.Context, in *model.MerchantLoginIn) ( if mAdmin[dao.MerchantAdmins.Columns().Status].Int() == consts.MerchantAdministratorDisable { return nil, ecode.Params.Sub("该用户已被禁用") } - if !utility.ComparePassword(mAdmin[dao.MerchantAdmins.Columns().PasswordHash].String(), in.Password) { + if !encrypt.ComparePassword(mAdmin[dao.MerchantAdmins.Columns().PasswordHash].String(), in.Password) { return nil, ecode.Params.Sub("密码错误") } value, err := dao.Roles.Ctx(ctx).WherePri(mAdmin[dao.MerchantAdmins.Columns().RoleId].Int()).Fields(dao.Roles.Columns().Code).Value() @@ -145,7 +145,7 @@ func (s *sMerchantAdmin) Register(ctx context.Context, in *model.MerchantAdminRe if code.String() != in.Code { return nil, ecode.Fail.Sub("验证码错误") } - hashPass, err := utility.EncryptPassword(in.Password) + hashPass, err := encrypt.EncryptPassword(in.Password) if err != nil { return nil, ecode.Fail.Sub("密码加密失败") } diff --git a/internal/logic/storeAdmin/storeAdmin.go b/internal/logic/storeAdmin/storeAdmin.go index 51e6429..929ca18 100644 --- a/internal/logic/storeAdmin/storeAdmin.go +++ b/internal/logic/storeAdmin/storeAdmin.go @@ -8,8 +8,9 @@ import ( "server/internal/model/do" "server/internal/model/entity" "server/internal/service" + "server/utility/encrypt" + "server/utility/ecode" - utility "server/utility/encrypt" "server/utility/jwt" ) @@ -56,7 +57,7 @@ func (s *sStoreAdmin) Login(ctx context.Context, in *model.StoreAdminLoginIn) (o if one[dao.StoreAdmins.Columns().Status].Int() == consts.StoreAdminDisable { return nil, ecode.Params.Sub("该用户已被禁用") } - if !utility.ComparePassword(one[dao.StoreAdmins.Columns().PasswordHash].String(), in.Password) { + if !encrypt.ComparePassword(one[dao.StoreAdmins.Columns().PasswordHash].String(), in.Password) { return nil, ecode.Params.Sub("密码错误") } value, err := dao.Roles.Ctx(ctx).WherePri(one[dao.StoreAdmins.Columns().RoleId].Int()).Fields(dao.Roles.Columns().Code).Value() diff --git a/internal/logic/user/user.go b/internal/logic/user/user.go index 1886245..a115a7f 100644 --- a/internal/logic/user/user.go +++ b/internal/logic/user/user.go @@ -3,6 +3,7 @@ package internal import ( "context" "fmt" + "github.com/go-resty/resty/v2" "github.com/gogf/gf/v2/frame/g" "server/internal/consts" "server/internal/dao" @@ -11,6 +12,7 @@ import ( "server/internal/model/entity" "server/internal/service" "server/utility/ecode" + "server/utility/gamelife" "server/utility/jwt" "github.com/gogf/gf/v2/os/gtime" @@ -200,3 +202,41 @@ func (s *sUser) List(ctx context.Context, in *model.UserListIn) (out *model.User // 用于系统管理员、商户、门店查看用户列表, 展示用户最近的相关信息 return } + +func (s *sUser) BoundUrl(ctx context.Context, in *model.UserBoundUrlIn) (out *model.UserBoundUrlOut, err error) { + url, err := gamelife.GetGamelifeClient(ctx).GetUrl(ctx, in.PopenId, in.AppName, "", in.BindType, in.IsBound) + if err != nil { + return nil, ecode.Fail.Sub("获取绑定链接失败") + } + return &model.UserBoundUrlOut{ + Url: url, + }, nil +} + +func (s *sUser) UnBoundUrl(ctx context.Context, in *model.UserBoundUrlIn) (out *model.UserUnBoundUrlOut, err error) { + url, err := gamelife.GetGamelifeClient(ctx).GetUrl(ctx, in.PopenId, in.AppName, in.Nickname, in.BindType, in.IsBound) + if err != nil { + return nil, ecode.Fail.Sub("获取绑定链接失败") + } + return &model.UserUnBoundUrlOut{ + Url: url, + }, nil +} +func (s *sUser) BoundInfo(ctx context.Context, in *model.UserBoundInfoIn) (out *model.UserBoundInfoOut, err error) { + url, err := gamelife.GetGamelifeClient(ctx).GetBound(ctx, in.PopenId) + if err != nil { + return nil, ecode.Fail.Sub("获取绑定信息失败") + } + var result model.UserBoundResult + resp, err := resty.New().R().SetResult(&result).Post(url) + if err != nil { + return nil, ecode.Fail.Sub("获取绑定信息失败") + } + if resp.StatusCode() != 200 { + return nil, ecode.Fail.Sub("获取绑定信息失败") + } + + return &model.UserBoundInfoOut{ + IsBound: result.Result, + }, nil +} diff --git a/internal/model/user.go b/internal/model/user.go index 58c69ae..d612774 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -133,3 +133,38 @@ type GetPhoneCodeIn struct { type GetPhoneCodeOut struct { Success bool } + +type UserGamelifeCache struct { + Aes string `json:"aes"` + IV string `json:"iv"` + Token string `json:"token"` +} + +type UserBoundResult struct { + Result bool `json:"result"` + Nick string `json:"nick"` + Ctime uint32 `json:"ctime"` + Utype int8 `json:"utype"` + AppName []string `json:"app_name"` +} +type UserBoundUrlIn struct { + PopenId string + BindType int + AppName string + Nickname string + IsBound bool +} + +type UserBoundUrlOut struct { + Url string +} +type UserUnBoundUrlOut struct { + Url string +} + +type UserBoundInfoIn struct { + PopenId string +} +type UserBoundInfoOut struct { + IsBound bool +} diff --git a/internal/packed/packed.go b/internal/packed/packed.go index 921e39a..6663497 100644 --- a/internal/packed/packed.go +++ b/internal/packed/packed.go @@ -4,6 +4,7 @@ import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" _ "server/utility/myCasbin" + _ "server/utility/rsa" _ "server/utility/snowid" _ "server/utility/wechat" ) diff --git a/internal/service/user.go b/internal/service/user.go index 25517fc..7d5b9b3 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -19,6 +19,9 @@ type ( Update(ctx context.Context, in *model.UserUpdateIn) (out *model.UpdateOut, err error) BindPhone(ctx context.Context, in *model.UserBindPhoneIn) (out *model.UserBindPhoneOut, err error) List(ctx context.Context, in *model.UserListIn) (out *model.UserListOut, err error) + BoundUrl(ctx context.Context, in *model.UserBoundUrlIn) (out *model.UserBoundUrlOut, err error) + UnBoundUrl(ctx context.Context, in *model.UserBoundUrlIn) (out *model.UserUnBoundUrlOut, err error) + BoundInfo(ctx context.Context, in *model.UserBoundInfoIn) (out *model.UserBoundInfoOut, err error) } ) diff --git a/manifest/config/private.pem b/manifest/config/private.pem new file mode 100644 index 0000000..1d99bcc --- /dev/null +++ b/manifest/config/private.pem @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHXJazSOFka3q+ +xyBqVuLtK+OElsgRQyQCRakBvcBkq5lIl1G+6ivuju+q3bemiYzOWah/H/+7848c +yu505CpJtRUvPiSaXqhwIAWAHvD1ckLo9xw3eh9uYdmnZx92+d+oKVcjZWz/M6qQ +g6GYf8kE0x8zDKw1Xd89y6xzo7DqFNnE9BHSxeFNSAgp+MOBPn7X2mdDOWl6eGLs +qXK6BTTFyeS0JaH1t0ra6JyQ5wau3WKpB22G3pK9+u8MQPKjM+JXdIZFA0eT2SeI +qbB0xRfU4fOmhn/eXVyNYws1s92hPq1Lu8/QHf6m6QNbmualgaSiEo/ogKGN1urj +/ARsTdbpAgMBAAECggEAF2DnakmhHA3wIjkUISmmgd39qq28GjclZfsQCIv0Sa7V +h5MS/E1HjylCvZkTmgDRv+X+Uw58xcJ4KjnmW2v43cgXw5QREFRe9RaivJEbftjg +M4pSZkaCXTcrN115MrxPY6TYNvXSkHUk9Va2tzcCygGItvFuYL04zFx8CXDxIkx7 +4NP3F3AGbQ4GACoIUsKNuQ0WIZO6WrxJP0sGH1iz+oxDIlEkEZAYHAKIIna3gaY5 +YBvOmRScGDO2K05fLxDJUK0x4nxkBRQ1NRu9Ud1Fo4zwUFxiwM4ZUjaLclgIgv8v +5h0urZY/j4994YtC4UvheC53UEAqDKjMWXnjH5u4nQKBgQDlwqSk6St7pAW1lpA+ +0OMKQgdDe+W+O2a0LPjaSevCX3Q9ml/ToRH+tmVUGOr2S301GqSRaPdauXBDvE29 +HSoD6gCEsT+TocSxyhn0Gg25fMDz3+H7XVOAj+tPg/IWJG8+KxO5gEwucb6Axl3w +NQ29l77SKFgMYitq8N1jjt6LMwKBgQCW0hJzx2dgaYmKrxctWOxfibTaCGBEn4+F +6YwmfGJg+qZ+FU+aBkwaocutIudHogIAOXp6e4J086a6cUwx/Eex1tvTztVa8A/W +8cRmk+DWIIYN4c/wePe3wDluddtRIgh54ukpFsgVsOEEmzp3GAa8fyUA7dVvuJlQ +z1torG11cwKBgQCImppzZiKxR0sRtOwcPOvQLIPPDroAyaZ9l4N5nZurnD8rZT52 +P/zH+T/zqUEBoM5XpXiU79ipOznRPALoXo+ddiJKwmuvZe3hWuzlYhwo3VCHbuQY +JFvCQ08/no5vtcfiKZB3qR0iPARs4gP2DkUWJUOSBeSbsD5qPb0TNV2BWwKBgAz+ +QBS1YxSNQwotl2OSu5pndKsr+Y8v599zhV1zbc5JCbrm/xqX3EqXEcLytNYZAO8g +BIs0xMJqkzyQsi3EPDD3/6w5r2vMLrEn1vG3X7FSz/m2MIHZCg5MgyYfBSvyMKS/ +hbLCga5MtLX+4YSND1eB5KA13SNo1dx+YLOd1zg9AoGAczXhM+n0beuXDhLkiAm3 +7Av8lJazrpZg2Oh79IrFJANjVyZZeOVaSfNXPumoDXMZvEMjkIaZZ50IzQfpVdLF +7J0Y/hFCTsX6b3JdkiluniiEh1a2I3tC3+Qa4sSPqKNdHcBJSmmcBcpzRLL5n2zc +bjt4KNf3ZQ3nHvPHnwrd7pg= +-----END RSA PRIVATE KEY----- diff --git a/manifest/config/public.pem b/manifest/config/public.pem new file mode 100644 index 0000000..f230ee3 --- /dev/null +++ b/manifest/config/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh1yWs0jhZGt6vscgalbi +7SvjhJbIEUMkAkWpAb3AZKuZSJdRvuor7o7vqt23pomMzlmofx//u/OPHMrudOQq +SbUVLz4kml6ocCAFgB7w9XJC6PccN3ofbmHZp2cfdvnfqClXI2Vs/zOqkIOhmH/J +BNMfMwysNV3fPcusc6Ow6hTZxPQR0sXhTUgIKfjDgT5+19pnQzlpenhi7KlyugU0 +xcnktCWh9bdK2uickOcGrt1iqQdtht6SvfrvDEDyozPiV3SGRQNHk9kniKmwdMUX +1OHzpoZ/3l1cjWMLNbPdoT6tS7vP0B3+pukDW5rmpYGkohKP6IChjdbq4/wEbE3W +6QIDAQAB +-----END PUBLIC KEY----- diff --git a/utility/encrypt/aes.go b/utility/encrypt/aes.go new file mode 100644 index 0000000..27e48a0 --- /dev/null +++ b/utility/encrypt/aes.go @@ -0,0 +1,58 @@ +package encrypt + +import ( + "crypto/aes" + "crypto/cipher" + "errors" +) + +// AesEncrypt 使用 AES-CBC 模式对数据进行加密。 +// +// 参数: +// - plainText: 原始明文数据(必须是任意长度) +// - key: 加密密钥(长度必须是 16、24 或 32 字节) +// - iv: 初始化向量(必须是 16 字节) +// +// 返回值: +// - 加密后的密文 +// - 错误信息(如果加密失败) +func AesEncrypt(plainText, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(iv) != aes.BlockSize { + return nil, errors.New("IV 长度必须为 16 字节") + } + + plainText = pkcs7Padding(plainText, aes.BlockSize) + cipherText := make([]byte, len(plainText)) + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(cipherText, plainText) + + return cipherText, nil +} + +// pkcs7Padding 对数据进行 PKCS7 填充。 +// +// 参数: +// - data: 原始数据 +// - blockSize: 块大小(通常为 16) +// +// 返回值: +// - 填充后的数据 +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytesRepeat(byte(padding), padding) + return append(data, padText...) +} + +// bytesRepeat 返回一个重复 count 次的字节切片。 +func bytesRepeat(b byte, count int) []byte { + buf := make([]byte, count) + for i := 0; i < count; i++ { + buf[i] = b + } + return buf +} diff --git a/utility/encrypt/bs4.go b/utility/encrypt/bs4.go new file mode 100644 index 0000000..1c7c844 --- /dev/null +++ b/utility/encrypt/bs4.go @@ -0,0 +1,19 @@ +package encrypt + +import ( + "encoding/base64" +) + +// Base64Encode 对字符串进行 base64 编码 +func Base64Encode(data []byte) string { + return base64.StdEncoding.EncodeToString(data) +} + +// Base64Decode 对 base64 编码的字符串进行解码 +func Base64Decode(encoded string) ([]byte, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + return decodedBytes, nil +} diff --git a/utility/encrypt/password.go b/utility/encrypt/password.go index 4a9cb55..dc79242 100644 --- a/utility/encrypt/password.go +++ b/utility/encrypt/password.go @@ -1,4 +1,4 @@ -package utility +package encrypt import ( "golang.org/x/crypto/bcrypt" diff --git a/utility/encrypt/phone.go b/utility/encrypt/phone.go index 93c7bd8..8c07a07 100644 --- a/utility/encrypt/phone.go +++ b/utility/encrypt/phone.go @@ -1,4 +1,4 @@ -package utility +package encrypt import ( "golang.org/x/crypto/bcrypt" diff --git a/utility/gamelife/gamelife.go b/utility/gamelife/gamelife.go new file mode 100644 index 0000000..c2674f8 --- /dev/null +++ b/utility/gamelife/gamelife.go @@ -0,0 +1,287 @@ +package gamelife + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-resty/resty/v2" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/glog" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/grand" + "net/url" + "server/internal/consts" + "server/internal/model" + "server/utility/ecode" + "server/utility/encrypt" + "server/utility/rsa" + "time" +) + +type gamelifeClient struct { + PlatId string `json:"platId" ` + Mode string `json:"mode" ` + keyivUrlMap map[string]string `json:"-"` // 存储获取用户 aes key 和 iv 的 url + boundUrlMap map[string]string `json:"-"` // 存储用户绑定状态的 url + unBoundUrlMap map[string]string `json:"-"` // 存储用户解绑状态的 url + getBoundUrl map[string]string `json:"-"` // 存储用户绑定状态的 url +} + +var ( + instance *gamelifeClient +) + +func newgamelifeClient(ctx context.Context) *gamelifeClient { + instance = &gamelifeClient{ + PlatId: g.Config().MustGet(ctx, "gamelife.platId").String(), + Mode: g.Config().MustGet(ctx, "gamelife.mode").String(), + keyivUrlMap: map[string]string{ + "test": "https://api-test.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", + "prod": "https://api.nes.smoba.qq.com/pvpesport.sgamenes.commcgi.commcgi/GetExplatSecret", + }, + boundUrlMap: map[string]string{ + "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/index", + "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/index", + }, + unBoundUrlMap: map[string]string{ + "test": "https://h5-test.cafe.qq.com/pmd-mobile.cafe.bind-account.pc.feature.bind-account/#/bind-manage", + "prod": "https://h5.cafe.qq.com/pmd-mobile.cafe.bind-account.pc/#/bind-manage", + }, + getBoundUrl: map[string]string{ + "test": "https://api-test。cafe.qq.com/tipmp.user.authinfoo_cgi.authinfo_cgi/GetPlatUserInfo", + "prod": "https://api.cafe.qq.com/tipmp.user.authinfoo_cgi.authinfo_cgi/GetPlatUserInfo", + }, + } + glog.Infof(ctx, "初始化 gamelifeClient 成功") + return instance +} + +func init() { + ctx := context.Background() + newgamelifeClient(ctx) +} + +func GetGamelifeClient(ctx context.Context) *gamelifeClient { + if instance == nil { + instance = newgamelifeClient(ctx) + } + return instance +} + +func (s *gamelifeClient) GetUserKeyIV(ctx context.Context, popenId string) (cache model.UserGamelifeCache, err error) { + // 创建加密原数据 + oriData := g.MapStrStr{ + "PlatId": s.PlatId, + "PopenId": popenId, + } + marshal, err := json.Marshal(oriData) + if err != nil { + err = ecode.Fail.Sub("序列化 json 数据出现异常") + return + } + key, err := rsa.GetRsaClient().EncryptWithRsaPublicKey(marshal) + if err != nil { + err = ecode.Fail.Sub("序列化 json 数据出现异常") + return + + } + // base64编码 + base64Encode := encrypt.Base64Encode(key) + // 向游戏人生发送请求 + result := struct { + secret string `json:"secret"` + key string `json:"key"` // 该用户的token,后续的账号绑定、登录态携带都需要此参数。 + }{} + resp, err := resty.New().R().SetBody(map[string]string{ + "plat_id": s.PlatId, + "key": base64Encode, + }).SetResult(result).Post(s.keyivUrlMap[s.Mode]) + if err != nil { + err = ecode.Fail.Sub("获取用户信息失败") + return + + } + if resp.StatusCode() != 200 { + err = ecode.Fail.Sub("获取用户信息失败") + return + + } + + decode, err := encrypt.Base64Decode(result.secret) + if err != nil { + err = ecode.Fail.Sub("解密用户信息失败") + return + + } + + plain, err := rsa.GetRsaClient().DecryptWithRsaPrivateKey(decode) + if err != nil { + err = ecode.Fail.Sub("解密用户信息失败") + return + + } + aesResult := struct { + key string `json:"key"` + iv string `json:"iv"` + }{} + if err = json.Unmarshal(plain, &aesResult); err != nil { + err = ecode.Fail.Sub("解密用户信息失败") + return + } + gamelifeCache := model.UserGamelifeCache{Aes: aesResult.key, IV: aesResult.iv, Token: result.key} + // 将用户的 aeskey 和 iv 存储到缓存当中,用于后续请求数据加密, 固定时间 2 小时同时不同用户加上一个随机时间 + if err = g.Redis().SetEX(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenId), gamelifeCache, int64(consts.GameLifeUserExpire+grand.Intn(1000))); err != nil { + err = ecode.Fail.Sub("设置用户信息失败") + return + } + return +} + +// GetUrl 为 GameLife 系统生成绑定或解绑定的 URL。 +// 它从缓存中获取用户加密密钥(若缓存过期则重新获取),加密用户数据, +// 并根据操作类型(绑定或解绑)构造包含查询参数的 URL。 +// +// 参数: +// - ctx: 用于控制请求生命周期和取消的上下文。 +// - popenid: 用户的唯一标识符。 +// - appname: 应用程序名称。 +// - nickname: 用户昵称,仅在解绑操作时需要(绑定操作时忽略)。 +// - bindType: 绑定类型(1 表示 QQ,其他值表示微信)。 +// - isBound: 布尔值,指示操作类型(true 表示绑定,false 表示解绑)。 +// +// 返回值: +// - string: 构造的绑定或解绑操作的 URL。 +// - error: 如果任何步骤(缓存获取、密钥获取、JSON 序列化、加密或 URL 构造)失败,则返回错误。 +// +// 错误: +// - 如果发生以下情况,将返回带有具体错误信息的错误: +// - 无法解析基础 URL。 +// - 无法从缓存或通过 GetUserKeyIV 获取用户信息。 +// - JSON 序列化或反序列化失败。 +// - AES 加密或 Base64 编码失败。 +func (s *gamelifeClient) GetUrl(ctx context.Context, popenid, appname, nickname string, bindType int, isBound bool) (string, error) { + // 从缓存中获取用户的 aes 和 iv + rooturl := s.boundUrlMap[s.Mode] + if !isBound { + rooturl = s.unBoundUrlMap[s.Mode] + } + baseUrl, err := url.Parse(rooturl) + if err != nil { + return "", ecode.Fail.Sub("解析基础 URL 失败") + } + + cacheData, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenid)) + if err != nil { + return "", ecode.Fail.Sub("从缓存中获取用户信息失败") + } + + var gamelifeCache model.UserGamelifeCache + if cacheData.IsEmpty() { + // 如果缓存不存在或已过期,重新调用 GetUserKeyIV + data, err := s.GetUserKeyIV(ctx, popenid) + if err != nil { + return "", ecode.Fail.Sub("获取用户信息失败") + } + gamelifeCache = model.UserGamelifeCache{Aes: data.Aes, IV: data.IV, Token: data.Token} + } else { + // 缓存存在,直接解析 + if err = json.Unmarshal(cacheData.Bytes(), &gamelifeCache); err != nil { + return "", ecode.Fail.Sub("解析用户信息失败") + } + } + + // 序列化原始数据 + oriData := g.Map{ + "PopenId": popenid, + "TimeStamp": time.Now().Unix(), + } + marshal, err := json.Marshal(oriData) + if err != nil { + return "", ecode.Fail.Sub("序列化用户信息失败") + } + + // 加密用户信息 + aesEncrypt, err := encrypt.AesEncrypt(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) + if err != nil { + return "", ecode.Fail.Sub("加密用户信息失败") + } + platUserInfoStr := encrypt.Base64Encode(aesEncrypt) + explatData := g.MapStrStr{"Token": gamelifeCache.Token, "PlatUserInfoStr": platUserInfoStr} + + // 构建查询参数 + queryParams := baseUrl.Query() + queryParams.Add("app_name", appname) + queryParams.Add("mini_program_band", consts.GamelifeMiniProgramBand) + queryParams.Add("extplat_plat", s.PlatId) + queryParams.Add("extplat_type", consts.GamelifeExtplatType) + queryParams.Add("extplat_extra", consts.GamelifeExtplatExtraPc) + queryParams.Add("extplat_data", url.QueryEscape(gconv.String(explatData))) + + // 根据 isBound 设置 bind_type 和 nickname + if bindType == 1 { + queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeQQ) + } else { + queryParams.Add("bind_type", consts.GamelifeExtplatBoundTypeWX) + } + + // 仅在解绑时添加 nickname + if !isBound { + queryParams.Add("nickname", nickname) + } + + baseUrl.RawQuery = queryParams.Encode() + return baseUrl.String(), nil +} + +// GetBound 获取用户绑定情况 +func (s *gamelifeClient) GetBound(ctx context.Context, popenid string) (string, error) { + baseUrl, err := url.Parse(s.getBoundUrl[s.Mode]) + if err != nil { + return "", ecode.Fail.Sub("解析基础 URL 失败") + } + + cacheData, err := g.Redis().Get(ctx, fmt.Sprintf(consts.GameLifeUserKey, popenid)) + if err != nil { + return "", ecode.Fail.Sub("从缓存中获取用户信息失败") + } + + var gamelifeCache model.UserGamelifeCache + if cacheData.IsEmpty() { + // 如果缓存不存在或已过期,重新调用 GetUserKeyIV + data, err := s.GetUserKeyIV(ctx, popenid) + if err != nil { + return "", ecode.Fail.Sub("获取用户信息失败") + } + gamelifeCache = model.UserGamelifeCache{Aes: data.Aes, IV: data.IV, Token: data.Token} + } else { + // 缓存存在,直接解析 + if err = json.Unmarshal(cacheData.Bytes(), &gamelifeCache); err != nil { + return "", ecode.Fail.Sub("解析用户信息失败") + } + } + // 序列化原始数据 + oriData := g.Map{ + "PopenId": popenid, + "TimeStamp": time.Now().Unix(), + } + marshal, err := json.Marshal(oriData) + if err != nil { + return "", ecode.Fail.Sub("序列化用户信息失败") + } + + // 加密用户信息 + aesEncrypt, err := encrypt.AesEncrypt(marshal, []byte(gamelifeCache.Aes), []byte(gamelifeCache.IV)) + if err != nil { + return "", ecode.Fail.Sub("加密用户信息失败") + } + platUserInfoStr := encrypt.Base64Encode(aesEncrypt) + explatData := g.MapStrStr{"Token": gamelifeCache.Token, "PlatUserInfoStr": platUserInfoStr} + + queryParams := baseUrl.Query() + queryParams.Add("plat_id", s.PlatId) + queryParams.Add("plat_user_info", popenid) + queryParams.Add("plat_user_str", url.QueryEscape(gconv.String(explatData))) + baseUrl.RawQuery = queryParams.Encode() + + return baseUrl.String(), nil +} diff --git a/utility/rsa/rsa.go b/utility/rsa/rsa.go new file mode 100644 index 0000000..6ed2980 --- /dev/null +++ b/utility/rsa/rsa.go @@ -0,0 +1,126 @@ +package rsa + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "os" + "sync" + + "github.com/gogf/gf/v2/frame/g" +) + +type rsaClient struct { + publicKey *rsa.PublicKey + privateKey *rsa.PrivateKey +} + +var ( + instance *rsaClient + once sync.Once +) + +// init 会在包初始化时自动调用,用于加载默认的 RSA 公钥和私钥。 +func init() { + ctx := context.Background() + once.Do(func() { + instance = &rsaClient{} + err := instance.loadKeys(g.Config().MustGet(ctx, "rsa.publickey").String(), g.Config().MustGet(ctx, "rsa.privatekey").String()) + if err != nil { + panic("加载 RSA 密钥失败: " + err.Error()) + } + }) +} + +// GetRsaClient 返回 RSA 客户端的单例实例。 +// +// 通常用于执行加解密操作。 +func GetRsaClient() *rsaClient { + return instance +} + +// EncryptWithRsaPublicKey 使用加载的 RSA 公钥对原始数据进行加密。 +// +// 参数: +// - plain: 待加密的明文数据。 +// +// 返回值: +// - 加密后的密文数据。 +// - 如果加密失败,则返回错误。 +func (c *rsaClient) EncryptWithRsaPublicKey(plain []byte) ([]byte, error) { + if c.publicKey == nil { + return nil, errors.New("公钥未加载") + } + return rsa.EncryptPKCS1v15(rand.Reader, c.publicKey, plain) +} + +// DecryptWithRsaPrivateKey 使用加载的 RSA 私钥对密文数据进行解密。 +// +// 参数: +// - cipher: 加密后的密文数据。 +// +// 返回值: +// - 解密后的明文数据。 +// - 如果解密失败,则返回错误。 +func (c *rsaClient) DecryptWithRsaPrivateKey(cipher []byte) ([]byte, error) { + if c.privateKey == nil { + return nil, errors.New("私钥未加载") + } + return rsa.DecryptPKCS1v15(rand.Reader, c.privateKey, cipher) +} + +// loadKeys 从指定文件中加载 RSA 公钥和私钥。 +// +// 参数: +// - publicKeyPath: 公钥 PEM 文件路径。 +// - privateKeyPath: 私钥 PEM 文件路径。 +// +// 返回值: +// - 成功返回 nil,否则返回错误信息。 +func (c *rsaClient) loadKeys(publicKeyPath, privateKeyPath string) error { + // 加载公钥 + pubBytes, err := os.ReadFile(publicKeyPath) + if err != nil { + return err + } + pubBlock, _ := pem.Decode(pubBytes) + if pubBlock == nil { + return errors.New("无法解析公钥 PEM 文件") + } + pubKey, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) + if err != nil { + return err + } + var ok bool + if c.publicKey, ok = pubKey.(*rsa.PublicKey); !ok { + return errors.New("公钥不是 RSA 公钥") + } + + // 加载私钥 + privBytes, err := os.ReadFile(privateKeyPath) + if err != nil { + return err + } + privBlock, _ := pem.Decode(privBytes) + if privBlock == nil { + return errors.New("无法解析私钥 PEM 文件") + } + // 尝试解析 PKCS#8 格式 + privKey, err := x509.ParsePKCS8PrivateKey(privBlock.Bytes) + if err != nil { + // 回退尝试 PKCS#1 格式 + privKey, err = x509.ParsePKCS1PrivateKey(privBlock.Bytes) + if err != nil { + return errors.New("解析私钥失败: 既不是 PKCS#8 也不是 PKCS#1 格式") + } + } + var ok2 bool + if c.privateKey, ok2 = privKey.(*rsa.PrivateKey); !ok2 { + return errors.New("私钥不是 RSA 私钥") + } + + return nil +}