批量优化
This commit is contained in:
1 parent
438145cae7
commit
c925b198d1
50 files changed
+5716
-291
No files matched your search
@@ -0,0 +1,348 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
authsvc "server/services/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// 统一认证中心自助账户能力:注册、找回密码、验证码。
|
||||
//
|
||||
// 与旧版 password_reset.go(按「租户名+账号」定位老表)不同,这里直接操作
|
||||
// 统一身份层 yz_auth_identity:手机号即全局账号,一处改密全平台生效。
|
||||
|
||||
var mobileRegexp = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
||||
|
||||
// identityCodeItem 短信验证码条目
|
||||
type identityCodeItem struct {
|
||||
Code string
|
||||
ExpiredAt time.Time
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
||||
var identityCodeStore sync.Map // key: purpose|phone
|
||||
|
||||
func identityCodeKey(purpose, phone string) string {
|
||||
return strings.TrimSpace(purpose) + "|" + strings.TrimSpace(phone)
|
||||
}
|
||||
|
||||
// IsMobile 校验手机号格式
|
||||
func IsMobile(phone string) bool {
|
||||
return mobileRegexp.MatchString(strings.TrimSpace(phone))
|
||||
}
|
||||
|
||||
// findIdentityByMobile 按手机号查身份(含软删过滤)
|
||||
func findIdentityByMobile(phone string) *models.AuthIdentity {
|
||||
if !IsMobile(phone) {
|
||||
return nil
|
||||
}
|
||||
var m models.AuthIdentity
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("mobile", strings.TrimSpace(phone)).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
// SendIdentityCode 发送自助账户短信验证码。
|
||||
// - purpose=register:手机号必须未注册
|
||||
// - purpose=reset:手机号必须已注册
|
||||
//
|
||||
// 60 秒服务端冷却,防短信轰炸;验证码 5 分钟有效。
|
||||
func SendIdentityCode(purpose, phone string) error {
|
||||
phone = strings.TrimSpace(phone)
|
||||
if !IsMobile(phone) {
|
||||
return errors.New("请输入正确的手机号")
|
||||
}
|
||||
if purpose != "register" && purpose != "reset" {
|
||||
return errors.New("参数错误")
|
||||
}
|
||||
|
||||
existed := findIdentityByMobile(phone)
|
||||
if purpose == "register" && existed != nil {
|
||||
return errors.New("该手机号已注册,请直接登录或找回密码")
|
||||
}
|
||||
if purpose == "reset" && existed == nil {
|
||||
// 不暴露「手机号是否存在」会牺牲可用性;注册页已明示手机号唯一,这里直接提示
|
||||
return errors.New("该手机号尚未注册")
|
||||
}
|
||||
|
||||
// 服务端 60 秒冷却
|
||||
if v, ok := identityCodeStore.Load(identityCodeKey(purpose, phone)); ok {
|
||||
if item, ok := v.(identityCodeItem); ok && time.Since(item.IssuedAt) < 60*time.Second {
|
||||
return errors.New("验证码发送过于频繁,请稍后再试")
|
||||
}
|
||||
}
|
||||
|
||||
// 短信网关配置校验前置,避免生成验证码后发送失败造成困惑
|
||||
if _, _, err := getDefaultSystemSMSConfig(); err != nil {
|
||||
return errors.New("短信服务未配置,请联系平台管理员")
|
||||
}
|
||||
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000)) //nolint:gosec // 验证码场景可接受
|
||||
content := "验证码:" + code + ",5分钟内有效。为保护账号安全,请勿泄露给他人。"
|
||||
if err := enqueueSMSTaskForLogin(0, phone, content, code); err != nil {
|
||||
return errors.New("短信发送失败,请稍后重试")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
identityCodeStore.Store(identityCodeKey(purpose, phone), identityCodeItem{
|
||||
Code: code,
|
||||
ExpiredAt: now.Add(5 * time.Minute),
|
||||
IssuedAt: now,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyIdentityCode 校验自助账户短信验证码,验证通过后立即销毁(一次性)
|
||||
func VerifyIdentityCode(purpose, phone, code string) error {
|
||||
phone = strings.TrimSpace(phone)
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return errors.New("请输入验证码")
|
||||
}
|
||||
key := identityCodeKey(purpose, phone)
|
||||
v, ok := identityCodeStore.Load(key)
|
||||
if !ok {
|
||||
return errors.New("请先获取验证码")
|
||||
}
|
||||
item, ok := v.(identityCodeItem)
|
||||
if !ok {
|
||||
return errors.New("验证码状态异常,请重新获取")
|
||||
}
|
||||
if time.Now().After(item.ExpiredAt) {
|
||||
identityCodeStore.Delete(key)
|
||||
return errors.New("验证码已过期,请重新获取")
|
||||
}
|
||||
if item.Code != code {
|
||||
return errors.New("验证码错误")
|
||||
}
|
||||
identityCodeStore.Delete(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterIdentityInput 自助注册入参
|
||||
type RegisterIdentityInput struct {
|
||||
Phone string
|
||||
Code string
|
||||
Password string
|
||||
Confirm string
|
||||
Nickname string
|
||||
TenantCode string // 选填:填了则注册后直接加入该企业(默认「租户用户」角色)
|
||||
}
|
||||
|
||||
// RegisterIdentityResult 注册结果
|
||||
type RegisterIdentityResult struct {
|
||||
IdentityID uint64
|
||||
JoinedTid uint64 // 成功加入的企业;0 表示未加入
|
||||
Warning string // 加入企业失败的提示(身份已创建成功)
|
||||
}
|
||||
|
||||
// RegisterIdentity 自助注册:创建统一身份,可选加入企业。
|
||||
//
|
||||
// 统一认证原则:手机号即全局账号,密码在身份层一份;加入企业只是建绑定,
|
||||
// 企业内角色默认「租户用户」,后续由企业管理员调整。
|
||||
func RegisterIdentity(in RegisterIdentityInput) (*RegisterIdentityResult, error) {
|
||||
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 len(strings.TrimSpace(in.Password)) < 6 {
|
||||
return nil, errors.New("密码长度不能少于6个字符")
|
||||
}
|
||||
if in.Password != in.Confirm {
|
||||
return nil, errors.New("两次输入的密码不一致")
|
||||
}
|
||||
|
||||
// 防并发重复注册
|
||||
if findIdentityByMobile(phone) != nil {
|
||||
return nil, errors.New("该手机号已注册,请直接登录")
|
||||
}
|
||||
|
||||
hashed, err := passwordutil.Hash(strings.TrimSpace(in.Password))
|
||||
if err != nil {
|
||||
return nil, errors.New("密码处理失败")
|
||||
}
|
||||
|
||||
nickname := strings.TrimSpace(in.Nickname)
|
||||
if nickname == "" {
|
||||
nickname = "用户" + phone[len(phone)-4:]
|
||||
}
|
||||
|
||||
// 选填企业编码:先校验企业存在且启用、配额未满,失败直接拒绝(身份尚未创建)
|
||||
tenantCode := strings.TrimSpace(in.TenantCode)
|
||||
var tenant *models.SystemTenant
|
||||
if tenantCode != "" {
|
||||
t := new(models.SystemTenant)
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("tenant_code", tenantCode).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(t)
|
||||
if err != nil {
|
||||
return nil, errors.New("企业编码不存在,请确认后重试")
|
||||
}
|
||||
if check, err := CheckTenantUserQuota(t.ID, 0); err == nil && !check.Allowed {
|
||||
return nil, fmt.Errorf("该企业用户数已达上限(%d/%d),请联系企业管理员", check.Used, check.Quota)
|
||||
}
|
||||
tenant = t
|
||||
}
|
||||
|
||||
identity := &models.AuthIdentity{
|
||||
UnionID: strings.ReplaceAll(uuid.NewString(), "-", ""),
|
||||
PasswordAlgo: passwordutil.AlgoOf(hashed),
|
||||
Status: models.AuthIdentityStatusEnabled,
|
||||
}
|
||||
identity.Mobile = &phone
|
||||
identity.Nickname = &nickname
|
||||
identity.PasswordHash = &hashed
|
||||
|
||||
id, err := models.Orm.Insert(identity)
|
||||
if err != nil {
|
||||
return nil, errors.New("注册失败,请稍后重试")
|
||||
}
|
||||
identity.ID = uint64(id)
|
||||
|
||||
result := &RegisterIdentityResult{IdentityID: identity.ID}
|
||||
|
||||
// 加入企业:建绑定(老表 + 认证中心),角色默认「租户用户」
|
||||
if tenant != nil {
|
||||
warn := joinTenantAfterRegister(identity, tenant, nickname, phone, hashed)
|
||||
if warn != "" {
|
||||
result.Warning = warn
|
||||
} else {
|
||||
result.JoinedTid = tenant.ID
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// joinTenantAfterRegister 注册后加入企业;返回警告信息(空串表示成功)。
|
||||
// 身份已创建成功,加入失败仅降级为警告,不回滚注册。
|
||||
func joinTenantAfterRegister(identity *models.AuthIdentity, tenant *models.SystemTenant, nickname, phone, passwordHash string) string {
|
||||
// 企业内账号默认用手机号;若已被本企业其他身份占用则放弃自动加入
|
||||
var dup models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("account", phone).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&dup); err == nil && dup.Uid != identity.ID {
|
||||
return "注册成功,但企业内账号已被占用,请联系企业管理员邀请你加入"
|
||||
}
|
||||
|
||||
phoneStr := phone
|
||||
nameStr := nickname
|
||||
hashStr := passwordHash
|
||||
if _, err := BindTenantUser(
|
||||
tenant.ID, identity.ID,
|
||||
&phoneStr, &nameStr, &phoneStr, nil,
|
||||
nil, nil, &hashStr,
|
||||
1, 1, nil,
|
||||
); err != nil {
|
||||
return "注册成功,但加入企业失败,请联系企业管理员邀请你加入"
|
||||
}
|
||||
|
||||
// 默认「租户用户」角色;角色 ID 一并写入认证中心绑定
|
||||
groupID := uint64(0)
|
||||
if roleID, err := models.GetTenantUserRole(); err == nil && roleID > 0 {
|
||||
groupID = roleID
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("uid", identity.ID).
|
||||
Update(map[string]interface{}{"group_id": roleID})
|
||||
}
|
||||
|
||||
// 双写认证中心绑定
|
||||
if err := authsvc.SyncTenantUser(authsvc.SyncTenantUserInput{
|
||||
Tid: tenant.ID,
|
||||
Account: phoneStr,
|
||||
Name: nameStr,
|
||||
Phone: phoneStr,
|
||||
PasswordHash: passwordHash,
|
||||
GroupID: groupID,
|
||||
Status: 1,
|
||||
IsDefault: 1,
|
||||
}); err != nil {
|
||||
return "注册成功,企业绑定已建立,但认证中心同步失败,请联系管理员"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResetIdentityPasswordByMobile 自助找回密码:验证码通过后重置身份层密码。
|
||||
// 密码属于身份层,重置后该用户在所有企业的登录密码同步更新。
|
||||
func ResetIdentityPasswordByMobile(phone, code, newPassword, confirm string) error {
|
||||
phone = strings.TrimSpace(phone)
|
||||
if !IsMobile(phone) {
|
||||
return errors.New("请输入正确的手机号")
|
||||
}
|
||||
if err := VerifyIdentityCode("reset", phone, code); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(strings.TrimSpace(newPassword)) < 6 {
|
||||
return errors.New("密码长度不能少于6个字符")
|
||||
}
|
||||
if newPassword != confirm {
|
||||
return errors.New("两次输入的密码不一致")
|
||||
}
|
||||
|
||||
identity := findIdentityByMobile(phone)
|
||||
if identity == nil {
|
||||
return errors.New("该手机号尚未注册")
|
||||
}
|
||||
|
||||
hashed, err := passwordutil.Hash(strings.TrimSpace(newPassword))
|
||||
if err != nil {
|
||||
return errors.New("密码处理失败")
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", identity.ID).
|
||||
Update(map[string]interface{}{
|
||||
"password_hash": hashed,
|
||||
"password_algo": passwordutil.AlgoOf(hashed),
|
||||
"fail_count": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("密码重置失败,请稍后重试")
|
||||
}
|
||||
|
||||
// 老表镜像同步:把新密码写到该身份在各企业的 yz_system_tenant_user 行,
|
||||
// 保持「老表密码 = 身份层密码」的双写口径(登录以认证中心为准)
|
||||
mirrorPasswordToTenantRows(identity.ID, hashed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mirrorPasswordToTenantRows 把身份层密码镜像到该身份所有企业绑定的老表行
|
||||
func mirrorPasswordToTenantRows(identityID uint64, passwordHash string) {
|
||||
var binds []models.AuthTenantUser
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&binds); err != nil {
|
||||
return
|
||||
}
|
||||
for _, b := range binds {
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", b.Tid).
|
||||
Filter("uid", identityID).
|
||||
Update(map[string]interface{}{"password": passwordHash})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user