package auth import ( "encoding/json" "io" "net/url" "strings" "time" "server/models" "server/services" "server/services/wechatmini" "server/services/wechatmp" beego "github.com/beego/beego/v2/server/web" ) // AuthAccountController 统一认证中心自助账户:两步式注册 / 找回密码 // // 注册两步: // 1. 核验身份:手机号(短信验证码)或 账号(服务号验证码),通过即创建身份并签发 register_token; // 2. 企业归属:绑定企业(join 申请) / 创建企业(资料+营业执照,平台审核) / 暂不加入。 type AuthAccountController struct { beego.Controller } func (c *AuthAccountController) serveJSON(data map[string]interface{}) { c.Data["json"] = data _ = c.ServeJSON() } func (c *AuthAccountController) readJSON(v interface{}) bool { body := c.Ctx.Input.RequestBody if len(body) == 0 { var err error body, err = io.ReadAll(c.Ctx.Request.Body) if err != nil || len(body) == 0 { c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) return false } } if err := json.Unmarshal(body, v); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) return false } return true } // loginBackURL 组装回登录页地址,透传 client_id/redirect 与提示参数 func loginBackURL(clientID, redirect, notice, warn string) string { params := url.Values{} if clientID != "" { params.Set("client_id", clientID) } if redirect != "" { params.Set("redirect", redirect) } if notice != "" { params.Set("notice", notice) } if warn != "" { params.Set("warn", warn) } qs := params.Encode() target := "/auth/login" if qs != "" { target += "?" + qs } return target } // RegisterPage 注册页 // GET /auth/register?client_id=xxx&redirect= func (c *AuthAccountController) RegisterPage() { c.Data["ClientID"] = strings.TrimSpace(c.GetString("client_id")) c.Data["Redirect"] = c.GetString("redirect") c.Data["Error"] = c.GetString("error") c.TplName = "auth/register.tpl" } // ==================== 第一步:核验身份 ==================== // SendRegisterCode 发送手机号注册短信验证码 // POST /auth/register/send-code { phone } func (c *AuthAccountController) SendRegisterCode() { var req struct { Phone string `json:"phone"` } if !c.readJSON(&req) { return } if err := services.SendIdentityCode("register", req.Phone); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } c.serveJSON(map[string]interface{}{"code": 200, "msg": "验证码已发送,请查收短信"}) } // WechatStart 账号注册:获取服务号带参二维码(扫码关注后服务号回复验证码)。 // 未认证公众号无法生成带参二维码时回退:qr_url 为空,notice 引导用户 // 在公众号内发送「注册」获取验证码。 // GET /auth/register/wechat/start func (c *AuthAccountController) WechatStart() { scene, qrURL, notice, expireAt, err := wechatmp.StartRegisterVerify(wechatmp.DefaultVerifyTTL) if err != nil { // 公众号未配置 / 未启用:仍回传「关注二维码」,用户可扫码进公众号发送「注册」取码 c.serveJSON(map[string]interface{}{ "code": 400, "msg": "微信扫码核验暂不可用,请改用手机号验证码注册", "data": map[string]interface{}{"follow_qrcode": wechatmp.FollowQrcodeURL()}, }) return } c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{ "scene": scene, "qr_url": qrURL, // 带参二维码生成失败(IP 白名单 / 未认证 48001 等)时前端改展示关注二维码, // 用户扫码关注后在公众号内发送「注册」照样能拿到验证码 "follow_qrcode": wechatmp.FollowQrcodeURL(), "notice": notice, "expire_at": expireAt.Format(time.RFC3339), }, }) } // WechatStatus 账号注册:轮询扫码状态(展示「已扫码」) // GET /auth/register/wechat/status?scene=xxx func (c *AuthAccountController) WechatStatus() { scene := strings.TrimSpace(c.GetString("scene")) scanned, expireAt, err := wechatmp.GetRegisterVerifyStatus(scene) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{ "scanned": scanned, "expire_at": expireAt.Format(time.RFC3339), }, }) } // WechatMiniStart 账号注册:获取小程序码(扫码后在小程序内确认注册)。 // 小程序未配置 / 小程序码生成失败时返回 400 + reason=mini_unavailable, // 前端据此回退公众号验证码或短信通道(详细错误只落服务端日志)。 // GET /auth/register/mini/start func (c *AuthAccountController) WechatMiniStart() { scene, qrURL, expireAt, err := wechatmini.StartRegisterVerify(wechatmini.DefaultRegisterTTL) if err != nil { c.serveJSON(map[string]interface{}{ "code": 400, "msg": "微信扫码核验暂不可用,请改用手机号验证码注册", "reason": "mini_unavailable", }) return } c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{ "scene": scene, "qr_url": qrURL, "expire_at": expireAt.Format(time.RFC3339), }, }) } // WechatMiniStatus 账号注册:轮询小程序端确认状态(展示「已确认」) // GET /auth/register/mini/status?scene=xxx func (c *AuthAccountController) WechatMiniStatus() { scene := strings.TrimSpace(c.GetString("scene")) confirmed, expireAt, err := wechatmini.GetRegisterStatus(scene) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{ "confirmed": confirmed, "expire_at": expireAt.Format(time.RFC3339), }, }) } // RegisterVerify 第一步核验:验证码通过即创建统一身份,返回 register_token // POST /auth/register/verify // // mode=phone { mode, phone, code, password, confirm } // mode=account { mode, account, scene, code, password, confirm } // mode=mini { mode, account, scene, password, confirm } // scene 为小程序码会话,已在小程序内确认 func (c *AuthAccountController) RegisterVerify() { var req struct { Mode string `json:"mode"` Phone string `json:"phone"` Account string `json:"account"` Scene string `json:"scene"` Code string `json:"code"` Password string `json:"password"` Confirm string `json:"confirm"` } if !c.readJSON(&req) { return } openID := "" unionID := "" provider := "" switch req.Mode { case "account": // 核销服务号验证码,得到扫码人 openid(注册完成后绑定微信登录): // 先按 scene 核销二维码会话,未扫码(用户改走公众号内发送「注册」)时按验证码认领。 // 账号注册不依赖手机号,服务号无法按手机号关联,故不以「是否已扫码」为前置条件。 var err error openID, err = wechatmp.ConfirmAccountRegisterVerify(req.Scene, req.Code) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } provider = models.IdPWechat case "mini": // 核销小程序注册会话:扫码人已在小程序内用 wx.login 确认,取 openid/unionid oid, uid, _, cerr := wechatmini.ConsumeRegister(req.Scene) if cerr != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": cerr.Error()}) return } openID = oid unionID = uid provider = models.IdPWechatMini } // 身份推迟到第二步归属操作时创建(services.CompleteRegister), // 第一步只签发待注册令牌,避免超时未完成留下占用账号的半成品身份 token, err := services.VerifyAndIssueRegisterToken(services.RegisterVerifyInput{ Mode: req.Mode, Phone: req.Phone, Account: req.Account, Code: req.Code, Password: req.Password, Confirm: req.Confirm, OpenID: openID, UnionID: unionID, Provider: provider, }) 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{}{"register_token": token}, }) } // ==================== 第二步:企业归属 ==================== // TenantSearch 搜索企业(绑定企业卡片) // GET /auth/register/tenant/search?keyword=xxx func (c *AuthAccountController) TenantSearch() { keyword := strings.TrimSpace(c.GetString("keyword")) if keyword == "" { c.serveJSON(map[string]interface{}{"code": 400, "msg": "请输入企业名称或编码"}) return } list, err := services.SearchTenantsForRegister(keyword) if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "搜索失败,请稍后重试"}) return } c.serveJSON(map[string]interface{}{"code": 200, "data": map[string]interface{}{"list": list}}) } // ApplyJoin 绑定企业:提交加入申请,等待企业管理员审核 // POST /auth/register/apply-join { token, tid, client_id, redirect } func (c *AuthAccountController) ApplyJoin() { var req struct { Token string `json:"token"` Tid uint64 `json:"tid"` ClientID string `json:"client_id"` Redirect string `json:"redirect"` } if !c.readJSON(&req) { return } // 第二步归属操作开始:此刻才真正创建身份(核验通过 ≠ 已建号) if strings.TrimSpace(req.Token) == "" { c.serveJSON(map[string]interface{}{ "code": 400, "msg": "注册会话已失效:未携带注册令牌,请返回第一步重新完成身份核验", }) return } identityID, err := services.CompleteRegister(req.Token) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } apply, err := services.CreateJoinApply(identityID, req.Tid) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } services.ConsumeRegisterToken(req.Token) c.serveJSON(map[string]interface{}{ "code": 200, "msg": "申请已提交", "redirect": loginBackURL(req.ClientID, req.Redirect, "registered_warn", "注册成功!加入「"+apply.TenantName+"」的申请已提交,请等待企业管理员审核通过后登录"), }) } // ApplyCreate 创建企业:提交企业资料(营业执照),等待平台审核 // POST /auth/register/apply-create { token, tenant_name, credit_code, legal_person, license_file, contact_person, contact_phone, contact_email, address, client_id, redirect } func (c *AuthAccountController) ApplyCreate() { var req struct { Token string `json:"token"` TenantName string `json:"tenant_name"` CreditCode string `json:"credit_code"` LegalPerson string `json:"legal_person"` LicenseFile string `json:"license_file"` ContactPerson string `json:"contact_person"` ContactPhone string `json:"contact_phone"` ContactEmail string `json:"contact_email"` Address string `json:"address"` ClientID string `json:"client_id"` Redirect string `json:"redirect"` } if !c.readJSON(&req) { return } // 第二步归属操作开始:此刻才真正创建身份(核验通过 ≠ 已建号) if strings.TrimSpace(req.Token) == "" { c.serveJSON(map[string]interface{}{ "code": 400, "msg": "注册会话已失效:未携带注册令牌,请返回第一步重新完成身份核验", }) return } identityID, err := services.CompleteRegister(req.Token) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } if _, err := services.CreateTenantApply(identityID, services.TenantCreateApplyInput{ TenantName: req.TenantName, CreditCode: req.CreditCode, LegalPerson: req.LegalPerson, LicenseFile: req.LicenseFile, ContactPerson: req.ContactPerson, ContactPhone: req.ContactPhone, ContactEmail: req.ContactEmail, Address: req.Address, }); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } services.ConsumeRegisterToken(req.Token) c.serveJSON(map[string]interface{}{ "code": 200, "msg": "申请已提交", "redirect": loginBackURL(req.ClientID, req.Redirect, "registered_warn", "注册成功!创建企业的申请已提交,请等待平台审核,审核结果将以短信通知"), }) } // FinishSkip 暂不加入企业,直接完成注册 // POST /auth/register/finish { token, client_id, redirect } func (c *AuthAccountController) FinishSkip() { var req struct { Token string `json:"token"` ClientID string `json:"client_id"` Redirect string `json:"redirect"` } if !c.readJSON(&req) { return } // 第二步归属操作开始:此刻才真正创建身份(核验通过 ≠ 已建号) if strings.TrimSpace(req.Token) == "" { c.serveJSON(map[string]interface{}{ "code": 400, "msg": "注册会话已失效:未携带注册令牌,请返回第一步重新完成身份核验", }) return } if _, err := services.CompleteRegister(req.Token); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } services.ConsumeRegisterToken(req.Token) c.serveJSON(map[string]interface{}{ "code": 200, "msg": "注册成功", "redirect": loginBackURL(req.ClientID, req.Redirect, "registered", "注册成功!你当前未加入任何企业,可稍后在登录后申请加入"), }) } // UploadLicense 上传营业执照(注册页未登录,专用无鉴权端点) // POST /auth/register/upload-license multipart 字段 file;仅 pdf/jpg/png,≤5MB func (c *AuthAccountController) UploadLicense() { file, header, err := c.GetFile("file") if err != nil || file == nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "请选择要上传的文件"}) return } defer file.Close() // 类型与大小限制 name := strings.ToLower(header.Filename) ok := strings.HasSuffix(name, ".pdf") || strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg") || strings.HasSuffix(name, ".png") if !ok { c.serveJSON(map[string]interface{}{"code": 400, "msg": "仅支持 PDF、JPG、PNG 格式"}) return } if header.Size > 5*1024*1024 { c.serveJSON(map[string]interface{}{"code": 400, "msg": "文件大小不能超过 5MB"}) return } storage, err := services.GetStorageService() if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "存储服务不可用,请稍后重试"}) return } staged, err := storage.Stage(file, header) if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "文件读取失败,请重试"}) return } result, err := storage.Commit(staged, services.UploadContext{Source: services.SourcePlatform, Tid: 0, Tuid: 0}) if err != nil { _ = storage.Discard(staged) c.serveJSON(map[string]interface{}{"code": 500, "msg": "上传失败,请重试"}) return } // 文件登记(平台文件管理可见) _, _ = services.CreateFileRecord(services.FileUploadParams{ Source: services.SourcePlatform, Scope: services.ScopeTenant, Tid: 0, Uid: 0, Name: header.Filename, Ext: staged.Ext, Cate: 0, Size: uint64(staged.Size), Src: result.URL, ObjectKey: result.Key, Storage: storage.Type(), MD5: staged.MD5, }) c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{"url": result.URL, "name": header.Filename}, }) } // ==================== 找回密码 ==================== // ForgotPage 找回密码页 // GET /auth/forgot?client_id=xxx&redirect= func (c *AuthAccountController) ForgotPage() { c.Data["ClientID"] = strings.TrimSpace(c.GetString("client_id")) c.Data["Redirect"] = c.GetString("redirect") c.Data["Error"] = c.GetString("error") c.TplName = "auth/forgot.tpl" } // SendResetCode 发送找回密码验证码 // POST /auth/forgot/send-code { phone } func (c *AuthAccountController) SendResetCode() { var req struct { Phone string `json:"phone"` } if !c.readJSON(&req) { return } if err := services.SendIdentityCode("reset", req.Phone); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } c.serveJSON(map[string]interface{}{"code": 200, "msg": "验证码已发送,请查收短信"}) } // ResetSubmit 提交重置密码 // POST /auth/forgot/reset { phone, code, password, confirm, client_id, redirect } func (c *AuthAccountController) ResetSubmit() { var req struct { Phone string `json:"phone"` Code string `json:"code"` Password string `json:"password"` Confirm string `json:"confirm"` ClientID string `json:"client_id"` Redirect string `json:"redirect"` } if !c.readJSON(&req) { return } if err := services.ResetIdentityPasswordByMobile(req.Phone, req.Code, req.Password, req.Confirm); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "msg": "密码已重置", "redirect": loginBackURL(req.ClientID, req.Redirect, "reset", ""), }) }