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,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",
})
}