feat(user): 实现基础用户系统

- 实现用户手机验证码、密码登录功能,支持 JWT 认证
- 实现用户注册功能
- 实现jwt中获取用户信息
This commit is contained in:
2024-11-03 19:06:09 +08:00
parent cca66faed5
commit d09e7a08cf
23 changed files with 168 additions and 246 deletions

View File

@@ -1,29 +0,0 @@
package auth
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/auth"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func SendSmsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SendSmsReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := auth.NewSendSmsLogic(r.Context(), svcCtx)
err := l.SendSms(&req)
result.HttpResult(r, w, nil, err)
}
}

View File

@@ -27,10 +27,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
// login
// mobile code login
Method: http.MethodPost,
Path: "/user/login",
Handler: user.LoginHandler(serverCtx),
Path: "/user/mobileCodeLogin",
Handler: user.MobileCodeLoginHandler(serverCtx),
},
{
// mobile login
Method: http.MethodPost,
Path: "/user/mobileLogin",
Handler: user.MobileLoginHandler(serverCtx),
},
{
// register

View File

@@ -1,29 +0,0 @@
package user
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/user"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func DetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UserInfoReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := user.NewDetailLogic(r.Context(), svcCtx)
resp, err := l.Detail(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,29 +0,0 @@
package user
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/user"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := user.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,29 +0,0 @@
package user
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/user"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := user.NewRegisterLogic(r.Context(), svcCtx)
resp, err := l.Register(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -1,29 +0,0 @@
package user
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"qnc-server/app/user/cmd/api/internal/logic/user"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/result"
"qnc-server/pkg/lzkit/validator"
)
func WxMiniAuthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.WXMiniAuthReq
if err := httpx.Parse(r, &req); err != nil {
result.ParamErrorResult(r, w, err)
return
}
if err := validator.Validate(req); err != nil {
result.ParamValidateErrorResult(r, w, err)
return
}
l := user.NewWxMiniAuthLogic(r.Context(), svcCtx)
resp, err := l.WxMiniAuth(&req)
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -37,12 +37,12 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
redisKey := fmt.Sprintf("%s:%s", req.ActionType, req.Mobile)
exists, err := l.svcCtx.Redis.Exists(redisKey)
if err != nil {
return err
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取redis缓存失败: %s", req.Mobile)
}
if exists {
// 如果 Redis 中已经存在标记,说明在 1 分钟内请求过,返回错误
return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "手机号1分钟内重复请求发送二维码, 手机号: %s", req.Mobile)
return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送二维码: %s", req.Mobile)
}
code := fmt.Sprintf("%06d", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000000))
@@ -50,21 +50,21 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
// 发送短信
smsResp, err := l.sendSmsRequest(req.Mobile, code)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送失败, 调用阿里客户端失败: %+v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 调用阿里客户端失败: %+v", err)
}
if *smsResp.Body.Code != "OK" {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送失败, 阿里客户端响应失败: %s", *smsResp.Body.Message)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 阿里客户端响应失败: %s", *smsResp.Body.Message)
}
// 将验证码保存到 Redis设置过期时间
err = l.svcCtx.Redis.Setex(req.Mobile, code, l.svcCtx.Config.VerifyCode.ValidTime) // 验证码有效期5分钟
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "缓存失败, 验证码设置过期时间失败: %+v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置过期时间失败: %+v", err)
}
// 在 Redis 中设置 1 分钟的标记,限制重复请求
err = l.svcCtx.Redis.Setex(redisKey, code, 60) // 标记 1 分钟内不能重复请求
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "缓存失败, 验证码设置限制重复请求失败: %+v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置限制重复请求失败: %+v", err)
}
return nil
}

View File

@@ -2,9 +2,12 @@ package user
import (
"context"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/zeromicro/go-zero/core/logx"
)
@@ -24,7 +27,20 @@ func NewDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DetailLogi
}
func (l *DetailLogic) Detail(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) {
// todo: add your logic here and delete this line
return
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %+v", err)
}
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户信息, 数据库查询用户信息失败, %+v", err)
}
var userInfo types.User
err = copier.Copy(&userInfo, user)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, 用户信息结构体复制失败, %+v", err)
}
return &types.UserInfoResp{
UserInfo: userInfo,
}, nil
}

View File

@@ -1,30 +0,0 @@
package user
import (
"context"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@@ -2,7 +2,9 @@ package user
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/user/cmd/api/internal/svc"
"qnc-server/app/user/cmd/api/internal/types"
@@ -14,8 +16,6 @@ import (
"github.com/zeromicro/go-zero/core/logx"
)
var ErrUserAlreadyRegisterError = xerr.NewErrMsg("该手机号码已注册")
type RegisterLogic struct {
logx.Logger
ctx context.Context
@@ -31,30 +31,42 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register
}
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
// 检查手机号是否在一分钟内已发送过验证码
redisKey := fmt.Sprintf("%s:%s", "registerCode", req.Mobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机注册, 验证码过期: %s", req.Mobile)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机注册, 读取验证码redis缓存失败, mobile: %s, err: %+v", req.Mobile, err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机注册, 验证码不正确: %s", req.Mobile)
}
hasUser, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, req.Mobile)
if findUserErr != nil && findUserErr != model.ErrNotFound {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "mobile:%s,err:%v", req.Mobile, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机注册, 读取数据库获取用户失败, mobile%s, err: %+v", req.Mobile, err)
}
if hasUser != nil {
return nil, errors.Wrapf(ErrUserAlreadyRegisterError, "Register user exists mobile:%s,err:%v", req.Mobile, err)
return nil, errors.Wrapf(xerr.NewErrMsg("该手机号码已注册"), "手机注册, 手机号码已注册, mobile:%s", req.Mobile)
}
var userId int64
if transErr := l.svcCtx.UserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
user := new(model.User)
user.Mobile = req.Mobile
if len(user.Nickname) == 0 {
user.Nickname = tool.Krand(8, tool.KC_RAND_KIND_ALL)
user.Nickname = req.Mobile
}
if len(req.Password) > 0 {
user.Password = tool.Md5ByString(req.Password)
}
insertResult, userInsertErr := l.svcCtx.UserModel.Insert(ctx, session, user)
if userInsertErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "Register db user Insert err:%v,user:%+v", userInsertErr, user)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机注册, 数据库插入新用户失败, mobile%s, err: %+v", req.Mobile, err)
}
lastId, lastInsertIdErr := insertResult.LastInsertId()
if lastInsertIdErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "Register db user insertResult.LastInsertId err:%v,user:%+v", lastInsertIdErr, user)
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机注册, 获取新用户ID失败, err:%+v, user:%+v", lastInsertIdErr, user)
}
userId = lastId
@@ -62,8 +74,8 @@ func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterRe
userAuth.UserId = lastId
userAuth.AuthKey = req.Mobile
userAuth.AuthType = model.UserAuthTypeAppMobile
if _, userAuthInsertErr := l.svcCtx.UserAuthModel.Insert(ctx, session, userAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "Register db user_auth Insert err:%v,userAuth:%v", err, userAuthInsertErr)
if _, userAuthInsertErr := l.svcCtx.UserAuthModel.Insert(ctx, session, userAuth); userAuthInsertErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机注册, 数据库插入用户认证失败, err:%+v", userAuthInsertErr)
}
return nil
}); transErr != nil {
@@ -72,9 +84,8 @@ func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterRe
token, generaErr := jwtx.GenerateJwtToken(userId, l.svcCtx.Config.JwtAuth.AccessSecret, l.svcCtx.Config.JwtAuth.AccessExpire)
if generaErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "GenerateToken error userId : %d", userId)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机注册, 生成jwt token失败, userid: %d, err:%+v", userId, generaErr)
}
return &types.RegisterResp{
AccessToken: token,
AccessExpire: l.svcCtx.Config.JwtAuth.AccessExpire,

View File

@@ -1,12 +1,23 @@
// Code generated by goctl. DO NOT EDIT.
package types
type LoginReq struct {
Mobile string `json:"mobile"`
Password string `json:"password"`
type MobileCodeLoginReq struct {
Mobile string `json:"mobile"`
Code string `json:"code" validate:"required"`
}
type LoginResp struct {
type MobileCodeLoginResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
type MobileLoginReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
Password string `json:"password" validate:"required"`
}
type MobileLoginResp struct {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
@@ -14,7 +25,8 @@ type LoginResp struct {
type RegisterReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
Password string `json:"password" validate:"required"`
Password string `json:"password" validate:"required,min=11,max=11,password"`
Code string `json:"code" validate:"required"`
}
type RegisterResp struct {
@@ -24,8 +36,9 @@ type RegisterResp struct {
}
type User struct {
Id int64 `json:"id"`
Mobile string `json:"mobile"`
Id int64 `json:"id"`
Mobile string `json:"mobile"`
NickName string `json:"nickName"`
}
type UserInfoReq struct {
@@ -49,5 +62,5 @@ type WXMiniAuthResp struct {
type SendSmsReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required"`
ActionType string `json:"actionType" validate:"required,oneof=loginCode registerCode QueryCode"`
}