Files
yunzerwebsiteallinone/go/services/identity_register.go
T

701 lines
23 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 services
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"server/models"
"server/pkg/passwordutil"
authsvc "server/services/auth"
"github.com/beego/beego/v2/client/orm"
"github.com/google/uuid"
)
// =============================================================
// 两步式自助注册(统一认证中心):
// 第一步 核验身份:手机号(短信验证码)或 账号(服务号验证码),
// 核验通过即创建统一身份 yz_auth_identity,并签发一次性
// register_token(10 分钟有效),token 携带身份与微信 openid。
// 第二步 企业归属(三选一,可跳过):
// - 绑定企业:创建 join 申请 → 目标企业管理员在 backend 审批;
// - 创建企业:提交企业资料(营业执照)→ 平台在 platform 审批,
// 通过后自动建租户并把申请人绑定为租户管理员;
// - 暂不加入:只完成身份注册。
// =============================================================
// registerTokenTTL register_token 有效期
const registerTokenTTL = 10 * time.Minute
// registerTokenItem 一次性注册令牌:身份已创建,等待第二步归属操作
type registerTokenItem struct {
IdentityID uint64
OpenID string // 账号注册模式扫码人的微信 openid(用于注册后绑定微信登录)
ExpiredAt time.Time
}
var registerTokenStore sync.Map // key: token
// IssueRegisterToken 身份创建成功后签发注册令牌
func IssueRegisterToken(identityID uint64, openid string) string {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("rt%d%d", identityID, time.Now().UnixNano())
}
token := hex.EncodeToString(b)
registerTokenStore.Store(token, registerTokenItem{
IdentityID: identityID,
OpenID: openid,
ExpiredAt: time.Now().Add(registerTokenTTL),
})
return token
}
// ConsumeRegisterToken 领取注册令牌(一次性,过期作废)
func ConsumeRegisterToken(token string) (identityID uint64, openid string, err error) {
identityID, openid, err = PeekRegisterToken(token)
if err != nil {
return 0, "", err
}
registerTokenStore.Delete(strings.TrimSpace(token))
return identityID, openid, nil
}
// PeekRegisterToken 查看注册令牌(不消费);第二步先 Peek 校验,
// 业务成功后再 Consume,避免申请失败导致令牌作废无法重试。
func PeekRegisterToken(token string) (identityID uint64, openid string, err error) {
token = strings.TrimSpace(token)
if token == "" {
return 0, "", errors.New("注册会话已失效,请重新注册")
}
v, ok := registerTokenStore.Load(token)
if !ok {
return 0, "", errors.New("注册会话已失效,请重新注册")
}
item, ok := v.(registerTokenItem)
if !ok {
return 0, "", errors.New("注册会话已失效,请重新注册")
}
if time.Now().After(item.ExpiredAt) {
registerTokenStore.Delete(token)
return 0, "", errors.New("注册会话已过期,请重新注册")
}
return item.IdentityID, item.OpenID, nil
}
// ---------------- 第一步:核验并创建身份 ----------------
// RegisterVerifyInput 第一步核验入参
type RegisterVerifyInput struct {
Mode string // phone / account
Phone string // mode=phone:手机号
Account string // mode=account:自定义账号
Code string // 验证码(短信 或 服务号下发)
Password string
Confirm string
OpenID string // mode=account:核销服务号验证码得到的 openid
}
// validateAccountName 账号名规则:4-32 位,字母开头,字母/数字/下划线/中划线
func validateAccountName(account string) error {
if len(account) < 4 || len(account) > 32 {
return errors.New("账号长度需为 4-32 个字符")
}
c := account[0]
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
return errors.New("账号需以字母开头")
}
for i := 0; i < len(account); i++ {
c := account[i]
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') {
return errors.New("账号仅支持字母、数字、下划线和中划线")
}
}
return nil
}
// findIdentityByAccountName 按全局账号列查身份
func findIdentityByAccountName(account string) *models.AuthIdentity {
var m models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("account", account).
Filter("delete_time__isnull", true).
One(&m); err != nil {
return nil
}
return &m
}
// VerifyAndCreateIdentity 第一步:校验验证码与密码,创建统一身份,返回身份与新令牌。
// 调用方保证:mode=phone 已发短信验证码;mode=account 已核销服务号验证码并传入 openid。
func VerifyAndCreateIdentity(in RegisterVerifyInput) (*models.AuthIdentity, string, error) {
in.Mode = strings.TrimSpace(in.Mode)
if in.Mode != "phone" && in.Mode != "account" {
return nil, "", errors.New("请选择注册方式")
}
if len(strings.TrimSpace(in.Password)) < 6 {
return nil, "", errors.New("密码长度不能少于6个字符")
}
if in.Password != in.Confirm {
return nil, "", errors.New("两次输入的密码不一致")
}
identity := &models.AuthIdentity{
UnionID: strings.ReplaceAll(uuid.NewString(), "-", ""),
}
switch in.Mode {
case "phone":
phone := strings.TrimSpace(in.Phone)
if !IsMobile(phone) {
return nil, "", errors.New("请输入正确的手机号")
}
if err := VerifyIdentityCode("register", phone, in.Code); err != nil {
return nil, "", err
}
if findIdentityByMobile(phone) != nil {
return nil, "", errors.New("该手机号已注册,请直接登录")
}
identity.Mobile = &phone
nick := "用户" + phone[len(phone)-4:]
identity.Nickname = &nick
case "account":
account := strings.TrimSpace(in.Account)
if err := validateAccountName(account); err != nil {
return nil, "", err
}
if findIdentityByAccountName(account) != nil {
return nil, "", errors.New("该账号已被占用,请换一个")
}
identity.Account = &account
nick := account
identity.Nickname = &nick
}
hashed, err := passwordutil.Hash(strings.TrimSpace(in.Password))
if err != nil {
return nil, "", errors.New("密码处理失败")
}
identity.PasswordHash = &hashed
identity.PasswordAlgo = passwordutil.AlgoOf(hashed)
identity.Status = models.AuthIdentityStatusEnabled
id, err := models.Orm.Insert(identity)
if err != nil {
return nil, "", errors.New("注册失败,请稍后重试")
}
identity.ID = uint64(id)
token := IssueRegisterToken(identity.ID, in.OpenID)
return identity, token, nil
}
// ---------------- 第二步:企业归属 ----------------
// TenantSearchItem 企业搜索结果
type TenantSearchItem struct {
Tid uint64 `json:"tid"`
TenantName string `json:"tenant_name"`
TenantCode string `json:"tenant_code"`
}
// SearchTenantsForRegister 注册页搜索启用中的企业(按名称/编码模糊,最多 10 条)
func SearchTenantsForRegister(keyword string) ([]TenantSearchItem, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return []TenantSearchItem{}, nil
}
var rows []models.SystemTenant
_, err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("status", 1).
Filter("delete_time__isnull", true).
Filter("tenant_name__icontains", keyword).
Limit(10).
All(&rows, "ID", "TenantName", "TenantCode")
if err != nil {
return nil, err
}
if len(rows) < 10 {
var byCode []models.SystemTenant
_, _ = models.Orm.QueryTable(new(models.SystemTenant)).
Filter("status", 1).
Filter("delete_time__isnull", true).
Filter("tenant_code__icontains", keyword).
Limit(10).
All(&byCode, "ID", "TenantName", "TenantCode")
seen := map[uint64]bool{}
for _, r := range rows {
seen[r.ID] = true
}
for _, r := range byCode {
if !seen[r.ID] {
rows = append(rows, r)
seen[r.ID] = true
if len(rows) >= 10 {
break
}
}
}
}
out := make([]TenantSearchItem, 0, len(rows))
for _, r := range rows {
out = append(out, TenantSearchItem{Tid: r.ID, TenantName: r.TenantName, TenantCode: r.TenantCode})
}
return out, nil
}
// CreateJoinApply 「绑定企业」申请:等待目标企业管理员审批
func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) {
if identityID == 0 || tid == 0 {
return nil, errors.New("参数错误")
}
tenant := new(models.SystemTenant)
if err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("id", tid).
Filter("status", 1).
Filter("delete_time__isnull", true).
One(tenant); err != nil {
return nil, errors.New("企业不存在或已停用")
}
identity, err := authsvc.GetIdentityByID(identityID)
if err != nil || identity == nil {
return nil, errors.New("注册身份异常,请重新注册")
}
// 已绑定该企业则无需申请
if _, berr := authsvc.GetTenantUser(identityID, tid); berr == nil {
return nil, errors.New("你已是该企业的成员,无需重复申请")
}
// 同企业待审申请去重
cnt, _ := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("identity_id", identityID).
Filter("tid", tid).
Filter("type", models.TenantApplyTypeJoin).
Filter("status", models.TenantApplyStatusPending).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
return nil, errors.New("已提交过该企业的加入申请,请等待管理员审核")
}
apply := &models.AuthTenantApply{
IdentityID: identityID,
Type: models.TenantApplyTypeJoin,
Tid: tid,
TenantName: tenant.TenantName,
Status: models.TenantApplyStatusPending,
}
apply.ContactPerson = identityNickname(identity)
if identity.Mobile != nil {
apply.ContactPhone = *identity.Mobile
}
if _, err := models.Orm.Insert(apply); err != nil {
return nil, errors.New("提交申请失败,请稍后重试")
}
return apply, nil
}
// TenantCreateApplyInput 「创建企业」申请入参
type TenantCreateApplyInput struct {
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"`
}
var creditCodeRegexp = regexp.MustCompile(`^[0-9A-Z]{18}$`)
// CreateTenantApply 「创建企业」申请:等待平台审批
func CreateTenantApply(identityID uint64, in TenantCreateApplyInput) (*models.AuthTenantApply, error) {
in.TenantName = strings.TrimSpace(in.TenantName)
in.CreditCode = strings.ToUpper(strings.TrimSpace(in.CreditCode))
in.LegalPerson = strings.TrimSpace(in.LegalPerson)
in.LicenseFile = strings.TrimSpace(in.LicenseFile)
in.ContactPerson = strings.TrimSpace(in.ContactPerson)
in.ContactPhone = strings.TrimSpace(in.ContactPhone)
in.ContactEmail = strings.TrimSpace(in.ContactEmail)
in.Address = strings.TrimSpace(in.Address)
if in.TenantName == "" {
return nil, errors.New("请填写公司名称")
}
if len(in.TenantName) > 128 {
return nil, errors.New("公司名称过长")
}
if !creditCodeRegexp.MatchString(in.CreditCode) {
return nil, errors.New("请填写正确的统一社会信用代码(18位)")
}
if in.LegalPerson == "" {
return nil, errors.New("请填写法定代表人姓名")
}
if in.LicenseFile == "" {
return nil, errors.New("请上传营业执照电子档")
}
if in.ContactPerson == "" {
return nil, errors.New("请填写联系人")
}
if !IsMobile(in.ContactPhone) {
return nil, errors.New("请填写正确的联系电话")
}
if in.ContactEmail == "" || !strings.Contains(in.ContactEmail, "@") {
return nil, errors.New("请填写正确的电子邮箱")
}
identity, err := authsvc.GetIdentityByID(identityID)
if err != nil || identity == nil {
return nil, errors.New("注册身份异常,请重新注册")
}
// 同一身份同时只允许一条待审的创建企业申请
cnt, _ := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("identity_id", identityID).
Filter("type", models.TenantApplyTypeCreate).
Filter("status", models.TenantApplyStatusPending).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
return nil, errors.New("已提交过创建企业申请,请等待平台审核")
}
// 信用代码防重:已有启用租户使用同一信用代码则拒绝
if cc := in.CreditCode; cc != "" {
dup, _ := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("credit_code", cc).
Filter("delete_time__isnull", true).
Exclude("status", models.TenantApplyStatusReject).
Count()
if dup > 0 {
return nil, errors.New("该统一社会信用代码已提交过申请")
}
}
apply := &models.AuthTenantApply{
IdentityID: identityID,
Type: models.TenantApplyTypeCreate,
TenantName: in.TenantName,
CreditCode: in.CreditCode,
LegalPerson: in.LegalPerson,
LicenseFile: in.LicenseFile,
ContactPerson: in.ContactPerson,
ContactPhone: in.ContactPhone,
ContactEmail: in.ContactEmail,
Address: in.Address,
Status: models.TenantApplyStatusPending,
}
if _, err := models.Orm.Insert(apply); err != nil {
return nil, errors.New("提交申请失败,请稍后重试")
}
return apply, nil
}
// ---------------- 审批:backend(加入申请) ----------------
// ListTenantAppliesForTenant 企业管理员的「加入申请」列表(tid 隔离)
func ListTenantAppliesForTenant(tid uint64, status *int8, page, pageSize int) ([]models.AuthTenantApply, int64, error) {
qs := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("type", models.TenantApplyTypeJoin).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if status != nil {
qs = qs.Filter("status", *status)
}
total, err := qs.Count()
if err != nil {
return nil, 0, err
}
var rows []models.AuthTenantApply
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
return rows, total, err
}
// ApproveJoinApply 企业管理员通过加入申请:建绑定(默认「租户用户」角色)。
// 绑定成功后才置申请为已通过;绑定失败则申请保持待审并返回错误。
func ApproveJoinApply(applyID, tid, reviewerID uint64, reviewerName string) error {
apply, err := getPendingApply(applyID, models.TenantApplyTypeJoin, tid)
if err != nil {
return err
}
identity, err := authsvc.GetIdentityByID(apply.IdentityID)
if err != nil || identity == nil {
return errors.New("申请人身份不存在")
}
// 配额校验
if check, qerr := CheckTenantUserQuota(tid, 0); qerr == nil && !check.Allowed {
return fmt.Errorf("企业用户数已达上限(%d/%d),请先扩容后再通过申请", check.Used, check.Quota)
}
if err := bindIdentityToTenant(identity, tid, models.GetTenantUserRole); err != nil {
return err
}
return finishApply(apply, models.TenantApplyStatusPassed, "", reviewerID, reviewerName)
}
// RejectJoinApply 企业管理员驳回加入申请
func RejectJoinApply(applyID, tid, reviewerID uint64, reviewerName, reason string) error {
apply, err := getPendingApply(applyID, models.TenantApplyTypeJoin, tid)
if err != nil {
return err
}
return finishApply(apply, models.TenantApplyStatusReject, reason, reviewerID, reviewerName)
}
// ---------------- 审批:platform(创建企业申请) ----------------
// ListTenantAppliesForPlatform 平台「企业创建审核」列表
func ListTenantAppliesForPlatform(applyType *int8, status *int8, page, pageSize int) ([]models.AuthTenantApply, int64, error) {
qs := models.Orm.QueryTable(new(models.AuthTenantApply)).Filter("delete_time__isnull", true)
if applyType != nil {
qs = qs.Filter("type", *applyType)
}
if status != nil {
qs = qs.Filter("status", *status)
}
total, err := qs.Count()
if err != nil {
return nil, 0, err
}
var rows []models.AuthTenantApply
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
return rows, total, err
}
// ApproveCreateApply 平台通过创建企业申请:建租户(默认套餐配额),
// 申请人绑定为租户管理员(全局「租户管理员」角色),并回填申请 tid。
func ApproveCreateApply(applyID, reviewerID uint64, reviewerName string) (tid uint64, err error) {
apply, err := getPendingApply(applyID, models.TenantApplyTypeCreate, 0)
if err != nil {
return 0, err
}
identity, err := authsvc.GetIdentityByID(apply.IdentityID)
if err != nil || identity == nil {
return 0, errors.New("申请人身份不存在")
}
// 建租户
tenant := &models.SystemTenant{
TenantName: apply.TenantName,
ContactPerson: applyStrPtr(apply.ContactPerson),
ContactPhone: applyStrPtr(apply.ContactPhone),
ContactEmail: applyStrPtr(apply.ContactEmail),
Address: applyStrPtr(apply.Address),
Status: 1,
Remark: applyStrPtr(fmt.Sprintf("注册创建;信用代码 %s;法定代表人 %s;营业执照 %s", apply.CreditCode, apply.LegalPerson, apply.LicenseFile)),
}
tenant.TenantCode, err = genUniqueTenantCode()
if err != nil {
return 0, err
}
if pkg, perr := GetDefaultTenantPackage(); perr == nil && pkg != nil {
tenant.PackageID = pkg.ID
if pkg.UserQuota > 0 {
tenant.UserQuota = pkg.UserQuota
}
}
if tenant.UserQuota <= 0 {
tenant.UserQuota = DefaultTenantUserQuota
}
newID, err := models.Orm.Insert(tenant)
if err != nil {
return 0, errors.New("创建租户失败:" + err.Error())
}
tid = uint64(newID)
// 申请人绑定为租户管理员(不新建账号、不覆盖密码)
if err := bindIdentityToTenant(identity, tid, models.GetTenantAdminRole); err != nil {
// 绑定失败:租户保留(平台可在租户管理里手动补绑),申请保持待审
return tid, fmt.Errorf("租户已创建(编码 %s),但管理员绑定失败:%s", tenant.TenantCode, err.Error())
}
// 申请已通过标记失败不影响结果(租户与绑定已生效)
apply.Tid = tid
if ferr := finishApply(apply, models.TenantApplyStatusPassed, "", reviewerID, reviewerName); ferr != nil {
return tid, nil
}
return tid, nil
}
// RejectCreateApply 平台驳回创建企业申请
func RejectCreateApply(applyID, reviewerID uint64, reviewerName, reason string) error {
apply, err := getPendingApply(applyID, models.TenantApplyTypeCreate, 0)
if err != nil {
return err
}
return finishApply(apply, models.TenantApplyStatusReject, reason, reviewerID, reviewerName)
}
// ---------------- 内部辅助 ----------------
// getPendingApply 取指定类型的待审申请;tid>0 时校验归属
func getPendingApply(applyID uint64, applyType int8, tid uint64) (*models.AuthTenantApply, error) {
apply := new(models.AuthTenantApply)
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("id", applyID).
Filter("delete_time__isnull", true).
One(apply); err != nil {
return nil, errors.New("申请不存在")
}
if apply.Type != applyType {
return nil, errors.New("申请类型不匹配")
}
if tid > 0 && apply.Tid != tid {
return nil, errors.New("无权处理该申请")
}
if apply.Status != models.TenantApplyStatusPending {
return nil, errors.New("该申请已处理,请刷新列表")
}
return apply, nil
}
// finishApply CAS 置申请终态,防止并发重复审批
func finishApply(apply *models.AuthTenantApply, status int8, reason string, reviewerID uint64, reviewerName string) error {
now := time.Now()
affected, err := models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("id", apply.ID).
Filter("status", models.TenantApplyStatusPending).
Update(map[string]interface{}{
"status": status,
"reject_reason": reason,
"reviewer_id": reviewerID,
"reviewer_name": reviewerName,
"review_time": now,
"update_time": now,
})
if err != nil {
return err
}
if affected == 0 {
return errors.New("该申请已处理,请刷新列表")
}
return nil
}
// bindIdentityToTenant 把身份绑定为租户成员(老表 + 认证中心双写),角色由 rolePicker 决定。
// 企业内账号:手机号 > user{id};姓名取昵称;密码镜像身份层现有哈希。
func bindIdentityToTenant(identity *models.AuthIdentity, tid uint64, rolePicker func() (uint64, error)) error {
account := ""
if identity.Mobile != nil && strings.TrimSpace(*identity.Mobile) != "" {
account = strings.TrimSpace(*identity.Mobile)
}
if account == "" {
account = fmt.Sprintf("user%d", identity.ID)
}
// 企业内账号被其他身份占用时拒绝
var dup models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("account", account).
Filter("delete_time__isnull", true).
One(&dup); err == nil && dup.Uid != identity.ID {
return errors.New("企业内账号已被占用(" + account + "),请管理员调整后重试")
}
name := identityNickname(identity)
var pwd *string
if identity.PasswordHash != nil {
h := *identity.PasswordHash
pwd = &h
}
if _, err := BindTenantUser(
tid, identity.ID,
&account, &name, identity.Mobile, identity.Email,
nil, nil, pwd,
0, 1, nil,
); err != nil {
return errors.New("建立企业绑定失败:" + err.Error())
}
groupID := uint64(0)
if roleID, rerr := rolePicker(); rerr == nil && roleID > 0 {
groupID = roleID
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", identity.ID).
Update(map[string]interface{}{"group_id": roleID})
}
if err := authsvc.SyncTenantUser(authsvc.SyncTenantUserInput{
Tid: tid,
Account: account,
Name: name,
Phone: applyStrVal(identity.Mobile),
Email: applyStrVal(identity.Email),
PasswordHash: applyStrVal(identity.PasswordHash),
GroupID: groupID,
Status: 1,
IsDefault: 0,
}); err != nil {
return errors.New("认证中心同步失败:" + err.Error())
}
return nil
}
// applyStrVal 取指针字符串值(nil 返回空串)
func applyStrVal(s *string) string {
if s == nil {
return ""
}
return *s
}
// applyStrPtr 字符串转指针
func applyStrPtr(s string) *string {
return &s
}
// identityNickname 身份昵称(空则用「用户{id}」兜底)
func identityNickname(identity *models.AuthIdentity) string {
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
return strings.TrimSpace(*identity.Nickname)
}
return fmt.Sprintf("用户%d", identity.ID)
}
// genUniqueTenantCode 生成唯一租户编码(T + 时间戳 + 随机)
func genUniqueTenantCode() (string, error) {
for i := 0; i < 5; i++ {
code := fmt.Sprintf("T%d%03d", time.Now().Unix(), time.Now().UnixNano()%1000)
cnt, err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_code", code).Count()
if err != nil {
return "", errors.New("生成租户编码失败")
}
if cnt == 0 {
return code, nil
}
}
return "", errors.New("生成租户编码失败,请稍后重试")
}
// BindRegisterWechat 注册完成后把服务号 openid 绑到新身份(账号注册模式)。
// openid 已绑定其他身份时不抢占,静默跳过。
func BindRegisterWechat(identityID uint64, openid string) {
openid = strings.TrimSpace(openid)
if openid == "" || identityID == 0 {
return
}
var row models.AuthIdentityThird
err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
Filter("provider", models.IdPWechat).
Filter("open_id", openid).
One(&row)
if err == nil {
return // 已绑定(无论绑给谁),不抢占
}
if err != orm.ErrNoRows {
return
}
_, _ = models.Orm.Insert(&models.AuthIdentityThird{
IdentityID: identityID,
Provider: models.IdPWechat,
OpenID: openid,
})
}