Files
yunzerwebsiteallinone/go/controllers/auth/account.go
T
2026-10-31 01:21:55 +08:00

514 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=<base64>
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=<base64>
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", ""),
})
}