feat: 实现前端卡密管理界面

- 卡密列表展示与分页功能

- 单个/批量创建卡密

- 卡密删除与批量删除

- 卡密导出功能 (file-saver)

- 启用/禁用状态切换

- 状态判断 (有效/已使用/已失效)

- Toast 通知系统 (vue-sonner)

- 登录页面错误提示优化

- 后端登录错误消息中文化
This commit is contained in:
sar
2026-01-13 21:34:56 +08:00
parent 42c423bd32
commit 8d60704eda
143 changed files with 6646 additions and 91 deletions

View File

@@ -0,0 +1,138 @@
package handler
import (
"encoding/json"
"net/http"
"gpt-manager-go/internal/auth"
"gpt-manager-go/internal/repository"
)
// AuthHandler 认证处理器
type AuthHandler struct {
adminRepo *repository.AdminRepository
}
// NewAuthHandler 创建认证处理器
func NewAuthHandler(adminRepo *repository.AdminRepository) *AuthHandler {
return &AuthHandler{adminRepo: adminRepo}
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Token string `json:"token,omitempty"`
User *UserInfo `json:"user,omitempty"`
}
// UserInfo 用户信息
type UserInfo struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsSuperAdmin bool `json:"is_super_admin"`
}
// Login 登录接口
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, LoginResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, LoginResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证必填字段
if req.Username == "" || req.Password == "" {
respondJSON(w, http.StatusBadRequest, LoginResponse{
Success: false,
Message: "Username and password are required",
})
return
}
// 查找用户
admin, err := h.adminRepo.FindByUsername(req.Username)
if err != nil {
respondJSON(w, http.StatusInternalServerError, LoginResponse{
Success: false,
Message: "Internal server error",
})
return
}
if admin == nil {
respondJSON(w, http.StatusUnauthorized, LoginResponse{
Success: false,
Message: "用户名或密码错误",
})
return
}
// 检查账号是否激活
if !admin.IsActive {
respondJSON(w, http.StatusForbidden, LoginResponse{
Success: false,
Message: "账号已被禁用",
})
return
}
// 验证密码
if !auth.CheckPassword(req.Password, admin.PasswordHash) {
respondJSON(w, http.StatusUnauthorized, LoginResponse{
Success: false,
Message: "用户名或密码错误",
})
return
}
// 生成 Token
token, err := auth.GenerateToken(admin.ID, admin.Username, admin.IsSuperAdmin)
if err != nil {
respondJSON(w, http.StatusInternalServerError, LoginResponse{
Success: false,
Message: "Failed to generate token",
})
return
}
// 更新最后登录时间
_ = h.adminRepo.UpdateLastLogin(admin.ID)
respondJSON(w, http.StatusOK, LoginResponse{
Success: true,
Message: "Login successful",
Token: token,
User: &UserInfo{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
IsSuperAdmin: admin.IsSuperAdmin,
},
})
}
// respondJSON 返回 JSON 响应
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}

View File

@@ -0,0 +1,367 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"gpt-manager-go/internal/middleware"
"gpt-manager-go/internal/models"
"gpt-manager-go/internal/repository"
)
// CardKeyHandler 卡密处理器
type CardKeyHandler struct {
repo *repository.CardKeyRepository
}
// NewCardKeyHandler 创建处理器
func NewCardKeyHandler(repo *repository.CardKeyRepository) *CardKeyHandler {
return &CardKeyHandler{repo: repo}
}
// CreateCardKeyRequest 创建卡密请求
type CreateCardKeyRequest struct {
ValidityDays int `json:"validity_days"` // 有效期天数默认30
MaxUses int `json:"max_uses"` // 最大使用次数默认1
}
// BatchCreateCardKeyRequest 批量创建卡密请求
type BatchCreateCardKeyRequest struct {
Count int `json:"count"` // 创建数量
ValidityDays int `json:"validity_days"` // 有效期天数默认30
MaxUses int `json:"max_uses"` // 最大使用次数默认1
}
// CardKeyResponse 卡密响应
type CardKeyResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data *models.CardKey `json:"data,omitempty"`
Keys []*models.CardKey `json:"keys,omitempty"`
Total int `json:"total,omitempty"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,omitempty"`
}
// Handle 处理卡密接口 (GET: 列表, POST: 创建)
func (h *CardKeyHandler) Handle(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.list(w, r)
case http.MethodPost:
h.create(w, r)
default:
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
}
}
// create 创建单个卡密
func (h *CardKeyHandler) create(w http.ResponseWriter, r *http.Request) {
var req CreateCardKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// 允许空请求体,使用默认值
req = CreateCardKeyRequest{}
}
// 设置默认值
if req.ValidityDays <= 0 {
req.ValidityDays = 30
}
if req.MaxUses <= 0 {
req.MaxUses = 1
}
// 获取当前用户
claims := middleware.GetUserFromContext(r.Context())
// 生成卡密
cardKey := &models.CardKey{
Key: generateCardKey(),
MaxUses: req.MaxUses,
UsedCount: 0,
ValidityType: "custom",
ExpiresAt: time.Now().AddDate(0, 0, req.ValidityDays),
IsActive: true,
CreatedByID: claims.UserID,
CreatedAt: time.Now(),
}
if err := h.repo.Create(cardKey); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to create card key: " + err.Error(),
})
return
}
respondJSON(w, http.StatusCreated, CardKeyResponse{
Success: true,
Message: "Card key created successfully",
Data: cardKey,
})
}
// BatchCreate 批量创建卡密 (POST /api/cardkeys/batch)
func (h *CardKeyHandler) BatchCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req BatchCreateCardKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证和设置默认值
if req.Count <= 0 {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Count must be greater than 0",
})
return
}
if req.Count > 100 {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Count cannot exceed 100",
})
return
}
if req.ValidityDays <= 0 {
req.ValidityDays = 30
}
if req.MaxUses <= 0 {
req.MaxUses = 1
}
// 获取当前用户
claims := middleware.GetUserFromContext(r.Context())
// 批量创建
var createdKeys []*models.CardKey
expiresAt := time.Now().AddDate(0, 0, req.ValidityDays)
for i := 0; i < req.Count; i++ {
cardKey := &models.CardKey{
Key: generateCardKey(),
MaxUses: req.MaxUses,
UsedCount: 0,
ValidityType: "custom",
ExpiresAt: expiresAt,
IsActive: true,
CreatedByID: claims.UserID,
CreatedAt: time.Now(),
}
if err := h.repo.Create(cardKey); err != nil {
// 如果创建失败,返回已创建的卡密
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to create some card keys",
Keys: createdKeys,
})
return
}
createdKeys = append(createdKeys, cardKey)
}
respondJSON(w, http.StatusCreated, CardKeyResponse{
Success: true,
Message: "Card keys created successfully",
Keys: createdKeys,
})
}
// list 获取卡密列表
func (h *CardKeyHandler) list(w http.ResponseWriter, r *http.Request) {
// 获取分页参数
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
page, _ := strconv.Atoi(pageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
// 默认值
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
// 获取总数
total, err := h.repo.Count()
if err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to count card keys",
})
return
}
// 获取分页数据
cardKeys, err := h.repo.FindAllPaginated(page, pageSize)
if err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to fetch card keys",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "OK",
Keys: cardKeys,
Total: total,
Page: page,
PageSize: pageSize,
})
}
// generateCardKey 生成卡密格式: XXXX-XXXX-XXXX-XXXX
func generateCardKey() string {
bytes := make([]byte, 8)
rand.Read(bytes)
hex := strings.ToUpper(hex.EncodeToString(bytes))
return hex[0:4] + "-" + hex[4:8] + "-" + hex[8:12] + "-" + hex[12:16]
}
// Delete 删除单个卡密 (DELETE /api/cardkeys/delete?id=xxx)
func (h *CardKeyHandler) Delete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
return
}
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid card key ID",
})
return
}
if err := h.repo.Delete(id); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to delete card key",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card key deleted successfully",
})
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteRequest struct {
IDs []int `json:"ids"`
}
// BatchDelete 批量删除卡密 (DELETE /api/cardkeys/batch)
func (h *CardKeyHandler) BatchDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req BatchDeleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if len(req.IDs) == 0 {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "No IDs provided",
})
return
}
if err := h.repo.BatchDelete(req.IDs); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to delete card keys",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card keys deleted successfully",
})
}
// ToggleActiveRequest 切换激活状态请求
type ToggleActiveRequest struct {
ID int `json:"id"`
IsActive bool `json:"is_active"`
}
// ToggleActive 切换卡密激活状态 (POST /api/cardkeys/toggle)
func (h *CardKeyHandler) ToggleActive(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, CardKeyResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req ToggleActiveRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, CardKeyResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if err := h.repo.ToggleActive(req.ID, req.IsActive); err != nil {
respondJSON(w, http.StatusInternalServerError, CardKeyResponse{
Success: false,
Message: "Failed to toggle card key status",
})
return
}
respondJSON(w, http.StatusOK, CardKeyResponse{
Success: true,
Message: "Card key status updated successfully",
})
}

View File

@@ -0,0 +1,322 @@
package handler
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"time"
"gpt-manager-go/internal/models"
"gpt-manager-go/internal/repository"
"gpt-manager-go/internal/service"
)
// ChatGPTAccountHandler ChatGPT 账号处理器
type ChatGPTAccountHandler struct {
repo *repository.ChatGPTAccountRepository
chatgptService *service.ChatGPTService
}
// NewChatGPTAccountHandler 创建处理器
func NewChatGPTAccountHandler(repo *repository.ChatGPTAccountRepository, chatgptService *service.ChatGPTService) *ChatGPTAccountHandler {
return &ChatGPTAccountHandler{
repo: repo,
chatgptService: chatgptService,
}
}
// CreateAccountRequest 创建账号请求
type CreateAccountRequest struct {
Name string `json:"name"`
TeamAccountID string `json:"team_account_id"`
AuthToken string `json:"auth_token"`
}
// AccountResponse 账号响应
type AccountResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data *models.ChatGPTAccount `json:"data,omitempty"`
}
// ListResponse 列表响应
type ListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data []*models.ChatGPTAccount `json:"data,omitempty"`
Total int `json:"total,omitempty"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,omitempty"`
}
// Create 创建账号
func (h *ChatGPTAccountHandler) Create(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req CreateAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, AccountResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证必填字段
if req.TeamAccountID == "" || req.AuthToken == "" {
respondJSON(w, http.StatusBadRequest, AccountResponse{
Success: false,
Message: "team_account_id and auth_token are required",
})
return
}
// 检查是否已存在
existing, err := h.repo.FindByTeamAccountID(req.TeamAccountID)
if err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Database error: " + err.Error(),
})
return
}
if existing != nil {
respondJSON(w, http.StatusConflict, AccountResponse{
Success: false,
Message: "Team account already exists",
})
return
}
// 调用 ChatGPT API 获取订阅信息
subInfo, err := h.chatgptService.GetSubscription(req.TeamAccountID, req.AuthToken)
if err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to verify team account: " + err.Error(),
})
return
}
// 设置账号名称
name := req.Name
if name == "" {
name = "Team-" + req.TeamAccountID[:8]
}
// 创建账号
account := &models.ChatGPTAccount{
Name: name,
AuthToken: req.AuthToken,
TeamAccountID: req.TeamAccountID,
SeatsInUse: subInfo.SeatsInUse,
SeatsEntitled: subInfo.SeatsEntitled,
IsActive: subInfo.IsValid,
}
// 设置时间
if subInfo.IsValid {
account.ActiveStart = sql.NullTime{Time: subInfo.ActiveStart, Valid: !subInfo.ActiveStart.IsZero()}
account.ActiveUntil = sql.NullTime{Time: subInfo.ActiveUntil, Valid: !subInfo.ActiveUntil.IsZero()}
account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true}
}
if err := h.repo.Create(account); err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to create account: " + err.Error(),
})
return
}
// 隐藏敏感信息
account.AuthToken = ""
respondJSON(w, http.StatusCreated, AccountResponse{
Success: true,
Message: "Account created successfully",
Data: account,
})
}
// List 获取账号列表
func (h *ChatGPTAccountHandler) List(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
respondJSON(w, http.StatusMethodNotAllowed, ListResponse{
Success: false,
Message: "Method not allowed",
})
return
}
// 获取分页参数
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
page, _ := strconv.Atoi(pageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
// 默认值
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
// 获取总数
total, err := h.repo.Count()
if err != nil {
respondJSON(w, http.StatusInternalServerError, ListResponse{
Success: false,
Message: "Failed to count accounts",
})
return
}
// 获取分页数据
accounts, err := h.repo.FindAllPaginated(page, pageSize)
if err != nil {
respondJSON(w, http.StatusInternalServerError, ListResponse{
Success: false,
Message: "Failed to fetch accounts",
})
return
}
// 隐藏敏感信息
for _, a := range accounts {
a.AuthToken = ""
}
respondJSON(w, http.StatusOK, ListResponse{
Success: true,
Message: "OK",
Data: accounts,
Total: total,
Page: page,
PageSize: pageSize,
})
}
// Refresh 刷新账号信息
func (h *ChatGPTAccountHandler) Refresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{
Success: false,
Message: "Method not allowed",
})
return
}
// 获取账号 ID
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, AccountResponse{
Success: false,
Message: "Invalid account ID",
})
return
}
account, err := h.repo.FindByID(id)
if err != nil || account == nil {
respondJSON(w, http.StatusNotFound, AccountResponse{
Success: false,
Message: "Account not found",
})
return
}
// 调用 ChatGPT API 获取订阅信息
subInfo, err := h.chatgptService.GetSubscription(account.TeamAccountID, account.AuthToken)
if err != nil {
// API 调用失败,增加失败计数并设为不活跃
account.ConsecutiveFailures++
account.IsActive = false
account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true}
h.repo.Update(account)
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to refresh: " + err.Error(),
})
return
}
// 更新账号信息
if subInfo.IsValid {
account.SeatsInUse = subInfo.SeatsInUse
account.SeatsEntitled = subInfo.SeatsEntitled
account.ActiveStart = sql.NullTime{Time: subInfo.ActiveStart, Valid: !subInfo.ActiveStart.IsZero()}
account.ActiveUntil = sql.NullTime{Time: subInfo.ActiveUntil, Valid: !subInfo.ActiveUntil.IsZero()}
account.IsActive = true
account.ConsecutiveFailures = 0
} else {
account.IsActive = false
account.ConsecutiveFailures++
}
account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true}
if err := h.repo.Update(account); err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to update account",
})
return
}
// 隐藏敏感信息
account.AuthToken = ""
respondJSON(w, http.StatusOK, AccountResponse{
Success: true,
Message: "Account refreshed successfully",
Data: account,
})
}
// Delete 删除账号
func (h *ChatGPTAccountHandler) Delete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, AccountResponse{
Success: false,
Message: "Method not allowed",
})
return
}
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, AccountResponse{
Success: false,
Message: "Invalid account ID",
})
return
}
if err := h.repo.Delete(id); err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to delete account",
})
return
}
respondJSON(w, http.StatusOK, AccountResponse{
Success: true,
Message: "Account deleted successfully",
})
}

View File

@@ -0,0 +1,445 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"time"
"gpt-manager-go/internal/models"
"gpt-manager-go/internal/repository"
"gpt-manager-go/internal/service"
)
// InviteHandler 邀请处理器
type InviteHandler struct {
accountRepo *repository.ChatGPTAccountRepository
invitationRepo *repository.InvitationRepository
cardKeyRepo *repository.CardKeyRepository
chatgptService *service.ChatGPTService
}
// NewInviteHandler 创建邀请处理器
func NewInviteHandler(
accountRepo *repository.ChatGPTAccountRepository,
invitationRepo *repository.InvitationRepository,
cardKeyRepo *repository.CardKeyRepository,
chatgptService *service.ChatGPTService,
) *InviteHandler {
return &InviteHandler{
accountRepo: accountRepo,
invitationRepo: invitationRepo,
cardKeyRepo: cardKeyRepo,
chatgptService: chatgptService,
}
}
// AdminInviteRequest 管理员邀请请求
type AdminInviteRequest struct {
Email string `json:"email"`
AccountID int `json:"account_id"` // 可选,不传则自动选择
}
// CardKeyInviteRequest 卡密邀请请求
type CardKeyInviteRequest struct {
CardKey string `json:"card_key"`
Email string `json:"email"`
}
// InviteResponse 邀请响应
type InviteResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
InvitationID int `json:"invitation_id,omitempty"`
AccountName string `json:"account_name,omitempty"`
}
// InviteListResponse 邀请列表响应
type InviteListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data []*models.Invitation `json:"data,omitempty"`
}
// ListByAccount 获取账号的邀请列表 (GET /api/invite?account_id=xxx)
func (h *InviteHandler) ListByAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
respondJSON(w, http.StatusMethodNotAllowed, InviteListResponse{
Success: false,
Message: "Method not allowed",
})
return
}
accountIDStr := r.URL.Query().Get("account_id")
if accountIDStr == "" {
respondJSON(w, http.StatusBadRequest, InviteListResponse{
Success: false,
Message: "account_id is required",
})
return
}
var accountID int
if _, err := fmt.Sscanf(accountIDStr, "%d", &accountID); err != nil {
respondJSON(w, http.StatusBadRequest, InviteListResponse{
Success: false,
Message: "Invalid account_id",
})
return
}
invitations, err := h.invitationRepo.FindByAccountID(accountID)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteListResponse{
Success: false,
Message: "Failed to fetch invitations",
})
return
}
respondJSON(w, http.StatusOK, InviteListResponse{
Success: true,
Message: "OK",
Data: invitations,
})
}
// InviteByAdmin 管理员邀请/移除接口 (POST/DELETE /api/invite)
func (h *InviteHandler) InviteByAdmin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
h.handleInvite(w, r)
case http.MethodDelete:
h.handleRemoveMember(w, r)
default:
respondJSON(w, http.StatusMethodNotAllowed, InviteResponse{
Success: false,
Message: "Method not allowed",
})
}
}
// handleInvite 处理邀请逻辑
func (h *InviteHandler) handleInvite(w http.ResponseWriter, r *http.Request) {
var req AdminInviteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证邮箱
if req.Email == "" {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Email is required",
})
return
}
// 获取账号
var account *models.ChatGPTAccount
var err error
if req.AccountID > 0 {
// 指定账号
account, err = h.accountRepo.FindByID(req.AccountID)
if err != nil || account == nil {
respondJSON(w, http.StatusNotFound, InviteResponse{
Success: false,
Message: "Account not found",
})
return
}
if !account.IsActive || account.AvailableSeats() <= 0 {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Account is not active or has no available seats",
})
return
}
} else {
// 自动选择可用账号
account, err = h.findAvailableAccount()
if err != nil || account == nil {
respondJSON(w, http.StatusServiceUnavailable, InviteResponse{
Success: false,
Message: "No available team accounts",
})
return
}
}
// 发送邀请
inviteResp, err := h.chatgptService.SendInvite(account.TeamAccountID, account.AuthToken, req.Email)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteResponse{
Success: false,
Message: "Failed to send invite: " + err.Error(),
})
return
}
// 创建邀请记录
invitation := &models.Invitation{
AccountID: account.ID,
InvitedEmail: req.Email,
Status: models.StatusSent,
CreatedAt: time.Now(),
}
if inviteResp.Success {
invitation.Status = models.StatusSent
} else {
invitation.Status = models.StatusFailed
invitation.ErrorMessage = sql.NullString{String: inviteResp.Message, Valid: true}
}
if err := h.invitationRepo.Create(invitation); err != nil {
// 记录失败不影响主流程
}
if !inviteResp.Success {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: inviteResp.Message,
})
return
}
// 更新账号使用时间
h.accountRepo.UpdateLastUsed(account.ID)
respondJSON(w, http.StatusOK, InviteResponse{
Success: true,
Message: "Invitation sent successfully",
InvitationID: invitation.ID,
AccountName: account.Name,
})
}
// InviteByCardKey 卡密邀请 (POST /api/invite/card)
func (h *InviteHandler) InviteByCardKey(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, InviteResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req CardKeyInviteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证必填字段
if req.CardKey == "" || req.Email == "" {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Card key and email are required",
})
return
}
// 验证卡密
cardKey, err := h.cardKeyRepo.FindByKey(req.CardKey)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteResponse{
Success: false,
Message: "Database error",
})
return
}
if cardKey == nil {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Invalid card key",
})
return
}
// 检查卡密是否可用
if !cardKey.IsUsable() {
if cardKey.IsExpired() {
respondJSON(w, http.StatusForbidden, InviteResponse{
Success: false,
Message: "Card key has expired",
})
} else if cardKey.UsedCount >= cardKey.MaxUses {
respondJSON(w, http.StatusForbidden, InviteResponse{
Success: false,
Message: "Card key usage limit exceeded",
})
} else {
respondJSON(w, http.StatusForbidden, InviteResponse{
Success: false,
Message: "Card key is not active",
})
}
return
}
// 查找可用账号
account, err := h.findAvailableAccount()
if err != nil || account == nil {
respondJSON(w, http.StatusServiceUnavailable, InviteResponse{
Success: false,
Message: "No available team accounts",
})
return
}
// 发送邀请
inviteResp, err := h.chatgptService.SendInvite(account.TeamAccountID, account.AuthToken, req.Email)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteResponse{
Success: false,
Message: "Failed to send invite: " + err.Error(),
})
return
}
// 创建邀请记录
invitation := &models.Invitation{
CardKeyID: sql.NullInt64{Int64: int64(cardKey.ID), Valid: true},
AccountID: account.ID,
InvitedEmail: req.Email,
CreatedAt: time.Now(),
}
if inviteResp.Success {
invitation.Status = models.StatusSent
} else {
invitation.Status = models.StatusFailed
invitation.ErrorMessage = sql.NullString{String: inviteResp.Message, Valid: true}
}
if err := h.invitationRepo.Create(invitation); err != nil {
// 记录失败不影响主流程
}
if !inviteResp.Success {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: inviteResp.Message,
})
return
}
// 增加卡密使用次数
h.cardKeyRepo.IncrementUsedCount(cardKey.ID)
// 更新账号使用时间
h.accountRepo.UpdateLastUsed(account.ID)
respondJSON(w, http.StatusOK, InviteResponse{
Success: true,
Message: "Invitation sent successfully",
})
}
// findAvailableAccount 查找可用账号(轮询)
func (h *InviteHandler) findAvailableAccount() (*models.ChatGPTAccount, error) {
accounts, err := h.accountRepo.FindActiveWithAvailableSeats()
if err != nil {
return nil, err
}
if len(accounts) == 0 {
return nil, nil
}
// 返回第一个(已按可用席位数降序、最后使用时间升序排序)
return accounts[0], nil
}
// RemoveMemberRequest 移除成员请求
type RemoveMemberRequest struct {
Email string `json:"email"`
AccountID int `json:"account_id"` // 必填,指定从哪个账号移除
}
// handleRemoveMember 处理移除成员逻辑
func (h *InviteHandler) handleRemoveMember(w http.ResponseWriter, r *http.Request) {
var req RemoveMemberRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Invalid request body",
})
return
}
// 验证必填字段
if req.Email == "" {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Email is required",
})
return
}
if req.AccountID <= 0 {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Account ID is required",
})
return
}
// 获取账号
account, err := h.accountRepo.FindByID(req.AccountID)
if err != nil || account == nil {
respondJSON(w, http.StatusNotFound, InviteResponse{
Success: false,
Message: "Account not found",
})
return
}
if !account.IsActive {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: "Account is not active",
})
return
}
// 调用 ChatGPT API 移除成员
removeResp, err := h.chatgptService.RemoveMember(account.TeamAccountID, account.AuthToken, req.Email)
if err != nil {
respondJSON(w, http.StatusInternalServerError, InviteResponse{
Success: false,
Message: "Failed to remove member: " + err.Error(),
})
return
}
if !removeResp.Success {
respondJSON(w, http.StatusBadRequest, InviteResponse{
Success: false,
Message: removeResp.Message,
})
return
}
// 删除邀请记录
if err := h.invitationRepo.DeleteByEmailAndAccountID(req.Email, req.AccountID); err != nil {
// 删除记录失败不影响主流程,只记录日志
}
respondJSON(w, http.StatusOK, InviteResponse{
Success: true,
Message: "Member removed successfully",
AccountName: account.Name,
})
}