├── main.go ├── .gitignore ├── internal ├── model │ ├── tguser.go │ ├── lotteryrecord.go │ ├── chatdiceconfig.go │ └── betrecord.go ├── bot │ ├── lock.go │ ├── bot.go │ └── handler.go └── database │ └── database.go ├── go.mod ├── Dockerfile ├── docker-compose.yml ├── go.sum └── README.md /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "tg-dice-bot/internal/bot" 5 | ) 6 | 7 | func main() { 8 | bot.StartBot() 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea 25 | package-lock.json 26 | yarn.lock -------------------------------------------------------------------------------- /internal/model/tguser.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TgUser struct { 4 | ID int `gorm:"primaryKey"` 5 | TgUserID int64 `json:"tg_user_id" gorm:"type:bigint(20);not null"` // Telegram 用户ID 6 | ChatID int64 `json:"chat_id" gorm:"type:bigint(20);not null;index"` 7 | Username string `json:"username" gorm:"type:varchar(500);not null"` // Telegram 用户名 8 | Balance int `json:"balance" gorm:"type:int(11);not null"` 9 | SignInTime string `json:"sign_in_time" gorm:"type:varchar(500)"` // 签到时间 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module tg-dice-bot 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.5 7 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 8 | gorm.io/driver/mysql v1.5.2 9 | gorm.io/gorm v1.25.5 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | github.com/go-sql-driver/mysql v1.7.0 // indirect 16 | github.com/jinzhu/inflection v1.0.0 // indirect 17 | github.com/jinzhu/now v1.1.5 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Golang 镜像作为构建阶段 2 | FROM golang AS builder 3 | 4 | # 设置环境变量 5 | ENV GO111MODULE=on \ 6 | CGO_ENABLED=0 \ 7 | GOOS=linux 8 | 9 | # 设置工作目录 10 | WORKDIR /build 11 | 12 | # 复制 go.mod 和 go.sum 文件,先下载依赖 13 | COPY go.mod go.sum ./ 14 | ENV GOPROXY=https://goproxy.cn,direct 15 | RUN go mod download 16 | 17 | # 复制整个项目并构建可执行文件 18 | COPY . . 19 | RUN go build -o /tg-dice-bot 20 | 21 | # 使用 Alpine 镜像作为最终镜像 22 | FROM alpine 23 | 24 | # 安装基本的运行时依赖 25 | RUN apk --no-cache add ca-certificates tzdata 26 | 27 | # 从构建阶段复制可执行文件 28 | COPY --from=builder /tg-dice-bot . 29 | 30 | # 暴露端口 31 | EXPOSE 3000 32 | # 工作目录 33 | WORKDIR /data 34 | # 设置入口命令 35 | ENTRYPOINT ["/tg-dice-bot"] 36 | -------------------------------------------------------------------------------- /internal/bot/lock.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import "sync" 4 | 5 | // 在包级别定义一个映射,存储每个userID对应的互斥锁 6 | var userLocks = make(map[int64]*sync.Mutex) 7 | var userLocksMutex sync.Mutex 8 | 9 | var chatLocks = make(map[int64]*sync.Mutex) 10 | var chatLocksMutex sync.Mutex 11 | 12 | // getUserLock 根据userID获取对应的互斥锁,如果不存在则创建一个新的锁 13 | func getUserLock(userID int64) *sync.Mutex { 14 | userLocksMutex.Lock() 15 | defer userLocksMutex.Unlock() 16 | 17 | if _, ok := userLocks[userID]; !ok { 18 | userLocks[userID] = &sync.Mutex{} 19 | } 20 | 21 | return userLocks[userID] 22 | } 23 | 24 | // getUserLock 根据userID获取对应的互斥锁,如果不存在则创建一个新的锁 25 | func getChatLock(chatId int64) *sync.Mutex { 26 | chatLocksMutex.Lock() 27 | defer chatLocksMutex.Unlock() 28 | 29 | if _, ok := userLocks[chatId]; !ok { 30 | chatLocks[chatId] = &sync.Mutex{} 31 | } 32 | 33 | return chatLocks[chatId] 34 | } 35 | -------------------------------------------------------------------------------- /internal/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "log" 9 | ) 10 | 11 | const ( 12 | DBConnectionString = "MYSQL_DSN" 13 | RedisDBConnectionString = "REDIS_CONN_STRING" 14 | ) 15 | 16 | func InitDB(dsn string) (*gorm.DB, error) { 17 | var err error 18 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ 19 | Logger: logger.Default.LogMode(logger.Info), 20 | }) 21 | if err != nil { 22 | log.Fatal("连接数据库失败:", err) 23 | return nil, err 24 | } 25 | 26 | return db, nil 27 | } 28 | func InitRedisDB(dsn string) (*redis.Client, error) { 29 | options, err := redis.ParseURL(dsn) 30 | if err != nil { 31 | log.Fatal("解析 Redis URL 失败:", err) 32 | } 33 | 34 | redisDB := redis.NewClient(options) 35 | 36 | _, err = redisDB.Ping(redisDB.Context()).Result() 37 | if err != nil { 38 | log.Fatal("连接到 Redis 失败:", err) 39 | } 40 | log.Printf("已连接到 Redis %s", dsn) 41 | return redisDB, nil 42 | } 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | tg-dice-bot: 5 | image: ghcr.io/deanxv/tg-dice-bot:latest 6 | container_name: tg-dice-bot 7 | restart: always 8 | volumes: 9 | - ./data/tgdicebot:/data 10 | environment: 11 | - MYSQL_DSN=tgdicebot:123456@tcp(db:3306)/dice_bot # 可修改此行 SQL连接信息 12 | - REDIS_CONN_STRING=redis://redis 13 | - TZ=Asia/Shanghai 14 | - TELEGRAM_API_TOKEN=6830xxxxxxxxxxxxxxxx3GawBHc7ywDuU # 必须修改此行telegram-bot的token 15 | depends_on: 16 | - redis 17 | - db 18 | 19 | redis: 20 | image: redis:latest 21 | container_name: redis 22 | restart: always 23 | 24 | db: 25 | image: mysql:8.2.0 26 | restart: always 27 | container_name: mysql 28 | volumes: 29 | - ./data/mysql:/var/lib/mysql # 挂载目录,持久化存储 30 | ports: 31 | - '3306:3306' 32 | environment: 33 | TZ: Asia/Shanghai # 可修改默认时区 34 | MYSQL_ROOT_PASSWORD: 'root@123456' # 可修改此行 root用户名 密码 35 | MYSQL_USER: tgdicebot # 可修改初始化专用用户用户名 36 | MYSQL_PASSWORD: '123456' # 可修改初始化专用用户密码 37 | MYSQL_DATABASE: dice_bot # 可修改初始化专用数据库 38 | -------------------------------------------------------------------------------- /internal/model/lotteryrecord.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | type LotteryRecord struct { 6 | ID uint `gorm:"primarykey"` 7 | ChatID int64 `json:"chat_id" gorm:"type:bigint(20);not null;index"` 8 | IssueNumber string `json:"issue_number" gorm:"type:varchar(64);not null"` 9 | ValueA int `json:"value_a" gorm:"type:int(11);not null"` 10 | ValueB int `json:"value_b" gorm:"type:int(11);not null"` 11 | ValueC int `json:"value_c" gorm:"type:int(11);not null"` 12 | Total int `json:"total" gorm:"type:int(11);not null"` 13 | SingleDouble string `json:"single_double" gorm:"type:varchar(255);not null"` 14 | BigSmall string `json:"big_small" gorm:"type:varchar(255);not null"` 15 | Triplet int `json:"triplet" gorm:"type:int(11);not null"` 16 | Timestamp string `json:"timestamp" gorm:"type:varchar(255);not null"` 17 | } 18 | 19 | func GetAllRecordsByChatID(db *gorm.DB, chatID int64) ([]LotteryRecord, error) { 20 | var records []LotteryRecord 21 | 22 | result := db.Where("chat_id = ?", chatID).Limit(10).Order("issue_number desc").Find(&records) 23 | if result.Error != nil { 24 | return nil, result.Error 25 | } 26 | 27 | return records, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/model/chatdiceconfig.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | type ChatDiceConfig struct { 6 | ID int `gorm:"primaryKey"` 7 | ChatID int64 `json:"chat_id" gorm:"type:bigint(20);not null;index"` 8 | LotteryDrawCycle int `json:"lottery_draw_cycle" gorm:"type:int(11);not null"` // 开奖周期(分钟) 9 | Enable int `json:"enable" gorm:"type:int(11);not null"` // 开启状态 10 | } 11 | 12 | func ListByEnable(db *gorm.DB, enable int) ([]*ChatDiceConfig, error) { 13 | var records []*ChatDiceConfig 14 | 15 | result := db.Where("enable = ?", enable).Find(&records) 16 | if result.Error != nil { 17 | return nil, result.Error 18 | } 19 | 20 | return records, nil 21 | } 22 | 23 | func GetByEnableAndChatId(db *gorm.DB, enable int, chatID int64) (*ChatDiceConfig, error) { 24 | var chatDiceConfig *ChatDiceConfig 25 | result := db.Where("enable = ? AND chat_id = ?", enable, chatID).First(&chatDiceConfig) 26 | if result.Error != nil { 27 | return nil, result.Error 28 | } 29 | return chatDiceConfig, nil 30 | } 31 | 32 | func GetByChatId(db *gorm.DB, chatID int64) (*ChatDiceConfig, error) { 33 | var chatDiceConfig *ChatDiceConfig 34 | result := db.Where("chat_id = ?", chatID).First(&chatDiceConfig) 35 | if result.Error != nil { 36 | return nil, result.Error 37 | } 38 | return chatDiceConfig, nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/model/betrecord.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | type BetRecord struct { 6 | ID uint `gorm:"primarykey"` 7 | TgUserID int64 `json:"tg_user_id" gorm:"type:bigint(20);not null"` // 用户ID 8 | ChatID int64 `json:"chat_id" gorm:"type:bigint(20);not null;index"` 9 | IssueNumber string `json:"issue_number" gorm:"type:varchar(64);not null"` 10 | BetType string `json:"bet_type" gorm:"type:varchar(64);not null"` // 下注类型 11 | BetAmount int `json:"bet_amount" gorm:"type:int(11);not null"` // 下注金额 12 | SettleStatus int `json:"settle_status" gorm:"type:int(11);not null"` // 结算状态 13 | BetResultType *int `json:"bet_result_type" gorm:"type:int(11);default:null"` // 下注结果输赢 14 | UpdateTime string `json:"update_time" gorm:"type:varchar(255);not null"` 15 | CreateTime string `json:"create_time" gorm:"type:varchar(255);not null"` 16 | } 17 | 18 | // GetBetRecordsByChatIDAndIssue 根据对话ID和期号获取用户下注记录 19 | func GetBetRecordsByChatIDAndIssue(db *gorm.DB, chatID int64, issueNumber string) ([]*BetRecord, error) { 20 | var betRecords []*BetRecord 21 | result := db.Where("chat_id = ? AND issue_number = ?", chatID, issueNumber).Find(&betRecords) 22 | if result.Error != nil { 23 | return nil, result.Error 24 | } 25 | return betRecords, nil 26 | } 27 | 28 | // ListBySettleStatus 29 | func ListBySettleStatus(db *gorm.DB, betRecord *BetRecord) ([]*BetRecord, error) { 30 | var betRecords []*BetRecord 31 | result := db.Where("tg_user_id = ? AND chat_id = ? AND settle_status = ?", betRecord.TgUserID, betRecord.ChatID, 0).Find(&betRecords) 32 | if result.Error != nil { 33 | return nil, result.Error 34 | } 35 | return betRecords, nil 36 | } 37 | 38 | func ListByChatAndUser(db *gorm.DB, betRecord *BetRecord) ([]*BetRecord, error) { 39 | var betRecords []*BetRecord 40 | result := db.Where("tg_user_id = ? AND chat_id = ?", betRecord.TgUserID, betRecord.ChatID).Limit(10).Order("issue_number desc").Find(&betRecords) 41 | if result.Error != nil { 42 | return nil, result.Error 43 | } 44 | return betRecords, nil 45 | } 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 4 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 5 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 6 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 7 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 8 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 9 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 10 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 11 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 12 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 13 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 14 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 15 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 16 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 17 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 18 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 19 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 20 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 21 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 22 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 23 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 24 | gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= 25 | gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= 26 | gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 27 | gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= 28 | gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 29 | -------------------------------------------------------------------------------- /internal/bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 7 | "gorm.io/gorm" 8 | "log" 9 | "os" 10 | "tg-dice-bot/internal/database" 11 | "tg-dice-bot/internal/model" 12 | "time" 13 | 14 | "github.com/go-redis/redis/v8" 15 | ) 16 | 17 | const ( 18 | TelegramAPIToken = "TELEGRAM_API_TOKEN" 19 | ) 20 | 21 | var ( 22 | db *gorm.DB 23 | redisDB *redis.Client 24 | ) 25 | 26 | func StartBot() { 27 | initDB() 28 | 29 | bot := initTelegramBot() 30 | 31 | initDiceTask(bot) 32 | 33 | updateConfig := tgbotapi.NewUpdate(0) 34 | updateConfig.Timeout = 60 35 | updates := bot.GetUpdatesChan(updateConfig) 36 | 37 | for update := range updates { 38 | if update.Message != nil { 39 | go handleMessage(bot, update.Message) 40 | } else if update.CallbackQuery != nil { 41 | go handleCallbackQuery(bot, update.CallbackQuery) 42 | } 43 | } 44 | } 45 | 46 | func initDiceTask(bot *tgbotapi.BotAPI) { 47 | 48 | // 查出所有已开启的对话 49 | chatDiceConfigs, err := model.ListByEnable(db, 1) 50 | if err != nil { 51 | log.Fatal("初始化任务失败:", err) 52 | } 53 | for _, config := range chatDiceConfigs { 54 | // 查询当前对话在缓存中是否有未执行的任务 55 | redisKey := fmt.Sprintf(RedisCurrentIssueKey, config.ChatID) 56 | issueNumberResult := redisDB.Get(redisDB.Context(), redisKey) 57 | if errors.Is(issueNumberResult.Err(), redis.Nil) || issueNumberResult == nil { 58 | // 没有未开奖的任务,开始新的期号 59 | log.Printf("键 %s 不存在", redisKey) 60 | issueNumber := time.Now().Format("20060102150405") 61 | 62 | go StartDice(bot, config.ChatID, issueNumber) 63 | continue 64 | } else if issueNumberResult.Err() != nil { 65 | log.Println("获取值时发生错误:", issueNumberResult.Err()) 66 | continue 67 | } else { 68 | // 有未开奖的任务 69 | result, _ := issueNumberResult.Result() 70 | log.Printf("有未开奖的任务期号:%s", result) 71 | go StartDice(bot, config.ChatID, result) 72 | continue 73 | } 74 | } 75 | 76 | } 77 | 78 | func initDB() { 79 | var err error 80 | db, err = database.InitDB(os.Getenv(database.DBConnectionString)) 81 | if err != nil { 82 | log.Fatal("连接数据库失败:", err) 83 | } 84 | 85 | err = db.AutoMigrate(&model.LotteryRecord{}) 86 | if err != nil { 87 | log.Fatal("自动迁移表结构失败:", err) 88 | } 89 | 90 | err = db.AutoMigrate(&model.TgUser{}) 91 | if err != nil { 92 | log.Fatal("自动迁移表结构失败:", err) 93 | } 94 | 95 | err = db.AutoMigrate(&model.BetRecord{}) 96 | if err != nil { 97 | log.Fatal("自动迁移表结构失败:", err) 98 | } 99 | 100 | err = db.AutoMigrate(&model.ChatDiceConfig{}) 101 | if err != nil { 102 | log.Fatal("自动迁移表结构失败:", err) 103 | } 104 | 105 | redisDB, err = database.InitRedisDB(os.Getenv(database.RedisDBConnectionString)) 106 | if err != nil { 107 | log.Fatal("连接Redis数据库失败:", err) 108 | } 109 | 110 | } 111 | func initTelegramBot() *tgbotapi.BotAPI { 112 | bot, err := tgbotapi.NewBotAPI(os.Getenv(TelegramAPIToken)) 113 | if err != nil { 114 | log.Panic(err) 115 | } 116 | 117 | bot.Debug = false 118 | log.Printf("已授权帐户 %s", bot.Self.UserName) 119 | return bot 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # tg-dice-bot 4 | 5 | _Telegram骰子机器人_ 6 | 7 | 该骰子机器人项目已**升级**为功能更强大的Telegram-Dice-Bot 骰子娱乐机器人 8 | 9 |
10 | 11 | ## 功能 12 | 13 | 1. 记录开奖历史 14 | 2. 记录下注记录 15 | 3. 支持积分系统 16 | 4. 支持签到奖励 17 | 5. 支持领取低保 18 | ... 19 | 20 | ### Bot命令 21 | 22 | ``` 23 | /help 帮助 24 | /start 开启 25 | /stop 关闭 26 | /register 用户注册 27 | /sign 用户签到 28 | /my 查询积分 29 | /myhistory 查询历史下注记录 30 | /iampoor 领取低保 31 | 玩法例子(竞猜-单,下注-20): #单 20 32 | 默认开奖周期: 1分钟 33 | 34 | 支持下注种类: 单、双、大、小、豹子 35 | ``` 36 | 37 | ### 功能示例 38 | 39 | ![IMG](https://s2.loli.net/2023/12/12/Y6mBkRM94rUKLul.gif) 40 | 41 | 42 | ## 部署 43 | 44 | ### 基于 Docker-Compose(All In One) 进行部署 45 | 46 | ```shell 47 | docker-compose pull && docker-compose up -d 48 | ``` 49 | 50 | #### docker-compose.yml 51 | ```docker 52 | version: '3.4' 53 | 54 | services: 55 | tg-dice-bot: 56 | image: ghcr.io/deanxv/tg-dice-bot:latest 57 | container_name: tg-dice-bot 58 | restart: always 59 | volumes: 60 | - ./data/tgdicebot:/data 61 | environment: 62 | - MYSQL_DSN=tgdicebot:123456@tcp(db:3306)/dice_bot # 可修改此行 SQL连接信息 63 | - REDIS_CONN_STRING=redis://redis 64 | - TZ=Asia/Shanghai 65 | - TELEGRAM_API_TOKEN=6830xxxxxxxxxxxxxxxx3GawBHc7ywDuU # 必须修改此行telegram-bot的token 66 | depends_on: 67 | - redis 68 | - db 69 | 70 | redis: 71 | image: redis:latest 72 | container_name: redis 73 | restart: always 74 | 75 | db: 76 | image: mysql:8.2.0 77 | restart: always 78 | container_name: mysql 79 | volumes: 80 | - ./data/mysql:/var/lib/mysql # 挂载目录,持久化存储 81 | ports: 82 | - '3306:3306' 83 | environment: 84 | TZ: Asia/Shanghai # 可修改默认时区 85 | MYSQL_ROOT_PASSWORD: 'root@123456' # 可修改此行 root用户名 密码 86 | MYSQL_USER: tgdicebot # 可修改初始化专用用户用户名 87 | MYSQL_PASSWORD: '123456' # 可修改初始化专用用户密码 88 | MYSQL_DATABASE: dice_bot # 可修改初始化专用数据库 89 | ``` 90 | 91 | ### 基于 Docker 进行部署 92 | 93 | ```shell 94 | docker run --name tg-dice-bot -d --restart always \ 95 | -e MYSQL_DSN="root:123456@tcp(localhost:3306)/dice_bot" \ 96 | -e REDIS_CONN_STRING="redis://default:@:" \ 97 | -e TELEGRAM_API_TOKEN="683091xxxxxxxxxxxxxxxxywDuU" \ 98 | deanxv/tg-dice-bot 99 | ``` 100 | 101 | 其中,`MYSQL_DSN`,`REDIS_CONN_STRING`,`TELEGRAM_API_TOKEN`修改为自己的,Mysql中新建名为`dice_bot`的db。 102 | 103 | 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `deanxv/tg-dice-bot` 104 | 替换为 `ghcr.io/deanxv/tg-dice-bot` 即可。 105 | 106 | ### 部署到第三方平台 107 | 108 |
109 | 部署到 Zeabur 110 |
111 | 112 | > Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用 113 | 114 | 点击一键部署: 115 | 116 | [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/SEFL7Z?referralCode=deanxv) 117 | 118 | **一键部署后 `MYSQL_DSN` `REDIS_CONN_STRING` `TELEGRAM_API_TOKEN`变量也需要替换!** 119 | 120 | 或手动部署: 121 | 122 | 1. 首先 fork 一份代码。 123 | 2. 进入 [Zeabur](https://zeabur.com?referralCode=deanxv),登录,进入控制台。 124 | 3. 新建一个 Project,在 Service -> Add Service 选择 prebuilt,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。 125 | 4. 新建一个 Project,在 Service -> Add Service 选择 prebuilt,选择 Redis,并记下连接参数(密码、地址、端口)。 126 | 5. 使用mysql视图化工具连接mysql,运行 ```create database `dice_bot` ``` 创建数据库。 127 | 6. 在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。 128 | 7. Deploy 会自动开始,先取消。 129 | 8. 添加环境变量 130 | 131 | `MYSQL_DSN`:`:@tcp(:)/dice_bot` 132 | 133 | `REDIS_CONN_STRING`:`redis://default:@:` 134 | 135 | `TELEGRAM_API_TOKEN`:`你的TG机器人的TOKEN` 136 | 137 | 保存。 138 | 9. 选择 Redeploy。 139 | 140 |
141 |
142 | 143 | 144 | 145 | ## 配置 146 | 147 | ### 环境变量 148 | 149 | 1. `MYSQL_DSN`:`MYSQL_DSN=root:123456@tcp(localhost:3306)/dice_bot` 150 | 2. `REDIS_CONN_STRING`:`REDIS_CONN_STRING:redis://default:@:` 151 | 3. `TELEGRAM_API_TOKEN`:`683091xxxxxxxxxxxxxxxxywDuU` 你的TG机器人的TOKEN 152 | -------------------------------------------------------------------------------- /internal/bot/handler.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | "gorm.io/gorm" 8 | "log" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 15 | "tg-dice-bot/internal/model" 16 | ) 17 | 18 | const ( 19 | RedisCurrentIssueKey = "current_issue:%d" 20 | ) 21 | 22 | var ( 23 | stopFlags = make(map[int64]chan struct{}) 24 | stopMutex sync.Mutex 25 | ) 26 | 27 | // handleCallbackQuery 处理回调查询。 28 | func handleCallbackQuery(bot *tgbotapi.BotAPI, callbackQuery *tgbotapi.CallbackQuery) { 29 | 30 | if callbackQuery.Data == "betting_history" { 31 | handleBettingHistoryQuery(bot, callbackQuery) 32 | } 33 | } 34 | 35 | // handleBettingHistoryQuery 处理 "betting_history" 回调查询。 36 | func handleBettingHistoryQuery(bot *tgbotapi.BotAPI, callbackQuery *tgbotapi.CallbackQuery) { 37 | records, err := model.GetAllRecordsByChatID(db, callbackQuery.Message.Chat.ID) 38 | if err != nil { 39 | log.Println("获取开奖历史异常:", err) 40 | return 41 | } 42 | msgText := generateBettingHistoryMessage(records) 43 | msg := tgbotapi.NewMessage(callbackQuery.Message.Chat.ID, msgText) 44 | 45 | sentMsg, err := bot.Send(msg) 46 | if err != nil { 47 | log.Println("发送消息异常:", err) 48 | // 检查错误是否为用户阻止了机器人 49 | delConfigByBlocked(err, callbackQuery.Message.Chat.ID) 50 | return 51 | } 52 | 53 | go func(messageID int) { 54 | time.Sleep(1 * time.Minute) 55 | deleteMsg := tgbotapi.NewDeleteMessage(callbackQuery.Message.Chat.ID, messageID) 56 | _, err := bot.Request(deleteMsg) 57 | if err != nil { 58 | log.Println("删除消息异常:", err) 59 | } 60 | }(sentMsg.MessageID) 61 | } 62 | 63 | func delConfigByBlocked(err error, chatID int64) { 64 | if err != nil { 65 | if strings.Contains(err.Error(), "Forbidden: bot was blocked") { 66 | log.Printf("The bot was blocked ChatId: %v", chatID) 67 | // 对话已被用户阻止 删除对话配置 68 | db.Where("chat_id = ?", chatID).Delete(&model.ChatDiceConfig{}) 69 | } else if strings.Contains(err.Error(), "Forbidden: bot was kicked") { 70 | log.Printf("The bot was kicked ChatId: %v", chatID) 71 | // 对话已被踢出群聊 删除对话配置 72 | db.Where("chat_id = ?", chatID).Delete(&model.ChatDiceConfig{}) 73 | } 74 | } 75 | 76 | } 77 | 78 | // generateBettingHistoryMessage 生成开奖历史消息文本。 79 | func generateBettingHistoryMessage(records []model.LotteryRecord) string { 80 | var msgText string 81 | 82 | for _, record := range records { 83 | triplet := "" 84 | if record.Triplet == 1 { 85 | triplet = "【豹子】" 86 | } 87 | msgText += fmt.Sprintf("%s期: %d %d %d %d %s %s %s\n", 88 | record.IssueNumber, record.ValueA, record.ValueB, record.ValueC, record.Total, record.SingleDouble, record.BigSmall, triplet) 89 | } 90 | return msgText 91 | } 92 | 93 | // handleMessage 处理传入的消息。 94 | func handleMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message) { 95 | user := message.From 96 | chatID := message.Chat.ID 97 | messageID := message.MessageID 98 | 99 | chatMember, err := getChatMember(bot, chatID, int(user.ID)) 100 | if err != nil { 101 | log.Println("获取聊天成员异常:", err) 102 | return 103 | } 104 | 105 | if message.IsCommand() { 106 | if message.Chat.IsSuperGroup() || message.Chat.IsGroup() { 107 | handleGroupCommand(bot, user.UserName, chatMember, message.Command(), chatID, messageID) 108 | } else { 109 | handlePrivateCommand(bot, chatMember, chatID, messageID, message.Command()) 110 | } 111 | } else if message.Text != "" { 112 | log.Println("text:" + message.Text) 113 | handleBettingCommand(bot, user.ID, chatID, messageID, message.Text) 114 | } 115 | } 116 | 117 | // handleBettingCommand 处理下注命令 118 | func handleBettingCommand(bot *tgbotapi.BotAPI, userID int64, chatID int64, messageID int, text string) { 119 | 120 | // 解析下注命令,示例命令格式:#单 20 121 | // 这里需要根据实际需求进行合适的解析,示例中只是简单示范 122 | parts := strings.Fields(text) 123 | if len(parts) != 2 || !strings.HasPrefix(parts[0], "#") { 124 | return 125 | } 126 | 127 | // 获取下注类型和下注积分 128 | betType := parts[0][1:] 129 | if betType != "单" && betType != "双" && betType != "大" && betType != "小" && betType != "豹子" { 130 | return 131 | } 132 | 133 | betAmount, err := strconv.Atoi(parts[1]) 134 | if err != nil || betAmount <= 0 { 135 | return 136 | } 137 | 138 | _, err = model.GetByEnableAndChatId(db, 1, chatID) 139 | if errors.Is(err, gorm.ErrRecordNotFound) { 140 | registrationMsg := tgbotapi.NewMessage(chatID, "功能未开启!") 141 | registrationMsg.ReplyToMessageID = messageID 142 | _, err := bot.Send(registrationMsg) 143 | if err != nil { 144 | log.Println("功能未开启提示消息异常:", err) 145 | delConfigByBlocked(err, chatID) 146 | } 147 | return 148 | } else if err != nil { 149 | log.Println("下注命令异常", err.Error()) 150 | return 151 | } 152 | // 获取当前进行的期号 153 | redisKey := fmt.Sprintf(RedisCurrentIssueKey, chatID) 154 | issueNumberResult := redisDB.Get(redisDB.Context(), redisKey) 155 | if errors.Is(issueNumberResult.Err(), redis.Nil) || issueNumberResult == nil { 156 | log.Printf("键 %s 不存在", redisKey) 157 | replyMsg := tgbotapi.NewMessage(chatID, "当前暂无开奖活动!") 158 | replyMsg.ReplyToMessageID = messageID 159 | _, err = bot.Send(replyMsg) 160 | delConfigByBlocked(err, chatID) 161 | return 162 | } else if issueNumberResult.Err() != nil { 163 | log.Println("获取值时发生异常:", issueNumberResult.Err()) 164 | return 165 | } 166 | 167 | issueNumber, _ := issueNumberResult.Result() 168 | 169 | // 存储下注记录到数据库,并扣除用户余额 170 | err = storeBetRecord(bot, userID, chatID, issueNumber, messageID, betType, betAmount) 171 | if err != nil { 172 | // 回复余额不足信息等 173 | log.Println("存储下注记录异常:", err) 174 | return 175 | } 176 | 177 | // 回复下注成功信息 178 | replyMsg := tgbotapi.NewMessage(chatID, "下注成功!") 179 | replyMsg.ReplyToMessageID = messageID 180 | 181 | _, err = bot.Send(replyMsg) 182 | if err != nil { 183 | log.Println("发送消息异常:", err) 184 | delConfigByBlocked(err, chatID) 185 | } 186 | } 187 | 188 | // storeBetRecord 函数中扣除用户余额并保存下注记录 189 | func storeBetRecord(bot *tgbotapi.BotAPI, userID int64, chatID int64, issueNumber string, messageID int, betType string, betAmount int) error { 190 | // 获取用户对应的互斥锁 191 | userLock := getUserLock(userID) 192 | userLock.Lock() 193 | defer userLock.Unlock() 194 | 195 | // 获取用户信息 196 | var user model.TgUser 197 | result := db.Where("tg_user_id = ? AND chat_id = ?", userID, chatID).First(&user) 198 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 199 | // 用户不存在,发送注册提示 200 | registrationMsg := tgbotapi.NewMessage(chatID, "您还未注册,使用 /register 进行注册。") 201 | registrationMsg.ReplyToMessageID = messageID 202 | _, err := bot.Send(registrationMsg) 203 | if err != nil { 204 | log.Println("发送注册提示消息异常:", err) 205 | delConfigByBlocked(err, chatID) 206 | return err 207 | } 208 | return result.Error 209 | } 210 | 211 | // 检查用户余额是否足够 212 | if user.Balance < betAmount { 213 | // 用户不存在,发送注册提示 214 | balanceInsufficientMsg := tgbotapi.NewMessage(chatID, "您的余额不足!") 215 | balanceInsufficientMsg.ReplyToMessageID = messageID 216 | _, err := bot.Send(balanceInsufficientMsg) 217 | if err != nil { 218 | log.Println("您的余额不足提示异常:", err) 219 | delConfigByBlocked(err, chatID) 220 | return err 221 | } else { 222 | return errors.New("余额不足") 223 | } 224 | } 225 | 226 | // 扣除用户余额 227 | user.Balance -= betAmount 228 | result = db.Save(&user) 229 | if result.Error != nil { 230 | log.Println("扣除用户余额异常:", result.Error) 231 | return result.Error 232 | } 233 | currentTime := time.Now().Format("2006-01-02 15:04:05") 234 | // 保存下注记录 235 | betRecord := model.BetRecord{ 236 | TgUserID: userID, 237 | ChatID: chatID, 238 | BetType: betType, 239 | BetAmount: betAmount, 240 | IssueNumber: issueNumber, 241 | SettleStatus: 0, 242 | BetResultType: nil, 243 | UpdateTime: currentTime, 244 | CreateTime: currentTime, 245 | } 246 | 247 | result = db.Create(&betRecord) 248 | if result.Error != nil { 249 | log.Println("保存下注记录异常:", result.Error) 250 | // 如果保存下注记录失败,需要返还用户余额 251 | user.Balance += betAmount 252 | db.Save(&user) 253 | return result.Error 254 | } 255 | 256 | return nil 257 | } 258 | 259 | // handleGroupCommand 处理群聊中的命令。 260 | func handleGroupCommand(bot *tgbotapi.BotAPI, username string, chatMember tgbotapi.ChatMember, command string, chatID int64, messageID int) { 261 | if command == "start" { 262 | if !chatMember.IsAdministrator() && !chatMember.IsCreator() { 263 | msgConfig := tgbotapi.NewMessage(chatID, "请勿使用管理员命令") 264 | msgConfig.ReplyToMessageID = messageID 265 | _, err := sendMessage(bot, &msgConfig) 266 | delConfigByBlocked(err, chatID) 267 | return 268 | } 269 | handleStartCommand(bot, chatID, messageID) 270 | } else if command == "stop" { 271 | if !chatMember.IsAdministrator() && !chatMember.IsCreator() { 272 | msgConfig := tgbotapi.NewMessage(chatID, "请勿使用管理员命令") 273 | msgConfig.ReplyToMessageID = messageID 274 | _, err := sendMessage(bot, &msgConfig) 275 | delConfigByBlocked(err, chatID) 276 | return 277 | } 278 | handleStopCommand(bot, chatID, messageID) 279 | } else if command == "register" { 280 | handleRegisterCommand(bot, chatMember, chatID, messageID) 281 | } else if command == "sign" { 282 | handleSignInCommand(bot, chatMember, chatID, messageID) 283 | } else if command == "my" { 284 | handleMyCommand(bot, chatMember, chatID, messageID) 285 | } else if command == "iampoor" { 286 | handlePoorCommand(bot, chatMember, chatID, messageID) 287 | } else if command == "help" { 288 | handleHelpCommand(bot, chatID, messageID) 289 | } else if command == "myhistory" { 290 | handleMyHistoryCommand(bot, chatMember, chatID, messageID) 291 | } 292 | 293 | } 294 | 295 | func handleRegisterCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int) { 296 | // 获取用户对应的互斥锁 297 | userLock := getUserLock(chatMember.User.ID) 298 | userLock.Lock() 299 | defer userLock.Unlock() 300 | 301 | var user model.TgUser 302 | result := db.Where("tg_user_id = ? AND chat_id = ?", chatMember.User.ID, chatID).First(&user) 303 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 304 | // 没有找到记录 305 | err := registerUser(chatMember.User.ID, chatMember.User.UserName, chatID) 306 | if err != nil { 307 | log.Println("用户注册异常:", err) 308 | } else { 309 | msgConfig := tgbotapi.NewMessage(chatID, "注册成功!奖励1000积分!") 310 | msgConfig.ReplyToMessageID = messageID 311 | _, err := sendMessage(bot, &msgConfig) 312 | delConfigByBlocked(err, chatID) 313 | } 314 | } else if result.Error != nil { 315 | log.Println("查询异常:", result.Error) 316 | } else { 317 | msgConfig := tgbotapi.NewMessage(chatID, "请勿重复注册!") 318 | msgConfig.ReplyToMessageID = messageID 319 | _, err := sendMessage(bot, &msgConfig) 320 | delConfigByBlocked(err, chatID) 321 | } 322 | } 323 | 324 | func handleSignInCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int) { 325 | // 获取用户对应的互斥锁 326 | userLock := getUserLock(chatMember.User.ID) 327 | userLock.Lock() 328 | defer userLock.Unlock() 329 | 330 | var user model.TgUser 331 | result := db.Where("tg_user_id = ? AND chat_id = ?", chatMember.User.ID, chatID).First(&user) 332 | 333 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 334 | // 没有找到记录 335 | msgConfig := tgbotapi.NewMessage(chatID, "请发送 /register 注册用户!") 336 | msgConfig.ReplyToMessageID = messageID 337 | _, err := sendMessage(bot, &msgConfig) 338 | delConfigByBlocked(err, chatID) 339 | return 340 | } else if result.Error != nil { 341 | log.Println("查询异常:", result.Error) 342 | } else { 343 | if user.SignInTime != "" { 344 | signInTime, err := time.Parse("2006-01-02 15:04:05", user.SignInTime) 345 | if err != nil { 346 | log.Println("时间解析异常:", err) 347 | return 348 | } 349 | // 获取当前时间 350 | currentTime := time.Now() 351 | currentMidnight := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 0, 0, 0, 0, currentTime.Location()) 352 | if !signInTime.Before(currentMidnight) { 353 | msgConfig := tgbotapi.NewMessage(chatID, "今天已签到过了哦!") 354 | msgConfig.ReplyToMessageID = messageID 355 | _, err := sendMessage(bot, &msgConfig) 356 | delConfigByBlocked(err, chatID) 357 | return 358 | } 359 | } 360 | user.SignInTime = time.Now().Format("2006-01-02 15:04:05") 361 | user.Balance += 1000 362 | result = db.Save(&user) 363 | msgConfig := tgbotapi.NewMessage(chatID, "签到成功!奖励1000积分!") 364 | msgConfig.ReplyToMessageID = messageID 365 | _, err := sendMessage(bot, &msgConfig) 366 | delConfigByBlocked(err, chatID) 367 | } 368 | } 369 | 370 | func handleMyCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int) { 371 | var user model.TgUser 372 | result := db.Where("tg_user_id = ? AND chat_id = ?", chatMember.User.ID, chatID).First(&user) 373 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 374 | // 没有找到记录 375 | msgConfig := tgbotapi.NewMessage(chatID, "请发送 /register 注册用户!") 376 | msgConfig.ReplyToMessageID = messageID 377 | sentMsg, err := sendMessage(bot, &msgConfig) 378 | if err != nil { 379 | delConfigByBlocked(err, chatID) 380 | return 381 | } 382 | go func(messageID int) { 383 | time.Sleep(1 * time.Minute) 384 | deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) 385 | _, err := bot.Request(deleteMsg) 386 | if err != nil { 387 | log.Println("删除消息异常:", err) 388 | } 389 | }(sentMsg.MessageID) 390 | } else if result.Error != nil { 391 | log.Println("查询异常:", result.Error) 392 | } else { 393 | msgConfig := tgbotapi.NewMessage(chatID, fmt.Sprintf("%s 您的积分余额为%d", chatMember.User.FirstName, user.Balance)) 394 | msgConfig.ReplyToMessageID = messageID 395 | sentMsg, err := sendMessage(bot, &msgConfig) 396 | if err != nil { 397 | delConfigByBlocked(err, chatID) 398 | return 399 | } 400 | go func(messageID int) { 401 | time.Sleep(1 * time.Minute) 402 | deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) 403 | _, err := bot.Request(deleteMsg) 404 | if err != nil { 405 | log.Println("删除消息异常:", err) 406 | } 407 | }(sentMsg.MessageID) 408 | } 409 | } 410 | 411 | func handlePoorCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int) { 412 | // 获取用户对应的互斥锁 413 | userLock := getUserLock(chatMember.User.ID) 414 | userLock.Lock() 415 | defer userLock.Unlock() 416 | 417 | var user model.TgUser 418 | result := db.Where("tg_user_id = ? AND chat_id = ?", chatMember.User.ID, chatID).First(&user) 419 | if errors.Is(result.Error, gorm.ErrRecordNotFound) { 420 | // 没有找到记录 421 | msgConfig := tgbotapi.NewMessage(chatID, "请发送 /register 注册用户!") 422 | msgConfig.ReplyToMessageID = messageID 423 | _, err := sendMessage(bot, &msgConfig) 424 | delConfigByBlocked(err, chatID) 425 | } else if result.Error != nil { 426 | log.Println("查询异常:", result.Error) 427 | } else { 428 | //查询下注记录 429 | 430 | var betRecord model.BetRecord 431 | betRecord.ChatID = chatID 432 | betRecord.TgUserID = chatMember.User.ID 433 | betRecord.SettleStatus = 0 434 | betRecords, err := model.ListBySettleStatus(db, &betRecord) 435 | 436 | if len(betRecords) == 0 { 437 | // 记录为空 438 | if user.Balance >= 1000 { 439 | msgConfig := tgbotapi.NewMessage(chatID, "1000积分以下才可以领取低保哦") 440 | msgConfig.ReplyToMessageID = messageID 441 | _, err := sendMessage(bot, &msgConfig) 442 | delConfigByBlocked(err, chatID) 443 | return 444 | } 445 | user.Balance += 1000 446 | result = db.Save(&user) 447 | msgConfig := tgbotapi.NewMessage(chatID, "领取低保成功!获得1000积分!") 448 | msgConfig.ReplyToMessageID = messageID 449 | _, err := sendMessage(bot, &msgConfig) 450 | delConfigByBlocked(err, chatID) 451 | return 452 | } else if err != nil { 453 | log.Println("查询下注记录异常", result.Error) 454 | return 455 | } else { 456 | msgConfig := tgbotapi.NewMessage(chatID, "您有未开奖的下注记录,开奖结算后再领取吧!") 457 | msgConfig.ReplyToMessageID = messageID 458 | _, err := sendMessage(bot, &msgConfig) 459 | delConfigByBlocked(err, chatID) 460 | } 461 | } 462 | } 463 | 464 | // registerUser 函数用于用户注册时插入初始数据到数据库 465 | func registerUser(userID int64, userName string, chatID int64) error { 466 | initialBalance := 1000 467 | newUser := model.TgUser{ 468 | TgUserID: userID, 469 | ChatID: chatID, 470 | Username: userName, 471 | Balance: initialBalance, 472 | } 473 | 474 | result := db.Create(&newUser) 475 | return result.Error 476 | } 477 | 478 | // handlePrivateCommand 处理私聊中的命令。 479 | func handlePrivateCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int, command string) { 480 | switch command { 481 | case "stop": 482 | handleStopCommand(bot, chatID, messageID) 483 | case "start": 484 | handleStartCommand(bot, chatID, messageID) 485 | case "help": 486 | handleHelpCommand(bot, chatID, messageID) 487 | case "register": 488 | handleRegisterCommand(bot, chatMember, chatID, messageID) 489 | case "sign": 490 | handleSignInCommand(bot, chatMember, chatID, messageID) 491 | case "my": 492 | handleMyCommand(bot, chatMember, chatID, messageID) 493 | case "iampoor": 494 | handlePoorCommand(bot, chatMember, chatID, messageID) 495 | case "myhistory": 496 | handleMyHistoryCommand(bot, chatMember, chatID, messageID) 497 | } 498 | } 499 | 500 | // handleStopCommand 处理 "stop" 命令。 501 | func handleStopCommand(bot *tgbotapi.BotAPI, chatID int64, messageID int) { 502 | 503 | var chatDiceConfig model.ChatDiceConfig 504 | // 更新开奖配置 505 | chatDiceConfigResult := db.First(&chatDiceConfig, "chat_id = ?", chatID) 506 | if errors.Is(chatDiceConfigResult.Error, gorm.ErrRecordNotFound) { 507 | msgConfig := tgbotapi.NewMessage(chatID, "开启后才可关闭!") 508 | msgConfig.ReplyToMessageID = messageID 509 | _, err := sendMessage(bot, &msgConfig) 510 | delConfigByBlocked(err, chatID) 511 | return 512 | } else if chatDiceConfigResult.Error != nil { 513 | log.Println("开奖配置初始化异常", chatDiceConfigResult.Error) 514 | return 515 | } else { 516 | chatDiceConfig.Enable = 0 517 | result := db.Model(&model.ChatDiceConfig{}).Where("chat_id = ?", chatID).Update("enable", 0) 518 | if result.Error != nil { 519 | log.Println("开奖配置初始化失败: " + result.Error.Error()) 520 | return 521 | } 522 | } 523 | 524 | msgConfig := tgbotapi.NewMessage(chatID, "已关闭") 525 | msgConfig.ReplyToMessageID = messageID 526 | _, err := sendMessage(bot, &msgConfig) 527 | if err != nil { 528 | delConfigByBlocked(err, chatID) 529 | return 530 | } 531 | stopDice(chatID) 532 | } 533 | 534 | // handleStartCommand 处理 "start" 命令。 535 | func handleStartCommand(bot *tgbotapi.BotAPI, chatID int64, messageID int) { 536 | var chatDiceConfig *model.ChatDiceConfig 537 | // 更新开奖配置 538 | chatDiceConfigResult := db.First(&chatDiceConfig, "chat_id = ?", chatID) 539 | if errors.Is(chatDiceConfigResult.Error, gorm.ErrRecordNotFound) { 540 | // 开奖配置不存在 则保存 541 | chatDiceConfig = &model.ChatDiceConfig{ 542 | ChatID: chatID, 543 | LotteryDrawCycle: 1, // 开奖周期(分钟) 544 | Enable: 1, // 开启状态 545 | } 546 | db.Create(&chatDiceConfig) 547 | } else if chatDiceConfigResult.Error != nil { 548 | log.Println("开奖配置初始化异常", chatDiceConfigResult.Error) 549 | return 550 | } else { 551 | chatDiceConfig.Enable = 1 552 | result := db.Model(&model.ChatDiceConfig{}).Where("chat_id = ?", chatID).Update("enable", 1) 553 | if result.Error != nil { 554 | log.Println("开奖配置初始化失败: " + result.Error.Error()) 555 | return 556 | } 557 | } 558 | 559 | msgConfig := tgbotapi.NewMessage(chatID, "已开启") 560 | msgConfig.ReplyToMessageID = messageID 561 | _, err := sendMessage(bot, &msgConfig) 562 | if err != nil { 563 | delConfigByBlocked(err, chatID) 564 | return 565 | } 566 | 567 | issueNumber := time.Now().Format("20060102150405") 568 | 569 | // 查找上个未开奖的期号 570 | redisKey := fmt.Sprintf(RedisCurrentIssueKey, chatID) 571 | issueNumberResult := redisDB.Get(redisDB.Context(), redisKey) 572 | if errors.Is(issueNumberResult.Err(), redis.Nil) || issueNumberResult == nil { 573 | lotteryDrawTipMsgConfig := tgbotapi.NewMessage(chatID, fmt.Sprintf("第%s期 %d分钟后开奖", issueNumber, chatDiceConfig.LotteryDrawCycle)) 574 | _, err := sendMessage(bot, &lotteryDrawTipMsgConfig) 575 | if err != nil { 576 | delConfigByBlocked(err, chatID) 577 | return 578 | } 579 | // 存储当前期号和对话ID 580 | err = redisDB.Set(redisDB.Context(), redisKey, issueNumber, 0).Err() 581 | if err != nil { 582 | log.Println("存储新期号和对话ID异常:", err) 583 | return 584 | } 585 | } else if issueNumberResult.Err() != nil { 586 | log.Println("获取值时发生异常:", issueNumberResult.Err()) 587 | return 588 | } else { 589 | result, _ := issueNumberResult.Result() 590 | issueNumber = result 591 | lotteryDrawTipMsgConfig := tgbotapi.NewMessage(chatID, fmt.Sprintf("第%s期 %d分钟后开奖", issueNumber, chatDiceConfig.LotteryDrawCycle)) 592 | _, err := sendMessage(bot, &lotteryDrawTipMsgConfig) 593 | if err != nil { 594 | delConfigByBlocked(err, chatID) 595 | return 596 | } 597 | } 598 | 599 | //redisKey := fmt.Sprintf(RedisCurrentIssueKey, chatID) 600 | go StartDice(bot, chatID, issueNumber) 601 | } 602 | 603 | // handleHelpCommand 处理 "help" 命令。 604 | func handleHelpCommand(bot *tgbotapi.BotAPI, chatID int64, messageID int) { 605 | msgConfig := tgbotapi.NewMessage(chatID, "/help 帮助\n"+ 606 | "/start 开启\n"+ 607 | "/stop 关闭\n"+ 608 | "/register 用户注册\n"+ 609 | "/sign 用户签到\n"+ 610 | "/my 查询积分\n"+ 611 | "/myhistory 查询历史下注记录\n"+ 612 | "/iampoor 领取低保\n"+ 613 | "玩法例子(竞猜-单,下注-20): #单 20\n"+ 614 | "默认开奖周期: 1分钟") 615 | msgConfig.ReplyToMessageID = messageID 616 | sentMsg, err := sendMessage(bot, &msgConfig) 617 | if err != nil { 618 | delConfigByBlocked(err, chatID) 619 | return 620 | } 621 | go func(messageID int) { 622 | time.Sleep(1 * time.Minute) 623 | deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) 624 | _, err := bot.Request(deleteMsg) 625 | if err != nil { 626 | log.Println("删除消息异常:", err) 627 | } 628 | }(sentMsg.MessageID) 629 | } 630 | 631 | // handleMyHistoryCommand 处理 "myhistory" 命令。 632 | func handleMyHistoryCommand(bot *tgbotapi.BotAPI, chatMember tgbotapi.ChatMember, chatID int64, messageID int) { 633 | // 查询下注记录 634 | var betRecord model.BetRecord 635 | betRecord.ChatID = chatID 636 | betRecord.TgUserID = chatMember.User.ID 637 | betRecords, err := model.ListByChatAndUser(db, &betRecord) 638 | 639 | msgConfig := tgbotapi.NewMessage(chatID, "") 640 | msgConfig.ReplyToMessageID = messageID 641 | 642 | if len(betRecords) == 0 { 643 | // 下注记录为空 644 | msgConfig.Text = "您还没有下注记录哦!" 645 | _, err := sendMessage(bot, &msgConfig) 646 | if err != nil { 647 | delConfigByBlocked(err, chatID) 648 | return 649 | } 650 | return 651 | } else if err != nil { 652 | log.Println("查询下注记录异常", err) 653 | return 654 | } else { 655 | msgText := "您的下注记录如下:\n" 656 | 657 | for _, record := range betRecords { 658 | betResultType := "" 659 | betResultAmount := "" 660 | if record.BetResultType != nil { 661 | if *record.BetResultType == 1 { 662 | if record.BetType == "单" || record.BetType == "双" || record.BetType == "大" || record.BetType == "小" { 663 | betResultAmount = fmt.Sprintf("+%d", record.BetAmount*2) 664 | } else if record.BetType == "豹子" { 665 | betResultAmount = fmt.Sprintf("+%d", record.BetAmount*10) 666 | } 667 | betResultType = "赢" 668 | } else if *record.BetResultType == 0 { 669 | betResultType = "输" 670 | betResultAmount = fmt.Sprintf("-%d", record.BetAmount) 671 | } 672 | } else { 673 | betResultType = "[未开奖]" 674 | } 675 | 676 | msgText += fmt.Sprintf("%s期: %s %d %s %s\n", record.IssueNumber, record.BetType, record.BetAmount, betResultType, betResultAmount) 677 | } 678 | 679 | msgConfig.Text = msgText 680 | sentMsg, err := sendMessage(bot, &msgConfig) 681 | if err != nil { 682 | delConfigByBlocked(err, chatID) 683 | return 684 | } 685 | go func(messageID int) { 686 | time.Sleep(1 * time.Minute) 687 | deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) 688 | _, err := bot.Request(deleteMsg) 689 | if err != nil { 690 | log.Println("删除消息异常:", err) 691 | } 692 | }(sentMsg.MessageID) 693 | 694 | return 695 | } 696 | 697 | } 698 | 699 | // sendMessage 使用提供的消息配置发送消息。 700 | func sendMessage(bot *tgbotapi.BotAPI, msgConfig *tgbotapi.MessageConfig) (tgbotapi.Message, error) { 701 | sentMsg, err := bot.Send(msgConfig) 702 | if err != nil { 703 | log.Println("发送消息异常:", err) 704 | return sentMsg, err 705 | } 706 | return sentMsg, nil 707 | } 708 | 709 | // getChatMember 获取有关聊天成员的信息。 710 | func getChatMember(bot *tgbotapi.BotAPI, chatID int64, userID int) (tgbotapi.ChatMember, error) { 711 | chatMemberConfig := tgbotapi.ChatConfigWithUser{ 712 | ChatID: chatID, 713 | UserID: int64(userID), 714 | } 715 | 716 | return bot.GetChatMember(tgbotapi.GetChatMemberConfig{ChatConfigWithUser: chatMemberConfig}) 717 | } 718 | 719 | // stopDice 停止特定聊天ID的骰子滚动。 720 | func stopDice(chatID int64) { 721 | chatLock := getChatLock(chatID) 722 | chatLock.Lock() 723 | defer chatLock.Unlock() 724 | 725 | if stopFlag, ok := stopFlags[chatID]; ok { 726 | log.Printf("停止聊天ID的任务:%v", chatID) 727 | close(stopFlag) 728 | delete(stopFlags, chatID) 729 | } else { 730 | log.Printf("没有要停止的聊天ID的任务:%v", chatID) 731 | } 732 | } 733 | 734 | // startDice 启动特定聊天ID的骰子滚动。 735 | func StartDice(bot *tgbotapi.BotAPI, chatID int64, issueNumber string) { 736 | stopDice(chatID) 737 | 738 | chatLock := getChatLock(chatID) 739 | chatLock.Lock() 740 | defer chatLock.Unlock() 741 | 742 | stopFlags[chatID] = make(chan struct{}) 743 | go func(stopCh <-chan struct{}) { 744 | 745 | chatDiceConfig, err := model.GetByChatId(db, chatID) 746 | if errors.Is(err, gorm.ErrRecordNotFound) { 747 | log.Printf("聊天ID %v 未找到配置", chatID) 748 | return 749 | } else if err != nil { 750 | log.Printf("聊天ID %v 查找配置异常 %s", chatID, err.Error()) 751 | return 752 | } else { 753 | ticker := time.NewTicker(time.Duration(chatDiceConfig.LotteryDrawCycle) * time.Minute) 754 | defer ticker.Stop() 755 | 756 | for { 757 | select { 758 | case <-ticker.C: 759 | nextIssueNumber := handleDiceRoll(bot, chatID, issueNumber) 760 | issueNumber = nextIssueNumber 761 | case <-stopCh: 762 | log.Printf("已关闭任务:%v", chatID) 763 | return 764 | } 765 | } 766 | } 767 | 768 | }(stopFlags[chatID]) 769 | } 770 | 771 | // handleDiceRoll 处理骰子滚动过程。 772 | func handleDiceRoll(bot *tgbotapi.BotAPI, chatID int64, issueNumber string) (nextIssueNumber string) { 773 | 774 | redisKey := fmt.Sprintf(RedisCurrentIssueKey, chatID) 775 | // 删除当前期号和对话ID 776 | err := redisDB.Del(redisDB.Context(), redisKey).Err() 777 | if err != nil { 778 | log.Println("删除当前期号和对话ID异常:", err) 779 | return 780 | } 781 | 782 | currentTime := time.Now().Format("2006-01-02 15:04:05") 783 | 784 | diceValues, err := rollDice(bot, chatID, 3) 785 | if err != nil { 786 | delConfigByBlocked(err, chatID) 787 | return 788 | } 789 | count := sumDiceValues(diceValues) 790 | singleOrDouble, bigOrSmall := determineResult(count) 791 | 792 | time.Sleep(3 * time.Second) 793 | triplet := 0 794 | if diceValues[0] == diceValues[1] && diceValues[1] == diceValues[2] { 795 | triplet = 1 796 | } 797 | message := formatMessage(diceValues[0], diceValues[1], diceValues[2], count, singleOrDouble, bigOrSmall, triplet, issueNumber) 798 | 799 | insertLotteryRecord(chatID, issueNumber, diceValues[0], diceValues[1], diceValues[2], count, singleOrDouble, bigOrSmall, triplet, currentTime) 800 | 801 | keyboard := tgbotapi.NewInlineKeyboardMarkup( 802 | tgbotapi.NewInlineKeyboardRow( 803 | tgbotapi.NewInlineKeyboardButtonData("开奖历史", "betting_history"), 804 | ), 805 | ) 806 | 807 | msg := tgbotapi.NewMessage(chatID, message) 808 | msg.ReplyMarkup = keyboard 809 | _, err = sendMessage(bot, &msg) 810 | if err != nil { 811 | delConfigByBlocked(err, chatID) 812 | return 813 | } 814 | 815 | //issueNumberInt, _ := strconv.Atoi(issueNumber) 816 | nextIssueNumber = time.Now().Format("20060102150405") 817 | var chatDiceConfig model.ChatDiceConfig 818 | db.Where("enable = ? AND chat_id = ?", 1, chatID).First(&chatDiceConfig) 819 | lotteryDrawTipMsgConfig := tgbotapi.NewMessage(chatID, fmt.Sprintf("第%s期 %d分钟后开奖", nextIssueNumber, chatDiceConfig.LotteryDrawCycle)) 820 | _, err = sendMessage(bot, &lotteryDrawTipMsgConfig) 821 | if err != nil { 822 | delConfigByBlocked(err, chatID) 823 | return 824 | } 825 | 826 | // 设置新的期号和对话ID 827 | err = redisDB.Set(redisDB.Context(), redisKey, nextIssueNumber, 0).Err() 828 | if err != nil { 829 | log.Println("存储新期号和对话ID异常:", err) 830 | } 831 | 832 | // 遍历下注记录,计算竞猜结果 833 | go func() { 834 | // 获取所有参与竞猜的用户下注记录 835 | betRecords, err := model.GetBetRecordsByChatIDAndIssue(db, chatID, issueNumber) 836 | if err != nil { 837 | log.Println("获取用户下注记录异常:", err) 838 | return 839 | } 840 | // 获取当前期数开奖结果 841 | var lotteryRecord model.LotteryRecord 842 | db.Where("issue_number = ? AND chat_id = ?", issueNumber, chatID).First(&lotteryRecord) 843 | 844 | for _, betRecord := range betRecords { 845 | // 更新用户余额 846 | updateBalance(betRecord, &lotteryRecord) 847 | } 848 | }() 849 | 850 | return nextIssueNumber 851 | } 852 | 853 | // updateBalance 更新用户余额 854 | func updateBalance(betRecord *model.BetRecord, lotteryRecord *model.LotteryRecord) { 855 | 856 | // 获取用户对应的互斥锁 857 | userLock := getUserLock(betRecord.TgUserID) 858 | userLock.Lock() 859 | defer userLock.Unlock() 860 | 861 | tx := db.Begin() 862 | 863 | var user model.TgUser 864 | result := tx.Where("tg_user_id = ? and chat_id = ?", betRecord.TgUserID, lotteryRecord.ChatID).First(&user) 865 | if result.Error != nil { 866 | log.Println("获取用户信息异常:", result.Error) 867 | return 868 | } 869 | 870 | if betRecord.BetType == lotteryRecord.SingleDouble || 871 | betRecord.BetType == lotteryRecord.BigSmall { 872 | user.Balance += betRecord.BetAmount * 2 873 | betResultType := 1 874 | betRecord.BetResultType = &betResultType 875 | } else if betRecord.BetType == "豹子" && lotteryRecord.Triplet == 1 { 876 | user.Balance += betRecord.BetAmount * 10 877 | betResultType := 1 878 | betRecord.BetResultType = &betResultType 879 | } else { 880 | betResultType := 0 881 | betRecord.BetResultType = &betResultType 882 | } 883 | 884 | result = tx.Save(&user) 885 | if result.Error != nil { 886 | log.Println("更新用户余额异常:", result.Error) 887 | tx.Rollback() 888 | return 889 | } 890 | 891 | // 更新下注记录表 892 | betRecord.SettleStatus = 1 893 | betRecord.UpdateTime = time.Now().Format("2006-01-02 15:04:05") 894 | result = tx.Save(&betRecord) 895 | if result.Error != nil { 896 | log.Println("更新下注记录异常:", result.Error) 897 | tx.Rollback() 898 | return 899 | } 900 | 901 | // 提交事务 902 | if err := tx.Commit().Error; err != nil { 903 | // 提交事务时出现异常,回滚事务 904 | tx.Rollback() 905 | } 906 | } 907 | 908 | // rollDice 模拟多次掷骰子。 909 | func rollDice(bot *tgbotapi.BotAPI, chatID int64, numDice int) ([]int, error) { 910 | diceValues := make([]int, numDice) 911 | diceConfig := tgbotapi.NewDiceWithEmoji(chatID, "🎲") 912 | 913 | for i := 0; i < numDice; i++ { 914 | diceMsg, err := bot.Send(diceConfig) 915 | if err != nil { 916 | log.Println("发送骰子消息异常:", err) 917 | return nil, err 918 | } 919 | diceValues[i] = diceMsg.Dice.Value 920 | } 921 | 922 | return diceValues, nil 923 | } 924 | 925 | // sumDiceValues 计算骰子值的总和。 926 | func sumDiceValues(diceValues []int) int { 927 | sum := 0 928 | for _, value := range diceValues { 929 | sum += value 930 | } 931 | return sum 932 | } 933 | 934 | // determineResult 根据骰子值的总和确定结果(单/双,大/小)。 935 | func determineResult(count int) (string, string) { 936 | var singleOrDouble string 937 | var bigOrSmall string 938 | 939 | if count <= 10 { 940 | bigOrSmall = "小" 941 | } else { 942 | bigOrSmall = "大" 943 | } 944 | 945 | if count%2 == 1 { 946 | singleOrDouble = "单" 947 | } else { 948 | singleOrDouble = "双" 949 | } 950 | 951 | return singleOrDouble, bigOrSmall 952 | } 953 | 954 | // formatMessage 格式化开奖结果消息。 955 | func formatMessage(valueA int, valueB int, valueC int, count int, singleOrDouble, bigOrSmall string, triplet int, issueNumber string) string { 956 | tripletStr := "" 957 | if triplet == 1 { 958 | tripletStr = "【豹子】" 959 | } 960 | return fmt.Sprintf(""+ 961 | "点数: %d %d %d %s\n"+ 962 | "总点数: %d \n"+ 963 | "[单/双]: %s \n"+ 964 | "[大/小]: %s \n"+ 965 | "期号: %s ", 966 | valueA, valueB, valueC, tripletStr, 967 | count, 968 | singleOrDouble, 969 | bigOrSmall, 970 | issueNumber, 971 | ) 972 | } 973 | 974 | // insertLotteryRecord 将开奖记录插入数据库。 975 | func insertLotteryRecord(chatID int64, issueNumber string, valueA, valueB, valueC, total int, singleOrDouble string, bigOrSmall string, triplet int, currentTime string) { 976 | record := model.LotteryRecord{ 977 | ChatID: chatID, 978 | IssueNumber: issueNumber, 979 | ValueA: valueA, 980 | ValueB: valueB, 981 | ValueC: valueC, 982 | Total: total, 983 | SingleDouble: singleOrDouble, 984 | BigSmall: bigOrSmall, 985 | Triplet: triplet, 986 | Timestamp: currentTime, 987 | } 988 | 989 | result := db.Create(&record) 990 | if result.Error != nil { 991 | log.Println("插入开奖记录异常:", result.Error) 992 | } 993 | } 994 | --------------------------------------------------------------------------------