From 814258931b7d5a34f584ebd13c265debdd765b57 Mon Sep 17 00:00:00 2001 From: liangliangit <248593893@qq.com> Date: Tue, 26 Dec 2023 16:10:08 +0800 Subject: [PATCH] update for new user --- apps/fushouxian-server/.processManager | 2 +- go.mod | 6 +- go.sum | 4 + init/Category.go | 2 +- init/usercenter.go | 7 + module/tencent/sms.go | 112 +++++++++ module/usercenter/SMSVerificationCode.go | 51 ++++ module/usercenter/api.user.go | 293 +++++++++++++++++++++++ module/usercenter/init.loader.go | 25 ++ module/usercenter/model.user.go | 280 ++++++++++++++++++++++ module/usercenter/model.user.token.go | 227 ++++++++++++++++++ 11 files changed, 1006 insertions(+), 3 deletions(-) create mode 100644 init/usercenter.go create mode 100644 module/tencent/sms.go create mode 100644 module/usercenter/SMSVerificationCode.go create mode 100644 module/usercenter/api.user.go create mode 100644 module/usercenter/init.loader.go create mode 100644 module/usercenter/model.user.go create mode 100644 module/usercenter/model.user.token.go diff --git a/apps/fushouxian-server/.processManager b/apps/fushouxian-server/.processManager index b0a1d06..eff7b67 100644 --- a/apps/fushouxian-server/.processManager +++ b/apps/fushouxian-server/.processManager @@ -1 +1 @@ -{"pid":27289} \ No newline at end of file +{"pid":28155} \ No newline at end of file diff --git a/go.mod b/go.mod index 439e91f..21d9f28 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,11 @@ module src go 1.21.5 -require github.com/towgo/towgo v0.0.0-20231224080502-c72f73646298 +require ( + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.825 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.825 + github.com/towgo/towgo v0.0.0-20231224080502-c72f73646298 +) require ( github.com/go-sql-driver/mysql v1.7.1 // indirect diff --git a/go.sum b/go.sum index 996c99c..e33536b 100644 --- a/go.sum +++ b/go.sum @@ -215,6 +215,10 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.825 h1:ijG2TLksawrFMdBpfFa5N/HjBZLGRPwmCgg0JFfFl6E= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.825/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.825 h1:F6NQyLl7IscbIV1NcXshJeQVt8nMcS/Ock77aBhYI+k= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.825/go.mod h1:TtDf9a/E/BsheaNyTdYUD3ttvTUIYODO/4bYv6ZW9tY= github.com/towgo/towgo v0.0.0-20231224080502-c72f73646298 h1:oYz9djtL9h+k2OBxsk7i8Emu233eYJJ5RpAGFua5fHA= github.com/towgo/towgo v0.0.0-20231224080502-c72f73646298/go.mod h1:mNKhCpd9uCgi1yKphteeXR0S2ZyUJ8UDVPwJ4LKos3s= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/init/Category.go b/init/Category.go index 4bf5381..62656c1 100644 --- a/init/Category.go +++ b/init/Category.go @@ -1,3 +1,3 @@ package init -import _ "src/module/category" +//import _ "src/module/category" diff --git a/init/usercenter.go b/init/usercenter.go new file mode 100644 index 0000000..202bebb --- /dev/null +++ b/init/usercenter.go @@ -0,0 +1,7 @@ +package init + +import "src/module/usercenter" + +func init() { + usercenter.InitManageApi() +} diff --git a/module/tencent/sms.go b/module/tencent/sms.go new file mode 100644 index 0000000..4d72cb2 --- /dev/null +++ b/module/tencent/sms.go @@ -0,0 +1,112 @@ +package tencent + +import ( + "encoding/json" + "fmt" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" // 引入sms +) + +func SendSMSVerificationCode(templateID, signName, mobile, verificationCode string) error { + /* 必要步骤: + * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。 + * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 + * 您也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人, + * 以免泄露密钥对危及您的财产安全。 + * SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */ + credential := common.NewCredential( + // os.Getenv("TENCENTCLOUD_SECRET_ID"), + // os.Getenv("TENCENTCLOUD_SECRET_KEY"), + "AKID1EXk8c8b0U5lPh2TrEa1QnCeBphR9wka", + "sjbOE4sidUlx5EIORRRL6jGkAZDmGb1T", + ) + /* 非必要步骤: + * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + cpf := profile.NewClientProfile() + + /* SDK默认使用POST方法。 + * 如果您一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */ + cpf.HttpProfile.ReqMethod = "POST" + + /* SDK有默认的超时时间,非必要请不要进行调整 + * 如有需要请在代码中查阅以获取最新的默认值 */ + // cpf.HttpProfile.ReqTimeout = 5 + + /* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */ + cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com" + + /* SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段 */ + cpf.SignMethod = "HmacSHA1" + + /* 实例化要请求产品(以sms为例)的client对象 + * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */ + client, _ := sms.NewClient(credential, "ap-guangzhou", cpf) + + /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 + * 您可以直接查询SDK源码确定接口有哪些属性可以设置 + * 属性可能是基本类型,也可能引用了另一个数据结构 + * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ + request := sms.NewSendSmsRequest() + + /* 基本类型的设置: + * SDK采用的是指针风格指定参数,即使对于基本类型您也需要用指针来对参数赋值。 + * SDK提供对基本类型的指针引用封装函数 + * 帮助链接: + * 短信控制台: https://console.cloud.tencent.com/smsv2 + * 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */ + + /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */ + // 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看 + request.SmsSdkAppId = common.StringPtr("1400878793") + + /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */ + // 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看 + request.SignName = common.StringPtr(signName) + + /* 模板 ID: 必须填写已审核通过的模板 ID */ + // 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看 + request.TemplateId = common.StringPtr(templateID) + + /* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空*/ + request.TemplateParamSet = common.StringPtrs([]string{verificationCode}) + + /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] + * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/ + request.PhoneNumberSet = common.StringPtrs([]string{"+86" + mobile}) + + /* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ + request.SessionContext = common.StringPtr("") + + /* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */ + request.ExtendCode = common.StringPtr("") + + /* 国内短信无需填写该项;国际/港澳台短信已申请独立 SenderId 需要填写该字段,默认使用公共 SenderId,无需填写该字段。注:月度使用量达到指定量级可申请独立 SenderId 使用,详情请联系 [腾讯云短信小助手](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。 */ + request.SenderId = common.StringPtr("") + + // 通过client对象调用想要访问的接口,需要传入请求对象 + response, err := client.SendSms(request) + // 处理异常 + if _, ok := err.(*errors.TencentCloudSDKError); ok { + fmt.Printf("An API error has returned: %s", err) + return err + } + // 非SDK异常,直接失败。实际代码中可以加入其他的处理。 + if err != nil { + return err + } + b, _ := json.Marshal(response.Response) + // 打印返回的json字符串 + fmt.Printf("%s", b) + + /* 当出现以下错误码时,快速解决方案参考 + * [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms) + */ + return nil +} diff --git a/module/usercenter/SMSVerificationCode.go b/module/usercenter/SMSVerificationCode.go new file mode 100644 index 0000000..7414f6d --- /dev/null +++ b/module/usercenter/SMSVerificationCode.go @@ -0,0 +1,51 @@ +package usercenter + +import ( + "bytes" + "crypto/rand" + "math/big" + "sync" + "time" +) + +var regSMSVerificationCode sync.Map + +// 登记注册用验证码 +func StoreRegSMSVerificationCode(mobile string) (string, error) { + codeInterface, ok := regSMSVerificationCode.Load(mobile) + if ok { + return codeInterface.(string), nil + } + verificationCode := randCharNumber(6) + regSMSVerificationCode.Store(mobile, verificationCode) + go func(mobile string) { + time.Sleep(time.Second * 5 * 60) + regSMSVerificationCode.Delete(mobile) + }(mobile) + return verificationCode, nil +} + +// 验证注册验证码 +func RegSMSVerification(mobile, verificationCode string) bool { + codeInterface, ok := regSMSVerificationCode.LoadAndDelete(mobile) + if !ok { + return false + } + if codeInterface.(string) == verificationCode { + return true + } else { + return false + } +} + +// 随机验证码 +func randCharNumber(size int) string { + char := "0123456789" + len64 := int64(len(char)) + var s bytes.Buffer + for i := 0; i < size; i++ { + in, _ := rand.Int(rand.Reader, big.NewInt(len64)) + s.WriteByte(char[in.Int64()]) + } + return s.String() +} diff --git a/module/usercenter/api.user.go b/module/usercenter/api.user.go new file mode 100644 index 0000000..aca9434 --- /dev/null +++ b/module/usercenter/api.user.go @@ -0,0 +1,293 @@ +package usercenter + +import ( + "encoding/json" + "regexp" + "src/module/tencent" + + "github.com/towgo/towgo/towgo" +) + +func InitManageApi() { + + //初始化API加载器 + initLoader() + + //注册JSON-RPC服务处理器method路由 + //账户登录 F + towgo.SetFunc(_methodHead+"/user/login", userLogin) + + //获取自己的账户信息 + towgo.SetFunc(_methodHead+"/user/myinfo", userMyinfo) + + //账户注销 F + towgo.SetFunc(_methodHead+"/user/logoff", userLogoff) + + //账户注册移动用户注册 + towgo.SetFunc(_methodHead+"/user/regByMobile", userRegByMobile) + + //获取注册短信验证码 + towgo.SetFunc(_methodHead+"/user/getRegSMSVerificationCode", getRegSMSVerificationCode) + + //修改密码 F + towgo.SetFunc(_methodHead+"/user/changepassword", userChangepassword) + +} + +func isPhoneNumber(input string) bool { + // 中国手机号码正则表达式 + // 13[0-9], 14[5,7,9], 15[0-3,5-9], 16[6], 17[0-8], 18[0-9], 19[1,8,9] + phoneNumberPattern := `^1([38][0-9]|14[579]|5[^4]|6[6]|7[0-8]|9[189])\d{8}$` + reg := regexp.MustCompile(phoneNumberPattern) + return reg.MatchString(input) +} + +func getRegSMSVerificationCode(rpcConn towgo.JsonRpcConnection) { + var params struct { + Mobile string `json:"mobile"` + } + rpcConn.ReadParams(¶ms) + + if !isPhoneNumber(params.Mobile) { + rpcConn.WriteError(500, "手机号码非法") + return + } + + code, err := StoreRegSMSVerificationCode(params.Mobile) + + err = tencent.SendSMSVerificationCode("2030693", "蕊鑫信息科技", params.Mobile, code) + if err != nil { + rpcConn.WriteError(500, err.Error()) + return + } + + rpcConn.WriteResult("验证码已经发送,请查收") + +} + +// 注册用户 +func userRegByMobile(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} //初始化结果参数 + + var params struct { + Mobile string `json:"mobile"` + VerificationCode string `json:"verification_code"` + } + + rpcConn.ReadParams(¶ms) + + if params.Mobile == "" { + rpcConn.WriteError(500, "手机号码不能为空") + return + } + if params.VerificationCode == "" { + rpcConn.WriteError(500, "验证码不能为空") + return + } + + if !RegSMSVerification(params.Mobile, params.VerificationCode) { + rpcConn.WriteError(500, "验证码错误") + return + } + + var user User + user.Username = params.Mobile + user.Password = randCharNumber(8) + + /* + user := user{} + user.Nickname = jsonObj.Params.Nickname + user.Email = jsonObj.Params.Email + */ + Err := user.Reg(user.Username, user.Password) + if Err != nil { + rpcConn.WriteError(500, Err.Error()) + return + } + + //拼装结果返回 + result["id"] = user.ID + result["username"] = user.Username + rpcConn.WriteResult(result) +} + +// 用户登陆 +func userLogin(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} //初始化结果参数 + var err error + + rpcResponse := rpcConn.GetRpcResponse() + jsonObj := struct { + Params struct { + Username string `json:"username"` + Password string `json:"password"` + } `json:"params"` + }{} + err = json.Unmarshal([]byte(rpcConn.Read()), &jsonObj) + if err != nil { + rpcResponse.Error.Set(1, err.Error()) + rpcConn.Write() + return + } + if jsonObj.Params.Username == "" { + rpcResponse.Error.Set(1001, "") + rpcConn.Write() + return + } + if jsonObj.Params.Password == "" { + rpcResponse.Error.Set(1002, "") + rpcConn.Write() + return + } + + user := User{} + loginErr := user.Login(jsonObj.Params.Username, jsonObj.Params.Password) + + if loginErr != nil { //模型层登陆成功 + //dblog.Write("user:info", fmt.Sprintf("%s@%s 登录失败! 错误信息:%s", user.Username, rpcConn.GetRemoteAddr(), loginErr.Error())) + rpcResponse.Error.Set(1, "用户名或密码错误") + rpcConn.Write() + return + } + + result["id"] = user.ID + result["username"] = user.Username + + result["token"] = user.UserToken.TokenKey + + //dblog.Write("user:info", fmt.Sprintf("%s@%s 登录成功!", user.Username, rpcConn.GetRemoteAddr())) + rpcConn.WriteResult(result) + +} + +// token check +func userTokenCheck(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} //初始化结果参数 + var err error + + rpcResponse := rpcConn.GetRpcResponse() + jsonObj := struct { + Session string `json:"session"` + Params struct { + Username string `json:"username"` + Userid int `json:"userid"` + Token string `json:"token"` + } `json:"params"` + }{} + err = json.Unmarshal([]byte(rpcConn.Read()), &jsonObj) + if err != nil { + rpcResponse.Error.Set(1, err.Error()) + rpcConn.Write() + return + } + + var user *User + user, err = user.LoginByToken(jsonObj.Params.Token) + if err != nil { + result["valid"] = false + rpcConn.WriteResult(result) + return + } + if user.ID == 0 { + result["valid"] = false + rpcConn.WriteResult(result) + return + } + + if !user.UserToken.Valid() { + result["valid"] = false + rpcConn.WriteResult(result) + return + } + + result["valid"] = true + rpcConn.WriteResult(result) +} + +// 用户注销 +func userLogoff(rpcConn towgo.JsonRpcConnection) { + //result := map[string]interface{}{} //初始化结果参数 + var err error + + rpcResponse := rpcConn.GetRpcResponse() + jsonObj := struct { + Session string `json:"session"` + }{} + err = json.Unmarshal([]byte(rpcConn.Read()), &jsonObj) + if err != nil { + rpcResponse.Error.Set(1, err.Error()) + rpcConn.Write() + return + } + + var user *User + user, err = user.LoginByToken(jsonObj.Session) + if err != nil { + rpcConn.WriteResult(map[string]string{"success": "ok"}) + return + } + if user.ID > 0 { + user.Logoff() + } + + rpcConn.WriteResult(map[string]string{"success": "ok"}) + +} + +func userMyinfo(rpcConn towgo.JsonRpcConnection) { + userSession, err := LoginByToken(rpcConn.GetRpcRequest().Session) + userSession.Token = rpcConn.GetRpcRequest().Session + if err != nil { + rpcConn.GetRpcResponse().Error.Set(401, err.Error()) + rpcConn.Write() + return + } + rpcConn.WriteResult(userSession) +} + +func userChangepassword(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} //初始化结果参数 + var err error + + rpcResponse := rpcConn.GetRpcResponse() + jsonObj := struct { + Session string `json:"session"` + Params struct { + Oldpassword string `json:"oldpassword"` + Newpassword string `json:"newpassword"` + } `json:"params"` + }{} + err = json.Unmarshal([]byte(rpcConn.Read()), &jsonObj) + + if err != nil { + rpcResponse.Error.Set(1, err.Error()) + rpcConn.WriteResult(result) + return + } + + var user *User + user, err = user.LoginByToken(jsonObj.Session) + if err != nil { + rpcResponse.Error.Set(401, err.Error()) + rpcConn.WriteResult(result) + return + } + + if user.ID == 0 { + rpcResponse.Error.Set(1003, "") + rpcConn.WriteResult(result) + return + } + + err = user.Changepassword(jsonObj.Params.Oldpassword, jsonObj.Params.Newpassword) + if err != nil { + rpcResponse.Error.Set(1, err.Error()) + rpcConn.WriteResult(result) + return + } + + rpcConn.WriteResult(struct { + Success string `json:"success"` + }{Success: "ok"}) + +} diff --git a/module/usercenter/init.loader.go b/module/usercenter/init.loader.go new file mode 100644 index 0000000..ba3ac7b --- /dev/null +++ b/module/usercenter/init.loader.go @@ -0,0 +1,25 @@ +package usercenter + +import ( + "github.com/towgo/towgo/dao/ormDriver/xormDriver" +) + +var _methodHead string +var _tableHead string + +func SetMethodHead(methodHead string) { + _methodHead = methodHead +} + +func SetTableHead(tableHead string) { + _tableHead = tableHead +} + +func initLoader() { + + //token任务 + InitTokenTask() + + xormDriver.Sync2(new(User), new(UserToken)) + +} diff --git a/module/usercenter/model.user.go b/module/usercenter/model.user.go new file mode 100644 index 0000000..ec85267 --- /dev/null +++ b/module/usercenter/model.user.go @@ -0,0 +1,280 @@ +package usercenter + +import ( + "errors" + + "github.com/towgo/towgo/dao/basedboperat" + "github.com/towgo/towgo/lib/system" +) + +func (User) TableName() string { + return _tableHead + "users" +} + +func (*User) CacheExpire() int64 { + return 5000 +} + +// 账户对象 关联账户信息 +type User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Password string `json:"password"` + Salt string `json:"-"` //密码加盐 + Email string `json:"email"` + Mobile string `json:"mobile"` + CanDelete bool `json:"-"` + AccessToken string `json:"-"` + Token string `json:"token" gorm:"-" xorm:"-"` + UserToken *UserToken `json:"-" gorm:"-" xorm:"-"` + CreatedAt int64 `json:"created_at"` //创建时间 + UpdatedAt int64 `json:"updated_at"` //更新时间 +} + +// 注册 +func (a *User) Reg(username, password string) error { + + e := a.CheckForInput(username, password) + if e != nil { + return e + } + + //数据库查询出用户信息 + + finduser := User{} + basedboperat.Get(&finduser, nil, "username = ?", username) + + //检查用户名是否存在 + if username == finduser.Username { + return errors.New("账户已经存在") + } + + //生成密码 + a.NewPassword(password) + + a.Username = username + a.CanDelete = true + + _, err := basedboperat.Create(a) // 通过数据的指针来创建 + if err != nil { + return err + } + _, err = a.CreateRelation() + return err +} + +// 检查输入参数 +func (a *User) CheckForInput(username, password string) error { + + if username == "" { + return errors.New("用户名不能为空") + } + + if password == "" { + return errors.New("密码不能为空") + } + + //防sql注入 + if system.FilteredSQLInject(username) { + return errors.New("用户名存在系统保留或非法的字符") + } + return nil +} + +func (a *User) NewPassword(newpassword string) { + if newpassword == "" { + return + } + //加密密码 + + password := system.MD5(newpassword) + + //生成salt + salt := system.RandCharCrypto(6) + + //密码加盐 + password = password + salt + + //混合加密 + password = system.MD5(password) + + a.Password = password + a.Salt = salt +} + +// 用户登陆 +func (a *User) Login(username, password string) error { + + erro := a.CheckForInput(username, password) + if erro != nil { + return erro + } + + //通过用户名查询用户数据 + err := basedboperat.Get(a, nil, "username = ?", username) + + if err != nil { + return err + } + //检查用户名是否存在 + + //判断用户是否存在 + if a.Username == "" { + return errors.New("用户名不存在") + } + + //加密密码 + upassword := system.MD5(password) + + //撒盐 + upassword = upassword + a.Salt + + //混合加密 + upassword = system.MD5(upassword) + + //判断密码是否一致 + if a.Password != upassword { + //不一致:返回错误 + return errors.New("密码错误") + } + + //验证通过 + + //生成用户信息 + a.UserToken = NewToken(a) + + return nil +} + +// 用户注销 +func (a *User) Logoff() { + DeleteToken(a.UserToken.TokenKey) +} + +func LoginByToken(tokenKey string) (*User, error) { + userToken, err := GetToken(tokenKey) + if err != nil { + return nil, err + } + sessionuser := userToken.Payload.(*User) + sessionuser.UserToken = userToken + return sessionuser, nil +} + +func (a *User) LoginByToken(tokenKey string) (*User, error) { + userToken, err := GetToken(tokenKey) + if err != nil { + return nil, err + } + sessionuser := userToken.Payload.(*User) + sessionuser.UserToken = userToken + return sessionuser, nil +} + +func (a *User) CheckToken(s string) bool { + //判断token是否正确 + if s != a.UserToken.TokenKey { + return false + } + //再判断token是否过期 + return a.UserToken.Valid() +} +func (a *User) Get() error { + + if a.ID > 0 { + return basedboperat.Get(a, nil, "id = ?", a.ID) + } + if a.Username != "" { + return basedboperat.Get(a, nil, "username = ?", a.Username) + } + return errors.New("id或username不能为空") +} + +// 修改密码 +func (a *User) Changepassword(oldpassword, newpassword string) error { + + //通过用户名查询用户数据 + + err := basedboperat.Get(a, nil, "id = ?", a.ID) + if err != nil { + return err + } + //检查用户名是否存在 + + //判断用户是否存在 + if a.Username == "" { + return errors.New("用户名不存在") + } + + //加密密码 + upassword := system.MD5(oldpassword) + + //撒盐 + upassword = upassword + a.Salt + + //混合加密 + upassword = system.MD5(upassword) + + //判断密码是否一致 + if a.Password != upassword { + //不一致:返回错误 + return errors.New("原始密码错误") + } + + a.NewPassword(newpassword) + + basedboperat.Update(a, []string{"password", "salt"}, "id = ?", a.ID) + + return nil +} + +func (a *User) Update() error { + var findModel User + basedboperat.Get(&findModel, nil, "id = ?", a.ID) + if findModel.ID <= 0 { + return errors.New("记录不存在") + } + a.DeleteRelation() + _, err := a.CreateRelation() + if err != nil { + return err + } + basedboperat.Update(a, []string{"nickname", "email"}, "id = ?", a.ID) + return nil +} + +func (a *User) Delete() (int64, error) { + var findModel User + basedboperat.Get(&findModel, nil, "id = ?", a.ID) + if !findModel.CanDelete { + return 0, errors.New("无法删除系统用户") + } + a.DeleteRelation() + return basedboperat.Delete(a, a.ID, nil) +} + +// 删除关联数据 +func (a *User) DeleteRelation() { + if a.ID == 0 { + return + } + +} + +// 创建关联数据 +func (a *User) CreateRelation() (int64, error) { + if a.ID == 0 { + return 0, nil + } + var rowsAffected int64 + + return rowsAffected, nil +} + +func (a *User) AfterQuery() { + if a.ID == 0 { + return + } + +} diff --git a/module/usercenter/model.user.token.go b/module/usercenter/model.user.token.go new file mode 100644 index 0000000..46cbcff --- /dev/null +++ b/module/usercenter/model.user.token.go @@ -0,0 +1,227 @@ +package usercenter + +import ( + "context" + "errors" + "log" + "sync" + "time" + + "github.com/towgo/towgo/dao/basedboperat" + "github.com/towgo/towgo/lib/system" +) + +// 缓存有效期 +var memCacheTimer int64 = 60 * 10 + +// token有效期 秒单位计算 +var expirationLimit int64 = 86400 * 20 + +var autoClearLimit int64 = 60 * 10 //10分钟清理一次过期的token + +// var expirationLimit int64 = 60 +var memCache *MemCache + +// UserToken结构体 +func (UserToken) TableName() string { + return _tableHead + "users_token" +} + +type UserToken struct { + TokenKey string + Uid int64 + Payload any `gorm:"-" xorm:"-"` + Expiration int64 + UpdatedAt int64 + CreatedAt int64 +} + +func InitTokenTask() { + memCache = &MemCache{} + autoTimeToClear() +} + +type MemCache struct { + timers sync.Map + cacheObject sync.Map + cancels sync.Map +} + +func (mc *MemCache) CreateTimerToDelete(tokenKey string) { + timer := time.NewTimer(time.Second * time.Duration(memCacheTimer)) + mc.timers.Store(tokenKey, timer) + ctx, cancel := context.WithCancel(context.Background()) + + mc.cancels.Store(tokenKey, cancel) + go mc.DeleteWhenTimeOut(ctx, tokenKey, timer) +} + +func (mc *MemCache) DeleteWhenTimeOut(ctx context.Context, tokenKey string, timer *time.Timer) { + select { + case <-timer.C: + mc.timers.Delete(tokenKey) + mc.cacheObject.Delete(tokenKey) + mc.cancels.Delete(tokenKey) + return + case <-ctx.Done(): + return + } +} + +func (mc *MemCache) ResetTimer(tokenKey string) { + timerInterface, ok := mc.timers.Load(tokenKey) + if ok { + timer := timerInterface.(*time.Timer) + timer.Reset(time.Second * time.Duration(memCacheTimer)) + } +} + +func (mc *MemCache) Add(tokenKey string, value any) { + mc.cacheObject.Store(tokenKey, value) + mc.CreateTimerToDelete(tokenKey) +} + +func (mc *MemCache) Del(tokenKey string) { + timerInterface, ok := mc.timers.Load(tokenKey) + if ok { + timer := timerInterface.(*time.Timer) + timer.Stop() //关闭定时器 + cancel_any, isLoaded := mc.cancels.LoadAndDelete(tokenKey) //关闭定时器线程 + if isLoaded { + cancel := cancel_any.(context.CancelFunc) + if cancel != nil { + cancel() + } + } + mc.timers.Delete(tokenKey) //清除定时器委托 + } + mc.cacheObject.Delete(tokenKey) //清除内存 +} + +func (mc *MemCache) Get(tokenKey string) (any, bool) { + return mc.cacheObject.Load(tokenKey) +} + +// 自动清理过期token定时器 +func autoTimeToClear() { + go func() { + defer func() { + err := recover() + if err != nil { + log.Print(err) + } + autoTimeToClear() + }() + var userToken UserToken + for { + time.Sleep(time.Second * time.Duration(autoClearLimit)) + basedboperat.SqlExec("delete from "+userToken.TableName()+" where expiration < ?", time.Now().Unix()) + } + }() +} + +func (t *UserToken) NewTokenGUID(salt string) { + guid := system.GetGUID().Hex() + saltEncode := system.MD5(salt) + tokenCode := system.MD5(guid + saltEncode) + t.TokenKey = tokenCode +} + +// 返回一个唯一标识的token令牌 +func (t *UserToken) String() string { + return t.TokenKey +} + +// token是否有效 检查有效期 +// 有效返回true +// 无效返回false +func (t *UserToken) Valid() bool { + return time.Now().Unix() < t.Expiration +} + +func (t *UserToken) Check(tokenKey string) bool { + return tokenKey == t.TokenKey +} + +// 更新token有效期 +func (t *UserToken) Update(expiration int64) { + if expiration > 0 { + t.Expiration = time.Now().Unix() + expiration + } else { + t.Expiration = time.Now().Unix() + expirationLimit + } +} + +// 新建一个token +func NewToken(user *User) *UserToken { + timenow := time.Now().Unix() + token := &UserToken{ + CreatedAt: timenow, + Uid: user.ID, + Payload: user, + Expiration: timenow + expirationLimit, + } + user.Token = token.TokenKey + token.NewTokenGUID(user.Salt) + basedboperat.Create(token) + memCache.Add(token.TokenKey, token) + return token +} + +func GetToken(tokenKey string) (*UserToken, error) { + var userToken *UserToken = &UserToken{} + + //缓存查询 + userTokenInterface, ok := memCache.Get(tokenKey) + if ok { + //缓存命中 + userToken = userTokenInterface.(*UserToken) + //缓存过期 清理 + if !userToken.Valid() { + memCache.Del(tokenKey) + return nil, errors.New("token过期(登录失效,请重新登录)") + } + } else { + //数据库查询 + + //查询持久化数据 + err := basedboperat.Get(userToken, nil, "token_key = ?", tokenKey) + if err != nil { + return nil, err + } + if userToken.Uid == 0 { + return nil, errors.New("token不存在(登录失效,请重新登录)") //数据不存在 + } + + //token过期 + if !userToken.Valid() { + return nil, errors.New("token过期(登录失效,请重新登录)") + } + + //查询token关联的用户 + var user *User = &User{} + basedboperat.Get(user, nil, "id = ?", userToken.Uid) + if user.ID == 0 { + return nil, errors.New("token关联用户不存在(登录失效,请重新登录)") //用户不存在 + } + + //写入缓存 + userToken.Payload = user + memCache.Add(userToken.TokenKey, userToken) + } + + return userToken, nil +} + +func DeleteToken(tokenKey string) { + memCache.Del(tokenKey) + var userToken *UserToken = &UserToken{} + userToken.TokenKey = tokenKey + + basedboperat.Delete(userToken, nil, "token_key = ?", tokenKey) + +} + +func SetExpiration(hour int64) { + expirationLimit = 3600 * hour +}