package services import ( "app/commons/core/redisclient" "app/commons/model/entity" "context" "crypto/rand" "encoding/json" "errors" "fmt" "math/big" "time" ) 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 检查是否已绑定,返回 (是否绑定, userId) // 优先用 telegramId 查,查不到时用 telegramUsername 兜底(后台手动绑定场景) // 兜底匹配成功后自动回填 telegram_id,后续直接用 id 查 func (s *TgBindService) IsUserBound(telegramId int64, telegramUsername ...string) (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 —— 优先用 telegram_id var bind entity.TgUserBind err = s.DB().Where("telegram_id = ? AND bind_status = 1", telegramId).First(&bind).Error if err != nil { // telegram_id 查不到,用 telegram_username 兜底 if len(telegramUsername) > 0 && telegramUsername[0] != "" { err = s.DB().Where("telegram_username = ? AND bind_status = 1", telegramUsername[0]).First(&bind).Error if err == nil { // 自动回填 telegram_id,下次直接用 id 查 s.DB().Model(&bind).Updates(map[string]interface{}{ "telegram_id": telegramId, }) bind.TelegramId = telegramId } } if err != nil { 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) }