1533 lines
46 KiB
Go
1533 lines
46 KiB
Go
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
|
||
}
|