diff --git a/app/http/controller/web/users_controller.go b/app/http/controller/web/users_controller.go index 35c56ce..6a90394 100644 --- a/app/http/controller/web/users_controller.go +++ b/app/http/controller/web/users_controller.go @@ -6,10 +6,13 @@ import ( "catface/app/model" "catface/app/service/users/curd" userstoken "catface/app/service/users/token" + "catface/app/service/weixin" "catface/app/utils/response" + "fmt" "time" "github.com/gin-gonic/gin" + ) type Users struct { @@ -17,12 +20,12 @@ type Users struct { // 1.用户注册 func (u *Users) Register(context *gin.Context) { - // 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、context.GetBool()、GetFloat64()等快捷获取需要的数据类型,注意:相关键名规则: 前缀+验证器结构体中的 json 标签 - // 注意:在 ginskeleton 中获取表单参数验证器中的数字键(字段),请统一使用 GetFloat64(),其它获取数字键(字段)的函数无效,例如:GetInt()、GetInt64()等 + // 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、context.GetBool()、GetFloat64()等快捷获取需要的数据类型,注意:相关键名规则: 前缀+验证器结构体中的 json 标签 + // ATT 注意:在 ginskeleton 中获取表单参数验证器中的数字键(字段),请统一使用 GetFloat64(),其它获取数字键(字段)的函数无效,例如:GetInt()、GetInt64()等 // 当然也可以通过gin框架的上下文原始方法获取,例如: context.PostForm("user_name") 获取,这样获取的数据格式为文本,需要自己继续转换 userName := context.GetString(consts.ValidatorPrefix + "user_name") pass := context.GetString(consts.ValidatorPrefix + "pass") - userIp := context.ClientIP() + userIp := context.ClientIP() // INFO 通过上下文获取 IP 信息。 if curd.CreateUserCurdFactory().Register(userName, pass, userIp) { response.Success(context, consts.CurdStatusOkMsg, "") } else { @@ -35,6 +38,8 @@ func (u *Users) Login(context *gin.Context) { userName := context.GetString(consts.ValidatorPrefix + "user_name") pass := context.GetString(consts.ValidatorPrefix + "pass") phone := context.GetString(consts.ValidatorPrefix + "phone") + + // 1. 先检查 账号密码是否正确,然后再检查 Token 状态。 userModelFact := model.CreateUserFactory("") userModel := userModelFact.Login(userName, pass) @@ -143,3 +148,39 @@ func (u *Users) Destroy(context *gin.Context) { response.Fail(context, consts.CurdDeleteFailCode, consts.CurdDeleteFailMsg, "") } } + +// MARK Start by Hav; +func (u *Users) WeixinLogin(context *gin.Context) { + code := context.GetString(consts.ValidatorPrefix + "code") + userAvatar := context.GetString(consts.ValidatorPrefix + "user_avatar") + userName := context.GetString(consts.ValidatorPrefix + "user_name") + userIp := context.ClientIP() // INFO 通过上下文获取 IP 信息。 + + // 1. 访问 微信 API 获取 openid + openId, err := weixin.Code2Session(code) + if err != nil { + // 解析微信登录成功,返回用户信息 + fmt.Println(err) // TODO 换成 LOG + response.Fail(context, consts.CurdLoginFailCode, consts.CurdLoginFailMsg, "") + } + + // 2. 执行 CURD + if UsersModel, err := curd.CreateUserFactory().WeixinLogin(openId, userName, userAvatar) { + if userId > 0 { + // 3. 生成 token + token, err := userstoken.CreateUserFactory().GenerateToken(userId, userName, "", 0) + if err != nil { + response.Fail(context, consts.CurdLoginFailCode, consts.CurdLoginFailMsg, "") + } + + // 4. 返回 token + res := gin.H{ + "userId": UsersModel.userId, + "permission": UsersModel.Permission + "token": token, + } + } + } else { + response.Fail(context, consts.CurdLoginFailCode, consts.CurdLoginFailMsg, "") + } +} diff --git a/app/http/validator/common/register_validator/web_register_validator.go b/app/http/validator/common/register_validator/web_register_validator.go index cda0961..f0de85b 100644 --- a/app/http/validator/common/register_validator/web_register_validator.go +++ b/app/http/validator/common/register_validator/web_register_validator.go @@ -21,6 +21,10 @@ func WebRegisterValidator() { containers.Set(key, users.Register{}) key = consts.ValidatorPrefix + "UsersLogin" containers.Set(key, users.Login{}) + + key = consts.ValidatorPrefix + "UsersWeixinLogin" + containers.Set(key, users.WeixinLogin{}) + key = consts.ValidatorPrefix + "RefreshToken" containers.Set(key, users.RefreshToken{}) diff --git a/app/http/validator/web/users/login.go b/app/http/validator/web/users/login.go index 5f88918..0d15602 100644 --- a/app/http/validator/web/users/login.go +++ b/app/http/validator/web/users/login.go @@ -32,5 +32,4 @@ func (l Login) CheckParams(context *gin.Context) { // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 (&web.Users{}).Login(extraAddBindDataContext) } - } diff --git a/app/http/validator/web/users/weixin_login.go b/app/http/validator/web/users/weixin_login.go new file mode 100644 index 0000000..6d8990f --- /dev/null +++ b/app/http/validator/web/users/weixin_login.go @@ -0,0 +1,33 @@ +package users + +import ( + "catface/app/global/consts" + "catface/app/http/controller/web" + "catface/app/http/validator/core/data_transfer" + "catface/app/utils/response" + + "github.com/gin-gonic/gin" +) + +type WeixinLogin struct { + Code string `json:"code"` + UserName string `json:"user_name"` + UserAvatar string `json:"user_avatar"` // INFO 本地缓存,不确定 url 的类型。 +} + +func (w WeixinLogin) CheckParams(context *gin.Context) { + //1.先按照验证器提供的基本语法,基本可以校验90%以上的不合格参数 + if err := context.ShouldBind(&w); err != nil { + response.ValidatorError(context, err) + return + } + + // INFO 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 + extraAddBindDataContext := data_transfer.DataAddContext(w, consts.ValidatorPrefix, context) + if extraAddBindDataContext == nil { + response.ErrorSystem(context, "userLogin表单验证器json化失败", "") + } else { + // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 + (&web.Users{}).WeixinLogin(extraAddBindDataContext) + } +} diff --git a/app/model/users.go b/app/model/users.go index d7e24e5..f4ffb04 100644 --- a/app/model/users.go +++ b/app/model/users.go @@ -7,6 +7,7 @@ import ( "time" "go.uber.org/zap" + "gorm.io/gorm" ) // 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码 @@ -22,24 +23,25 @@ func CreateUserFactory(sqlType string) *UsersModel { type UsersModel struct { BaseModel - UserName string `gorm:"column:user_name" json:"user_name"` + UserName string `gorm:"column:user_name;size:20" json:"user_name"` Pass string `json:"-"` // INFO 暂时用不到,但先保留。 Phone string `json:"phone"` RealName string `gorm:"column:real_name" json:"real_name"` // TAG 状态管理 - Status int `json:"status"` // QUESTION + Status uint8 `json:"status"` // QUESTION Token string `json:"token"` LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"` // TAG MySELF - Permissions int `json:"permissions"` + UserAvatar string `gorm:"column:user_avatar;size:255" json:"user_avatar"` // TODO 暂时存储 url,之后考虑需要把文件上传到 Nginx + Permission uint8 `json:"permission" gorm:"default:9"` // TAG 微信登录相关 - OpenId string `gorm:"column:open_id;size:35" json:"open_id"` + OpenId string `gorm:"column:open_id;size:35;index" json:"open_id"` SessionKey string `gorm:"column:session_key;size:35" json:"session_key"` } // 表名 -func (u *UsersModel) TableName() string { - return "tb_users" +func (u *UsersModel) TableName() string { // TIP GORM 也会自动调用这个函数。 + return "users" } // 用户注册(写一个最简单的使用账号、密码注册即可) @@ -306,3 +308,26 @@ func (u *UsersModel) DelTokenCacheFromRedis(userId int64) { tokenCacheRedisFact.ClearUserToken() tokenCacheRedisFact.ReleaseRedisConn() } + +/** + * @description + * @return {*} + */ +func (u *UsersModel) WeixinLogin(openId string, name string, avatar string) (temp *UsersModel, err error) { + db := u.DB + + var user UsersModel + if result := db.Where("open_id = ?", openId).First(&user); result.Error != nil { + temp = &user + } else if result.Error == gorm.ErrRecordNotFound { + newUser := UsersModel{OpenId: openId, UserName: name, UserAvatar: avatar} + if err := db.Create(&newUser).Error; err != nil { + return nil, err + } + temp = &newUser // INFO 这里应该就是 GORM 插入后得到的对象。 + } else { + return nil, result.Error + } + + return temp, nil +} diff --git a/app/service/weixin/code2Session.go b/app/service/weixin/code2Session.go new file mode 100644 index 0000000..1d6649d --- /dev/null +++ b/app/service/weixin/code2Session.go @@ -0,0 +1,39 @@ +package weixin + +import ( + "catface/app/global/variable" + "fmt" + "io" + "net/http" +) + +func Code2Session(js_code string) (string, error) { + appid := variable.ConfigYml.GetString("Weixin.AppId") + appSecret := variable.ConfigYml.GetString("Weixin.AppSecret") + grantType := variable.ConfigYml.GetString("Weixin.Code2Session.GrantType") + + url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=%s", appid, appSecret, js_code, grantType) + + // 创建一个新的HTTP请求 + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + + // 发送HTTP请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error sending request: %v", err) + } + defer resp.Body.Close() + + // 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %v", err) + } + + // 返回响应体 + return string(body), nil +} diff --git a/config/config.yml b/config/config.yml index 4c6acb3..136c5a4 100644 --- a/config/config.yml +++ b/config/config.yml @@ -140,4 +140,12 @@ RabbitMq: Captcha: captchaId: "captcha_id" # 验证码id提交时的键名 captchaValue: "captcha_value" #验证码值提交时的键名 - length: 4 # 验证码生成时的长度 \ No newline at end of file + length: 4 # 验证码生成时的长度 + +WeixinServer: + AppId: "wxe1ff76a57cc6eed3" + AppSecret: "46a3557653462da34c6e69f17a472c7c" + + Code2Session: + grant_type: "authorization_code" # 主要就是想避免硬编码。 + \ No newline at end of file diff --git a/test/usersModel_test.go b/test/usersModel_test.go new file mode 100644 index 0000000..1fe3f2a --- /dev/null +++ b/test/usersModel_test.go @@ -0,0 +1,17 @@ +// add_test.go +package test + +import ( + "catface/app/model" + "testing" +) + +func TestUsers(t *testing.T) { + Init() + + user := model.UsersModel{} + err := DB.AutoMigrate(&user) + if err != nil { + t.Error(err) + } +}