Files
iotlcm/server/internal/logic/app/app_auth.go
T

1533 lines
46 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 app
import (
"context"
cryptorand "crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/wenlng/go-captcha-assets/resources/images"
"github.com/wenlng/go-captcha-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/slide"
"gorm.io/gorm"
)
const appSideCaptchaCachePrefix = "app:side:captcha:"
const appSmsCachePrefix = "app:sms:"
const appSmsCooldownPrefix = "app:sms:cooldown:"
const appSmsDailyPhonePrefix = "app:sms:daily:phone:"
const appSmsDailyIPPrefix = "app:sms:daily:ip:"
const appWechatBindTicketCachePrefix = "app:wechat:bind:"
const appWechatMiniAppSecretPlaceholder = "CHANGE_ME_WECHAT_MINI_APP_SECRET"
const appWechatBindTicketExpire = 10 * time.Minute
const (
appSmsCodeTTL = 5 * time.Minute
appSmsCooldownTTL = 60 * time.Second
appSmsDailyLimit = 10
appSmsDailyExpire = 24 * time.Hour
)
var appSlideCaptcha slide.Captcha
type appWechatCode2SessionRes struct {
OpenId string `json:"openid"`
SessionKey string `json:"session_key"`
UnionId string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
type appWechatBindTicketCache struct {
TenantId string `json:"tenantId"`
OpenId string `json:"openId"`
UnionId string `json:"unionId"`
VerifiedPhone string `json:"verifiedPhone"`
}
type appCodeHttpSenderConfig struct {
Url string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
BodyTemplate string `json:"bodyTemplate"`
SuccessStatus int `json:"successStatus"`
}
type appSmsHTTPResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
type sAppAuth struct{}
func NewAppAuth() *sAppAuth {
return &sAppAuth{}
}
func init() {
service.RegisterAppAuth(NewAppAuth())
initAppSlideCaptcha()
}
func (s *sAppAuth) Captcha(ctx context.Context) (captchaID string, captchaImage string, err error) {
return service.SysCaptcha().GenerateCaptcha(ctx)
}
func initAppSlideCaptcha() {
builder := slide.NewBuilder(slide.WithEnableGraphVerticalRandom(true))
imgs, err := images.GetImages()
if err != nil {
panic(err)
}
graphs, err := tiles.GetTiles()
if err != nil {
panic(err)
}
newGraphs := make([]*slide.GraphImage, 0, len(graphs))
for _, graph := range graphs {
newGraphs = append(newGraphs, &slide.GraphImage{
OverlayImage: graph.OverlayImage,
MaskImage: graph.MaskImage,
ShadowImage: graph.ShadowImage,
})
}
builder.SetResources(
slide.WithGraphImages(newGraphs),
slide.WithBackgrounds(imgs),
)
builder.SetOptions(slide.WithImageSize(option.Size{Width: 280, Height: 170}))
appSlideCaptcha = builder.Make()
}
func (s *sAppAuth) SideCaptcha(ctx context.Context) (res *model.AppSideCaptchaModel, err error) {
captchaData, err := appSlideCaptcha.Generate()
if err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
blockData := captchaData.GetData()
if blockData == nil {
return nil, gerror.New("获取滑块验证码出错")
}
imageBase64, err := captchaData.GetMasterImage().ToBase64()
if err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
tileBase64, err := captchaData.GetTileImage().ToBase64()
if err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
dotsByte, err := json.Marshal(blockData)
if err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
key, err := gmd5.EncryptBytes(dotsByte)
if err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
if err = mcache.Set(ctx, appSideCaptchaCachePrefix+key, hex.EncodeToString(dotsByte), 5*time.Minute); err != nil {
return nil, gerror.New("获取滑块验证码出错")
}
return &model.AppSideCaptchaModel{
CaptchaKey: key,
ImageBase64: imageBase64,
TileBase64: tileBase64,
TileWidth: blockData.Width,
TileHeight: blockData.Height,
TileX: blockData.DX,
TileY: blockData.DY,
}, nil
}
func (s *sAppAuth) CheckSideCaptcha(ctx context.Context, param *model.AppCheckSideCaptchaParam) (err error) {
cacheKey := appSideCaptchaCachePrefix + param.CaptchaKey
value, err := mcache.Get(ctx, cacheKey)
if err != nil || value == nil {
return gerror.New("滑块验证已失效")
}
defer mcache.Remove(ctx, cacheKey)
dotsByte, err := hex.DecodeString(fmt.Sprintf("%v", value))
if err != nil {
return gerror.New("滑块验证出错")
}
var blockData *slide.Block
if err = json.Unmarshal(dotsByte, &blockData); err != nil {
return gerror.New("滑块验证出错")
}
point := strings.Split(param.Point, ",")
if len(point) != 2 {
return gerror.New("滑块验证出错")
}
x, _ := strconv.ParseFloat(fmt.Sprintf("%v", point[0]), 64)
y, _ := strconv.ParseFloat(fmt.Sprintf("%v", point[1]), 64)
if !slide.Validate(int(x), int(y), int(blockData.X), int(blockData.Y), 4) {
return gerror.New("滑块验证失败")
}
return nil
}
func (s *sAppAuth) SendSms(ctx context.Context, param *model.AppSendSmsParam) (res *model.AppSendSmsModel, err error) {
if param == nil {
return nil, gerror.New("短信验证码参数不能为空")
}
param.TenantId = strings.TrimSpace(param.TenantId)
param.Phone = strings.TrimSpace(param.Phone)
param.Scene = strings.TrimSpace(param.Scene)
if err = s.validateAppSmsScene(ctx, param); err != nil {
return nil, err
}
if err = s.reserveAppSmsCooldown(ctx, param.TenantId, param.Phone); err != nil {
return nil, err
}
if err = s.checkAppSmsRateLimit(ctx, param.TenantId, param.Phone); err != nil {
_ = releaseAppSmsCooldown(ctx, param.TenantId, param.Phone)
return nil, err
}
code, err := appRandomDigits(6)
if err != nil {
_ = releaseAppSmsCooldown(ctx, param.TenantId, param.Phone)
return nil, gerror.Wrap(err, "生成短信验证码失败")
}
cacheKey := appSmsCacheKey(param.TenantId, param.Phone, param.Scene)
if err = setAppSmsCode(ctx, cacheKey, code); err != nil {
_ = releaseAppSmsCooldown(ctx, param.TenantId, param.Phone)
return nil, gerror.New("短信验证码发送失败")
}
sentByHTTP, err := sendAppPhoneCode(ctx, param.Phone, code, appSmsServiceScene(param.Scene))
if err != nil {
_ = deleteAppSmsCode(ctx, cacheKey)
_ = releaseAppSmsCooldown(ctx, param.TenantId, param.Phone)
g.Log().Errorf(ctx, "APP短信验证码发送失败 scene=%s phone=%s err=%+v", param.Scene, param.Phone, err)
return nil, gerror.New("短信验证码发送失败,请稍后重试")
}
res = &model.AppSendSmsModel{Success: true}
if !sentByHTTP {
res.SmsCode = code
}
return res, nil
}
func (s *sAppAuth) validateAppSmsScene(ctx context.Context, param *model.AppSendSmsParam) error {
if err := service.AppAuth().CheckSideCaptcha(ctx, &model.AppCheckSideCaptchaParam{
CaptchaKey: strings.TrimSpace(param.CaptchaKey),
Point: strings.TrimSpace(param.CaptchaPoint),
}); err != nil {
return err
}
users, err := s.findAppLoginUsersByAccount(ctx, param.TenantId, param.Phone)
if err != nil {
return err
}
switch param.Scene {
case "register":
if len(users) > 0 {
return gerror.New("手机号已注册")
}
case "login", "forgotPassword", "wechatBindPhone":
if len(users) == 0 {
return gerror.New("手机号未注册")
}
}
return nil
}
func (s *sAppAuth) checkAppSmsRateLimit(ctx context.Context, tenantId string, phone string) error {
tenantId = strings.TrimSpace(tenantId)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
clientIP := strings.TrimSpace(utility.GetClientIp(ctx))
for _, item := range []struct {
key string
name string
skip bool
}{
{appSmsDailyPhonePrefix + strings.TrimSpace(tenantId) + ":" + phone, "手机号", false},
{appSmsDailyIPPrefix + clientIP, "当前网络", clientIP == ""},
} {
if item.skip {
continue
}
value, err := g.Redis().Do(ctx, "INCR", item.key)
if err != nil {
return gerror.Wrap(err, "短信服务暂不可用")
}
if value.Int() == 1 {
_, _ = g.Redis().Do(ctx, "EXPIRE", item.key, int(appSmsDailyExpire.Seconds()))
}
if value.Int() > appSmsDailyLimit {
return gerror.Newf("%s今日短信发送次数已达上限", item.name)
}
}
return nil
}
func (s *sAppAuth) reserveAppSmsCooldown(ctx context.Context, tenantId string, phone string) error {
key := appSmsCooldownCacheKey(tenantId, phone)
value, err := g.Redis().Do(ctx, "SET", key, "1", "NX", "EX", int(appSmsCooldownTTL.Seconds()))
if err != nil {
return gerror.Wrap(err, "短信服务暂不可用")
}
if value.IsNil() || strings.EqualFold(value.String(), "") {
return gerror.New("短信验证码发送过于频繁,请稍后再试")
}
return nil
}
func releaseAppSmsCooldown(ctx context.Context, tenantId string, phone string) error {
_, err := g.Redis().Do(ctx, "DEL", appSmsCooldownCacheKey(tenantId, phone))
return err
}
func appSmsCooldownCacheKey(tenantId string, phone string) string {
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
return fmt.Sprintf("%s%s:%s", appSmsCooldownPrefix, tenantId, phone)
}
func appSmsServiceScene(scene string) string {
switch scene {
case "wechatBindPhone":
return "bind_phone"
default:
return scene
}
}
func sendAppPhoneCode(ctx context.Context, phone string, code string, scene string) (sentByHTTP bool, err error) {
var cfg appCodeHttpSenderConfig
if v, cfgErr := g.Cfg().Get(ctx, "register.sms"); cfgErr == nil && !v.IsNil() {
_ = v.Scan(&cfg)
}
if strings.TrimSpace(cfg.Url) != "" {
return true, callAppCodeHTTP(ctx, cfg, phone, code, scene)
}
if !strings.EqualFold(strings.TrimSpace(g.Cfg().MustGet(ctx, "system.gfmode", "prod").String()), "dev") {
return false, gerror.New("短信服务未配置")
}
g.Log().Infof(ctx, "APP短信验证码已生成 phone=%s code=%s", phone, code)
return false, nil
}
func callAppCodeHTTP(ctx context.Context, cfg appCodeHttpSenderConfig, contact string, code string, scene string) error {
method := strings.ToUpper(strings.TrimSpace(cfg.Method))
if method == "" {
method = http.MethodPost
}
successStatus := cfg.SuccessStatus
if successStatus <= 0 {
successStatus = http.StatusOK
}
body := renderAppCodeTemplate(cfg.BodyTemplate, contact, code, scene)
if strings.TrimSpace(body) == "" {
bodyBytes, _ := json.Marshal(g.Map{
"contact": contact,
"phone": contact,
"code": code,
})
body = string(bodyBytes)
}
headers := cfg.Headers
if headers == nil {
headers = map[string]string{}
}
if _, ok := headers["Content-Type"]; !ok {
headers["Content-Type"] = "application/json"
}
resp, err := g.Client().SetHeaderMap(headers).DoRequest(ctx, method, cfg.Url, body)
if err != nil {
return gerror.Wrap(err, "请求短信服务失败")
}
defer resp.Close()
respBody := strings.TrimSpace(resp.ReadAllString())
if resp.StatusCode != successStatus {
if respBody != "" {
return gerror.Newf("短信服务响应异常:%d,%s", resp.StatusCode, respBody)
}
return gerror.Newf("短信服务响应异常:%d", resp.StatusCode)
}
if respBody == "" {
return nil
}
var result appSmsHTTPResponse
if err = json.Unmarshal([]byte(respBody), &result); err != nil {
return gerror.Wrap(err, "解析短信服务响应失败")
}
if result.Code != 0 {
message := strings.TrimSpace(result.Message)
if message == "" {
message = "短信服务返回失败"
}
return gerror.New(message)
}
return nil
}
func renderAppCodeTemplate(template string, contact string, code string, scene string) string {
template = strings.TrimSpace(template)
if template == "" {
return ""
}
replacer := strings.NewReplacer(
"{contact}", contact,
"{{contact}}", contact,
"${contact}", contact,
"{email}", contact,
"{{email}}", contact,
"${email}", contact,
"{phone}", contact,
"{{phone}}", contact,
"${phone}", contact,
"{scene}", scene,
"{{scene}}", scene,
"${scene}", scene,
"{code}", code,
"{{code}}", code,
"${code}", code,
)
return replacer.Replace(template)
}
func appSmsCacheKey(tenantId string, phone string, scene string) string {
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
return fmt.Sprintf("%s%s:%s:%s", appSmsCachePrefix, tenantId, scene, phone)
}
func setAppSmsCode(ctx context.Context, key string, code string) error {
_, err := g.Redis().Do(ctx, "SETEX", key, int(appSmsCodeTTL.Seconds()), code)
return err
}
func deleteAppSmsCode(ctx context.Context, key string) error {
_, err := g.Redis().Do(ctx, "DEL", key)
return err
}
func verifyAppSmsCode(ctx context.Context, tenantId string, phone string, scene string, code string) error {
value, err := g.Redis().Get(ctx, appSmsCacheKey(tenantId, phone, scene))
if err != nil || value == nil {
return gerror.NewCode(consts.CodeCaptchaError, "短信验证码已失效")
}
if strings.TrimSpace(value.String()) != strings.TrimSpace(code) {
return gerror.NewCode(consts.CodeCaptchaError, "短信验证码错误")
}
return nil
}
func appRandomDigits(length int) (string, error) {
if length <= 0 {
length = 6
}
limit := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(length)), nil)
value, err := cryptorand.Int(cryptorand.Reader, limit)
if err != nil {
return "", err
}
return fmt.Sprintf("%0*d", length, value.Int64()), nil
}
func (s *sAppAuth) PhoneRegister(ctx context.Context, param *model.AppPhoneRegisterParam) (res *model.AppAuthTokenModel, err error) {
if param == nil {
return nil, gerror.New("请求参数不能为空")
}
param.TenantId = strings.TrimSpace(param.TenantId)
param.Phone = strings.TrimSpace(param.Phone)
param.SmsCode = strings.TrimSpace(param.SmsCode)
if err = verifyAppSmsCode(ctx, param.TenantId, param.Phone, "register", param.SmsCode); err != nil {
return nil, err
}
if param.TenantId == "" {
param.TenantId = consts.DefaultSystemTenantCode
}
settings, err := appLoadRegisterSettings(ctx, param.TenantId)
if err != nil {
return nil, err
}
salt := utility.RandomString(5)
password := appPasswordEncryptForStore(param.Password, salt)
nickName := param.NickName
if nickName == "" {
nickName = "用户" + param.Phone[len(param.Phone)-4:]
}
now := gtime.Now()
err = dao.SysUser.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if err := s.releaseDeletedPhoneRegisterAccounts(ctx, tx, param.TenantId, param.Phone, now); err != nil {
return err
}
userId, err := dao.SysUser.Ctx(ctx).TX(tx).Data(do.SysUser{
TenantId: param.TenantId,
DeptId: settings.DeptId,
UserName: param.Phone,
NickName: nickName,
UserType: consts.SysUserTypeAPP,
Phonenumber: param.Phone,
Sex: "2",
Salt: salt,
Password: password,
Status: consts.SysUserStatusNormal,
CreatedDept: settings.DeptId,
CreatedAt: now,
UpdatedAt: now,
}).InsertAndGetId()
if err != nil {
return err
}
if _, err = dao.SysUserRole.Ctx(ctx).TX(tx).Data(do.SysUserRole{
UserId: userId,
RoleId: settings.RoleId,
}).Insert(); err != nil {
return err
}
_, err = dao.TTerminal.Ctx(ctx).TX(tx).Data(do.TTerminal{
TerminalId: userId,
TerminalName: nickName,
Phone: param.Phone,
Status: consts.SysUserStatusNormal,
FullName: nickName,
LegalPersonName: nickName,
LegalPersonIdCard: "000000000000000000",
Balance: 0,
TenantId: param.TenantId,
CreatedDept: settings.DeptId,
CreatedBy: userId,
CreatedAt: now,
UpdatedBy: userId,
UpdatedAt: now,
}).Insert()
return err
})
if err != nil {
if isSysUserNameUniqueKeyError(err) {
return nil, gerror.New("手机号已注册或账号已被占用")
}
return nil, err
}
_ = deleteAppSmsCode(ctx, appSmsCacheKey(param.TenantId, param.Phone, "register"))
return s.PhoneLogin(ctx, &model.AppPhoneLoginParam{
TenantId: param.TenantId,
Account: param.Phone,
Password: param.Password,
})
}
func (s *sAppAuth) releaseDeletedPhoneRegisterAccounts(ctx context.Context, tx gdb.TX, tenantId string, phone string, now *gtime.Time) error {
tenantId = strings.TrimSpace(tenantId)
phone = strings.TrimSpace(phone)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
if phone == "" {
return nil
}
if now == nil {
now = gtime.Now()
}
mod := dao.SysUser.Ctx(ctx).Unscoped()
if tx != nil {
mod = dao.SysUser.Ctx(ctx).TX(tx).Unscoped()
}
var users []*entity.SysUser
if err := mod.
Where(dao.SysUser.Columns().TenantId, tenantId).
Where("("+dao.SysUser.Columns().Phonenumber+" = ? OR "+dao.SysUser.Columns().UserName+" = ?)", phone, phone).
OrderAsc(dao.SysUser.Columns().UserId).
Scan(&users); err != nil {
return gerror.Wrap(err, "检查手机号注册状态失败")
}
for _, user := range users {
if user == nil || user.UserId <= 0 {
continue
}
if user.DeletedAt == nil || user.DeletedAt.IsZero() {
return gerror.New("手机号已注册")
}
}
for _, user := range users {
if user == nil || user.UserId <= 0 {
continue
}
data := do.SysUser{
UpdatedAt: now,
}
needsUpdate := false
if strings.TrimSpace(user.UserName) == phone {
data.UserName = deletedRegisterUserName(user.UserId, phone)
needsUpdate = true
}
if strings.TrimSpace(user.Phonenumber) == phone {
data.Phonenumber = ""
needsUpdate = true
}
if !needsUpdate {
continue
}
updateMod := dao.SysUser.Ctx(ctx).Unscoped()
if tx != nil {
updateMod = dao.SysUser.Ctx(ctx).TX(tx).Unscoped()
}
if _, err := updateMod.
WherePri(user.UserId).
Data(data).
OmitNil().
Update(); err != nil {
if isSysUserNameUniqueKeyError(err) {
return gerror.New("手机号已注册或账号已被占用")
}
return gerror.Wrap(err, "释放已删除账号手机号占用失败")
}
}
return nil
}
func deletedRegisterUserName(userId int64, phone string) string {
phone = strings.TrimSpace(phone)
if len(phone) > 6 {
phone = phone[len(phone)-6:]
}
return fmt.Sprintf("del%d_%s", userId, phone)
}
func isSysUserNameUniqueKeyError(err error) bool {
if err == nil {
return false
}
errText := err.Error()
return strings.Contains(errText, "Duplicate entry") &&
strings.Contains(errText, "uk_user_name")
}
func (s *sAppAuth) PhoneLogin(ctx context.Context, param *model.AppPhoneLoginParam) (res *model.AppAuthTokenModel, err error) {
if param == nil {
param = &model.AppPhoneLoginParam{}
}
user, candidates, err := s.resolveAppLoginUser(ctx, param)
if err != nil {
return nil, err
}
if len(candidates) > 0 {
return &model.AppAuthTokenModel{
NeedSelect: true,
Candidates: candidates,
}, nil
}
return s.buildAppAuthToken(ctx, user)
}
func (s *sAppAuth) PhoneSmsLogin(ctx context.Context, param *model.AppPhoneSmsLoginParam) (res *model.AppAuthTokenModel, err error) {
if param == nil {
return nil, gerror.New("请求参数不能为空")
}
param.TenantId = strings.TrimSpace(param.TenantId)
param.Phone = strings.TrimSpace(param.Phone)
param.SmsCode = strings.TrimSpace(param.SmsCode)
if err = verifyAppSmsCode(ctx, param.TenantId, param.Phone, "login", param.SmsCode); err != nil {
return nil, err
}
users, err := s.findAppWechatBindUsersByPhone(ctx, param.TenantId, param.Phone)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, gerror.New("手机号未注册")
}
if len(users) > 1 && param.SelectedUserId <= 0 {
return &model.AppAuthTokenModel{NeedSelect: true, Candidates: appLoginCandidateModels(ctx, users)}, nil
}
user := users[0]
if param.SelectedUserId > 0 {
user = nil
for _, item := range users {
if item != nil && item.UserId == param.SelectedUserId {
user = item
break
}
}
if user == nil {
return nil, gerror.New("选择的账号不属于当前手机号")
}
}
_ = deleteAppSmsCode(ctx, appSmsCacheKey(param.TenantId, param.Phone, "login"))
return s.buildAppAuthToken(ctx, user)
}
func (s *sAppAuth) WechatLogin(ctx context.Context, param *model.AppWechatLoginParam) (res *model.AppWechatLoginModel, err error) {
if param == nil {
return nil, gerror.New("请求参数不能为空")
}
tenantId := strings.TrimSpace(param.TenantId)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
session, err := appWechatMiniSessionByLoginCode(ctx, param.WxCode, false)
if err != nil {
return nil, err
}
social, err := s.getWechatSocialByOpenId(ctx, tenantId, session.OpenId)
if err != nil {
return nil, err
}
if social != nil && social.UserId > 0 {
user, err := s.getAppUserByIdForWechat(ctx, tenantId, social.UserId)
if err != nil {
return nil, err
}
token, err := s.buildAppAuthToken(ctx, user)
if err != nil {
return nil, err
}
return &model.AppWechatLoginModel{AppAuthTokenModel: *token}, nil
}
ticket, err := appCreateWechatBindTicket(ctx, tenantId, session.OpenId, session.UnionId)
if err != nil {
return nil, err
}
return &model.AppWechatLoginModel{
NeedBindPhone: true,
BindTicket: ticket,
BindExpireIn: int64(appWechatBindTicketExpire / time.Second),
}, nil
}
func (s *sAppAuth) WechatBindPhone(ctx context.Context, param *model.AppWechatBindPhoneParam) (res *model.AppWechatBindPhoneModel, err error) {
if param == nil {
return nil, gerror.New("请求参数不能为空")
}
tenantId := strings.TrimSpace(param.TenantId)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
phone := strings.TrimSpace(param.Phone)
if phone == "" {
return nil, gerror.New("手机号不能为空")
}
ticketData, err := appLoadWechatBindTicket(ctx, param.BindTicket)
if err != nil {
return nil, err
}
if ticketData.TenantId != tenantId {
return nil, gerror.New("微信绑定凭证与当前租户不匹配,请重新发起微信登录")
}
if err = appEnsureWechatBindPhoneVerified(ctx, param, ticketData, phone); err != nil {
return nil, err
}
users, err := s.findAppWechatBindUsersByPhone(ctx, tenantId, phone)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, gerror.New("该手机号未找到可绑定账号,请先注册或联系管理员分配账号")
}
if len(users) > 1 && param.SelectedUserId <= 0 {
return &model.AppWechatBindPhoneModel{
NeedBindPhone: true,
BindTicket: strings.TrimSpace(param.BindTicket),
AppAuthTokenModel: model.AppAuthTokenModel{
NeedSelect: true,
Candidates: appLoginCandidateModels(ctx, users),
},
}, nil
}
user := users[0]
if param.SelectedUserId > 0 {
user = nil
for _, item := range users {
if item != nil && item.UserId == param.SelectedUserId {
user = item
break
}
}
if user == nil {
return nil, gerror.New("选择的账号不属于当前手机号")
}
}
if err = s.saveWechatOpenId(ctx, user, ticketData.OpenId, ticketData.UnionId); err != nil {
return nil, err
}
_ = deleteAppSmsCode(ctx, appSmsCacheKey(tenantId, phone, "wechatBindPhone"))
_ = mcache.Remove(ctx, appWechatBindTicketKey(param.BindTicket))
token, err := s.buildAppAuthToken(ctx, user)
if err != nil {
return nil, err
}
return &model.AppWechatBindPhoneModel{AppAuthTokenModel: *token}, nil
}
func (s *sAppAuth) BindWechatOpenIdByLoginCode(ctx context.Context, user *entity.SysUser, wxCode string) error {
wxCode = strings.TrimSpace(wxCode)
if wxCode == "" {
return nil
}
if user == nil || user.UserId <= 0 {
return gerror.New("当前登录账号不存在,无法绑定微信openid")
}
session, err := appWechatMiniSessionByLoginCode(ctx, wxCode, true)
if err != nil {
return err
}
if session == nil {
return nil
}
return s.saveWechatOpenId(ctx, user, session.OpenId, session.UnionId)
}
func appWechatMiniSessionByLoginCode(ctx context.Context, wxCode string, skipWhenDisabled bool) (*appWechatCode2SessionRes, error) {
wxCode = strings.TrimSpace(wxCode)
if wxCode == "" {
return nil, gerror.New("微信登录凭证不能为空")
}
cfg, err := appLoadWechatConfig(ctx)
if err != nil {
return nil, err
}
if !cfg.Mini.Enabled {
if skipWhenDisabled {
return nil, nil
}
return nil, gerror.New("微信小程序登录未启用")
}
appId := strings.TrimSpace(cfg.Mini.AppId)
appSecret := strings.TrimSpace(cfg.Mini.AppSecret)
if appId == "" {
return nil, gerror.New("微信小程序AppID未配置,无法登录")
}
if appWechatMiniSecretMissing(appSecret) {
return nil, gerror.New("微信小程序AppSecret未配置,无法登录")
}
return appWechatMiniCode2Session(ctx, appId, appSecret, wxCode)
}
func appWechatMiniSecretMissing(secret string) bool {
secret = strings.TrimSpace(secret)
if secret == "" {
return true
}
upper := strings.ToUpper(secret)
return upper == appWechatMiniAppSecretPlaceholder ||
strings.Contains(upper, "CHANGE_ME") ||
strings.Contains(upper, "PLACEHOLDER") ||
strings.Contains(secret, "请填写")
}
func appWechatMiniCode2Session(ctx context.Context, appId string, appSecret string, wxCode string) (*appWechatCode2SessionRes, error) {
params := url.Values{}
params.Set("appid", appId)
params.Set("secret", appSecret)
params.Set("js_code", wxCode)
params.Set("grant_type", "authorization_code")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+params.Encode(), nil)
if err != nil {
return nil, gerror.Wrap(err, "创建微信openid换取请求失败")
}
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, gerror.Wrap(err, "请求微信openid失败")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, gerror.Wrap(err, "读取微信openid响应失败")
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, gerror.Newf("请求微信openid失败,HTTP状态码:%d", resp.StatusCode)
}
var result appWechatCode2SessionRes
if err = json.Unmarshal(body, &result); err != nil {
return nil, gerror.Wrap(err, "解析微信openid响应失败")
}
if result.ErrCode != 0 {
msg := strings.TrimSpace(result.ErrMsg)
if msg == "" {
msg = "微信返回错误"
}
return nil, gerror.Newf("换取微信openid失败:%s(%d)", msg, result.ErrCode)
}
if strings.TrimSpace(result.OpenId) == "" {
return nil, gerror.New("微信未返回openid")
}
return &result, nil
}
func appCreateWechatBindTicket(ctx context.Context, tenantId string, openId string, unionId string) (string, error) {
openId = strings.TrimSpace(openId)
if openId == "" {
return "", gerror.New("微信openid为空,无法生成绑定凭证")
}
ticket, err := appRandomHex(24)
if err != nil {
return "", gerror.Wrap(err, "生成微信绑定凭证失败")
}
data := &appWechatBindTicketCache{
TenantId: strings.TrimSpace(tenantId),
OpenId: openId,
UnionId: strings.TrimSpace(unionId),
}
if data.TenantId == "" {
data.TenantId = consts.DefaultSystemTenantCode
}
if err = mcache.Set(ctx, appWechatBindTicketKey(ticket), data, appWechatBindTicketExpire); err != nil {
return "", gerror.Wrap(err, "保存微信绑定凭证失败")
}
return ticket, nil
}
func appRandomHex(size int) (string, error) {
if size <= 0 {
size = 16
}
buf := make([]byte, size)
if _, err := cryptorand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
func appWechatBindTicketKey(ticket string) string {
return appWechatBindTicketCachePrefix + strings.TrimSpace(ticket)
}
func appLoadWechatBindTicket(ctx context.Context, ticket string) (*appWechatBindTicketCache, error) {
ticket = strings.TrimSpace(ticket)
if ticket == "" {
return nil, gerror.New("微信绑定凭证不能为空")
}
value, err := mcache.Get(ctx, appWechatBindTicketKey(ticket))
if err != nil || value == nil {
return nil, gerror.New("微信绑定凭证已过期,请重新发起微信登录")
}
var data appWechatBindTicketCache
if err = gvarToStruct(value, &data); err != nil {
return nil, gerror.Wrap(err, "读取微信绑定凭证失败")
}
data.TenantId = strings.TrimSpace(data.TenantId)
data.OpenId = strings.TrimSpace(data.OpenId)
data.UnionId = strings.TrimSpace(data.UnionId)
if data.TenantId == "" {
data.TenantId = consts.DefaultSystemTenantCode
}
if data.OpenId == "" {
return nil, gerror.New("微信绑定凭证无效,请重新发起微信登录")
}
return &data, nil
}
func gvarToStruct(value interface{}, pointer interface{}) error {
switch v := value.(type) {
case *gvar.Var:
return v.Scan(pointer)
case *appWechatBindTicketCache:
if data, ok := pointer.(*appWechatBindTicketCache); ok {
*data = *v
return nil
}
case appWechatBindTicketCache:
if data, ok := pointer.(*appWechatBindTicketCache); ok {
*data = v
return nil
}
}
bytes, err := json.Marshal(value)
if err != nil {
return err
}
return json.Unmarshal(bytes, pointer)
}
func appEnsureWechatBindPhoneVerified(ctx context.Context, param *model.AppWechatBindPhoneParam, ticketData *appWechatBindTicketCache, phone string) error {
if param == nil {
return gerror.New("请求参数不能为空")
}
if ticketData != nil && strings.TrimSpace(ticketData.VerifiedPhone) == strings.TrimSpace(phone) {
return nil
}
if ticketData == nil || strings.TrimSpace(ticketData.OpenId) == "" || strings.TrimSpace(phone) == "" {
return gerror.New("微信绑定参数不完整")
}
if err := verifyAppSmsCode(ctx, ticketData.TenantId, phone, "wechatBindPhone", param.SmsCode); err != nil {
return err
}
ticketData.VerifiedPhone = strings.TrimSpace(phone)
if err := mcache.Set(ctx, appWechatBindTicketKey(param.BindTicket), ticketData, appWechatBindTicketExpire); err != nil {
return gerror.Wrap(err, "保存手机号验证状态失败")
}
return nil
}
func (s *sAppAuth) getWechatSocialByOpenId(ctx context.Context, tenantId string, openId string) (*entity.SysSocial, error) {
openId = strings.TrimSpace(openId)
if openId == "" {
return nil, nil
}
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
var socials []*entity.SysSocial
err := dao.SysSocial.Ctx(ctx).
Where(dao.SysSocial.Columns().TenantId, tenantId).
Where(dao.SysSocial.Columns().Source, consts.SourceWechatMp).
Where(dao.SysSocial.Columns().OpenId, openId).
WhereNull(dao.SysSocial.Columns().DeletedAt).
OrderDesc(dao.SysSocial.Columns().Id).
Limit(2).
Scan(&socials)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerror.Wrap(err, "查询微信绑定账号失败")
}
if len(socials) == 0 {
return nil, nil
}
if len(socials) > 1 && socials[0] != nil && socials[1] != nil && socials[0].UserId != socials[1].UserId {
return nil, gerror.New("当前微信绑定了多个账号,请联系管理员处理")
}
return socials[0], nil
}
func (s *sAppAuth) getAppUserByIdForWechat(ctx context.Context, tenantId string, userId int64) (*entity.SysUser, error) {
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
var user *entity.SysUser
err := dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().UserId, userId).
WhereNull(dao.SysUser.Columns().DeletedAt).
Limit(1).
Scan(&user)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if user == nil || user.UserId == 0 {
return nil, gerror.New("微信绑定账号不存在")
}
if err = s.checkAppLoginUserStatus(user); err != nil {
return nil, err
}
return user, nil
}
func (s *sAppAuth) findAppWechatBindUsersByPhone(ctx context.Context, tenantId string, phone string) ([]*entity.SysUser, error) {
tenantId = strings.TrimSpace(tenantId)
phone = strings.TrimSpace(phone)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
if phone == "" {
return nil, gerror.New("手机号不能为空")
}
var users []*entity.SysUser
err := dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().Phonenumber, phone).
WhereNull(dao.SysUser.Columns().DeletedAt).
OrderAsc(dao.SysUser.Columns().UserId).
Scan(&users)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
available := make([]*entity.SysUser, 0, len(users))
var firstStatusErr error
for _, user := range users {
if user == nil || user.UserId <= 0 {
continue
}
if err = s.checkAppLoginUserStatus(user); err != nil {
if firstStatusErr == nil {
firstStatusErr = err
}
continue
}
available = append(available, user)
}
if len(available) == 0 && firstStatusErr != nil {
return nil, firstStatusErr
}
return available, nil
}
func (s *sAppAuth) saveWechatOpenId(ctx context.Context, user *entity.SysUser, openId string, unionId string) error {
openId = strings.TrimSpace(openId)
if openId == "" {
return gerror.New("微信openid为空,无法绑定")
}
if user == nil || user.UserId <= 0 {
return gerror.New("绑定账号不存在")
}
if user.TenantId == "" {
user.TenantId = consts.DefaultSystemTenantCode
}
now := gtime.Now()
if err := s.ensureWechatOpenIdNotBoundToOtherUser(ctx, user, openId); err != nil {
return err
}
var social *entity.SysSocial
err := dao.SysSocial.Ctx(ctx).
Where(dao.SysSocial.Columns().TenantId, user.TenantId).
Where(dao.SysSocial.Columns().UserId, user.UserId).
Where(dao.SysSocial.Columns().Source, consts.SourceWechatMp).
WhereNull(dao.SysSocial.Columns().DeletedAt).
OrderDesc(dao.SysSocial.Columns().Id).
Limit(1).
Scan(&social)
if err != nil && err != gorm.ErrRecordNotFound {
return gerror.Wrap(err, "查询微信openid绑定信息失败")
}
data := do.SysSocial{
UserId: user.UserId,
TenantId: user.TenantId,
AuthId: consts.SourceWechatMp + ":" + openId,
Source: consts.SourceWechatMp,
OpenId: openId,
UserName: strings.TrimSpace(user.UserName),
NickName: strings.TrimSpace(user.NickName),
Avatar: strings.TrimSpace(user.Avatar),
AccessToken: openId,
UnionId: strings.TrimSpace(unionId),
UpdatedBy: user.UserId,
UpdatedAt: now,
CreatedDept: user.DeptId,
}
if social != nil && social.Id > 0 {
_, err = dao.SysSocial.Ctx(ctx).
WherePri(social.Id).
Data(data).
OmitNil().
Update()
if err != nil {
g.Log().Errorf(ctx, "更新微信openid绑定失败 userId=%d source=%s err=%v", user.UserId, consts.SourceWechatMp, err)
return gerror.New("绑定微信openid失败,请稍后重试")
}
return nil
}
data.CreatedBy = user.UserId
data.CreatedAt = now
_, err = dao.SysSocial.Ctx(ctx).Data(data).OmitNil().Insert()
if err != nil {
g.Log().Errorf(ctx, "创建微信openid绑定失败 userId=%d source=%s err=%v", user.UserId, consts.SourceWechatMp, err)
return gerror.New("绑定微信openid失败,请稍后重试")
}
return nil
}
func (s *sAppAuth) ensureWechatOpenIdNotBoundToOtherUser(ctx context.Context, user *entity.SysUser, openId string) error {
var exists *entity.SysSocial
err := dao.SysSocial.Ctx(ctx).
Fields(dao.SysSocial.Columns().Id, dao.SysSocial.Columns().UserId).
Where(dao.SysSocial.Columns().TenantId, user.TenantId).
Where(dao.SysSocial.Columns().Source, consts.SourceWechatMp).
Where(dao.SysSocial.Columns().OpenId, strings.TrimSpace(openId)).
WhereNot(dao.SysSocial.Columns().UserId, user.UserId).
WhereNull(dao.SysSocial.Columns().DeletedAt).
Limit(1).
Scan(&exists)
if err != nil && err != gorm.ErrRecordNotFound {
return gerror.Wrap(err, "检查微信绑定冲突失败")
}
if exists != nil && exists.Id > 0 {
return gerror.New("当前微信已绑定其他账号,请先解绑或更换微信")
}
return nil
}
func (s *sAppAuth) buildAppAuthToken(ctx context.Context, user *entity.SysUser) (*model.AppAuthTokenModel, error) {
if user == nil || user.UserId == 0 {
return nil, gerror.New("账号不存在")
}
ip := g.RequestFromCtx(ctx).GetClientIp()
if err := service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil {
g.Log().Warningf(ctx, "APP账号登录更新登录信息失败 userId=%d err=%v", user.UserId, err)
}
userInfo, err := getAppUserInfoById(ctx, user.UserId)
if err != nil {
return nil, err
}
ctx = contexts.Set(ctx, &model.Context{
User: &model.Identity{
BaseClaims: model.BaseClaims{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
TenantId: user.TenantId,
DeptId: user.DeptId,
Roles: []model.Role{},
LoginAt: time.Now().Unix(),
},
},
})
_, token, err := service.SysAuth().GenerateToken(ctx, &model.LoginUserOut{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
DeptId: user.DeptId,
})
if err != nil {
return nil, err
}
expiresText := g.Cfg().MustGet(ctx, "jwt.expiresTime", "7d").String()
expiresDuration, _ := utility.ParseDuration(expiresText)
return &model.AppAuthTokenModel{
Token: token,
ExpiresIn: int64(expiresDuration / time.Second),
UserInfo: userInfo,
}, nil
}
func (s *sAppAuth) ForgotPassword(ctx context.Context, param *model.AppForgotPasswordParam) (err error) {
if err = verifyAppSmsCode(ctx, param.TenantId, param.Phone, "forgotPassword", param.SmsCode); err != nil {
return err
}
user, err := s.getUserByPhone(ctx, param.TenantId, param.Phone)
if err != nil {
return err
}
_ = deleteAppSmsCode(ctx, appSmsCacheKey(param.TenantId, param.Phone, "forgotPassword"))
salt := utility.RandomString(5)
password := appPasswordEncryptForStore(param.Password, salt)
_, err = dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().UserId, user.UserId).
Data(do.SysUser{
Password: password,
Salt: salt,
UpdatedBy: user.UserId,
UpdatedAt: gtime.Now(),
}).
OmitNil().
Update()
return err
}
func appLoginAccount(param *model.AppPhoneLoginParam) string {
if param == nil {
return ""
}
if account := strings.TrimSpace(param.Account); account != "" {
return account
}
if username := strings.TrimSpace(param.Username); username != "" {
return username
}
return strings.TrimSpace(param.Phone)
}
func appPasswordEncryptForStore(password string, salt string) string {
return utility.PasswordEncrypt(utility.Md5(password), salt)
}
func appPasswordMatches(password string, salt string, storedPassword string) bool {
return appPasswordEncryptForStore(password, salt) == storedPassword
}
func (s *sAppAuth) resolveAppLoginUser(ctx context.Context, param *model.AppPhoneLoginParam) (*entity.SysUser, []*model.AppLoginCandidateModel, error) {
account := appLoginAccount(param)
if account == "" {
return nil, nil, gerror.New("请输入手机号或用户名")
}
tenantId := strings.TrimSpace(param.TenantId)
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
if param.SelectedUserId > 0 {
user, err := s.getSelectedAppLoginUser(ctx, tenantId, account, param.SelectedUserId)
if err != nil {
return nil, nil, err
}
if !appPasswordMatches(param.Password, user.Salt, user.Password) {
return nil, nil, gerror.New("账号或密码错误")
}
return user, nil, s.checkAppLoginUserStatus(user)
}
users, err := s.findAppLoginUsersByAccount(ctx, tenantId, account)
if err != nil {
return nil, nil, err
}
switch len(users) {
case 0:
return nil, nil, gerror.New("账号或密码错误")
case 1:
user := users[0]
if !appPasswordMatches(param.Password, user.Salt, user.Password) {
return nil, nil, gerror.New("账号或密码错误")
}
return user, nil, s.checkAppLoginUserStatus(user)
default:
matchedUsers, err := s.availableAppLoginUsersByPassword(users, param.Password)
if err != nil {
return nil, nil, err
}
switch len(matchedUsers) {
case 0:
return nil, nil, gerror.New("账号或密码错误")
case 1:
return matchedUsers[0], nil, nil
default:
return nil, appLoginCandidateModels(ctx, matchedUsers), nil
}
}
}
func (s *sAppAuth) getSelectedAppLoginUser(ctx context.Context, tenantId string, account string, userId int64) (*entity.SysUser, error) {
var user *entity.SysUser
err := dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().UserId, userId).
Where("("+dao.SysUser.Columns().UserName+" = ? OR "+dao.SysUser.Columns().Phonenumber+" = ?)", account, account).
Limit(1).
Scan(&user)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if user == nil || user.UserId == 0 {
return nil, gerror.New("选择的登录账号不存在")
}
return user, nil
}
func (s *sAppAuth) findAppLoginUsersByAccount(ctx context.Context, tenantId string, account string) ([]*entity.SysUser, error) {
var users []*entity.SysUser
account = strings.TrimSpace(account)
err := dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where("("+dao.SysUser.Columns().UserName+" = ? OR "+dao.SysUser.Columns().Phonenumber+" = ?)", account, account).
OrderAsc(dao.SysUser.Columns().UserId).
Scan(&users)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if len(users) <= 1 {
return users, nil
}
unique := make([]*entity.SysUser, 0, len(users))
seen := make(map[int64]struct{}, len(users))
for _, user := range users {
if user == nil || user.UserId == 0 {
continue
}
if _, ok := seen[user.UserId]; ok {
continue
}
seen[user.UserId] = struct{}{}
unique = append(unique, user)
}
return unique, nil
}
func (s *sAppAuth) availableAppLoginUsersByPassword(users []*entity.SysUser, password string) ([]*entity.SysUser, error) {
matched := make([]*entity.SysUser, 0, len(users))
var firstStatusErr error
for _, user := range users {
if user == nil || user.UserId == 0 {
continue
}
if !appPasswordMatches(password, user.Salt, user.Password) {
continue
}
if err := s.checkAppLoginUserStatus(user); err != nil {
if firstStatusErr == nil {
firstStatusErr = err
}
continue
}
matched = append(matched, user)
}
if len(matched) == 0 && firstStatusErr != nil {
return nil, firstStatusErr
}
return matched, nil
}
func appLoginCandidateModels(ctx context.Context, users []*entity.SysUser) []*model.AppLoginCandidateModel {
deptNames := appLoginDeptNames(ctx, users)
candidates := make([]*model.AppLoginCandidateModel, 0, len(users))
for _, user := range users {
if user == nil || user.UserId == 0 {
continue
}
role := "企业用户"
if appIsTerminalUser(ctx, user) {
role = "终端用户"
}
candidates = append(candidates, &model.AppLoginCandidateModel{
UserId: user.UserId,
Username: strings.TrimSpace(user.UserName),
NickName: strings.TrimSpace(user.NickName),
Phone: appMaskPhone(user.Phonenumber),
DeptId: user.DeptId,
DeptName: deptNames[user.DeptId],
UserType: strings.TrimSpace(user.UserType),
Role: role,
})
}
return candidates
}
func appLoginDeptNames(ctx context.Context, users []*entity.SysUser) map[int64]string {
deptIds := make([]int64, 0, len(users))
seen := make(map[int64]struct{}, len(users))
for _, user := range users {
if user == nil || user.DeptId <= 0 {
continue
}
if _, exists := seen[user.DeptId]; exists {
continue
}
seen[user.DeptId] = struct{}{}
deptIds = append(deptIds, user.DeptId)
}
result := make(map[int64]string, len(deptIds))
if len(deptIds) == 0 {
return result
}
var depts []*entity.SysDept
if err := dao.SysDept.Ctx(ctx).
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().DeptName).
WhereIn(dao.SysDept.Columns().DeptId, deptIds).
Scan(&depts); err != nil {
return result
}
for _, dept := range depts {
if dept != nil && dept.DeptId > 0 {
result[dept.DeptId] = strings.TrimSpace(dept.DeptName)
}
}
return result
}
func (s *sAppAuth) getUserByAccount(ctx context.Context, tenantId string, account string) (user *entity.SysUser, err error) {
account = strings.TrimSpace(account)
if account == "" {
return nil, gerror.New("请输入手机号或用户名")
}
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
err = dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().UserName, account).
Scan(&user)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if user != nil {
return user, s.checkAppLoginUserStatus(user)
}
var users []*entity.SysUser
err = dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().Phonenumber, account).
Limit(2).
Scan(&users)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if len(users) > 1 {
return nil, gerror.New("该手机号绑定多个账号,请使用用户名登录")
}
if len(users) == 0 || users[0] == nil {
return nil, gerror.New("账号不存在")
}
return users[0], s.checkAppLoginUserStatus(users[0])
}
func (s *sAppAuth) getUserByPhone(ctx context.Context, tenantId string, phone string) (user *entity.SysUser, err error) {
if tenantId == "" {
tenantId = consts.DefaultSystemTenantCode
}
var users []*entity.SysUser
err = dao.SysUser.Ctx(ctx).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().Phonenumber, phone).
Limit(2).
Scan(&users)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if len(users) > 1 {
return nil, gerror.New("该手机号绑定多个账号,请联系管理员处理")
}
if len(users) == 0 || users[0] == nil {
return nil, gerror.New("手机号未注册")
}
user = users[0]
return user, s.checkAppLoginUserStatus(user)
}
func (s *sAppAuth) checkAppLoginUserStatus(user *entity.SysUser) error {
if user == nil {
return gerror.New("账号不存在")
}
switch user.Status {
case consts.SysUserStatusDisable:
return gerror.New("账号已停用")
case consts.SysUserStatusLocked:
return gerror.New("账号已锁定")
case consts.SysUserStatusExpired:
return gerror.New("账号已过期")
case consts.SysUserStatusDeleted:
return gerror.New("账号已删除")
}
return nil
}