diff --git a/go/conf/app.conf b/go/conf/app.conf index 29f9f07..c60a5df 100644 --- a/go/conf/app.conf +++ b/go/conf/app.conf @@ -18,7 +18,7 @@ maxmemory = 10485760 # MySQL - 远程连接配置 mysqluser = go-platform mysqlpass = FSmJCSJ5wk8pjjDC -mysqlurls = 212.64.112.158:3388 +mysqlurls = 10.31.100.3:3306 mysqldb = go-platform # ORM配置 diff --git a/go/controllers/app_auth.go b/go/controllers/app_auth.go new file mode 100644 index 0000000..1ca2923 --- /dev/null +++ b/go/controllers/app_auth.go @@ -0,0 +1,381 @@ +package controllers + +import ( + "encoding/json" + "io" + "strings" + + "server/models" + "server/pkg/jwtutil" + "server/services" + + beego "github.com/beego/beego/v2/server/web" +) + +// AppAuthController App移动端认证控制器 +type AppAuthController struct { + beego.Controller +} + +func (c *AppAuthController) serveJSON(data map[string]interface{}) { + c.Data["json"] = data + _ = c.ServeJSON() +} + +// LoginBackend App端登录(需要租户) +func (c *AppAuthController) LoginBackend() { + var req struct { + TenantName string `json:"tenant_name"` + Account string `json:"account"` + Password string `json:"password"` + Code string `json:"code"` + } + + body := c.Ctx.Input.RequestBody + if len(body) == 0 { + var err error + body, err = io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + } + if len(body) == 0 { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + if err := json.Unmarshal(body, &req); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + + req.TenantName = strings.TrimSpace(req.TenantName) + req.Account = strings.TrimSpace(req.Account) + req.Password = strings.TrimSpace(req.Password) + if req.TenantName == "" || req.Account == "" || req.Password == "" { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "租户名称、用户名或密码不能为空"}) + return + } + + token, loginUser, err := services.BackendLogin(req.TenantName, req.Account, req.Password) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 401, "msg": err.Error()}) + return + } + + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "登录成功", + "data": map[string]interface{}{ + "token": token, + "user": map[string]interface{}{ + "id": loginUser.ID, + "account": loginUser.Account, + "name": loginUser.Name, + "tid": loginUser.Tid, + "rid": loginUser.Rid, + "avatar": loginUser.Avatar, + "role_name": loginUser.RoleName, + }, + }, + }) +} + +// GetCurrentUser App端当前登录用户信息,需 Bearer Token +func (c *AppAuthController) GetCurrentUser() { + authHeader := c.Ctx.Request.Header.Get("Authorization") + if authHeader == "" { + c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"}) + return + } + authParts := strings.SplitN(authHeader, " ", 2) + if len(authParts) != 2 || authParts[0] != "Bearer" { + c.serveJSON(map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}) + return + } + claims, err := jwtutil.ParseToken(authParts[1]) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 401, "msg": "无效的token"}) + return + } + if claims.UserType != "backend" && claims.UserType != "app" { + c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权访问"}) + return + } + + var tenantUser models.SystemTenantUser + err = models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("uid", claims.UserID). + Filter("tid", claims.TenantId). + One(&tenantUser) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 401, "msg": "用户不存在"}) + return + } + if tenantUser.Status == 0 { + c.serveJSON(map[string]interface{}{"code": 401, "msg": "账号已禁用"}) + return + } + + account := "" + if tenantUser.Account != nil { + account = strings.TrimSpace(*tenantUser.Account) + } + name := "" + if tenantUser.Name != nil { + name = strings.TrimSpace(*tenantUser.Name) + } + + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "success", + "data": map[string]interface{}{ + "id": tenantUser.Uid, + "account": account, + "name": name, + "tid": tenantUser.Tid, + "rid": 0, + "avatar": "", + "role_name": "", + }, + }) +} + +// SendLoginCode 发送App端登录验证码 +func (c *AppAuthController) SendLoginCode() { + var req struct { + Account string `json:"account"` + TenantName string `json:"tenant_name"` + Channel string `json:"channel"` + } + body := c.Ctx.Input.RequestBody + if len(body) == 0 { + var err error + body, err = io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + } + if err := json.Unmarshal(body, &req); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + + cfg, _ := models.GetPlatformLoginVerify() + if cfg.OpenVerifyEnabled != 1 { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "当前未开启验证"}) + return + } + channel := strings.TrimSpace(req.Channel) + if channel == "" { + channel = cfg.VerifyType + } + if channel != "sms" && channel != "email" { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "仅支持短信/邮箱验证码"}) + return + } + if err := services.SendBackendLoginCode(req.TenantName, req.Account, channel); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) + return + } + c.serveJSON(map[string]interface{}{"code": 200, "msg": "验证码已发送"}) +} + +// LoginBySms App端手机号验证码登录(占位实现) +func (c *AppAuthController) LoginBySms() { + c.serveJSON(map[string]interface{}{ + "code": 501, + "msg": "手机号验证码登录暂未实现", + }) +} + +// Logout App端退出登录(当前为无状态直接返回成功) +func (c *AppAuthController) Logout() { + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "退出成功", + }) +} + +// GetGeetest3Infos 获取App端极验3.0配置 +func (c *AppAuthController) GetGeetest3Infos() { + cfg, _ := models.GetPlatformLoginVerify() + if cfg.Geetest3ID == nil || cfg.Geetest3Key == nil { + c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验3参数"}) + return + } + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "success", + "data": map[string]interface{}{ + "captcha_id": *cfg.Geetest3ID, + "captcha_key": *cfg.Geetest3Key, + }, + }) +} + +// GetGeetest4Infos 获取App端极验4.0配置 +func (c *AppAuthController) GetGeetest4Infos() { + cfg, _ := models.GetPlatformLoginVerify() + if cfg.Geetest4ID == nil || cfg.Geetest4Key == nil { + c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验4参数"}) + return + } + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "success", + "data": map[string]interface{}{ + "captcha_id": *cfg.Geetest4ID, + "captcha_key": *cfg.Geetest4Key, + }, + }) +} + +// GetOpenVerify 判断是否开启App端登录验证 +func (c *AppAuthController) GetOpenVerify() { + cfg, _ := models.GetPlatformLoginVerify() + openVerify := "0" + if cfg.OpenVerifyEnabled == 1 { + openVerify = "1" + } + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "ok", + "data": []map[string]string{ + { + "label": "openVerify", + "value": openVerify, + }, + { + "label": "verifyType", + "value": cfg.VerifyType, + }, + }, + }) +} + +// VerifyAccount 验证租户和账号是否存在(忘记密码第一步) +func (c *AppAuthController) VerifyAccount() { + var req struct { + TenantName string `json:"tenant_name"` + Account string `json:"account"` + } + body := c.Ctx.Input.RequestBody + if len(body) == 0 { + var err error + body, err = io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + } + if err := json.Unmarshal(body, &req); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + + phone, email, err := services.VerifyTenantAccount(req.TenantName, req.Account) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) + return + } + + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "验证成功", + "data": map[string]interface{}{ + "phone": phone, + "email": email, + }, + }) +} + +// SendResetCode 发送找回密码验证码(忘记密码第二步) +func (c *AppAuthController) SendResetCode() { + var req struct { + TenantName string `json:"tenant_name"` + Account string `json:"account"` + Phone string `json:"phone"` + Channel string `json:"channel"` + } + body := c.Ctx.Input.RequestBody + if len(body) == 0 { + var err error + body, err = io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + } + if err := json.Unmarshal(body, &req); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + + channel := strings.TrimSpace(req.Channel) + if channel == "" { + channel = "sms" + } + + if err := services.SendResetCode(req.TenantName, req.Account, req.Phone, channel); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) + return + } + + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "验证码已发送", + }) +} + +// ResetPassword 重置密码(忘记密码第三步) +func (c *AppAuthController) ResetPassword() { + var req struct { + TenantName string `json:"tenant_name"` + Account string `json:"account"` + Phone string `json:"phone"` + SmsCode string `json:"sms_code"` + NewPassword string `json:"new_password"` + ConfirmPassword string `json:"confirm_password"` + } + body := c.Ctx.Input.RequestBody + if len(body) == 0 { + var err error + body, err = io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + } + if err := json.Unmarshal(body, &req); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) + return + } + + if err := services.ResetPassword(req.TenantName, req.Account, req.Phone, req.SmsCode, req.NewPassword, req.ConfirmPassword); err != nil { + c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) + return + } + + c.serveJSON(map[string]interface{}{ + "code": 200, + "msg": "密码重置成功", + }) +} + +// Register App端注册(占位实现) +func (c *AppAuthController) Register() { + c.serveJSON(map[string]interface{}{ + "code": 501, + "msg": "注册暂未实现", + }) +} + +// SendRegisterCode 发送App端注册验证码(占位实现) +func (c *AppAuthController) SendRegisterCode() { + c.serveJSON(map[string]interface{}{ + "code": 501, + "msg": "发送注册验证码暂未实现", + }) +} \ No newline at end of file diff --git a/go/controllers/backend_auth.go b/go/controllers/backend_auth.go index b09bf29..a33206d 100644 --- a/go/controllers/backend_auth.go +++ b/go/controllers/backend_auth.go @@ -67,7 +67,7 @@ func (c *BackendAuthController) LoginBackend() { cfg, _ := models.GetPlatformLoginVerify() if cfg.OpenVerifyEnabled == 1 { - if cfg.VerifyType == "geetest4" { + if cfg.VerifyType == "geetest4" || cfg.VerifyType == "geetest" { if req.LotNumber == "" || req.PassToken == "" || req.GenTime == "" || req.CaptchaOutput == "" { c.serveJSON(map[string]interface{}{"code": 400, "msg": "请完成人机验证"}) return @@ -319,4 +319,4 @@ func (c *BackendAuthController) SendResetCode() { "code": 501, "msg": "发送找回密码验证码暂未实现", }) -} +} \ No newline at end of file diff --git a/go/routers/app/app.go b/go/routers/app/app.go index 208db7c..266098e 100644 --- a/go/routers/app/app.go +++ b/go/routers/app/app.go @@ -8,25 +8,26 @@ import ( // Register 注册移动端(app)路由。 func Register() { - // 登录相关(复用 BackendAuthController) - beego.Router("/app/login", &controllers.BackendAuthController{}, "post:LoginBackend") - beego.Router("/app/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode") - beego.Router("/app/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms") - beego.Router("/app/logout", &controllers.BackendAuthController{}, "post:Logout") + // 登录相关(使用 AppAuthController) + beego.Router("/app/login", &controllers.AppAuthController{}, "post:LoginBackend") + beego.Router("/app/sendLoginCode", &controllers.AppAuthController{}, "post:SendLoginCode") + beego.Router("/app/loginBySms", &controllers.AppAuthController{}, "post:LoginBySms") + beego.Router("/app/logout", &controllers.AppAuthController{}, "post:Logout") // 当前用户信息 - beego.Router("/app/currentUser", &controllers.BackendAuthController{}, "get:GetCurrentUser") + beego.Router("/app/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser") // 极验与登录验证配置 - beego.Router("/app/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") - beego.Router("/app/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") - beego.Router("/app/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") + beego.Router("/app/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos") + beego.Router("/app/login/getGeetest4Infos", &controllers.AppAuthController{}, "get:GetGeetest4Infos") + beego.Router("/app/login/getOpenVerify", &controllers.AppAuthController{}, "get:GetOpenVerify") - // 找回密码 - beego.Router("/app/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword") - beego.Router("/app/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode") + // 找回密码(三步流程) + beego.Router("/app/verifyAccount", &controllers.AppAuthController{}, "post:VerifyAccount") + beego.Router("/app/sendResetCode", &controllers.AppAuthController{}, "post:SendResetCode") + beego.Router("/app/resetPassword", &controllers.AppAuthController{}, "post:ResetPassword") // 注册 - beego.Router("/app/register", &controllers.BackendAuthController{}, "post:Register") - beego.Router("/app/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode") + beego.Router("/app/register", &controllers.AppAuthController{}, "post:Register") + beego.Router("/app/sendRegisterCode", &controllers.AppAuthController{}, "post:SendRegisterCode") } diff --git a/go/services/password_reset.go b/go/services/password_reset.go new file mode 100644 index 0000000..bd9288d --- /dev/null +++ b/go/services/password_reset.go @@ -0,0 +1,260 @@ +package services + +import ( + "errors" + "fmt" + "math/rand" + "strings" + "sync" + "time" + + "server/models" + "server/pkg/passwordutil" +) + +// resetCodeItem 存储找回密码的验证码 +type resetCodeItem struct { + Code string + Channel string + ExpiredAt time.Time +} + +var resetCodeStore sync.Map + +// resetCodeKey 生成密码重置验证码的存储key +func resetCodeKey(tenantName, account, phone, channel string) string { + key := strings.ToLower(strings.TrimSpace(tenantName)) + "|" + + strings.ToLower(strings.TrimSpace(account)) + "|" + + strings.ToLower(strings.TrimSpace(phone)) + "|" + + strings.TrimSpace(channel) + return key +} + +// VerifyTenantAccount 第一步:验证租户和账号是否存在 +// 返回该账号关联的手机号和邮箱(用于第二步选择验证方式) +func VerifyTenantAccount(tenantName, account string) (phone, email string, err error) { + tenantName = strings.TrimSpace(tenantName) + account = strings.TrimSpace(account) + + if tenantName == "" || account == "" { + return "", "", errors.New("租户名称和账号不能为空") + } + + // 验证租户是否存在 + var tenant models.SystemTenant + if err := models.Orm.QueryTable(new(models.SystemTenant)). + Filter("tenant_name", tenantName). + One(&tenant); err != nil { + return "", "", errors.New("租户不存在") + } + if tenant.Status != 1 { + return "", "", errors.New("租户已停用") + } + + // 验证该租户下的账号是否存在 + var tenantUser models.SystemTenantUser + if err := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("tid", tenant.ID). + Filter("account", account). + One(&tenantUser); err != nil { + return "", "", errors.New("账号不存在") + } + if tenantUser.Status == 0 { + return "", "", errors.New("账号已禁用") + } + + // 返回该账号的手机号和邮箱 + phoneStr := "" + if tenantUser.Phone != nil { + phoneStr = strings.TrimSpace(*tenantUser.Phone) + } + emailStr := "" + if tenantUser.Email != nil { + emailStr = strings.TrimSpace(*tenantUser.Email) + } + + if phoneStr == "" && emailStr == "" { + return "", "", errors.New("账号未绑定手机号或邮箱,无法重置密码") + } + + return phoneStr, emailStr, nil +} + +// SendResetCode 第二步:验证手机号并发送验证码 +func SendResetCode(tenantName, account, phone, channel string) error { + tenantName = strings.TrimSpace(tenantName) + account = strings.TrimSpace(account) + phone = strings.TrimSpace(phone) + channel = strings.TrimSpace(channel) + + if tenantName == "" || account == "" || phone == "" { + return errors.New("租户名称、账号和手机号不能为空") + } + if channel != "sms" && channel != "email" { + return errors.New("仅支持短信或邮箱验证码") + } + + // 验证租户是否存在 + var tenant models.SystemTenant + if err := models.Orm.QueryTable(new(models.SystemTenant)). + Filter("tenant_name", tenantName). + One(&tenant); err != nil { + return errors.New("租户不存在") + } + + // 验证该租户下的账号和手机号是否匹配 + var tenantUser models.SystemTenantUser + if err := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("tid", tenant.ID). + Filter("account", account). + One(&tenantUser); err != nil { + return errors.New("账号不存在") + } + if tenantUser.Status == 0 { + return errors.New("账号已禁用") + } + + // 根据验证渠道验证用户信息 + if channel == "sms" { + if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone { + return errors.New("手机号不匹配,请确认您输入的手机号正确") + } + } else if channel == "email" { + if tenantUser.Email == nil || strings.TrimSpace(*tenantUser.Email) != phone { + return errors.New("邮箱不匹配,请确认您输入的邮箱正确") + } + } + + // 生成验证码 + rand.Seed(time.Now().UnixNano()) + code := fmt.Sprintf("%06d", rand.Intn(1000000)) + + // 发送验证码 + if channel == "sms" { + content := "密码重置验证码:" + code + if err := enqueueSMSTaskForPasswordReset(tenant.ID, phone, content, code); err != nil { + return errors.New("短信发送失败,请重试") + } + } + // TODO: 实现邮箱验证码发送逻辑 + + // 存储验证码(5分钟有效期) + resetCodeStore.Store(resetCodeKey(tenantName, account, phone, channel), resetCodeItem{ + Code: code, + Channel: channel, + ExpiredAt: time.Now().Add(5 * time.Minute), + }) + + return nil +} + +// VerifyResetCode 验证重置密码的验证码 +func VerifyResetCode(tenantName, account, phone, channel, code string) error { + tenantName = strings.TrimSpace(tenantName) + account = strings.TrimSpace(account) + phone = strings.TrimSpace(phone) + channel = strings.TrimSpace(channel) + code = strings.TrimSpace(code) + + if code == "" { + return errors.New("验证码不能为空") + } + + key := resetCodeKey(tenantName, account, phone, channel) + val, ok := resetCodeStore.Load(key) + if !ok { + return errors.New("验证码不存在或已失效") + } + + item, ok := val.(resetCodeItem) + if !ok { + return errors.New("验证码状态异常") + } + + if time.Now().After(item.ExpiredAt) { + resetCodeStore.Delete(key) + return errors.New("验证码已过期") + } + + if item.Code != code { + return errors.New("验证码错误") + } + + // 验证通过后删除验证码 + resetCodeStore.Delete(key) + return nil +} + +// ResetPassword 第三步:重置密码 +func ResetPassword(tenantName, account, phone, smsCode, newPassword, confirmPassword string) error { + tenantName = strings.TrimSpace(tenantName) + account = strings.TrimSpace(account) + phone = strings.TrimSpace(phone) + smsCode = strings.TrimSpace(smsCode) + newPassword = strings.TrimSpace(newPassword) + confirmPassword = strings.TrimSpace(confirmPassword) + + if tenantName == "" || account == "" || phone == "" { + return errors.New("租户名称、账号和手机号不能为空") + } + if newPassword == "" { + return errors.New("新密码不能为空") + } + if newPassword != confirmPassword { + return errors.New("两次密码不一致") + } + if len(newPassword) < 6 { + return errors.New("密码长度不能少于6个字符") + } + + // 验证验证码(验证码验证后会被删除) + if err := VerifyResetCode(tenantName, account, phone, "sms", smsCode); err != nil { + return err + } + + // 验证租户 + var tenant models.SystemTenant + if err := models.Orm.QueryTable(new(models.SystemTenant)). + Filter("tenant_name", tenantName). + One(&tenant); err != nil { + return errors.New("租户不存在") + } + + // 查询用户 + var tenantUser models.SystemTenantUser + if err := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("tid", tenant.ID). + Filter("account", account). + One(&tenantUser); err != nil { + return errors.New("账号不存在") + } + + // 验证手机号 + if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone { + return errors.New("手机号不匹配") + } + + // 哈希新密码 + hashedPassword, err := passwordutil.Hash(newPassword) + if err != nil { + return errors.New("密码处理失败") + } + + // 更新密码 + _, err = models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("id", tenantUser.ID). + Update(map[string]interface{}{ + "password": hashedPassword, + }) + if err != nil { + return errors.New("密码更新失败") + } + + return nil +} + +// enqueueSMSTaskForPasswordReset 发送密码重置短信任务 +func enqueueSMSTaskForPasswordReset(tid uint64, phone, content, code string) error { + // 重用已有的短信发送逻辑 + return enqueueSMSTaskForLogin(tid, phone, content, code) +} diff --git a/go/test.exe b/go/test.exe deleted file mode 100644 index 167123e..0000000 Binary files a/go/test.exe and /dev/null differ diff --git a/uniapp/App.vue b/uniapp/App.vue index d3b8474..f692da2 100644 --- a/uniapp/App.vue +++ b/uniapp/App.vue @@ -1,7 +1,9 @@ + + diff --git a/uniapp/pages/login/geetest-webview.vue b/uniapp/pages/login/geetest-webview.vue new file mode 100644 index 0000000..683809f --- /dev/null +++ b/uniapp/pages/login/geetest-webview.vue @@ -0,0 +1,30 @@ + + + diff --git a/uniapp/pages/login/login.vue b/uniapp/pages/login/login.vue index cab5be1..9daca8a 100644 --- a/uniapp/pages/login/login.vue +++ b/uniapp/pages/login/login.vue @@ -1,16 +1,11 @@ @@ -238,23 +435,6 @@ function socialLogin(type) { margin-bottom: 64rpx; } -.logo { - width: 96rpx; - height: 96rpx; - border-radius: $radius-lg; - background: $color-primary-bg; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 32rpx; -} - -.logo-char { - font-size: 44rpx; - font-weight: 600; - color: $color-primary; -} - .title { display: block; font-size: 48rpx; @@ -296,9 +476,6 @@ function socialLogin(type) { .form-card { @include card; padding: 32rpx 28rpx; - display: flex; - flex-direction: column; - gap: 24rpx; } .form { @@ -339,6 +516,47 @@ function socialLogin(type) { } } +.verify-hint { + font-size: 22rpx; + color: $color-text-muted; + line-height: 1.5; + padding: 0 4rpx; +} + +.remember-row { + display: flex; + align-items: center; + gap: 12rpx; + padding: 4rpx; +} + +.remember-dot { + width: 32rpx; + height: 32rpx; + border-radius: 50%; + border: 1rpx solid $color-border; + background: $color-card; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &.on { + background: $color-primary; + border-color: $color-primary; + } +} + +.remember-check { + font-size: 18rpx; + color: #fff; +} + +.remember-text { + font-size: 26rpx; + color: $color-text-secondary; +} + .btn-primary { height: 96rpx; background: $color-primary; @@ -354,22 +572,21 @@ function socialLogin(type) { &:active { background: $color-primary-dark; } + + &.disabled { + opacity: 0.7; + } } -.btn-ghost { - height: 96rpx; - background: $color-card; - border: 1rpx solid $color-border; - border-radius: $radius-md; +.link-row { display: flex; - align-items: center; - justify-content: center; - font-size: 28rpx; - color: $color-text-secondary; + justify-content: space-between; + padding: 8rpx 4rpx 0; +} - &:active { - background: $color-bg-page; - } +.link { + font-size: 26rpx; + color: $color-primary; } .agreement { @@ -377,6 +594,7 @@ function socialLogin(type) { align-items: flex-start; gap: 12rpx; margin-top: 32rpx; + padding-bottom: 60rpx; } .agree-dot { @@ -411,58 +629,4 @@ function socialLogin(type) { .agree-link { color: $color-primary; } - -.oauth { - margin-top: 64rpx; - display: flex; - flex-direction: column; - align-items: center; - gap: 28rpx; - padding-bottom: 60rpx; -} - -.oauth-label { - font-size: 24rpx; - color: $color-text-muted; - position: relative; - padding: 0 32rpx; - - &::before, - &::after { - content: ''; - position: absolute; - top: 50%; - width: 80rpx; - height: 1rpx; - background: $color-border; - } - - &::before { - right: 100%; - } - - &::after { - left: 100%; - } -} - -.oauth-icons { - display: flex; - gap: 48rpx; -} - -.oauth-btn { - width: 88rpx; - height: 88rpx; - border-radius: 50%; - background: $color-card; - box-shadow: $shadow-card; - display: flex; - align-items: center; - justify-content: center; - - &:active { - background: $color-bg-page; - } -} diff --git a/uniapp/pages/login/register.vue b/uniapp/pages/login/register.vue new file mode 100644 index 0000000..b89a8bc --- /dev/null +++ b/uniapp/pages/login/register.vue @@ -0,0 +1,341 @@ + + + + + diff --git a/uniapp/pages/profile/profile.vue b/uniapp/pages/profile/profile.vue index 5614945..e0e45f9 100644 --- a/uniapp/pages/profile/profile.vue +++ b/uniapp/pages/profile/profile.vue @@ -6,8 +6,8 @@ {{ avatarText }} - {{ user?.nickname || '云泽用户' }} - ID: {{ userId }} + {{ user?.nickname || user?.name || '云泽用户' }} + {{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }} @@ -43,7 +43,7 @@ - + 退出登录 @@ -56,20 +56,24 @@ + + + + + + diff --git a/uniapp/static/html/gt4.js b/uniapp/static/html/gt4.js new file mode 100644 index 0000000..6b2f6ae --- /dev/null +++ b/uniapp/static/html/gt4.js @@ -0,0 +1,487 @@ +"v4.2.0 Geetest Inc."; + +(function (window) { + "use strict"; + if (typeof window === 'undefined') { + throw new Error('Geetest requires browser environment'); + } + +var document = window.document; +var Math = window.Math; +var head = document.getElementsByTagName("head")[0]; +var TIMEOUT = 10000; + +function _Object(obj) { + this._obj = obj; +} + +_Object.prototype = { + _each: function (process) { + var _obj = this._obj; + for (var k in _obj) { + if (_obj.hasOwnProperty(k)) { + process(k, _obj[k]); + } + } + return this; + }, + _extend: function (obj){ + var self = this; + new _Object(obj)._each(function (key, value){ + self._obj[key] = value; + }) + } +}; + +var uuid = function () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0; + var v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }; + +function Config(config) { + var self = this; + new _Object(config)._each(function (key, value) { + self[key] = value; + }); +} + +Config.prototype = { + apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'], + staticServers: ["static.geetest.com",'static.geevisit.com'], + protocol: 'http://', + typePath: '/load', + fallback_config: { + bypass: { + staticServers: ["static.geetest.com",'static.geevisit.com'], + type: 'bypass', + bypass: '/v4/bypass.js' + } + }, + _get_fallback_config: function () { + var self = this; + if (isString(self.type)) { + return self.fallback_config[self.type]; + } else { + return self.fallback_config.bypass; + } + }, + _extend: function (obj) { + var self = this; + new _Object(obj)._each(function (key, value) { + self[key] = value; + }) + } +}; +var isNumber = function (value) { + return (typeof value === 'number'); +}; +var isString = function (value) { + return (typeof value === 'string'); +}; +var isBoolean = function (value) { + return (typeof value === 'boolean'); +}; +var isObject = function (value) { + return (typeof value === 'object' && value !== null); +}; +var isFunction = function (value) { + return (typeof value === 'function'); +}; +var MOBILE = /Mobi/i.test(navigator.userAgent); + +var callbacks = {}; +var status = {}; + +var random = function () { + return parseInt(Math.random() * 10000) + (new Date()).valueOf(); +}; + +// bind 函数polify, 不带new功能的bind + +var bind = function(target,context){ + if(typeof target !== 'function'){ + return; + } + var args = Array.prototype.slice.call(arguments,2); + + if(Function.prototype.bind){ + return target.bind(context, args); + }else { + return function(){ + var _args = Array.prototype.slice.call(arguments); + return target.apply(context,args.concat(_args)); + } + } +} + + + +var toString = Object.prototype.toString; + +var _isFunction = function(obj) { + return typeof(obj) === 'function'; +}; +var _isObject = function(obj) { + return obj === Object(obj); +}; +var _isArray = function(obj) { + return toString.call(obj) == '[object Array]'; +}; +var _isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; +var _isRegExp = function(obj) { + return toString.call(obj) == '[object RegExp]'; +}; +var _isBoolean = function(obj) { + return toString.call(obj) == '[object Boolean]'; +}; + + +function resolveKey(input){ + return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){ + return $1 + $3.toUpperCase() || ""; + }) +} + +function camelizeKeys(input, convert){ + if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){ + return convert ? resolveKey(input) : input; + } + + if(_isArray(input)){ + var temp = []; + for(var i = 0; i < input.length; i++){ + temp.push(camelizeKeys(input[i])); + } + + }else { + var temp = {}; + for(var prop in input){ + if(input.hasOwnProperty(prop)){ + temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]); + } + } + } + return temp; +} + +var loadScript = function (url, cb, timeout) { + var script = document.createElement("script"); + script.charset = "UTF-8"; + script.async = true; + + // 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin + if ( /static\.geetest\.com/g.test(url)) { + script.crossOrigin = "anonymous"; + } + + script.onerror = function () { + cb(true); + // 错误触发了,超时逻辑就不用了 + loaded = true; + }; + var loaded = false; + script.onload = script.onreadystatechange = function () { + if (!loaded && + (!script.readyState || + "loaded" === script.readyState || + "complete" === script.readyState)) { + + loaded = true; + setTimeout(function () { + cb(false); + }, 0); + } + }; + script.src = url; + head.appendChild(script); + + setTimeout(function () { + if (!loaded) { + script.onerror = script.onload = null; + script.remove && script.remove(); + cb(true); + } + }, timeout || TIMEOUT); +}; + +var normalizeDomain = function (domain) { + // special domain: uems.sysu.edu.cn/jwxt/geetest/ + // return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn + return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest +}; +var normalizePath = function (path) { + + path = path && path.replace(/\/+/g, '/'); + if (path.indexOf('/') !== 0) { + path = '/' + path; + } + return path; +}; +var normalizeQuery = function (query) { + if (!query) { + return ''; + } + var q = '?'; + new _Object(query)._each(function (key, value) { + if (isString(value) || isNumber(value) || isBoolean(value)) { + q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&'; + } + }); + if (q === '?') { + q = ''; + } + return q.replace(/&$/, ''); +}; +var makeURL = function (protocol, domain, path, query) { + domain = normalizeDomain(domain); + + var url = normalizePath(path) + normalizeQuery(query); + if (domain) { + url = protocol + domain + url; + } + + return url; +}; + +var load = function (config, protocol, domains, path, query, cb, handleCb) { + var tryRequest = function (at) { + // 处理jsonp回调,这里为了保证每个不同jsonp都有唯一的回调函数 + if(handleCb){ + var cbName = "geetest_" + random(); + // 需要与预先定义好cbnameå‚æ•°ï¼Œåˆ é™¤å¯¹è±¡ + window[cbName] = bind(handleCb, null, cbName); + query.callback = cbName; + } + var url = makeURL(protocol, domains[at], path, query); + loadScript(url, function (err) { + if (err) { + // 超时或者出错的时候 移除回调 + if(cbName){ + try { + window[cbName] = function(){ + window[cbName] = null; + } + } catch (e) {} + } + + if (at >= domains.length - 1) { + cb(true); + // report gettype error + } else { + tryRequest(at + 1); + } + } else { + cb(false); + } + }, config.timeout); + }; + tryRequest(0); +}; + + +var jsonp = function (domains, path, config, callback) { + + var handleCb = function (cbName, data) { + + // 保证只执行一次,全部超时的情况下不会再触发; + + if (data.status == 'success') { + callback(data.data); + } else if (!data.status) { + callback(data); + } else { + //接口有返回,但是返回了错误状态,进入报错逻辑 + callback(data); + } + window[cbName] = undefined; + try { + delete window[cbName]; + } catch (e) { + } + }; + load(config, config.protocol, domains, path, { + callback: '', + captcha_id: config.captchaId, + challenge: config.challenge || uuid(), + client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'), + risk_type: config.riskType, + user_info: config.userInfo, + call_type: config.callType, + lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase() + }, function (err) { + // ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”å›žï¼Œç›´æŽ¥ä½¿ç”¨æœ¬åœ°éªŒè¯ç ï¼Œèµ°å®•æœºæ¨¡å¼ + // è¿™é‡Œå¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘ + if(err && typeof config.offlineCb === 'function'){ + // 执行自己的宕机 + config.offlineCb(); + return; + } + if(err){ + callback(config._get_fallback_config()); + } + }, handleCb); +}; + +var reportError = function (config, url) { + load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', { + time: Date.now().getTime(), + captcha_id: config.gt, + challenge: config.challenge, + exception_url: url, + error_code: config.error_code + }, function (err) {}) +} + +var throwError = function (errorType, config, errObj) { + var errors = { + networkError: '网络错误', + gtTypeError: 'gt字段不是字符串类型' + }; + if (typeof config.onError === 'function') { + config.onError({ + desc: errObj.desc, + msg: errObj.msg, + code: errObj.code + }); + } else { + throw new Error(errors[errorType]); + } +}; + +var detect = function () { + return window.Geetest || document.getElementById("gt_lib"); +}; + +if (detect()) { + status.slide = "loaded"; +} +var GeetestIsLoad = function (fname) { + var GeetestIsLoad = false; + var tags = { js: 'script', css: 'link' }; + var tagname = fname && tags[fname.split('.').pop()]; + if (tagname !== undefined) { + var elts = document.getElementsByTagName(tagname); + for (var i in elts) { + if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0) + || (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) { + GeetestIsLoad = true; + } + } + } + return GeetestIsLoad; +}; +window.initGeetest4 = function (userConfig,callback) { + + var config = new Config(userConfig); + if (userConfig.https) { + config.protocol = 'https://'; + } else if (!userConfig.protocol) { + config.protocol = window.location.protocol + '//'; + } + + + if (isObject(userConfig.getType)) { + config._extend(userConfig.getType); + } + + jsonp(config.apiServers , config.typePath, config, function (newConfig) { + //错误捕获,第一个load请求可能直接报错 + var newConfig = camelizeKeys(newConfig); + + if(newConfig.status === 'error'){ + return throwError('networkError', config, newConfig); + } + + var type = newConfig.type; + if(config.debug){ + new _Object(newConfig)._extend(config.debug) + } + var init = function () { + config._extend(newConfig); + callback(new window.Geetest4(config)); + }; + + callbacks[type] = callbacks[type] || []; + + var s = status[type] || 'init'; + if (s === 'init') { + status[type] = 'loading'; + + callbacks[type].push(init); + + if(newConfig.gctPath){ + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){ + if(err){ + throwError('networkError', config, { + code: '60205', + msg: 'Network failure', + desc: { + detail: 'gct resource load timeout' + } + }); + } + }) + } + + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) { + if (err) { + status[type] = 'fail'; + throwError('networkError', config, { + code: '60204', + msg: 'Network failure', + desc: { + detail: 'js resource load timeout' + } + }); + } else { + + status[type] = 'loaded'; + var cbs = callbacks[type]; + for (var i = 0, len = cbs.length; i < len; i = i + 1) { + var cb = cbs[i]; + if (isFunction(cb)) { + cb(); + } + } + callbacks[type] = []; + status[type] = 'init'; + } + }); + } else if (s === "loaded") { + // 判断gctæ˜¯å¦éœ€è¦é‡æ–°åŠ è½½ + if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){ + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){ + if(err){ + throwError('networkError', config, { + code: '60205', + msg: 'Network failure', + desc: { + detail: 'gct resource load timeout' + } + }); + } + }) + } + return init(); + } else if (s === "fail") { + throwError('networkError', config, { + code: '60204', + msg: 'Network failure', + desc: { + detail: 'js resource load timeout' + } + }); + } else if (s === "loading") { + callbacks[type].push(init); + } + }); + +}; + + +})(window); diff --git a/uniapp/static/js/gt4.js b/uniapp/static/js/gt4.js new file mode 100644 index 0000000..6b2f6ae --- /dev/null +++ b/uniapp/static/js/gt4.js @@ -0,0 +1,487 @@ +"v4.2.0 Geetest Inc."; + +(function (window) { + "use strict"; + if (typeof window === 'undefined') { + throw new Error('Geetest requires browser environment'); + } + +var document = window.document; +var Math = window.Math; +var head = document.getElementsByTagName("head")[0]; +var TIMEOUT = 10000; + +function _Object(obj) { + this._obj = obj; +} + +_Object.prototype = { + _each: function (process) { + var _obj = this._obj; + for (var k in _obj) { + if (_obj.hasOwnProperty(k)) { + process(k, _obj[k]); + } + } + return this; + }, + _extend: function (obj){ + var self = this; + new _Object(obj)._each(function (key, value){ + self._obj[key] = value; + }) + } +}; + +var uuid = function () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0; + var v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }; + +function Config(config) { + var self = this; + new _Object(config)._each(function (key, value) { + self[key] = value; + }); +} + +Config.prototype = { + apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'], + staticServers: ["static.geetest.com",'static.geevisit.com'], + protocol: 'http://', + typePath: '/load', + fallback_config: { + bypass: { + staticServers: ["static.geetest.com",'static.geevisit.com'], + type: 'bypass', + bypass: '/v4/bypass.js' + } + }, + _get_fallback_config: function () { + var self = this; + if (isString(self.type)) { + return self.fallback_config[self.type]; + } else { + return self.fallback_config.bypass; + } + }, + _extend: function (obj) { + var self = this; + new _Object(obj)._each(function (key, value) { + self[key] = value; + }) + } +}; +var isNumber = function (value) { + return (typeof value === 'number'); +}; +var isString = function (value) { + return (typeof value === 'string'); +}; +var isBoolean = function (value) { + return (typeof value === 'boolean'); +}; +var isObject = function (value) { + return (typeof value === 'object' && value !== null); +}; +var isFunction = function (value) { + return (typeof value === 'function'); +}; +var MOBILE = /Mobi/i.test(navigator.userAgent); + +var callbacks = {}; +var status = {}; + +var random = function () { + return parseInt(Math.random() * 10000) + (new Date()).valueOf(); +}; + +// bind 函数polify, 不带new功能的bind + +var bind = function(target,context){ + if(typeof target !== 'function'){ + return; + } + var args = Array.prototype.slice.call(arguments,2); + + if(Function.prototype.bind){ + return target.bind(context, args); + }else { + return function(){ + var _args = Array.prototype.slice.call(arguments); + return target.apply(context,args.concat(_args)); + } + } +} + + + +var toString = Object.prototype.toString; + +var _isFunction = function(obj) { + return typeof(obj) === 'function'; +}; +var _isObject = function(obj) { + return obj === Object(obj); +}; +var _isArray = function(obj) { + return toString.call(obj) == '[object Array]'; +}; +var _isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; +var _isRegExp = function(obj) { + return toString.call(obj) == '[object RegExp]'; +}; +var _isBoolean = function(obj) { + return toString.call(obj) == '[object Boolean]'; +}; + + +function resolveKey(input){ + return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){ + return $1 + $3.toUpperCase() || ""; + }) +} + +function camelizeKeys(input, convert){ + if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){ + return convert ? resolveKey(input) : input; + } + + if(_isArray(input)){ + var temp = []; + for(var i = 0; i < input.length; i++){ + temp.push(camelizeKeys(input[i])); + } + + }else { + var temp = {}; + for(var prop in input){ + if(input.hasOwnProperty(prop)){ + temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]); + } + } + } + return temp; +} + +var loadScript = function (url, cb, timeout) { + var script = document.createElement("script"); + script.charset = "UTF-8"; + script.async = true; + + // 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin + if ( /static\.geetest\.com/g.test(url)) { + script.crossOrigin = "anonymous"; + } + + script.onerror = function () { + cb(true); + // 错误触发了,超时逻辑就不用了 + loaded = true; + }; + var loaded = false; + script.onload = script.onreadystatechange = function () { + if (!loaded && + (!script.readyState || + "loaded" === script.readyState || + "complete" === script.readyState)) { + + loaded = true; + setTimeout(function () { + cb(false); + }, 0); + } + }; + script.src = url; + head.appendChild(script); + + setTimeout(function () { + if (!loaded) { + script.onerror = script.onload = null; + script.remove && script.remove(); + cb(true); + } + }, timeout || TIMEOUT); +}; + +var normalizeDomain = function (domain) { + // special domain: uems.sysu.edu.cn/jwxt/geetest/ + // return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn + return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest +}; +var normalizePath = function (path) { + + path = path && path.replace(/\/+/g, '/'); + if (path.indexOf('/') !== 0) { + path = '/' + path; + } + return path; +}; +var normalizeQuery = function (query) { + if (!query) { + return ''; + } + var q = '?'; + new _Object(query)._each(function (key, value) { + if (isString(value) || isNumber(value) || isBoolean(value)) { + q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&'; + } + }); + if (q === '?') { + q = ''; + } + return q.replace(/&$/, ''); +}; +var makeURL = function (protocol, domain, path, query) { + domain = normalizeDomain(domain); + + var url = normalizePath(path) + normalizeQuery(query); + if (domain) { + url = protocol + domain + url; + } + + return url; +}; + +var load = function (config, protocol, domains, path, query, cb, handleCb) { + var tryRequest = function (at) { + // 处理jsonp回调,这里为了保证每个不同jsonp都有唯一的回调函数 + if(handleCb){ + var cbName = "geetest_" + random(); + // 需要与预先定义好cbnameå‚æ•°ï¼Œåˆ é™¤å¯¹è±¡ + window[cbName] = bind(handleCb, null, cbName); + query.callback = cbName; + } + var url = makeURL(protocol, domains[at], path, query); + loadScript(url, function (err) { + if (err) { + // 超时或者出错的时候 移除回调 + if(cbName){ + try { + window[cbName] = function(){ + window[cbName] = null; + } + } catch (e) {} + } + + if (at >= domains.length - 1) { + cb(true); + // report gettype error + } else { + tryRequest(at + 1); + } + } else { + cb(false); + } + }, config.timeout); + }; + tryRequest(0); +}; + + +var jsonp = function (domains, path, config, callback) { + + var handleCb = function (cbName, data) { + + // 保证只执行一次,全部超时的情况下不会再触发; + + if (data.status == 'success') { + callback(data.data); + } else if (!data.status) { + callback(data); + } else { + //接口有返回,但是返回了错误状态,进入报错逻辑 + callback(data); + } + window[cbName] = undefined; + try { + delete window[cbName]; + } catch (e) { + } + }; + load(config, config.protocol, domains, path, { + callback: '', + captcha_id: config.captchaId, + challenge: config.challenge || uuid(), + client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'), + risk_type: config.riskType, + user_info: config.userInfo, + call_type: config.callType, + lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase() + }, function (err) { + // ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”å›žï¼Œç›´æŽ¥ä½¿ç”¨æœ¬åœ°éªŒè¯ç ï¼Œèµ°å®•æœºæ¨¡å¼ + // è¿™é‡Œå¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘ + if(err && typeof config.offlineCb === 'function'){ + // 执行自己的宕机 + config.offlineCb(); + return; + } + if(err){ + callback(config._get_fallback_config()); + } + }, handleCb); +}; + +var reportError = function (config, url) { + load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', { + time: Date.now().getTime(), + captcha_id: config.gt, + challenge: config.challenge, + exception_url: url, + error_code: config.error_code + }, function (err) {}) +} + +var throwError = function (errorType, config, errObj) { + var errors = { + networkError: '网络错误', + gtTypeError: 'gt字段不是字符串类型' + }; + if (typeof config.onError === 'function') { + config.onError({ + desc: errObj.desc, + msg: errObj.msg, + code: errObj.code + }); + } else { + throw new Error(errors[errorType]); + } +}; + +var detect = function () { + return window.Geetest || document.getElementById("gt_lib"); +}; + +if (detect()) { + status.slide = "loaded"; +} +var GeetestIsLoad = function (fname) { + var GeetestIsLoad = false; + var tags = { js: 'script', css: 'link' }; + var tagname = fname && tags[fname.split('.').pop()]; + if (tagname !== undefined) { + var elts = document.getElementsByTagName(tagname); + for (var i in elts) { + if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0) + || (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) { + GeetestIsLoad = true; + } + } + } + return GeetestIsLoad; +}; +window.initGeetest4 = function (userConfig,callback) { + + var config = new Config(userConfig); + if (userConfig.https) { + config.protocol = 'https://'; + } else if (!userConfig.protocol) { + config.protocol = window.location.protocol + '//'; + } + + + if (isObject(userConfig.getType)) { + config._extend(userConfig.getType); + } + + jsonp(config.apiServers , config.typePath, config, function (newConfig) { + //错误捕获,第一个load请求可能直接报错 + var newConfig = camelizeKeys(newConfig); + + if(newConfig.status === 'error'){ + return throwError('networkError', config, newConfig); + } + + var type = newConfig.type; + if(config.debug){ + new _Object(newConfig)._extend(config.debug) + } + var init = function () { + config._extend(newConfig); + callback(new window.Geetest4(config)); + }; + + callbacks[type] = callbacks[type] || []; + + var s = status[type] || 'init'; + if (s === 'init') { + status[type] = 'loading'; + + callbacks[type].push(init); + + if(newConfig.gctPath){ + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){ + if(err){ + throwError('networkError', config, { + code: '60205', + msg: 'Network failure', + desc: { + detail: 'gct resource load timeout' + } + }); + } + }) + } + + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) { + if (err) { + status[type] = 'fail'; + throwError('networkError', config, { + code: '60204', + msg: 'Network failure', + desc: { + detail: 'js resource load timeout' + } + }); + } else { + + status[type] = 'loaded'; + var cbs = callbacks[type]; + for (var i = 0, len = cbs.length; i < len; i = i + 1) { + var cb = cbs[i]; + if (isFunction(cb)) { + cb(); + } + } + callbacks[type] = []; + status[type] = 'init'; + } + }); + } else if (s === "loaded") { + // 判断gctæ˜¯å¦éœ€è¦é‡æ–°åŠ è½½ + if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){ + load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){ + if(err){ + throwError('networkError', config, { + code: '60205', + msg: 'Network failure', + desc: { + detail: 'gct resource load timeout' + } + }); + } + }) + } + return init(); + } else if (s === "fail") { + throwError('networkError', config, { + code: '60204', + msg: 'Network failure', + desc: { + detail: 'js resource load timeout' + } + }); + } else if (s === "loading") { + callbacks[type].push(init); + } + }); + +}; + + +})(window); diff --git a/uniapp/utils/auth.js b/uniapp/utils/auth.js index 9a4a8fe..a788252 100644 --- a/uniapp/utils/auth.js +++ b/uniapp/utils/auth.js @@ -1,8 +1,13 @@ const TOKEN_KEY = 'app_token' const USER_KEY = 'app_user' +const TENANT_KEY = 'app_tenant_name' +const REMEMBER_KEY = 'app_remember_me' +const REMEMBER_TENANT_KEY = 'app_remember_tenant' +const REMEMBER_ACCOUNT_KEY = 'app_remember_account' +const REMEMBER_PASSWORD_KEY = 'app_remember_password' export function setToken(token) { - uni.setStorageSync(TOKEN_KEY, token) + uni.setStorageSync(TOKEN_KEY, token || '') } export function getToken() { @@ -10,13 +15,21 @@ export function getToken() { } export function setUser(user) { - uni.setStorageSync(USER_KEY, user) + uni.setStorageSync(USER_KEY, user || null) } export function getUser() { return uni.getStorageSync(USER_KEY) || null } +export function setTenantName(name) { + uni.setStorageSync(TENANT_KEY, name || '') +} + +export function getTenantName() { + return uni.getStorageSync(TENANT_KEY) || '' +} + export function isLoggedIn() { return !!getToken() } @@ -26,12 +39,67 @@ export function logout() { uni.removeStorageSync(USER_KEY) } -export function loginSuccess(user = {}) { - setToken('demo_token_' + Date.now()) - setUser({ - nickname: user.nickname || '云泽用户', - avatar: user.avatar || '', - phone: user.phone || '', - ...user - }) +/** 是否开启记住账号登录信息 */ +export function isRememberLogin() { + return uni.getStorageSync(REMEMBER_KEY) === '1' +} + +/** 读取记住的账号登录信息 */ +export function getRememberLogin() { + if (!isRememberLogin()) { + return { rememberMe: false, tenantName: '', account: '', password: '' } + } + return { + rememberMe: true, + tenantName: uni.getStorageSync(REMEMBER_TENANT_KEY) || '', + account: uni.getStorageSync(REMEMBER_ACCOUNT_KEY) || '', + password: uni.getStorageSync(REMEMBER_PASSWORD_KEY) || '' + } +} + +/** 保存账号登录信息(租户、账号、密码) */ +export function saveRememberLogin({ tenantName = '', account = '', password = '' } = {}) { + uni.setStorageSync(REMEMBER_KEY, '1') + uni.setStorageSync(REMEMBER_TENANT_KEY, tenantName) + uni.setStorageSync(REMEMBER_ACCOUNT_KEY, account) + uni.setStorageSync(REMEMBER_PASSWORD_KEY, password) +} + +/** 清除记住的账号登录信息 */ +export function clearRememberLogin() { + uni.removeStorageSync(REMEMBER_KEY) + uni.removeStorageSync(REMEMBER_TENANT_KEY) + uni.removeStorageSync(REMEMBER_ACCOUNT_KEY) + uni.removeStorageSync(REMEMBER_PASSWORD_KEY) +} + +/** + * 登录成功写入 token 与用户信息 + * @param {{ token?: string, user?: object, nickname?: string, phone?: string, account?: string, name?: string, avatar?: string, id?: number|string }} payload + */ +export function loginSuccess(payload = {}) { + const token = payload.token || payload.access_token || '' + if (token) { + setToken(token) + } + + const rawUser = payload.user || payload + const user = { + id: rawUser.id, + account: rawUser.account || '', + name: rawUser.name || '', + nickname: rawUser.nickname || rawUser.name || rawUser.account || '云泽用户', + avatar: rawUser.avatar || '', + phone: rawUser.phone || payload.phone || '', + tid: rawUser.tid, + rid: rawUser.rid, + role_name: rawUser.role_name || '', + ...rawUser + } + // 保证 nickname 可用 + if (!user.nickname) { + user.nickname = user.name || user.account || '云泽用户' + } + setUser(user) + return user } diff --git a/uniapp/utils/geetest.js b/uniapp/utils/geetest.js new file mode 100644 index 0000000..75b2301 --- /dev/null +++ b/uniapp/utils/geetest.js @@ -0,0 +1,107 @@ +/** + * 极验 4.0 — 点击登录后弹出 bind 模式验证码,成功后返回校验参数供登录接口使用。 + */ +import { getGeetest4Infos } from '@/api/auth.js' +import { GEETEST4_CAPTCHA_ID } from '@/api/config.js' + +// #ifdef H5 +import '@/static/js/gt4.js' +// #endif + +function normalizeValidate(result, fallbackCaptchaId) { + return { + captcha_id: result?.captcha_id || fallbackCaptchaId, + lot_number: result?.lot_number || '', + pass_token: result?.pass_token || '', + gen_time: result?.gen_time || '', + captcha_output: result?.captcha_output || '' + } +} + +/** 优先读后端配置,失败时使用本地 captcha_id */ +export async function fetchGeetest4CaptchaId() { + try { + const data = await getGeetest4Infos() + if (data?.captcha_id) return data.captcha_id + } catch { + // 后端未配置或未开启时走本地兜底 + } + return GEETEST4_CAPTCHA_ID +} + +// #ifdef H5 +function showGeetest4H5(captchaId) { + return new Promise((resolve, reject) => { + if (typeof window === 'undefined' || !window.initGeetest4) { + reject(new Error('极验 SDK 未加载')) + return + } + window.initGeetest4( + { + captchaId, + product: 'bind', + language: 'zh-CN' + }, + (instance) => { + instance.onSuccess(() => { + resolve(normalizeValidate(instance.getValidate(), captchaId)) + if (typeof instance.destroy === 'function') { + instance.destroy() + } + }) + instance.onFail(() => { + reject(new Error('人机验证未通过')) + }) + instance.onError(() => { + reject(new Error('人机验证加载失败')) + }) + instance.showCaptcha() + } + ) + }) +} +// #endif + +// #ifdef APP-PLUS +function showGeetest4App(captchaId) { + return new Promise((resolve, reject) => { + uni.navigateTo({ + url: `/pages/login/geetest-webview?captchaId=${encodeURIComponent(captchaId)}`, + events: { + geetestSuccess(data) { + resolve(normalizeValidate(data, captchaId)) + }, + geetestFail(msg) { + reject(new Error(msg || '人机验证未通过')) + } + }, + fail(err) { + reject(new Error(err?.errMsg || '无法打开验证页面')) + } + }) + }) +} +// #endif + +/** + * 弹出极验 4.0 验证 + * @returns {Promise<{ captcha_id, lot_number, pass_token, gen_time, captcha_output }>} + */ +export async function showGeetest4() { + const captchaId = await fetchGeetest4CaptchaId() + if (!captchaId) { + throw new Error('未配置极验 captcha_id') + } + + // #ifdef H5 + return showGeetest4H5(captchaId) + // #endif + + // #ifdef APP-PLUS + return showGeetest4App(captchaId) + // #endif + + // #ifndef H5 || APP-PLUS + throw new Error('当前平台暂不支持极验验证,请使用 H5 或 App') + // #endif +} diff --git a/uniapp/vite.config.js b/uniapp/vite.config.js index ecb6bf2..3c625c8 100644 --- a/uniapp/vite.config.js +++ b/uniapp/vite.config.js @@ -1,16 +1,29 @@ -import { defineConfig } from 'vite' - import uni from '@dcloudio/vite-plugin-uni' +import { defineConfig, loadEnv } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' - export default defineConfig({ - plugins: [ - uni() - ], - css: { - preprocessorOptions: { - scss: { - // 关闭废弃 API 警告 - silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'], - } - } - } - }) +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const apiTarget = (env.VITE_API_BASE_URL || 'http://localhost:9000').replace(/\/$/, '') + + return { + plugins: [uni()], + server: { + proxy: { + // 勿用 /api:会与 uniapp/api/ 源码目录冲突,导致 request.js 等模块 404 + '/proxy-api': { + target: apiTarget, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/proxy-api/, '') + } + } + }, + css: { + preprocessorOptions: { + scss: { + // 关闭废弃 API 警告 + silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'] + } + } + } + } +})