From 9254b8fafe1927e9da5f4ea6e4a99c954abf73ac Mon Sep 17 00:00:00 2001 From: GeekMaster Date: Sat, 30 Aug 2025 20:30:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E7=99=BB=E5=BD=95=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/handler/admin/order_handler.go | 4 +- api/handler/payment_handler.go | 2 +- api/handler/user_handler.go | 435 ++++++++++++------------ api/service/wxlogin_service.go | 8 + web/src/components/LoginDialog.vue | 427 +++++++++++++++++++---- web/src/components/ui/CustomTabPane.vue | 63 ++-- web/src/components/ui/CustomTabs.vue | 42 ++- 7 files changed, 655 insertions(+), 326 deletions(-) diff --git a/api/handler/admin/order_handler.go b/api/handler/admin/order_handler.go index a4199b32..053e8b68 100644 --- a/api/handler/admin/order_handler.go +++ b/api/handler/admin/order_handler.go @@ -129,8 +129,8 @@ func (h *OrderHandler) Clear(c *gin.Context) { } deleteIds := make([]uint, 0) for _, order := range orders { - // 只删除 15 分钟内的未支付订单 - if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) { + // 只删除 5 分钟内的未支付订单 + if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) { deleteIds = append(deleteIds, order.Id) } } diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 76d4fee9..9b67327d 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -114,7 +114,7 @@ func (h *PaymentHandler) SyncOrders() error { for _, order := range orders { time.Sleep(time.Second * 1) //超时15分钟的订单,直接标记为已关闭 - if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) { + if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) { h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true) logger.Errorf("订单超时:%v", order) continue diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 33e34397..79fbb684 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -8,6 +8,7 @@ package handler // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "context" "fmt" "geekai/core" "geekai/core/middleware" @@ -37,6 +38,8 @@ type UserHandler struct { licenseService *service.LicenseService captchaService *service.CaptchaService userService *service.UserService + wxLoginService *service.WxLoginService + ipSearcher *xdb.Searcher } func NewUserHandler( @@ -47,6 +50,8 @@ func NewUserHandler( levelDB *store.LevelDB, captcha *service.CaptchaService, userService *service.UserService, + wxLoginService *service.WxLoginService, + ipSearcher *xdb.Searcher, licenseService *service.LicenseService) *UserHandler { return &UserHandler{ BaseHandler: BaseHandler{DB: db, App: app}, @@ -56,6 +61,8 @@ func NewUserHandler( captchaService: captcha, licenseService: licenseService, userService: userService, + wxLoginService: wxLoginService, + ipSearcher: ipSearcher, } } @@ -67,9 +74,10 @@ func (h *UserHandler) RegisterRoutes() { group.POST("register", h.Register) group.POST("login", h.Login) group.POST("resetPass", h.ResetPass) - group.GET("clogin", h.CLogin) + group.GET("login/qrcode", h.GetWxLoginQRCode) + group.POST("login/callback", h.WxLoginCallback) + group.GET("login/status", h.GetWxLoginState) group.GET("logout", h.Logout) - group.GET("clogin/callback", h.CLoginCallback) // 需要用户授权的接口 group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) @@ -150,30 +158,8 @@ func (h *UserHandler) Register(c *gin.Context) { } } - // 验证邀请码 - inviteCode := model.InviteCode{} - if data.InviteCode != "" { - res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode) - if res.Error != nil { - resp.ERROR(c, "无效的邀请码") - return - } - } - - salt := utils.RandString(8) - user := model.User{ - Username: data.Username, - Password: utils.GenPassword(data.Password, salt), - Avatar: "/images/avatar/user.png", - Salt: salt, - Status: true, - ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 - ChatConfig: "{}", - ChatModels: "{}", - Power: h.App.SysConfig.Base.InitPower, - } - // check if the username is existing + user := model.User{Username: data.Username, Password: data.Password} var item model.User session := h.DB.Session(&gorm.Session{}) if data.Mobile != "" { @@ -193,78 +179,19 @@ func (h *UserHandler) Register(c *gin.Context) { return } - // 被邀请人也获得赠送算力 - if data.InviteCode != "" { - user.Power += h.App.SysConfig.Base.InvitePower - } - - if h.licenseService.GetLicense().Configs.DeCopy { - user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) - } else { - defaultNickname := h.App.SysConfig.Base.DefaultNickname - if defaultNickname == "" { - defaultNickname = "极客学长" - } - user.Nickname = fmt.Sprintf("%s@%d", defaultNickname, utils.RandomNumber(6)) - } - - tx := h.DB.Begin() - if err := tx.Create(&user).Error; err != nil { + user, err := h.createNewUser(user, data.InviteCode) + if err != nil { resp.ERROR(c, err.Error()) return } - // 记录邀请关系 - if data.InviteCode != "" { - // 增加邀请数量 - h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) - if h.App.SysConfig.Base.InvitePower > 0 { - err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{ - Type: types.PowerInvite, - Model: "Invite", - Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username), - }) - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - } - - // 添加邀请记录 - err := tx.Create(&model.InviteLog{ - InviterId: inviteCode.UserId, - UserId: user.Id, - Username: user.Username, - InviteCode: inviteCode.Code, - Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower), - }).Error - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - } - tx.Commit() - - _ = h.redis.Del(c, key) // 注册成功,删除短信验证码 - // 自动登录创建 token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.Id, - "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), - }) - tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) + token, err := h.doLogin(&user, c.ClientIP()) if err != nil { - resp.ERROR(c, "Failed to generate token, "+err.Error()) + resp.ERROR(c, err.Error()) return } - // 保存到 redis - key = fmt.Sprintf("users/%d", user.Id) - if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil { - resp.ERROR(c, "error with save token: "+err.Error()) - return - } - resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username}) + + resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username}) } // Login 用户登录 @@ -311,35 +238,13 @@ func (h *UserHandler) Login(c *gin.Context) { return } - // 更新最后登录时间和IP - user.LastLoginIp = c.ClientIP() - user.LastLoginAt = time.Now().Unix() - h.DB.Model(&user).Updates(user) - - h.DB.Create(&model.UserLoginLog{ - UserId: user.Id, - Username: user.Username, - LoginIp: c.ClientIP(), - LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()), - }) - - // 创建 token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.Id, - "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), - }) - tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) + token, err := h.doLogin(&user, c.ClientIP()) if err != nil { - resp.ERROR(c, "Failed to generate token, "+err.Error()) + resp.ERROR(c, err.Error()) return } - // 保存到 redis - sessionKey := fmt.Sprintf("users/%d", user.Id) - if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil { - resp.ERROR(c, "error with save token: "+err.Error()) - return - } - resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username}) + + resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username}) } // Logout 注 销 @@ -351,130 +256,208 @@ func (h *UserHandler) Logout(c *gin.Context) { resp.SUCCESS(c) } -// CLogin 第三方登录请求二维码 -func (h *UserHandler) CLogin(c *gin.Context) { +// GetWxLoginQRCode 获取微信登录二维码URL +func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) { + if !h.wxLoginService.GetConfig().Enabled { + resp.ERROR(c, "微信登录功能未启用") + return + } + if h.wxLoginService.GetConfig().ApiKey == "" { + resp.ERROR(c, "微信登录服务令牌未配置") + return + } + + state := utils.RandString(32) + qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, gin.H{ + "url": qrCodeURL, + "state": state, + }) } -// CLoginCallback 第三方登录回调 -func (h *UserHandler) CLoginCallback(c *gin.Context) { - // loginType := c.Query("login_type") - // code := c.Query("code") - // userId := h.GetInt(c, "user_id", 0) - // action := c.Query("action") +// 查询微信登录状态 +func (h *UserHandler) GetWxLoginState(c *gin.Context) { + state := c.Query("state") + if state == "" { + resp.ERROR(c, "参数错误") + return + } - // var res types.BizVo - // apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL) - // r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}). - // SetHeader("AppId", h.App.Config.ApiConfig.AppId). - // SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)). - // SetSuccessResult(&res). - // Post(apiURL) - // if err != nil { - // resp.ERROR(c, err.Error()) - // return - // } - // if r.IsErrorState() { - // resp.ERROR(c, "error with login http status: "+r.Status) - // return - // } + status, err := h.wxLoginService.GetLoginStatus(state) + if err != nil { + resp.ERROR(c, err.Error()) + return + } - // if res.Code != types.Success { - // resp.ERROR(c, "error with http response: "+res.Message) - // return - // } + if status.Status != service.LoginStatusSuccess { + resp.SUCCESS(c, status) + return + } - // // login successfully - // data := res.Data.(map[string]interface{}) - // var user model.User - // if action == "bind" && userId > 0 { - // err = h.DB.Where("openid", data["openid"]).First(&user).Error - // if err == nil { - // resp.ERROR(c, "该微信已经绑定其他账号,请先解绑") - // return - // } + // 登录成功 + var user model.User + h.DB.Where("openid = ?", status.OpenID).First(&user) + if user.Id == 0 { + // 创建新用户 + user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "") + if err != nil { + resp.ERROR(c, err.Error()) + return + } + } - // err = h.DB.Where("id", userId).First(&user).Error - // if err != nil { - // resp.ERROR(c, "绑定用户不存在") - // return - // } + token, err := h.doLogin(&user, c.ClientIP()) + if err != nil { + resp.ERROR(c, err.Error()) + return + } - // err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error - // if err != nil { - // resp.ERROR(c, "更新用户信息失败,"+err.Error()) - // return - // } + status.Status = service.LoginStatusExpired + h.wxLoginService.SetLoginStatus(state, *status) - // resp.SUCCESS(c, gin.H{"token": ""}) - // return - // } + status.Status = service.LoginStatusSuccess + status.Token = token + resp.SUCCESS(c, status) +} - // session := gin.H{} - // tx := h.DB.Where("openid", data["openid"]).First(&user) - // if tx.Error != nil { - // // create new user - // var totalUser int64 - // h.DB.Model(&model.User{}).Count(&totalUser) - // if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum { - // resp.ERROR(c, "当前注册用户数已达上限,请请升级 License") - // return - // } +// createNewUser 创建新用户 +func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) { + if user.OpenId != "" { + user.Platform = "wechat" + user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6)) + user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8)) + user.Password = "geekai123" + } else { + user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) + if user.Username == "" || user.Password == "" { + return user, fmt.Errorf("用户名或密码不能为空") + } + } - // salt := utils.RandString(8) - // password := fmt.Sprintf("%d", utils.RandomNumber(8)) - // user = model.User{ - // Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)), - // Password: utils.GenPassword(password, salt), - // Avatar: fmt.Sprintf("%s", data["avatar"]), - // Salt: salt, - // Status: true, - // ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 - // Power: h.App.SysConfig.InitPower, - // OpenId: fmt.Sprintf("%s", data["openid"]), - // Nickname: fmt.Sprintf("%s", data["nickname"]), - // } + salt := utils.RandString(8) + user.Salt = salt + user.Password = utils.GenPassword(user.Password, salt) + user.Avatar = "/images/avatar/user.png" + user.Status = true + user.ChatRoles = utils.JsonEncode([]string{"gpt"}) + user.ChatConfig = "{}" + user.ChatModels = "{}" + user.Power = h.App.SysConfig.Base.InitPower - // tx = h.DB.Create(&user) - // if tx.Error != nil { - // resp.ERROR(c, "保存数据失败") - // logger.Error(tx.Error) - // return - // } - // session["username"] = user.Username - // session["password"] = password - // } else { // login directly - // // 更新最后登录时间和IP - // user.LastLoginIp = c.ClientIP() - // user.LastLoginAt = time.Now().Unix() - // h.DB.Model(&user).Updates(user) + // 创建用户 + tx := h.DB.Begin() + if err := tx.Create(&user).Error; err != nil { + return user, err + } - // h.DB.Create(&model.UserLoginLog{ - // UserId: user.Id, - // Username: user.Username, - // LoginIp: c.ClientIP(), - // LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()), - // }) - // } + // 记录邀请关系 + if inviteCode != "" { + inviteCode := model.InviteCode{} + err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error + if err != nil { + return user, fmt.Errorf("无效的邀请码") + } - // // 创建 token - // token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - // "user_id": user.Id, - // "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), - // }) - // tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) - // if err != nil { - // resp.ERROR(c, "Failed to generate token, "+err.Error()) - // return - // } - // // 保存到 redis - // key := fmt.Sprintf("users/%d", user.Id) - // if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil { - // resp.ERROR(c, "error with save token: "+err.Error()) - // return - // } - // session["token"] = tokenString - // resp.SUCCESS(c, session) + // 增加邀请数量 + h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) + if h.App.SysConfig.Base.InvitePower > 0 { + err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{ + Type: types.PowerInvite, + Model: "Invite", + Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username), + }) + if err != nil { + tx.Rollback() + return user, err + } + + // 添加邀请记录 + err = tx.Create(&model.InviteLog{ + InviterId: inviteCode.UserId, + UserId: user.Id, + Username: user.Username, + InviteCode: inviteCode.Code, + Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower), + }).Error + if err != nil { + tx.Rollback() + return user, err + } + } + } + + tx.Commit() + + return user, nil +} + +// doLogin 执行登录操作 +func (h *UserHandler) doLogin(user *model.User, ip string) (string, error) { + // 更新最后登录时间和IP + user.LastLoginIp = ip + user.LastLoginAt = time.Now().Unix() + err := h.DB.Model(user).Updates(user).Error + if err != nil { + return "", fmt.Errorf("failed to update user: %v", err) + } + + // 记录登录日志 + h.DB.Create(&model.UserLoginLog{ + UserId: user.Id, + Username: user.Username, + LoginIp: ip, + LoginAddress: utils.Ip2Region(h.ipSearcher, ip), + }) + + // 创建 token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.Id, + "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), + }) + tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) + if err != nil { + return "", fmt.Errorf("failed to generate token: %v", err) + } + + // 保存到 redis + sessionKey := fmt.Sprintf("users/%d", user.Id) + if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil { + return "", fmt.Errorf("error with save token: %v", err) + } + + return tokenString, nil +} + +// WxLoginCallback 微信登录回调处理 +func (h *UserHandler) WxLoginCallback(c *gin.Context) { + var data struct { + OpenID string `json:"openid"` + State string `json:"state"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + if data.OpenID == "" || data.State == "" { + resp.ERROR(c, "参数错误") + return + } + + // 设置登录状态 + status := service.LoginStatus{ + Status: service.LoginStatusSuccess, + OpenID: data.OpenID, + } + h.wxLoginService.SetLoginStatus(data.State, status) + + resp.SUCCESS(c, status) } // Session 获取/验证会话 diff --git a/api/service/wxlogin_service.go b/api/service/wxlogin_service.go index ac6c704b..4a812572 100644 --- a/api/service/wxlogin_service.go +++ b/api/service/wxlogin_service.go @@ -51,6 +51,14 @@ func (s *WxLoginService) UpdateConfig(config types.WxLoginConfig) { s.config = config } +func (s *WxLoginService) GetConfig() types.WxLoginConfig { + return s.config +} + +func (s *WxLoginService) SetConfig(config types.WxLoginConfig) { + s.config = config +} + func (s *WxLoginService) GetLoginQrCodeUrl(state string) (string, error) { if s.config.ApiKey == "" { return "", errors.New("无效的 API Key") diff --git a/web/src/components/LoginDialog.vue b/web/src/components/LoginDialog.vue index 67e073d2..418eface 100644 --- a/web/src/components/LoginDialog.vue +++ b/web/src/components/LoginDialog.vue @@ -1,68 +1,116 @@ diff --git a/web/src/components/ui/CustomTabs.vue b/web/src/components/ui/CustomTabs.vue index 18e5fd58..08f68c25 100644 --- a/web/src/components/ui/CustomTabs.vue +++ b/web/src/components/ui/CustomTabs.vue @@ -1,3 +1,23 @@ +