514 lines
17 KiB
Go
514 lines
17 KiB
Go
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", ""),
|
||
})
|
||
}
|