批量优化

This commit is contained in:
hero920103 committed 2026-10-03 23:46:55 +08:00
1 parent e3db229f41
commit aa92ca40a2
79 files changed
+7495 -1104

No files matched your search

+309
View File
@@ -0,0 +1,309 @@
package auth
import (
"encoding/json"
"io"
"strings"
"server/models"
authsvc "server/services/auth"
"server/services/wechatmini"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/core/logs"
)
// 小程序登录失败原因(前端按此分支给出不同引导)
const (
MiniReasonNotConfigured = "not_configured" // 服务端未配置小程序
MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到云泽账号
MiniReasonNoTenant = "no_tenant" // 账号未加入任何企业
MiniReasonWechatAPI = "wechat_api_error" // 微信接口报错
)
// idPWechatMini 第三方身份源标识(写入 yz_auth_identity_third.provider)
const idPWechatMini = "wechat_mini"
// AuthWechatMiniController 微信小程序一键登录(统一认证中心)
//
// 与公众号确认链接登录(/api/wechat/mp/login)不同,小程序端由前端直接
// 携带 wx.login 的 code 与 getPhoneNumber 的动态 code 兑换登录态:
//
// POST /auth/wechat/miniapp/login
// { "code": "wx.login的code", "phone_code": "getPhoneNumber的动态code", "client_id": "yz-miniapp" }
//
// 返回结构与 POST /auth/login 完全一致(tokens / need_choose_tenant / tenants 均在顶层),
// 前端可复用同一套「写入令牌 → 选企业 → switch-tenant」流程。
type AuthWechatMiniController struct {
beego.Controller
}
func (c *AuthWechatMiniController) serveJSON(data map[string]interface{}) {
c.Data["json"] = data
_ = c.ServeJSON()
}
func (c *AuthWechatMiniController) fail(reason string, bizCode int, msg string) {
c.Ctx.Output.SetStatus(400)
c.serveJSON(map[string]interface{}{"code": bizCode, "msg": msg, "reason": reason})
}
// Config 小程序登录是否可用
// GET /auth/wechat/miniapp/config
func (c *AuthWechatMiniController) Config() {
cfg, err := wechatmini.LoadEnabledConfig()
if err != nil {
c.serveJSON(map[string]interface{}{
"code": 200,
"data": map[string]interface{}{"enabled": false, "auto_bind": false},
})
return
}
c.serveJSON(map[string]interface{}{
"code": 200,
"data": map[string]interface{}{"enabled": true, "auto_bind": cfg.AutoBind},
})
}
// Login 小程序一键登录
func (c *AuthWechatMiniController) Login() {
var req struct {
Code string `json:"code"`
PhoneCode string `json:"phone_code"`
ClientID string `json:"client_id"`
}
body := c.Ctx.Input.RequestBody
if len(body) == 0 {
body, _ = io.ReadAll(c.Ctx.Request.Body)
}
if err := json.Unmarshal(body, &req); err != nil {
c.fail("", 400, "参数错误")
return
}
req.Code = strings.TrimSpace(req.Code)
req.PhoneCode = strings.TrimSpace(req.PhoneCode)
req.ClientID = strings.TrimSpace(req.ClientID)
if req.ClientID == "" {
req.ClientID = "yz-miniapp"
}
if req.Code == "" {
c.fail("", 400, "缺少登录凭证,请重新进入小程序")
return
}
cfg, err := wechatmini.LoadEnabledConfig()
if err != nil {
c.fail(MiniReasonNotConfigured, 400, "小程序登录未开通,请使用账号密码登录")
return
}
// code → openid / unionid
session, err := wechatmini.Code2Session(cfg, req.Code)
if err != nil {
msg := err.Error()
if hint, ok := wechatmini.ExplainError(err); ok {
msg = hint
}
logs.Warn("[wechat-mini] code 换取 openid 失败: %v", err)
c.fail(MiniReasonWechatAPI, 400, msg)
return
}
// 定位云泽身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid
var mobile string
if req.PhoneCode != "" {
phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode)
if perr != nil {
// 手机号能力未开通 / code 失效时不阻断:仍尝试按已绑定 openid 登录
logs.Warn("[wechat-mini] 获取手机号失败: %v", perr)
} else {
mobile = strings.TrimSpace(phone.PurePhoneNumber)
if mobile == "" {
mobile = strings.TrimSpace(phone.PhoneNumber)
}
}
}
identity, err := c.resolveIdentity(cfg, mobile, session)
if err != nil {
c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定云泽账号,请使用手机号一键登录或先注册")
return
}
if identity.Status != models.AuthIdentityStatusEnabled {
c.fail("", 403, "账号已被禁用或锁定,无法登录")
return
}
// 企业选择:单企业直接进入,多企业签发待选令牌由前端展示选择界面
tenants, lerr := authsvc.ListTenantOptions(identity.ID)
if lerr != nil || len(tenants) == 0 {
c.fail(MiniReasonNoTenant, 403, "该账号未加入任何企业,请联系管理员")
return
}
tid := uint64(0)
needChoose := false
if len(tenants) == 1 {
tid = tenants[0].Tid
} else {
tid = authsvc.PendingTenantID
needChoose = true
}
username := "微信用户"
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
username = strings.TrimSpace(*identity.Nickname)
} else if identity.Mobile != nil && *identity.Mobile != "" {
username = *identity.Mobile
}
tokens, terr := c.issueTokens(identity.ID, tid, req.ClientID, username, needChoose)
if terr != nil {
c.fail("", 400, terr.Error())
return
}
resp := map[string]interface{}{
"code": 200,
"msg": "登录成功",
"tokens": tokens,
"tenants": tenants,
"identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)},
}
if needChoose {
resp["need_choose_tenant"] = true
resp["sid"] = tokens.Sid
}
c.serveJSON(resp)
}
// resolveIdentity 定位身份并维护小程序绑定关系。
// mobile 非空表示本次已通过微信手机号验证,是可信的强证据,优先采用。
func (c *AuthWechatMiniController) resolveIdentity(cfg *wechatmini.Config, mobile string, session *wechatmini.Session) (*models.AuthIdentity, error) {
// 1) 手机号命中身份(手机号已由微信验证,属于当前用户的强凭证)
if mobile != "" {
if identity, err := authsvc.FindIdentityByAccount(mobile); err == nil {
c.syncBinding(cfg, identity.ID, session, true)
return identity, nil
}
}
// 2) 已绑定过该小程序 openid / unionid
bind := findMiniBinding(session.OpenID, session.UnionID)
if bind == nil {
return nil, orm.ErrNoRows
}
var identity models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", bind.IdentityID).
Filter("delete_time__isnull", true).
One(&identity); err != nil {
return nil, err
}
return &identity, nil
}
// syncBinding 把当前小程序 openid 绑定到该身份。
// verified 表示本次带通过了微信手机号验证:此时手机号即身份凭证,
// 允许覆盖历史绑定(用户换绑手机号 / 换微信号场景);
// 未验证时只允许「新增绑定」,不抢占已绑定到其他身份的 openid。
func (c *AuthWechatMiniController) syncBinding(cfg *wechatmini.Config, identityID uint64, session *wechatmini.Session, verified bool) {
bind := findMiniBinding(session.OpenID, session.UnionID)
if bind != nil && bind.IdentityID == identityID {
return
}
if bind != nil {
if !verified {
return
}
_, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("id", bind.ID).Delete()
}
if !verified && !cfg.AutoBind {
return
}
item := &models.AuthIdentityThird{
IdentityID: identityID,
Provider: idPWechatMini,
OpenID: session.OpenID,
}
if session.UnionID != "" {
uid := session.UnionID
item.UnionID = &uid
}
_, _ = models.Orm.Insert(item)
}
// findMiniBinding 按 openid 查绑定,其次按 unionid 查
func findMiniBinding(openID, unionID string) *models.AuthIdentityThird {
qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", idPWechatMini)
if openID != "" {
var bind models.AuthIdentityThird
if err := qs.Filter("open_id", openID).One(&bind); err == nil {
return &bind
}
}
if unionID != "" {
var bind models.AuthIdentityThird
if err := qs.Filter("union_id", unionID).One(&bind); err == nil {
return &bind
}
}
return nil
}
// issueTokens 建会话并签发 UAC 令牌(含登录日志,失败不影响主流程)
func (c *AuthWechatMiniController) issueTokens(identityID, tid uint64, clientID, username string, needChoose bool) (*authsvc.TokenPair, error) {
accessTTL := 0
if needChoose {
accessTTL = 600 // 待选状态令牌只够用户完成企业选择
}
sess, serr := authsvc.CreateSession(authsvc.SessionInfo{
IdentityID: identityID,
Tid: tid,
ClientID: clientID,
IP: c.Ctx.Input.IP(),
UserAgent: c.Ctx.Request.UserAgent(),
LoginType: "wechat_mini",
Amr: "wechat_mini",
})
if serr != nil {
return nil, serr
}
tokens, terr := authsvc.IssueTokens(authsvc.TokenIssue{
IdentityID: identityID,
Tid: tid,
ClientID: clientID,
Sid: sess.Sid,
Username: username,
UserType: "app",
Amr: "wechat_mini",
AccessTTL: accessTTL,
})
if terr != nil {
return nil, terr
}
identityIDCopy := identityID
tidCopy := tid
amr := "wechat_mini"
_, _ = models.Orm.Insert(&models.AuthLoginLog{
Tid: &tidCopy,
IdentityID: &identityIDCopy,
UserName: username,
ClientID: clientID,
LoginType: "wechat_mini",
Amr: &amr,
Status: 1,
Message: "微信小程序一键登录成功",
IP: c.Ctx.Input.IP(),
UserAgent: c.Ctx.Request.UserAgent(),
})
return tokens, nil
}
func mobileOr(identity *models.AuthIdentity) string {
if identity != nil && identity.Mobile != nil {
return *identity.Mobile
}
return ""
}