批量优化

This commit is contained in:
hero920103 committed 2026-10-08 15:30:37 +08:00
1 parent 438145cae7
commit c925b198d1
50 files changed
+5716 -291

No files matched your search

+700
View File
@@ -0,0 +1,700 @@
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,
})
}