Files
yunzerwebsiteallinone/go/controllers/auth/account.go
T
2026-10-08 15:30:37 +08:00

429 lines
13 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/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=<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": 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=<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", ""),
})
}