├── 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 |
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 | 
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 | [](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 |
--------------------------------------------------------------------------------