diff --git a/Dockerfile b/Dockerfile index 57beadc..b34a8ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.21-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ @@ -15,6 +15,7 @@ ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=builder /app/go-helper . +COPY image.png . COPY .env.example .env ENTRYPOINT ["./go-helper"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..619b009 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.8" + +services: + # PostgreSQL 数据库 + db: + image: postgres:16-alpine + container_name: gpt-team-db + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: teamhelper + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 5s + timeout: 5s + retries: 5 + + # Go Helper Bot + bot: + build: . + container_name: gpt-team-bot + restart: always + depends_on: + db: + condition: service_healthy + env_file: + - .env + environment: + # 覆盖 .env 中的数据库连接串,指向 docker 内部的 db 服务 + DATABASE_URL: postgres://postgres:postgres@db:5432/teamhelper?sslmode=disable + +volumes: + pgdata: diff --git a/internal/bot/telegram.go b/internal/bot/telegram.go index 91f55b4..ea3f21b 100644 --- a/internal/bot/telegram.go +++ b/internal/bot/telegram.go @@ -1,4 +1,4 @@ -package bot +package bot import ( "fmt" @@ -589,17 +589,13 @@ func (b *Bot) callbackActionPick(chatID int64, msgID int, action string, userID } backCallback = "cmd:list_accounts" - prefix := "📧" if action == "del" { label = "🗑️ 选择要删除的账号" - prefix = "🗑" } else if action == "ref" { label = "🔄 选择要刷新的账号" - prefix = "🔄" } else if action == "pending_invite" { label = "📩 选择管理账户" - prefix = "📧" backCallback = "cmd:back" } @@ -622,17 +618,29 @@ func (b *Bot) callbackActionPick(chatID int64, msgID int, action string, userID shown := accounts[start:end] var rows [][]tgbotapi.InlineKeyboardButton + var lns []string + var rBtns []tgbotapi.InlineKeyboardButton - // Create a button map - for _, a := range shown { + for i, a := range shown { btnAction := fmt.Sprintf("%s:%d", action, a.ID) + idx := start + i + 1 + + lns = append(lns, fmt.Sprintf("*%d.* ID: `%d` (%s)", idx, a.ID, a.Email)) - rows = append(rows, tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData( - fmt.Sprintf("%s %s", prefix, a.Email), - btnAction, - ), + rBtns = append(rBtns, tgbotapi.NewInlineKeyboardButtonData( + fmt.Sprintf("%d", idx), + btnAction, )) + + // Row break every 5 items + if len(rBtns) == 5 { + rows = append(rows, tgbotapi.NewInlineKeyboardRow(rBtns...)) + rBtns = nil + } + } + // Append remaining buttons + if len(rBtns) > 0 { + rows = append(rows, tgbotapi.NewInlineKeyboardRow(rBtns...)) } // Pagination buttons @@ -647,9 +655,16 @@ func (b *Bot) callbackActionPick(chatID int64, msgID int, action string, userID rows = append(rows, tgbotapi.NewInlineKeyboardRow(navRow...)) } - rows = append(rows, tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", backCallback), - )) + if action == "ref" { + rows = append(rows, tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonData("🔄 刷新全部", "cmd:refresh_all"), + tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", backCallback), + )) + } else { + rows = append(rows, tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", backCallback), + )) + } kb := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rows} @@ -658,7 +673,7 @@ func (b *Bot) callbackActionPick(chatID int64, msgID int, action string, userID pageInfo = fmt.Sprintf("\n(第 %d/%d 页)", page+1, totalPages) } - b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("%s%s:", label, pageInfo), &kb) + b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("%s%s:\n\n%s", label, pageInfo, strings.Join(lns, "\n")), &kb) } func (b *Bot) callbackDelAccount(chatID int64, msgID int, idStr string) { @@ -722,6 +737,16 @@ func (b *Bot) callbackRefAccount(chatID int64, msgID int, idStr string) { _ = b.db.UpdateAccountTokens(id, result.AccessToken, result.RefreshToken) + // Sync member counts after token refresh. + acc.Token = result.AccessToken + if userTotal, _, err2 := b.client.GetUsers(acc); err2 == nil { + invTotal := acc.InviteCount + if inv, _, err3 := b.client.GetInvites(acc); err3 == nil { + invTotal = inv + } + _ = b.db.UpdateAccountCounts(id, userTotal, invTotal) + } + kb := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"), @@ -1014,8 +1039,10 @@ func (b *Bot) handleMessage(msg *tgbotapi.Message) { if hasSess && !strings.HasPrefix(text, "/") { switch sess.flowType { case "redeem_code": + b.deleteMsg(chatID, msg.MessageID) b.handleRedeemCode(chatID, text) case "redeem": + b.deleteMsg(chatID, msg.MessageID) b.handleRedeemEmail(chatID, sess, text) case "login": b.handleLoginCallback(chatID, msg.MessageID, text) @@ -1030,7 +1057,7 @@ func (b *Bot) handleMessage(msg *tgbotapi.Message) { if email == "" { return } - accounts, err := b.db.GetOpenAccounts(b.cfg.TeamCapacity) + accounts, err := b.db.GetOpenAccounts(b.cfg.TeamCapacity - 1) if err != nil || len(accounts) == 0 { kb := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( @@ -1230,7 +1257,7 @@ func (b *Bot) handleRedeemEmail(chatID int64, sess *chatSession, email string) { if panelMsgID != 0 { b.editMsgWithKeyboard(chatID, panelMsgID, "⏳ 正在处理兑换,请稍候...", nil) - result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity) + result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity-1) if err != nil { b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("❌ 兑换失败: %s", err.Error()), &backKb) return @@ -1239,7 +1266,7 @@ func (b *Bot) handleRedeemEmail(chatID int64, sess *chatSession, email string) { } else { msgID := b.sendAndGetID(chatID, "⏳ 正在处理兑换,请稍候...") - result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity) + result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity-1) if err != nil { b.editMsg(chatID, msgID, fmt.Sprintf("❌ 兑换失败: %s", err.Error())) return @@ -1337,9 +1364,9 @@ func (b *Bot) handleAddAccount(chatID int64, panelMsgID int, args string) { continue } - // Generate codes based on remaining capacity: 6 - (current user count + pending invites). + // Generate codes based on remaining capacity: (b.cfg.TeamCapacity - 1) - (current user count + pending invites). newAcct, _ := b.db.GetAccountByID(id) - codeCount := 6 + codeCount := b.cfg.TeamCapacity if newAcct != nil { var userTotal, inviteTotal int if ut, _, err2 := b.client.GetUsers(newAcct); err2 == nil { @@ -1348,7 +1375,7 @@ func (b *Bot) handleAddAccount(chatID int64, panelMsgID int, args string) { if it, _, err3 := b.client.GetInvites(newAcct); err3 == nil { inviteTotal = it } - codeCount = 6 - (userTotal + inviteTotal) + codeCount = b.cfg.TeamCapacity - (userTotal + inviteTotal) _ = b.db.UpdateAccountCounts(id, userTotal, inviteTotal) } if codeCount < 0 { @@ -1549,7 +1576,7 @@ func (b *Bot) handleGenCodes(chatID int64, panelMsgID int, args string) { email := eligible[i].Email existing, _ := b.db.CountAvailableCodesByAccount(email) - maxCodes := b.cfg.TeamCapacity - eligible[i].UserCount - eligible[i].InviteCount + maxCodes := (b.cfg.TeamCapacity - 1) - eligible[i].UserCount - eligible[i].InviteCount if maxCodes < 0 { maxCodes = 0 }