|
@@ -0,0 +1,192 @@
|
|
|
|
|
+package services
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "app/commons/core/redisclient"
|
|
|
|
|
+ "app/commons/model/entity"
|
|
|
|
|
+ "context"
|
|
|
|
|
+ "crypto/rand"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "math/big"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "gorm.io/gorm"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type TgBindService struct {
|
|
|
|
|
+ CommonService
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const (
|
|
|
|
|
+ redisKeyBindToken = "tg_bind_token:%s" // 绑定令牌
|
|
|
|
|
+ redisKeyBindLimit = "tg_bind_limit:%d" // 生成限频
|
|
|
|
|
+ redisKeyBindCache = "tg_bind_cache:%d" // 绑定缓存
|
|
|
|
|
+ bindTokenExpire = 5 * time.Minute // 令牌有效期
|
|
|
|
|
+ bindLimitExpire = 60 * time.Second // 限频间隔
|
|
|
|
|
+ bindCacheExpire = 1 * time.Hour // 缓存有效期
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type bindTokenData struct {
|
|
|
|
|
+ TelegramId int64 `json:"telegramId"`
|
|
|
|
|
+ TelegramUsername string `json:"telegramUsername"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GenerateBindToken 生成绑定令牌
|
|
|
|
|
+func (s *TgBindService) GenerateBindToken(telegramId int64, telegramUsername string) (string, error) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ rdb := redisclient.DefaultClient()
|
|
|
|
|
+
|
|
|
|
|
+ // 限频检查
|
|
|
|
|
+ limitKey := fmt.Sprintf(redisKeyBindLimit, telegramId)
|
|
|
|
|
+ if rdb.Exists(ctx, limitKey).Val() > 0 {
|
|
|
|
|
+ return "", errors.New("请稍后再试(60秒内只能生成一次)")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生成6位码
|
|
|
|
|
+ token := s.randomAlphanumeric(6)
|
|
|
|
|
+
|
|
|
|
|
+ // 存 Redis
|
|
|
|
|
+ tokenKey := fmt.Sprintf(redisKeyBindToken, token)
|
|
|
|
|
+ data := bindTokenData{
|
|
|
|
|
+ TelegramId: telegramId,
|
|
|
|
|
+ TelegramUsername: telegramUsername,
|
|
|
|
|
+ }
|
|
|
|
|
+ jsonBytes, _ := json.Marshal(data)
|
|
|
|
|
+ rdb.Set(ctx, tokenKey, string(jsonBytes), bindTokenExpire)
|
|
|
|
|
+
|
|
|
|
|
+ // 存 DB
|
|
|
|
|
+ bindToken := &entity.TgBindToken{
|
|
|
|
|
+ Token: token,
|
|
|
|
|
+ TelegramId: telegramId,
|
|
|
|
|
+ TelegramUsername: telegramUsername,
|
|
|
|
|
+ Status: 0,
|
|
|
|
|
+ ExpireAt: time.Now().Add(bindTokenExpire),
|
|
|
|
|
+ }
|
|
|
|
|
+ s.DB().Create(bindToken)
|
|
|
|
|
+
|
|
|
|
|
+ // 设置限频
|
|
|
|
|
+ rdb.Set(ctx, limitKey, "1", bindLimitExpire)
|
|
|
|
|
+
|
|
|
|
|
+ return token, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// BindTelegramUser 使用令牌绑定 Telegram 用户到平台账户
|
|
|
|
|
+func (s *TgBindService) BindTelegramUser(userId int64, token string) (*entity.TgUserBind, error) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ rdb := redisclient.DefaultClient()
|
|
|
|
|
+
|
|
|
|
|
+ // 从 Redis 查令牌
|
|
|
|
|
+ tokenKey := fmt.Sprintf(redisKeyBindToken, token)
|
|
|
|
|
+ val, err := rdb.Get(ctx, tokenKey).Result()
|
|
|
|
|
+
|
|
|
|
|
+ var data bindTokenData
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ json.Unmarshal([]byte(val), &data)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Redis 没有,查 DB
|
|
|
|
|
+ var dbToken entity.TgBindToken
|
|
|
|
|
+ if err := s.DB().Where("token = ? AND status = 0", token).First(&dbToken).Error; err != nil {
|
|
|
|
|
+ return nil, errors.New("绑定码无效或已过期")
|
|
|
|
|
+ }
|
|
|
|
|
+ if time.Now().After(dbToken.ExpireAt) {
|
|
|
|
|
+ return nil, errors.New("绑定码已过期")
|
|
|
|
|
+ }
|
|
|
|
|
+ data.TelegramId = dbToken.TelegramId
|
|
|
|
|
+ data.TelegramUsername = dbToken.TelegramUsername
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查 telegramId 是否已绑定其他用户
|
|
|
|
|
+ var existBind entity.TgUserBind
|
|
|
|
|
+ err = s.DB().Where("telegram_id = ? AND bind_status = 1", data.TelegramId).First(&existBind).Error
|
|
|
|
|
+ if err == nil && existBind.UserId != userId {
|
|
|
|
|
+ return nil, errors.New("该Telegram账户已绑定其他平台账户")
|
|
|
|
|
+ }
|
|
|
|
|
+ if err == nil && existBind.UserId == userId {
|
|
|
|
|
+ // 已绑定同一用户,直接返回
|
|
|
|
|
+ return &existBind, nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查 userId 是否已绑定其他 telegramId
|
|
|
|
|
+ err = s.DB().Where("user_id = ? AND bind_status = 1", userId).First(&existBind).Error
|
|
|
|
|
+ if err == nil && existBind.TelegramId != data.TelegramId {
|
|
|
|
|
+ return nil, errors.New("该平台账户已绑定其他Telegram账户")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建绑定记录
|
|
|
|
|
+ bind := &entity.TgUserBind{
|
|
|
|
|
+ UserId: userId,
|
|
|
|
|
+ TelegramId: data.TelegramId,
|
|
|
|
|
+ TelegramUsername: data.TelegramUsername,
|
|
|
|
|
+ TelegramFirstName: "",
|
|
|
|
|
+ BindStatus: 1,
|
|
|
|
|
+ BindTime: time.Now(),
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := s.DB().Create(bind).Error; err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("绑定失败: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新令牌状态
|
|
|
|
|
+ s.DB().Model(&entity.TgBindToken{}).Where("token = ?", token).Updates(map[string]interface{}{
|
|
|
|
|
+ "status": 1,
|
|
|
|
|
+ "user_id": userId,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 删除 Redis 令牌,设置绑定缓存
|
|
|
|
|
+ rdb.Del(ctx, tokenKey)
|
|
|
|
|
+ cacheKey := fmt.Sprintf(redisKeyBindCache, data.TelegramId)
|
|
|
|
|
+ rdb.Set(ctx, cacheKey, fmt.Sprintf("%d", userId), bindCacheExpire)
|
|
|
|
|
+
|
|
|
|
|
+ return bind, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// IsUserBound 检查 telegramId 是否已绑定,返回 (是否绑定, userId)
|
|
|
|
|
+func (s *TgBindService) IsUserBound(telegramId int64) (bool, int64) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ rdb := redisclient.DefaultClient()
|
|
|
|
|
+
|
|
|
|
|
+ // 查 Redis 缓存
|
|
|
|
|
+ cacheKey := fmt.Sprintf(redisKeyBindCache, telegramId)
|
|
|
|
|
+ val, err := rdb.Get(ctx, cacheKey).Result()
|
|
|
|
|
+ if err == nil && val != "" {
|
|
|
|
|
+ var userId int64
|
|
|
|
|
+ fmt.Sscanf(val, "%d", &userId)
|
|
|
|
|
+ if userId > 0 {
|
|
|
|
|
+ return true, userId
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查 DB
|
|
|
|
|
+ var bind entity.TgUserBind
|
|
|
|
|
+ err = s.DB().Where("telegram_id = ? AND bind_status = 1", telegramId).First(&bind).Error
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
+ return false, 0
|
|
|
|
|
+ }
|
|
|
|
|
+ return false, 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 写入缓存
|
|
|
|
|
+ rdb.Set(ctx, cacheKey, fmt.Sprintf("%d", bind.UserId), bindCacheExpire)
|
|
|
|
|
+ return true, bind.UserId
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetBindByUserId 根据平台 userId 查询绑定记录
|
|
|
|
|
+func (s *TgBindService) GetBindByUserId(userId int64) (*entity.TgUserBind, error) {
|
|
|
|
|
+ var bind entity.TgUserBind
|
|
|
|
|
+ if err := s.DB().Where("user_id = ? AND bind_status = 1", userId).First(&bind).Error; err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return &bind, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// randomAlphanumeric 生成 n 位大写字母+数字随机码
|
|
|
|
|
+func (s *TgBindService) randomAlphanumeric(n int) string {
|
|
|
|
|
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
|
+ result := make([]byte, n)
|
|
|
|
|
+ for i := range result {
|
|
|
|
|
+ num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
|
|
|
+ result[i] = charset[num.Int64()]
|
|
|
|
|
+ }
|
|
|
|
|
+ return string(result)
|
|
|
|
|
+}
|