- 卡密列表展示与分页功能 - 单个/批量创建卡密 - 卡密删除与批量删除 - 卡密导出功能 (file-saver) - 启用/禁用状态切换 - 状态判断 (有效/已使用/已失效) - Toast 通知系统 (vue-sonner) - 登录页面错误提示优化 - 后端登录错误消息中文化
446 lines
11 KiB
Go
446 lines
11 KiB
Go
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,
|
|
})
|
|
}
|