package auth import ( "encoding/json" "io" "net/url" "strings" "time" "server/services" "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": err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "data": map[string]interface{}{ "scene": scene, "qr_url": qrURL, "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), }, }) } // RegisterVerify 第一步核验:验证码通过即创建统一身份,返回 register_token // POST /auth/register/verify // // mode=phone { mode, phone, code, password, confirm } // mode=account { mode, account, scene, code, password, confirm } 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 := "" if req.Mode == "account" { // 核销服务号验证码,得到扫码人 openid(注册完成后绑定微信登录): // 有 scene 走二维码会话核销;无 scene(公众号发送「注册」)按验证码认领 var err error if strings.TrimSpace(req.Scene) != "" { openID, err = wechatmp.ConfirmRegisterVerify(req.Scene, req.Code) } else { openID, err = wechatmp.ConfirmRegisterVerifyByCode(req.Code) } if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } } identity, token, err := services.VerifyAndCreateIdentity(services.RegisterVerifyInput{ Mode: req.Mode, Phone: req.Phone, Account: req.Account, Code: req.Code, Password: req.Password, Confirm: req.Confirm, OpenID: openID, }) 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, "identity_id": identity.ID}, }) } // ==================== 第二步:企业归属 ==================== // 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 } identityID, openID, err := services.PeekRegisterToken(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) services.BindRegisterWechat(identityID, openID) 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 } identityID, openID, err := services.PeekRegisterToken(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) services.BindRegisterWechat(identityID, openID) 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 } identityID, openID, err := services.PeekRegisterToken(req.Token) if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()}) return } services.ConsumeRegisterToken(req.Token) services.BindRegisterWechat(identityID, openID) 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", ""), }) }