mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Remove the card password related
New application-related
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.DS_Store
|
||||
networkDev
|
||||
node.txt
|
||||
recharge.db
|
||||
config.json
|
||||
data.db
|
||||
database.db
|
||||
模板
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
package constants
|
||||
|
||||
|
||||
|
||||
// 卡密状态常量
|
||||
// CardStatus 定义卡密的状态
|
||||
const (
|
||||
// CardStatusUnused 未使用
|
||||
CardStatusUnused = 0
|
||||
// CardStatusUsed 已使用
|
||||
CardStatusUsed = 1
|
||||
// CardStatusDisabled 禁用
|
||||
CardStatusDisabled = 2
|
||||
)
|
||||
|
||||
// 登录类型状态常量
|
||||
// LoginTypeStatus 定义登录类型的状态
|
||||
const (
|
||||
// LoginTypeStatusDisabled 禁用
|
||||
LoginTypeStatusDisabled = 0
|
||||
// LoginTypeStatusEnabled 启用
|
||||
LoginTypeStatusEnabled = 1
|
||||
)
|
||||
|
||||
// 卡密类型状态常量
|
||||
// CardTypeStatus 定义卡密类型的状态
|
||||
const (
|
||||
// CardTypeStatusDisabled 禁用
|
||||
CardTypeStatusDisabled = 0
|
||||
// CardTypeStatusEnabled 启用
|
||||
CardTypeStatusEnabled = 1
|
||||
)
|
||||
|
||||
// 验证码类型常量
|
||||
// VerificationCodeType 定义验证码的类型
|
||||
const (
|
||||
|
||||
@@ -2,6 +2,7 @@ package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -87,6 +88,141 @@ func AppsListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppGetAnnouncementHandler 获取应用程序公告处理器
|
||||
func AppGetAnnouncementHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取UUID参数
|
||||
uuid := r.URL.Query().Get("uuid")
|
||||
if uuid == "" {
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "应用UUID不能为空",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "数据库连接失败",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找应用
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", uuid).First(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to find app")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "应用不存在",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 解码base64公告内容
|
||||
var announcement string
|
||||
if app.Announcement != "" {
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(app.Announcement)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to decode announcement")
|
||||
// 如果解码失败,返回空字符串
|
||||
announcement = ""
|
||||
} else {
|
||||
announcement = string(decodedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "获取成功",
|
||||
"data": map[string]interface{}{
|
||||
"announcement": announcement,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppResetSecretHandler 重置应用密钥API处理器
|
||||
func AppResetSecretHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UUID string `json:"uuid"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.UUID == "" {
|
||||
http.Error(w, "应用UUID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找应用
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", req.UUID).First(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to find app by UUID")
|
||||
http.Error(w, "应用不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的密钥
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
logrus.WithError(err).Error("Failed to generate random secret")
|
||||
http.Error(w, "生成密钥失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
newSecret := strings.ToUpper(hex.EncodeToString(bytes))
|
||||
|
||||
// 更新密钥
|
||||
if err := db.Model(&app).Update("secret", newSecret).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update app secret")
|
||||
http.Error(w, "更新密钥失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "密钥重置成功",
|
||||
"data": map[string]interface{}{
|
||||
"uuid": app.UUID,
|
||||
"secret": newSecret,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppCreateHandler 创建应用API处理器
|
||||
func AppCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -386,3 +522,226 @@ func AppsBatchUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppUpdateAnnouncementHandler 更新应用程序公告处理器
|
||||
func AppUpdateAnnouncementHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var req struct {
|
||||
UUID string `json:"uuid"`
|
||||
Announcement string `json:"announcement"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
logrus.WithError(err).Error("Failed to decode request body")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "请求参数格式错误",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证UUID
|
||||
if req.UUID == "" {
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "应用UUID不能为空",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证UUID格式
|
||||
if _, err := uuid.Parse(req.UUID); err != nil {
|
||||
logrus.WithError(err).Error("Invalid UUID format")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "无效的UUID格式",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "数据库连接失败",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找应用
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", req.UUID).First(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to find app")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "应用不存在",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// 对公告内容进行base64编码
|
||||
encodedAnnouncement := base64.StdEncoding.EncodeToString([]byte(req.Announcement))
|
||||
|
||||
// 更新应用的公告内容
|
||||
if err := db.Model(&app).Update("announcement", encodedAnnouncement).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update app announcement")
|
||||
response := map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "更新程序公告失败",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"app_uuid": req.UUID,
|
||||
"app_name": app.Name,
|
||||
}).Info("App announcement updated successfully")
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "程序公告更新成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
// AppGetMultiConfigHandler 获取应用多开配置
|
||||
func AppGetMultiConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
appUUID := r.URL.Query().Get("uuid")
|
||||
if appUUID == "" {
|
||||
http.Error(w, "缺少应用UUID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证UUID格式
|
||||
if _, err := uuid.Parse(appUUID); err != nil {
|
||||
http.Error(w, "无效的UUID格式", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", appUUID).First(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to find app")
|
||||
http.Error(w, "应用不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回多开配置信息
|
||||
response := map[string]interface{}{
|
||||
"login_type": app.LoginType,
|
||||
"multi_open_scope": app.MultiOpenScope,
|
||||
"clean_interval": app.CleanInterval,
|
||||
"check_interval": app.CheckInterval,
|
||||
"multi_open_count": app.MultiOpenCount,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppUpdateMultiConfigHandler 更新应用多开配置
|
||||
func AppUpdateMultiConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
UUID string `json:"uuid"`
|
||||
LoginType int `json:"login_type"`
|
||||
MultiOpenScope int `json:"multi_open_scope"`
|
||||
CleanInterval int `json:"clean_interval"`
|
||||
CheckInterval int `json:"check_interval"`
|
||||
MultiOpenCount int `json:"multi_open_count"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证UUID格式
|
||||
if _, err := uuid.Parse(req.UUID); err != nil {
|
||||
http.Error(w, "无效的UUID格式", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数范围
|
||||
if req.LoginType < 0 || req.LoginType > 1 {
|
||||
http.Error(w, "登录方式参数无效", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.MultiOpenScope < 0 || req.MultiOpenScope > 2 {
|
||||
http.Error(w, "多开范围参数无效", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CleanInterval < 1 {
|
||||
http.Error(w, "清理间隔必须大于0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CheckInterval < 1 {
|
||||
http.Error(w, "校验间隔必须大于0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.MultiOpenCount < 1 {
|
||||
http.Error(w, "多开数量必须大于0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找应用
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", req.UUID).First(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to find app")
|
||||
http.Error(w, "应用不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新多开配置
|
||||
updates := map[string]interface{}{
|
||||
"login_type": req.LoginType,
|
||||
"multi_open_scope": req.MultiOpenScope,
|
||||
"clean_interval": req.CleanInterval,
|
||||
"check_interval": req.CheckInterval,
|
||||
"multi_open_count": req.MultiOpenCount,
|
||||
}
|
||||
|
||||
if err := db.Model(&app).Updates(updates).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update app multi config")
|
||||
http.Error(w, "更新多开配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "多开配置更新成功"})
|
||||
}
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
// 移除 CSV 导出,改为自定义分隔符文本导出
|
||||
// "encoding/csv"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 生成指定长度的十六进制随机字符串
|
||||
// 入参 n 表示需要的随机字符数(非字节数);返回小写十六进制字符串
|
||||
func genRandomHex(n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
// 由于 hex 每个字节会转成 2 个字符,因此需要 (n+1)/2 个字节
|
||||
byteLen := (n + 1) / 2
|
||||
b := make([]byte, byteLen)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
s := hex.EncodeToString(b)
|
||||
if len(s) > n {
|
||||
s = s[:n]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 根据前缀和总长度构建卡号
|
||||
// - totalLen <= 0 时按 18 处理
|
||||
// - 若前缀长度 >= totalLen,则自动扩展为 前缀长度+18
|
||||
// - uppercase=true 表示最终结果转为大写;false 表示小写
|
||||
func buildCardNumber(prefix string, totalLen int, uppercase bool) string {
|
||||
if totalLen <= 0 {
|
||||
totalLen = 18
|
||||
}
|
||||
if len(prefix) >= totalLen {
|
||||
totalLen = len(prefix) + 18
|
||||
}
|
||||
rest := totalLen - len(prefix)
|
||||
s := prefix + genRandomHex(rest)
|
||||
if uppercase {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// CardsFragmentHandler 卡密管理片段渲染
|
||||
// - 渲染 cards.html 列表与表单界面
|
||||
func CardsFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "cards.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// CardsListHandler 获取卡密列表
|
||||
// - 支持GET
|
||||
// - 支持分页查询参数:page、page_size
|
||||
// - 支持筛选参数:card_type_id、status、batch、keyword(卡号/备注/批次模糊匹配)
|
||||
// - 返回卡密列表和分页信息
|
||||
func CardsListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
cardTypeIDStr := r.URL.Query().Get("card_type_id")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
batch := r.URL.Query().Get("batch")
|
||||
// 中文注释:keyword 支持在 card_number、remark、batch 三个字段上进行模糊匹配
|
||||
keyword := strings.TrimSpace(r.URL.Query().Get("keyword"))
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件(去除无效的 Preload,前端已通过 card_type_id 自行映射类型名称)
|
||||
query := db.Model(&models.Card{})
|
||||
|
||||
// 筛选条件
|
||||
if cardTypeIDStr != "" {
|
||||
if cardTypeID, err := strconv.Atoi(cardTypeIDStr); err == nil && cardTypeID > 0 {
|
||||
query = query.Where("card_type_id = ?", cardTypeID)
|
||||
}
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
if batch != "" {
|
||||
query = query.Where("batch LIKE ?", "%"+batch+"%")
|
||||
}
|
||||
// 中文注释:当提供 keyword 时,在卡号、备注、批次三个字段上进行 OR 模糊匹配
|
||||
if keyword != "" {
|
||||
kw := "%" + keyword + "%"
|
||||
query = query.Where("(card_number LIKE ? OR remark LIKE ? OR batch LIKE ?)", kw, kw, kw)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var cards []models.Card
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id desc").Offset(offset).Limit(pageSize).Find(&cards).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 中文注释:为每条卡记录补充类型名称,避免前端依赖异步类型映射导致显示“未知类型”
|
||||
// 1) 先查询类型列表并构建 id->name 的映射表
|
||||
var cardTypeList []models.CardType
|
||||
_ = db.Model(&models.CardType{}).Find(&cardTypeList).Error
|
||||
typeNameMap := make(map[uint]string, len(cardTypeList))
|
||||
for _, t := range cardTypeList {
|
||||
typeNameMap[t.ID] = t.Name
|
||||
}
|
||||
// 2) 将卡列表转换为通用 map 列表,并附加 card_type_name 字段
|
||||
items := make([]map[string]interface{}, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": c.ID,
|
||||
"card_number": c.CardNumber,
|
||||
"card_type_id": c.CardTypeID,
|
||||
"card_type_name": typeNameMap[c.CardTypeID],
|
||||
"status": c.Status,
|
||||
"batch": c.Batch,
|
||||
"remark": c.Remark,
|
||||
"used_at": c.UsedAt,
|
||||
"created_at": c.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// CardCreateHandler 新增卡密
|
||||
// - 接收JSON: {card_type_id, status, remark, prefix, length, uppercase, count}
|
||||
// - card_number 与 batch 不再由前端传入,后端将自动生成:
|
||||
// 1. 卡号:按 prefix 与 length 生成随机十六进制字符串,支持大小写控制(uppercase,默认小写)
|
||||
// 2. 批次:基于设置表 card_batch_counter 自增,格式为 YYYYMMDD-000001
|
||||
// 3. 生成数量:通过 count 控制一次生成的数量,默认1,最大500
|
||||
func CardCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Status int `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
Prefix string `json:"prefix"`
|
||||
Length int `json:"length"`
|
||||
Uppercase bool `json:"uppercase"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.CardTypeID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型ID不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查卡密类型是否存在且启用
|
||||
var cardType models.CardType
|
||||
if err := db.First(&cardType, body.CardTypeID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
// 检查卡密类型是否被禁用
|
||||
if cardType.Status != 1 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型已被禁用,无法创建卡密", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 规范化长度与大小写、生成数量参数
|
||||
if body.Length <= 0 {
|
||||
body.Length = 18
|
||||
}
|
||||
if body.Count <= 0 {
|
||||
body.Count = 1
|
||||
}
|
||||
if body.Count > 500 {
|
||||
body.Count = 500
|
||||
}
|
||||
|
||||
// 生成批次(基于设置表 card_batch_counter 自增)
|
||||
// 格式:YYYYMMDD-000001(每天不重置,仅简单自增计数)
|
||||
var batch string
|
||||
var setting models.Settings
|
||||
if err := db.Where("name = ?", "card_batch_counter").First(&setting).Error; err != nil {
|
||||
// 若不存在该设置项,则创建并从 1 开始
|
||||
setting = models.Settings{Name: "card_batch_counter", Value: "1", Description: "卡密批次号计数器(用于记录上次生成批次号的序号,自增使用)"}
|
||||
if e := db.Create(&setting).Error; e != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "初始化批次计数器失败", nil)
|
||||
return
|
||||
}
|
||||
batch = time.Now().Format("20060102") + "-" + fmt.Sprintf("%06d", 1)
|
||||
} else {
|
||||
cnt, _ := strconv.Atoi(setting.Value)
|
||||
cnt++
|
||||
newVal := strconv.Itoa(cnt)
|
||||
if e := db.Model(&models.Settings{}).Where("id = ?", setting.ID).Update("value", newVal).Error; e != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新批次计数器失败", nil)
|
||||
return
|
||||
}
|
||||
batch = time.Now().Format("20060102") + "-" + fmt.Sprintf("%06d", cnt)
|
||||
}
|
||||
|
||||
// 中文注释:计算合法状态值(1=已使用,2=禁用,其它按未使用0处理)
|
||||
safeStatus := body.Status
|
||||
if safeStatus != 1 && safeStatus != 2 {
|
||||
safeStatus = 0
|
||||
}
|
||||
|
||||
// 中文注释:循环生成 count 条卡密,若单条创建失败则重试最多5次
|
||||
success := 0
|
||||
for i := 0; i < body.Count; i++ {
|
||||
card := models.Card{
|
||||
CardNumber: buildCardNumber(body.Prefix, body.Length, body.Uppercase),
|
||||
CardTypeID: body.CardTypeID,
|
||||
Status: safeStatus,
|
||||
Batch: batch,
|
||||
Remark: body.Remark,
|
||||
}
|
||||
var createErr error
|
||||
for j := 0; j < 5; j++ {
|
||||
createErr = db.Create(&card).Error
|
||||
if createErr == nil {
|
||||
success++
|
||||
break
|
||||
}
|
||||
// 失败则重新生成一次卡号后重试
|
||||
card.CardNumber = buildCardNumber(body.Prefix, body.Length, body.Uppercase)
|
||||
}
|
||||
}
|
||||
|
||||
if success == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是卡密号码重复", nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"created": success,
|
||||
"batch": batch,
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, fmt.Sprintf("创建成功:%d条", success), result)
|
||||
}
|
||||
|
||||
// CardUpdateHandler 更新卡密
|
||||
// - 接收JSON: {id, card_number(可选), card_type_id(可选), status, batch(可选), remark}
|
||||
// - 说明:card_number 与 batch 若未提供或为空,则不会更新对应字段
|
||||
func CardUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
CardNumber string `json:"card_number"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Status int `json:"status"`
|
||||
Batch string `json:"batch"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查卡密类型是否存在且启用(如果提供了新的卡密类型ID)
|
||||
if body.CardTypeID > 0 {
|
||||
var cardType models.CardType
|
||||
if err := db.First(&cardType, body.CardTypeID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
// 检查卡密类型是否被禁用
|
||||
if cardType.Status != 1 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型已被禁用,无法更新为此类型", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 中文注释:若尝试将状态置为未使用(0),则直接允许
|
||||
if body.Status == 0 {
|
||||
var existing models.Card
|
||||
if err := db.First(&existing, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密不存在", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
updates := map[string]interface{}{}
|
||||
if body.CardNumber != "" {
|
||||
updates["card_number"] = body.CardNumber
|
||||
}
|
||||
if body.CardTypeID > 0 {
|
||||
updates["card_type_id"] = body.CardTypeID
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
// 仅当提供非空 batch 时才更新,防止被清空
|
||||
if strings.TrimSpace(body.Batch) != "" {
|
||||
updates["batch"] = body.Batch
|
||||
}
|
||||
updates["remark"] = body.Remark
|
||||
|
||||
if err := db.Model(&models.Card{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是卡密号码重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// CardDeleteHandler 删除单个卡密
|
||||
// - 接收JSON: {id}
|
||||
func CardDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.Card{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// CardsBatchDeleteHandler 批量删除卡密
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardsBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.Card{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// CardsBatchUpdateStatusHandler 批量更新卡密状态
|
||||
// - 接收JSON: {ids: [], status: int}
|
||||
// - status: 0=未使用,1=已使用,2=禁用
|
||||
func CardsBatchUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
// 中文注释:允许批量重置为未使用(0)
|
||||
if err := db.Model(&models.Card{}).Where("id IN ?", body.IDs).Update("status", body.Status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
|
||||
// GetCardTypesHandler 获取卡密类型列表(供前端下拉选择)
|
||||
// - 仅支持GET请求
|
||||
// - 只返回启用状态的卡密类型,用于前端下拉选择
|
||||
func GetCardTypesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
var cardTypes []models.CardType
|
||||
// 中文注释:根据可选参数 all 决定是否仅返回启用类型
|
||||
// - 未提供或为其它值:仅返回启用(status=1)
|
||||
// - all=1/true/yes:返回所有状态的类型(用于筛选下拉场景)
|
||||
q := db.Model(&models.CardType{})
|
||||
all := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("all")))
|
||||
if !(all == "1" || all == "true" || all == "yes") {
|
||||
q = q.Where("status = ?", 1)
|
||||
}
|
||||
if err := q.Order("id asc").Find(&cardTypes).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", cardTypes)
|
||||
}
|
||||
|
||||
// CardsExportHandler 导出卡密为文本文件
|
||||
// - 支持GET
|
||||
// - 筛选参数:card_type_id、status、batch、remark
|
||||
// - 导出字段(按顺序):卡号、状态、创建时间;使用“----”分隔
|
||||
func CardsExportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析筛选参数
|
||||
cardTypeIDStr := strings.TrimSpace(r.URL.Query().Get("card_type_id"))
|
||||
statusStr := strings.TrimSpace(r.URL.Query().Get("status"))
|
||||
batch := strings.TrimSpace(r.URL.Query().Get("batch"))
|
||||
remark := strings.TrimSpace(r.URL.Query().Get("remark"))
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := db.Model(&models.Card{})
|
||||
if cardTypeIDStr != "" {
|
||||
if id, err := strconv.Atoi(cardTypeIDStr); err == nil && id > 0 {
|
||||
query = query.Where("card_type_id = ?", id)
|
||||
}
|
||||
}
|
||||
if statusStr != "" {
|
||||
if s, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", s)
|
||||
}
|
||||
}
|
||||
if batch != "" {
|
||||
query = query.Where("batch LIKE ?", "%"+batch+"%")
|
||||
}
|
||||
if remark != "" {
|
||||
query = query.Where("remark LIKE ?", "%"+remark+"%")
|
||||
}
|
||||
|
||||
// 查询数据(按ID倒序)
|
||||
var cards []models.Card
|
||||
if err := query.Order("id desc").Find(&cards).Error; err != nil {
|
||||
http.Error(w, "查询失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头(文本下载)
|
||||
now := time.Now().Format("20060102150405")
|
||||
filename := fmt.Sprintf("cards_%s.txt", now)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
|
||||
// 写入UTF-8 BOM,避免Excel/记事本中文乱码
|
||||
_, _ = w.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||
|
||||
// 写入表头
|
||||
_, _ = w.Write([]byte("卡号----状态----创建时间\n"))
|
||||
|
||||
// 时间格式
|
||||
const tf = "2006-01-02 15:04:05"
|
||||
|
||||
// 状态转文字
|
||||
statusText := func(s int) string {
|
||||
switch s {
|
||||
case 0:
|
||||
return "未使用"
|
||||
case 1:
|
||||
return "已使用"
|
||||
default:
|
||||
return "禁用"
|
||||
}
|
||||
}
|
||||
|
||||
// 写入数据行(以“----”分隔)
|
||||
for _, c := range cards {
|
||||
record := []string{
|
||||
c.CardNumber,
|
||||
statusText(c.Status),
|
||||
c.CreatedAt.Format(tf),
|
||||
}
|
||||
line := strings.Join(record, "----") + "\n"
|
||||
if _, err := w.Write([]byte(line)); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CardsExportSelectedHandler 导出选中的卡密为文本文件
|
||||
// - 支持GET
|
||||
// - 参数:ids(逗号分隔的卡密ID列表)
|
||||
// - 导出字段(按顺序):卡号、状态、创建时间;使用"----"分隔
|
||||
func CardsExportSelectedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析选中的卡密ID列表
|
||||
idsStr := strings.TrimSpace(r.URL.Query().Get("ids"))
|
||||
if idsStr == "" {
|
||||
http.Error(w, "请提供要导出的卡密ID列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析ID列表
|
||||
idStrings := strings.Split(idsStr, ",")
|
||||
var ids []uint
|
||||
for _, idStr := range idStrings {
|
||||
if id, err := strconv.Atoi(strings.TrimSpace(idStr)); err == nil && id > 0 {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
http.Error(w, "无效的卡密ID列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询选中的卡密数据(按ID倒序)
|
||||
var cards []models.Card
|
||||
if err := db.Where("id IN ?", ids).Order("id desc").Find(&cards).Error; err != nil {
|
||||
logrus.WithError(err).Error("查询选中卡密失败")
|
||||
http.Error(w, "查询卡密数据失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(cards) == 0 {
|
||||
http.Error(w, "未找到指定的卡密数据", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头,触发下载
|
||||
filename := fmt.Sprintf("selected_cards_%s.txt", time.Now().Format("20060102_150405"))
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
|
||||
// 写入数据
|
||||
tf := "2006-01-02 15:04:05"
|
||||
for _, c := range cards {
|
||||
// 状态转换
|
||||
var statusText string
|
||||
switch c.Status {
|
||||
case 0:
|
||||
statusText = "未使用"
|
||||
case 1:
|
||||
statusText = "已使用"
|
||||
default:
|
||||
statusText = "禁用"
|
||||
}
|
||||
|
||||
// 格式:卡号----状态----创建时间
|
||||
record := []string{
|
||||
c.CardNumber,
|
||||
statusText,
|
||||
c.CreatedAt.Format(tf),
|
||||
}
|
||||
line := strings.Join(record, "----") + "\n"
|
||||
if _, err := w.Write([]byte(line)); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/constants"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CardStatsOverviewHandler 卡密统计概览API
|
||||
// - 返回当日和所有卡密的统计信息
|
||||
// - 包括:总数、使用/未使用/禁用状态分布
|
||||
func CardStatsOverviewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当日统计
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStart := today + " 00:00:00"
|
||||
todayEnd := today + " 23:59:59"
|
||||
|
||||
// 当日卡密统计
|
||||
var todayTotal int64
|
||||
var todayByStatus = make(map[int]int64)
|
||||
|
||||
// 当日总数
|
||||
db.Model(&models.Card{}).Where("created_at >= ? AND created_at <= ?", todayStart, todayEnd).Count(&todayTotal)
|
||||
|
||||
// 当日按状态分布
|
||||
var todayStatusCounts []struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
db.Model(&models.Card{}).
|
||||
Select("status, count(*) as count").
|
||||
Where("created_at >= ? AND created_at <= ?", todayStart, todayEnd).
|
||||
Group("status").
|
||||
Find(&todayStatusCounts)
|
||||
|
||||
for _, sc := range todayStatusCounts {
|
||||
todayByStatus[sc.Status] = sc.Count
|
||||
}
|
||||
|
||||
// 所有卡密统计
|
||||
var allTotal int64
|
||||
var allByStatus = make(map[int]int64)
|
||||
|
||||
// 总数
|
||||
db.Model(&models.Card{}).Count(&allTotal)
|
||||
|
||||
// 按状态分布
|
||||
var allStatusCounts []struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
db.Model(&models.Card{}).
|
||||
Select("status, count(*) as count").
|
||||
Group("status").
|
||||
Find(&allStatusCounts)
|
||||
|
||||
for _, sc := range allStatusCounts {
|
||||
allByStatus[sc.Status] = sc.Count
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
data := map[string]interface{}{
|
||||
"today": map[string]interface{}{
|
||||
"total": todayTotal,
|
||||
"by_status": todayByStatus,
|
||||
},
|
||||
"all": map[string]interface{}{
|
||||
"total": allTotal,
|
||||
"by_status": allByStatus,
|
||||
},
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
|
||||
// CardStatsTrend30DaysHandler 卡密30天趋势API
|
||||
// - 返回近30天的卡密创建和使用趋势
|
||||
func CardStatsTrend30DaysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成近30天的日期列表
|
||||
var dates []string
|
||||
var totalCounts []int64
|
||||
var usedCounts []int64
|
||||
var unusedCounts []int64
|
||||
|
||||
for i := 29; i >= 0; i-- {
|
||||
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
|
||||
dates = append(dates, date)
|
||||
|
||||
dayStart := date + " 00:00:00"
|
||||
dayEnd := date + " 23:59:59"
|
||||
|
||||
// 当天创建的卡密总数
|
||||
var totalCount int64
|
||||
db.Model(&models.Card{}).Where("created_at >= ? AND created_at <= ?", dayStart, dayEnd).Count(&totalCount)
|
||||
totalCounts = append(totalCounts, totalCount)
|
||||
|
||||
// 当天创建且已使用的卡密数
|
||||
var usedCount int64
|
||||
db.Model(&models.Card{}).
|
||||
Where("created_at >= ? AND created_at <= ? AND status = ?", dayStart, dayEnd, constants.CardStatusUsed).
|
||||
Count(&usedCount)
|
||||
usedCounts = append(usedCounts, usedCount)
|
||||
|
||||
// 当天创建且未使用的卡密数
|
||||
var unusedCount int64
|
||||
db.Model(&models.Card{}).
|
||||
Where("created_at >= ? AND created_at <= ? AND status = ?", dayStart, dayEnd, constants.CardStatusUnused).
|
||||
Count(&unusedCount)
|
||||
unusedCounts = append(unusedCounts, unusedCount)
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
data := map[string]interface{}{
|
||||
"dates": dates,
|
||||
"total": totalCounts,
|
||||
"used": usedCounts,
|
||||
"unused": unusedCounts,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
|
||||
// CardStatsSimpleHandler 简单卡密统计API
|
||||
// - 返回卡密的基本统计信息:总数、已使用、未使用、禁用
|
||||
func CardStatsSimpleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计各状态的卡密数量
|
||||
var total int64
|
||||
var used int64
|
||||
var unused int64
|
||||
var disabled int64
|
||||
|
||||
db.Model(&models.Card{}).Count(&total)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusUsed).Count(&used)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusUnused).Count(&unused)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusDisabled).Count(&disabled)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"total": total,
|
||||
"used": used,
|
||||
"unused": unused,
|
||||
"disabled": disabled,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CardTypesFragmentHandler 卡密类型管理片段渲染
|
||||
// - 渲染 card_types.html 列表与表单界面
|
||||
func CardTypesFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "card_types.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// CardTypesListHandler 获取卡密类型列表
|
||||
// - 支持GET
|
||||
// - 支持分页和筛选
|
||||
func CardTypesListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
query := db.Model(&models.CardType{})
|
||||
|
||||
// 筛选条件
|
||||
if keyword != "" {
|
||||
query = query.Where("name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var items []models.CardType
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id asc").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// CardTypeCreateHandler 新增卡密类型
|
||||
// - 接收JSON: {name, status, login_types}
|
||||
// - Name 必填且唯一
|
||||
func CardTypeCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
LoginTypes string `json:"login_types"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "名称不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验登录方式ID是否存在
|
||||
if errMsg := validateLoginTypes(body.LoginTypes); errMsg != "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, errMsg, nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
item := models.CardType{
|
||||
Name: body.Name,
|
||||
Status: body.Status,
|
||||
LoginTypes: body.LoginTypes,
|
||||
}
|
||||
if item.Status != 0 {
|
||||
item.Status = 1
|
||||
}
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "创建成功", item)
|
||||
}
|
||||
|
||||
// checkCardTypeInUse 检查卡密类型是否被卡密使用
|
||||
// - 通过 cards 表中 card_type_id 外键计数
|
||||
// - 返回是否被使用以及被使用的数量
|
||||
func checkCardTypeInUse(cardTypeID uint) (bool, int64, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&models.Card{}).Where("card_type_id = ?", cardTypeID).Count(&count).Error; err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
return count > 0, count, nil
|
||||
}
|
||||
|
||||
// CardTypeUpdateHandler 更新卡密类型
|
||||
// - 接收JSON: {id, name, status, login_types}
|
||||
func CardTypeUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
LoginTypes string `json:"login_types"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验登录方式名称是否存在且未被禁用
|
||||
if errMsg := validateLoginTypes(body.LoginTypes); errMsg != "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, errMsg, nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询原始记录,便于后续在用校验(重命名/禁用)
|
||||
var original models.CardType
|
||||
if err := db.First(&original, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果名称发生变化且该卡密类型已被卡密使用,则不允许修改名称
|
||||
if body.Name != "" && body.Name != original.Name {
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法修改名称", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当尝试禁用(status=0)且原状态不是禁用时,如该类型已被卡密使用则禁止禁用
|
||||
if body.Status == 0 && original.Status != 0 {
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法禁用", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
updates := map[string]interface{}{}
|
||||
if body.Name != "" {
|
||||
updates["name"] = body.Name
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
updates["login_types"] = body.LoginTypes
|
||||
if err := db.Model(&models.CardType{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// CardTypeDeleteHandler 删除单个卡密类型
|
||||
// - 接收JSON: {id}
|
||||
func CardTypeDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 在用校验
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法删除", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.CardType{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// CardTypesBatchDeleteHandler 批量删除卡密类型
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 批量在用校验
|
||||
var blocking []string
|
||||
for _, id := range body.IDs {
|
||||
inUse, count, err := checkCardTypeInUse(id)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
var ct models.CardType
|
||||
if db.First(&ct, id).Error == nil {
|
||||
blocking = append(blocking, ct.Name+"(数量:"+strconv.FormatInt(count, 10)+")")
|
||||
} else {
|
||||
blocking = append(blocking, strconv.FormatUint(uint64(id), 10)+"(数量:"+strconv.FormatInt(count, 10)+")")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(blocking) > 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "以下卡密类型已被卡密使用,无法删除:"+strings.Join(blocking, ";"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.CardType{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// validateLoginTypes 校验登录方式名称是否存在且未被禁用
|
||||
// - 接收逗号分隔的登录方式名称字符串
|
||||
// - 检查登录方式是否存在且状态为启用(status=1)
|
||||
// - 返回错误信息,如果所有名称都存在且启用则返回空字符串
|
||||
func validateLoginTypes(loginTypesStr string) string {
|
||||
if loginTypesStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 分割登录方式名称字符串
|
||||
nameStrs := strings.Split(loginTypesStr, ",")
|
||||
var names []string
|
||||
|
||||
// 去重并清理空格
|
||||
nameSet := make(map[string]bool)
|
||||
for _, nameStr := range nameStrs {
|
||||
nameStr = strings.TrimSpace(nameStr)
|
||||
if nameStr == "" {
|
||||
continue
|
||||
}
|
||||
nameSet[nameStr] = true
|
||||
}
|
||||
|
||||
// 转换为切片
|
||||
for name := range nameSet {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 查询数据库检查名称是否存在
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return "数据库连接失败"
|
||||
}
|
||||
|
||||
// 查询所有匹配的登录方式,包括状态信息
|
||||
var loginTypes []models.LoginType
|
||||
if err := db.Where("name IN ?", names).Find(&loginTypes).Error; err != nil {
|
||||
return "查询登录方式失败"
|
||||
}
|
||||
|
||||
// 检查是否有不存在的名称和被禁用的登录方式
|
||||
existingSet := make(map[string]bool)
|
||||
disabledNames := []string{}
|
||||
for _, loginType := range loginTypes {
|
||||
existingSet[loginType.Name] = true
|
||||
// 检查登录方式是否被禁用 (status != 1 表示禁用)
|
||||
if loginType.Status != 1 {
|
||||
disabledNames = append(disabledNames, loginType.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查不存在的名称
|
||||
var notFoundNames []string
|
||||
for _, name := range names {
|
||||
if !existingSet[name] {
|
||||
notFoundNames = append(notFoundNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回错误信息
|
||||
if len(notFoundNames) > 0 {
|
||||
return "以下登录方式名称不存在: " + strings.Join(notFoundNames, ", ")
|
||||
}
|
||||
if len(disabledNames) > 0 {
|
||||
return "以下登录方式已被禁用,无法使用: " + strings.Join(disabledNames, ", ")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CardTypesBatchEnableHandler 批量启用
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchEnableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateStatus(w, r, 1)
|
||||
}
|
||||
|
||||
// CardTypesBatchDisableHandler 批量禁用
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchDisableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateStatus(w, r, 0)
|
||||
}
|
||||
|
||||
// batchUpdateStatus 批量更新状态的通用函数
|
||||
// - status: 1 启用,0 禁用
|
||||
func batchUpdateStatus(w http.ResponseWriter, r *http.Request, status int) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Model(&models.CardType{}).Where("id IN ?", body.IDs).Update("status", status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoginTypesFragmentHandler 登录方式管理片段渲染
|
||||
// - 渲染 login_types.html 列表与表单界面
|
||||
func LoginTypesFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "login_types.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// LoginTypesListHandler 获取登录方式列表
|
||||
// - 支持GET
|
||||
// - 支持分页和筛选
|
||||
func LoginTypesListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
query := db.Model(&models.LoginType{})
|
||||
|
||||
// 筛选条件
|
||||
if keyword != "" {
|
||||
query = query.Where("name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var items []models.LoginType
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id asc").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// LoginTypeCreateHandler 新增登录方式
|
||||
// - 接收JSON: {name, description, status}
|
||||
// - Name 必填且唯一
|
||||
func LoginTypeCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
Name string `json:"name"`
|
||||
VerifyTypes string `json:"verify_types"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "名称不能为空", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
item := models.LoginType{
|
||||
Name: body.Name,
|
||||
Status: body.Status,
|
||||
VerifyTypes: body.VerifyTypes,
|
||||
}
|
||||
if item.Status != 0 {
|
||||
item.Status = 1
|
||||
}
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "创建成功", item)
|
||||
}
|
||||
|
||||
// checkLoginTypeInUse 检查登录类型是否被卡密类型使用
|
||||
// - 检查 card_types 表中的 login_types 字段是否包含该登录类型名称
|
||||
// - 返回是否被使用和使用该登录类型的卡密类型名称列表
|
||||
func checkLoginTypeInUse(loginTypeName string) (bool, []string, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var cardTypes []models.CardType
|
||||
// 查询包含该登录类型名称的卡密类型
|
||||
if err := db.Where("login_types LIKE ?", "%"+loginTypeName+"%").Find(&cardTypes).Error; err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var usingCardTypes []string
|
||||
for _, cardType := range cardTypes {
|
||||
// 精确匹配登录类型名称(避免部分匹配)
|
||||
loginTypes := strings.Split(cardType.LoginTypes, ",")
|
||||
for _, lt := range loginTypes {
|
||||
if strings.TrimSpace(lt) == loginTypeName {
|
||||
usingCardTypes = append(usingCardTypes, cardType.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(usingCardTypes) > 0, usingCardTypes, nil
|
||||
}
|
||||
|
||||
// checkLoginTypesByIDsInUse 批量检查登录类型ID是否被使用
|
||||
// - 先查询登录类型ID对应的名称,再检查是否被使用
|
||||
func checkLoginTypesByIDsInUse(loginTypeIDs []uint) (bool, map[uint][]string, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
// 查询登录类型名称
|
||||
var loginTypes []models.LoginType
|
||||
if err := db.Where("id IN ?", loginTypeIDs).Find(&loginTypes).Error; err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
hasUsage := false
|
||||
usageMap := make(map[uint][]string)
|
||||
|
||||
for _, loginType := range loginTypes {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(loginType.Name)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if inUse {
|
||||
hasUsage = true
|
||||
usageMap[loginType.ID] = usingCardTypes
|
||||
}
|
||||
}
|
||||
|
||||
return hasUsage, usageMap, nil
|
||||
}
|
||||
|
||||
// LoginTypeUpdateHandler 更新登录方式
|
||||
// - 接收JSON: {id, name, description, status}
|
||||
func LoginTypeUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
VerifyTypes string `json:"verify_types"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 始终查询原始记录,便于后续校验(重命名/禁用)
|
||||
var originalLoginType models.LoginType
|
||||
if err := db.First(&originalLoginType, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "登录类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果名称发生变化,检查原名称是否被使用(与删除逻辑一致)
|
||||
if body.Name != "" && originalLoginType.Name != body.Name {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(originalLoginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法修改名称:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当尝试禁用(status=0)时,如被卡密类型使用则禁止禁用
|
||||
if body.Status == 0 && originalLoginType.Status != 0 {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(originalLoginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法禁用:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if body.Name != "" {
|
||||
updates["name"] = body.Name
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
updates["verify_types"] = body.VerifyTypes
|
||||
if err := db.Model(&models.LoginType{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypeDeleteHandler 删除单个登录方式
|
||||
// - 接收JSON: {id}
|
||||
func LoginTypeDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询登录类型名称
|
||||
var loginType models.LoginType
|
||||
if dbErr := db.First(&loginType, body.ID).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "登录类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否被卡密类型使用
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(loginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法删除:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.LoginType{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypesBatchDeleteHandler 批量删除登录方式
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查批量删除的登录类型是否被使用
|
||||
hasUsage, usageMap, err := checkLoginTypesByIDsInUse(body.IDs)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if hasUsage {
|
||||
// 构建详细的错误信息
|
||||
var errorMessages []string
|
||||
db, _ := database.GetDB()
|
||||
for loginTypeID, usingCardTypes := range usageMap {
|
||||
var loginType models.LoginType
|
||||
if db.First(&loginType, loginTypeID).Error == nil {
|
||||
errorMessages = append(errorMessages, loginType.Name+"(被"+strings.Join(usingCardTypes, "、")+"使用)")
|
||||
}
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "以下登录类型正在被使用,无法删除:"+strings.Join(errorMessages, ";"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.LoginType{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypesBatchEnableHandler 批量启用
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchEnableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateLoginTypeStatus(w, r, 1)
|
||||
}
|
||||
|
||||
// LoginTypesBatchDisableHandler 批量禁用
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchDisableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateLoginTypeStatus(w, r, 0)
|
||||
}
|
||||
|
||||
// batchUpdateLoginTypeStatus 批量更新登录方式状态的通用函数
|
||||
// - status: 1 启用,0 禁用
|
||||
func batchUpdateLoginTypeStatus(w http.ResponseWriter, r *http.Request, status int) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Model(&models.LoginType{}).Where("id IN ?", body.IDs).Update("status", status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func AutoMigrate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.LoginType{}, &models.CardType{}, &models.Card{}, &models.App{}, &models.API{}); err != nil {
|
||||
if err := db.AutoMigrate(&models.User{}, &models.Settings{}, &models.App{}, &models.API{}); err != nil {
|
||||
logrus.WithError(err).Error("AutoMigrate 执行失败")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -88,11 +88,6 @@ func SeedDefaultSettings() error {
|
||||
Value: "https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11000002000001",
|
||||
Description: "公安备案查询链接,留空则不显示",
|
||||
},
|
||||
{
|
||||
Name: "card_batch_counter",
|
||||
Value: "0",
|
||||
Description: "卡密批次号计数器(用于记录上次生成批次号的序号,自增使用)",
|
||||
},
|
||||
}
|
||||
|
||||
// 逐个检查并创建不存在的设置项
|
||||
|
||||
@@ -38,6 +38,18 @@ type App struct {
|
||||
DownloadType int `gorm:"default:0;not null;comment:更新方式,0=不启用更新,1=自动更新,2=手动下载" json:"download_type"`
|
||||
// DownloadURL:下载地址
|
||||
DownloadURL string `gorm:"size:500;comment:下载地址" json:"download_url"`
|
||||
// Announcement:程序公告内容(base64编码存储)
|
||||
Announcement string `gorm:"type:text;comment:程序公告内容,base64编码存储" json:"announcement"`
|
||||
// LoginType:登陆方式(0=顶号登录(默认),1=非顶号登录)
|
||||
LoginType int `gorm:"default:0;not null;comment:登陆方式,0=顶号登录,1=非顶号登录" json:"login_type"`
|
||||
// MultiOpenScope:多开范围(0=单电脑,1=单IP,2=全部电脑(默认))
|
||||
MultiOpenScope int `gorm:"default:2;not null;comment:多开范围,0=单电脑,1=单IP,2=全部电脑" json:"multi_open_scope"`
|
||||
// CleanInterval:清理间隔(单位:小时,默认1小时)
|
||||
CleanInterval int `gorm:"default:1;not null;comment:清理间隔,单位小时" json:"clean_interval"`
|
||||
// CheckInterval:校验间隔(单位:分钟,默认10分钟)
|
||||
CheckInterval int `gorm:"default:10;not null;comment:校验间隔,单位分钟" json:"check_interval"`
|
||||
// MultiOpenCount:多开数量(默认1)
|
||||
MultiOpenCount int `gorm:"default:1;not null;comment:多开数量" json:"multi_open_count"`
|
||||
// CreatedAt/UpdatedAt:时间字段,返回为 created_at/updated_at,便于前端展示
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Card 卡密模型
|
||||
// 用于存储和管理系统中的卡密信息,包括卡密号码、状态、使用情况等
|
||||
type Card struct {
|
||||
// ID:主键,自增
|
||||
ID uint `gorm:"primaryKey;comment:卡密ID,自增主键" json:"id"`
|
||||
// CardNumber:卡密号码,唯一且非空
|
||||
CardNumber string `gorm:"size:200;not null;comment:卡密号码(十六进制字符串)" json:"card_number"`
|
||||
// CardTypeID:所属卡密类型ID(外键)
|
||||
CardTypeID uint `gorm:"not null;index;comment:所属卡密类型ID(外键)" json:"card_type_id"`
|
||||
// Status:状态(0=未使用,1=已使用,2=禁用)
|
||||
Status int `gorm:"default:0;not null;comment:状态,0=未使用,1=已使用,2=禁用" json:"status"`
|
||||
// Batch:批次标识,用于区分导入或生成批次
|
||||
Batch string `gorm:"size:100;comment:批次标识" json:"batch"`
|
||||
// Remark:备注信息
|
||||
Remark string `gorm:"size:255;comment:备注信息" json:"remark"`
|
||||
// UsedAt:使用时间,未使用为NULL(调整到创建时间前面,以便前端展示顺序一致)
|
||||
UsedAt *time.Time `gorm:"comment:使用时间" json:"used_at"`
|
||||
// CreatedAt/UpdatedAt:时间字段
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// CardType 卡密类型表模型
|
||||
// 用于管理不同类型的卡密(如:ChatGPT、Claude、Suno、Grok等)
|
||||
// ID 为自增主键
|
||||
// Name 为卡密类型名称,唯一索引
|
||||
// Status 为状态(1:启用 0:禁用),默认为1
|
||||
// CreatedAt/UpdatedAt 由 GORM 自动维护
|
||||
|
||||
type CardType struct {
|
||||
// ID:主键,自增,同时通过 json 标签保证前端接收为 id
|
||||
ID uint `gorm:"primaryKey;comment:卡密类型ID,自增主键" json:"id"`
|
||||
// Name:名称,唯一;json 名称与前端一致
|
||||
Name string `gorm:"uniqueIndex;size:100;not null;comment:卡密类型名称,唯一索引" json:"name"`
|
||||
// Status:状态(1=启用,0=禁用);json 名称与前端一致
|
||||
Status int `gorm:"default:1;not null;comment:状态,1=启用,0=禁用" json:"status"`
|
||||
// LoginTypes:登录方式(逗号分隔);json 使用 login_types
|
||||
LoginTypes string `gorm:"type:varchar(500);default:'';comment:登录方式,多个用逗号分隔" json:"login_types"`
|
||||
// CreatedAt/UpdatedAt:时间字段,返回为 created_at/updated_at,便于前端展示
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// LoginType 登录类型表模型
|
||||
// 用于管理不同的登录方式,如直登、Google、Microsoft、Apple等
|
||||
// ID 为自增主键
|
||||
// Name 为登录类型名称,唯一索引
|
||||
// Status 为状态(1:启用 0:禁用),默认为1
|
||||
// CreatedAt/UpdatedAt 由 GORM 自动维护
|
||||
|
||||
type LoginType struct {
|
||||
// ID:主键,自增,同时通过 json 标签保证前端接收为 id
|
||||
ID uint `gorm:"primaryKey;comment:登录类型ID,自增主键" json:"id"`
|
||||
// Name:名称,唯一;json 名称与前端一致
|
||||
Name string `gorm:"uniqueIndex;size:100;not null;comment:登录类型名称,唯一索引" json:"name"`
|
||||
// Status:状态(1=启用,0=禁用);json 名称与前端一致
|
||||
Status int `gorm:"default:1;not null;comment:状态,1=启用,0=禁用" json:"status"`
|
||||
// VerifyTypes:验证方式(逗号分隔);json 使用 verify_types;用于记录多种验证方式,输入内容用多个用逗号分隔
|
||||
VerifyTypes string `gorm:"type:varchar(500);default:'';comment:验证方式,输入内容用多个用逗号分隔" json:"verify_types"`
|
||||
// CreatedAt/UpdatedAt:时间字段,返回为 created_at/updated_at,便于前端展示
|
||||
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
BIN
recharge.db
Normal file
BIN
recharge.db
Normal file
Binary file not shown.
@@ -40,9 +40,7 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/admin/user", adminctl.AdminAuthRequired(adminctl.UserFragmentHandler))
|
||||
mux.HandleFunc("/admin/settings", adminctl.AdminAuthRequired(adminctl.SettingsFragmentHandler))
|
||||
mux.HandleFunc("/admin/apps", adminctl.AdminAuthRequired(adminctl.AppsFragmentHandler))
|
||||
mux.HandleFunc("/admin/logintypes", adminctl.AdminAuthRequired(adminctl.LoginTypesFragmentHandler))
|
||||
mux.HandleFunc("/admin/cardtypes", adminctl.AdminAuthRequired(adminctl.CardTypesFragmentHandler))
|
||||
mux.HandleFunc("/admin/cards", adminctl.AdminAuthRequired(adminctl.CardsFragmentHandler))
|
||||
|
||||
|
||||
// 个人资料API
|
||||
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
|
||||
@@ -52,8 +50,6 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/admin/api/settings", adminctl.AdminAuthRequired(adminctl.SettingsQueryHandler))
|
||||
mux.HandleFunc("/admin/api/settings/update", adminctl.AdminAuthRequired(adminctl.SettingsUpdateHandler))
|
||||
|
||||
// 供前端下拉选择卡密类型
|
||||
mux.HandleFunc("/admin/api/cards/types", adminctl.AdminAuthRequired(adminctl.GetCardTypesHandler))
|
||||
// 应用管理API
|
||||
mux.HandleFunc("/admin/api/apps/list", adminctl.AdminAuthRequired(adminctl.AppsListHandler))
|
||||
mux.HandleFunc("/admin/api/apps/create", adminctl.AdminAuthRequired(adminctl.AppCreateHandler))
|
||||
@@ -61,39 +57,13 @@ func RegisterAdminRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(adminctl.AppDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(adminctl.AppsBatchDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(adminctl.AppsBatchUpdateStatusHandler))
|
||||
// 登录方式管理API
|
||||
mux.HandleFunc("/admin/api/login_types/list", adminctl.AdminAuthRequired(adminctl.LoginTypesListHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/create", adminctl.AdminAuthRequired(adminctl.LoginTypeCreateHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/update", adminctl.AdminAuthRequired(adminctl.LoginTypeUpdateHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/delete", adminctl.AdminAuthRequired(adminctl.LoginTypeDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/batch_delete", adminctl.AdminAuthRequired(adminctl.LoginTypesBatchDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/batch_enable", adminctl.AdminAuthRequired(adminctl.LoginTypesBatchEnableHandler))
|
||||
mux.HandleFunc("/admin/api/login_types/batch_disable", adminctl.AdminAuthRequired(adminctl.LoginTypesBatchDisableHandler))
|
||||
// 卡密类型管理API
|
||||
mux.HandleFunc("/admin/api/card_types/list", adminctl.AdminAuthRequired(adminctl.CardTypesListHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/create", adminctl.AdminAuthRequired(adminctl.CardTypeCreateHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/update", adminctl.AdminAuthRequired(adminctl.CardTypeUpdateHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/delete", adminctl.AdminAuthRequired(adminctl.CardTypeDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/batch_delete", adminctl.AdminAuthRequired(adminctl.CardTypesBatchDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/batch_enable", adminctl.AdminAuthRequired(adminctl.CardTypesBatchEnableHandler))
|
||||
mux.HandleFunc("/admin/api/card_types/batch_disable", adminctl.AdminAuthRequired(adminctl.CardTypesBatchDisableHandler))
|
||||
// 卡密管理API
|
||||
mux.HandleFunc("/admin/api/cards/list", adminctl.AdminAuthRequired(adminctl.CardsListHandler))
|
||||
mux.HandleFunc("/admin/api/cards/create", adminctl.AdminAuthRequired(adminctl.CardCreateHandler))
|
||||
mux.HandleFunc("/admin/api/cards/update", adminctl.AdminAuthRequired(adminctl.CardUpdateHandler))
|
||||
mux.HandleFunc("/admin/api/cards/delete", adminctl.AdminAuthRequired(adminctl.CardDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/cards/batch_delete", adminctl.AdminAuthRequired(adminctl.CardsBatchDeleteHandler))
|
||||
mux.HandleFunc("/admin/api/cards/batch_update_status", adminctl.AdminAuthRequired(adminctl.CardsBatchUpdateStatusHandler))
|
||||
// 新增:卡密导出API(CSV下载)
|
||||
mux.HandleFunc("/admin/api/cards/export", adminctl.AdminAuthRequired(adminctl.CardsExportHandler))
|
||||
// 新增:导出选中卡密API
|
||||
mux.HandleFunc("/admin/api/cards/export_selected", adminctl.AdminAuthRequired(adminctl.CardsExportSelectedHandler))
|
||||
mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(adminctl.AppResetSecretHandler))
|
||||
mux.HandleFunc("/admin/api/apps/get_announcement", adminctl.AdminAuthRequired(adminctl.AppGetAnnouncementHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_announcement", adminctl.AdminAuthRequired(adminctl.AppUpdateAnnouncementHandler))
|
||||
mux.HandleFunc("/admin/api/apps/get_multi_config", adminctl.AdminAuthRequired(adminctl.AppGetMultiConfigHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(adminctl.AppUpdateMultiConfigHandler))
|
||||
|
||||
|
||||
// 系统信息API(用于仪表盘定时刷新)
|
||||
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
|
||||
|
||||
// 卡密统计API(用于仪表盘统计显示)
|
||||
mux.HandleFunc("/admin/api/cards/stats_overview", adminctl.AdminAuthRequired(adminctl.CardStatsOverviewHandler))
|
||||
mux.HandleFunc("/admin/api/cards/trend_30days", adminctl.AdminAuthRequired(adminctl.CardStatsTrend30DaysHandler))
|
||||
mux.HandleFunc("/admin/api/cards/stats_simple", adminctl.AdminAuthRequired(adminctl.CardStatsSimpleHandler))
|
||||
}
|
||||
|
||||
@@ -12,37 +12,7 @@ import (
|
||||
|
||||
|
||||
|
||||
// FindCardByCardNumber 根据卡号查找卡密
|
||||
// cardNumber: 卡号
|
||||
// db: 数据库连接
|
||||
// 返回: 卡密信息和错误
|
||||
func FindCardByCardNumber(cardNumber string, db *gorm.DB) (*models.Card, error) {
|
||||
key := fmt.Sprintf("card:number:%s", cardNumber)
|
||||
return utils.RedisGetOrSet(context.Background(), key, 60*time.Second, func() (*models.Card, error) {
|
||||
var card models.Card
|
||||
err := db.Where("card_number = ?", cardNumber).First(&card).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &card, nil
|
||||
})
|
||||
}
|
||||
|
||||
// FindCardTypeByID 根据ID查找卡密类型
|
||||
// id: 卡密类型ID
|
||||
// db: 数据库连接
|
||||
// 返回: 卡密类型信息和错误
|
||||
func FindCardTypeByID(id uint, db *gorm.DB) (*models.CardType, error) {
|
||||
key := fmt.Sprintf("card_type:id:%d", id)
|
||||
return utils.RedisGetOrSet(context.Background(), key, 30*time.Minute, func() (*models.CardType, error) {
|
||||
var cardType models.CardType
|
||||
err := db.Where("id = ?", id).First(&cardType).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cardType, nil
|
||||
})
|
||||
}
|
||||
|
||||
// FindSettingByName 根据名称查找设置
|
||||
// name: 设置名称
|
||||
|
||||
@@ -33,8 +33,13 @@
|
||||
<div class="layui-card-body">
|
||||
<table id="appsTable" lay-filter="appsTableFilter"></table>
|
||||
<script type="text/html" id="tpl-apps-ops">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
<div style="white-space: nowrap;">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="more">
|
||||
更多 <i class="layui-icon layui-icon-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="tpl-apps-status">
|
||||
{{`{{# if(d.status === 1) { }}`}}
|
||||
@@ -57,37 +62,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">版本号</label>
|
||||
<label class="layui-form-label">应用版本</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="version" placeholder="请输入版本号,默认1.0.0" autocomplete="off" class="layui-input" />
|
||||
<input type="text" name="version" placeholder="请输入应用版本,默认1.0.0" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-form-item" pane>
|
||||
<label class="layui-form-label">应用状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="1" selected>启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" checked>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-form-item" pane>
|
||||
<label class="layui-form-label">强制更新</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="force_update">
|
||||
<option value="0" selected>不开启</option>
|
||||
<option value="1">开启</option>
|
||||
</select>
|
||||
<input type="checkbox" name="force_update" lay-skin="switch" lay-text="开启|关闭">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-form-item" pane>
|
||||
<label class="layui-form-label">更新方式</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="download_type" lay-filter="downloadTypeChange">
|
||||
<option value="0" selected>不启用更新</option>
|
||||
<option value="1">自动更新</option>
|
||||
<option value="2">手动下载</option>
|
||||
</select>
|
||||
<input type="radio" name="download_type" value="0" title="不启用" checked lay-filter="downloadTypeChange">
|
||||
<input type="radio" name="download_type" value="1" title="自动更新" lay-filter="downloadTypeChange">
|
||||
<input type="radio" name="download_type" value="2" title="手动下载" lay-filter="downloadTypeChange">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="downloadUrlItem">
|
||||
@@ -106,10 +103,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'layer', 'element'], function() {
|
||||
layui.use(['table', 'form', 'layer', 'element', 'dropdown'], function() {
|
||||
const table = layui.table;
|
||||
const form = layui.form;
|
||||
const layer = layui.layer;
|
||||
const dropdown = layui.dropdown;
|
||||
const $ = layui.$;
|
||||
|
||||
// 格式化时间函数
|
||||
@@ -149,10 +147,10 @@
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||||
{ field: 'name', title: '应用名称', minWidth: 180 },
|
||||
{ field: 'uuid', title: 'UUID', minWidth: 320 },
|
||||
{ field: 'version', title: '版本', width: 100 },
|
||||
{ field: 'version', title: '应用版本', width: 100 },
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
title: '应用状态',
|
||||
width: 100,
|
||||
templet: (d) => {
|
||||
if (d.status === 1) return '<span style="color: #5FB878;">启用</span>';
|
||||
@@ -171,7 +169,7 @@
|
||||
width: 180,
|
||||
templet: (d) => formatDateTime(d.created_at)
|
||||
},
|
||||
{ fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 120 }
|
||||
{ fixed: 'right', title: '操作', toolbar: '#tpl-apps-ops', width: 180 }
|
||||
]]
|
||||
});
|
||||
|
||||
@@ -216,7 +214,7 @@
|
||||
});
|
||||
|
||||
// 监听更新方式切换(保留事件监听器以备将来扩展)
|
||||
form.on('select(downloadTypeChange)', function(data) {
|
||||
form.on('radio(downloadTypeChange)', function(data) {
|
||||
// 下载地址字段现在始终显示,无需切换显示状态
|
||||
});
|
||||
|
||||
@@ -228,9 +226,9 @@
|
||||
// 转换字段类型为正确的数据类型
|
||||
const formData = {
|
||||
...data.field,
|
||||
status: parseInt(data.field.status) || 0,
|
||||
status: data.field.status === 'on' ? 1 : 0, // switch开关处理
|
||||
download_type: parseInt(data.field.download_type) || 0,
|
||||
force_update: parseInt(data.field.force_update) || 0
|
||||
force_update: data.field.force_update === 'on' ? 1 : 0 // switch开关处理
|
||||
};
|
||||
|
||||
// 如果是编辑模式,确保id也是整数
|
||||
@@ -274,10 +272,13 @@
|
||||
$('input[name="id"]').val(data.id);
|
||||
$('input[name="name"]').val(data.name);
|
||||
$('input[name="version"]').val(data.version);
|
||||
$('select[name="status"]').val(data.status);
|
||||
$('select[name="download_type"]').val(data.download_type || 0);
|
||||
// 设置应用状态开关
|
||||
$('input[name="status"]').prop('checked', data.status === 1);
|
||||
// 设置更新方式单选按钮
|
||||
$('input[name="download_type"][value="' + (data.download_type || 0) + '"]').prop('checked', true);
|
||||
$('input[name="download_url"]').val(data.download_url || '');
|
||||
$('select[name="force_update"]').val(data.force_update || 0);
|
||||
// 设置强制更新开关
|
||||
$('input[name="force_update"]').prop('checked', data.force_update === 1);
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
@@ -311,6 +312,248 @@
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (obj.event === 'more') {
|
||||
// 更多操作下拉菜单
|
||||
dropdown.render({
|
||||
elem: this, // 使用 this 而不是查找元素
|
||||
show: true, // 外部事件触发即显示
|
||||
data: [
|
||||
{
|
||||
title: '程序公告',
|
||||
id: 'announcement'
|
||||
},
|
||||
{
|
||||
title: '多开配置',
|
||||
id: 'multi_instance'
|
||||
},
|
||||
{
|
||||
title: '重置密钥',
|
||||
id: 'reset_secret'
|
||||
}
|
||||
],
|
||||
click: function(menudata, othis) {
|
||||
if (menudata.id === 'announcement') {
|
||||
// 程序公告
|
||||
// 先获取当前公告内容
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/get_announcement?uuid=' + obj.data.uuid,
|
||||
type: 'GET',
|
||||
success: function(res) {
|
||||
var currentAnnouncement = '';
|
||||
if (res.code === 0 && res.data && res.data.announcement) {
|
||||
currentAnnouncement = res.data.announcement;
|
||||
}
|
||||
|
||||
// 显示编辑弹窗
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '编辑程序公告 - ' + obj.data.name,
|
||||
area: ['600px', '400px'],
|
||||
content: '<div style="padding: 20px;">' +
|
||||
'<textarea id="announcementEditor" class="layui-textarea" placeholder="请输入程序公告内容..." style="height: 250px;">' +
|
||||
currentAnnouncement +
|
||||
'</textarea>' +
|
||||
'</div>',
|
||||
btn: ['保存', '取消'],
|
||||
yes: function(index, layero) {
|
||||
var announcementContent = $('#announcementEditor').val();
|
||||
|
||||
// 发送更新请求
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/update_announcement',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
uuid: obj.data.uuid,
|
||||
announcement: announcementContent
|
||||
}),
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('程序公告更新成功!', {
|
||||
icon: 1,
|
||||
time: 2000
|
||||
});
|
||||
layer.close(index);
|
||||
} else {
|
||||
layer.msg(res.msg || '更新程序公告失败', {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('网络错误,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
},
|
||||
btn2: function(index) {
|
||||
layer.close(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('获取程序公告失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
} else if (menudata.id === 'multi_instance') {
|
||||
// 多开配置
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/get_multi_config?uuid=' + obj.data.uuid,
|
||||
type: 'GET',
|
||||
success: function(config) {
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '多开配置 - ' + obj.data.name,
|
||||
area: ['550px', '450px'],
|
||||
content: '<div style="padding: 20px;">' +
|
||||
'<form class="layui-form layui-form-pane" lay-filter="multiConfigForm">' +
|
||||
'<div class="layui-form-item" pane>' +
|
||||
'<label class="layui-form-label">登录方式</label>' +
|
||||
'<div class="layui-input-block">' +
|
||||
'<input type="radio" name="login_type" value="0" title="顶号登录" ' + (config.login_type === 0 ? 'checked' : '') + '>' +
|
||||
'<input type="radio" name="login_type" value="1" title="非顶号登录" ' + (config.login_type === 1 ? 'checked' : '') + '>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-item" pane>' +
|
||||
'<label class="layui-form-label">多开范围</label>' +
|
||||
'<div class="layui-input-block">' +
|
||||
'<input type="radio" name="multi_open_scope" value="0" title="单电脑" ' + (config.multi_open_scope === 0 ? 'checked' : '') + '>' +
|
||||
'<input type="radio" name="multi_open_scope" value="1" title="单IP" ' + (config.multi_open_scope === 1 ? 'checked' : '') + '>' +
|
||||
'<input type="radio" name="multi_open_scope" value="2" title="全部电脑" ' + (config.multi_open_scope === 2 ? 'checked' : '') + '>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-item">' +
|
||||
'<div class="layui-inline">' +
|
||||
'<label class="layui-form-label">清理间隔</label>' +
|
||||
'<div class="layui-input-inline">' +
|
||||
'<input type="number" name="clean_interval" class="layui-input" value="' + config.clean_interval + '" placeholder="请输入" lay-verify="required|number" min="1">' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-mid layui-text-em">小时</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-item">' +
|
||||
'<div class="layui-inline">' +
|
||||
'<label class="layui-form-label">校验间隔</label>' +
|
||||
'<div class="layui-input-inline">' +
|
||||
'<input type="number" name="check_interval" class="layui-input" value="' + config.check_interval + '" placeholder="请输入" lay-verify="required|number" min="1">' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-mid layui-text-em">分钟</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="layui-form-item">' +
|
||||
'<label class="layui-form-label">多开数量</label>' +
|
||||
'<div class="layui-input-block">' +
|
||||
'<input type="number" name="multi_open_count" class="layui-input" value="' + config.multi_open_count + '" placeholder="请输入允许的多开数量" lay-verify="required|number" min="1">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>',
|
||||
btn: ['保存', '取消'],
|
||||
yes: function(index, layero) {
|
||||
var formData = {
|
||||
uuid: obj.data.uuid,
|
||||
login_type: parseInt($('input[name="login_type"]:checked').val()),
|
||||
multi_open_scope: parseInt($('input[name="multi_open_scope"]:checked').val()),
|
||||
clean_interval: parseInt($('input[name="clean_interval"]').val()),
|
||||
check_interval: parseInt($('input[name="check_interval"]').val()),
|
||||
multi_open_count: parseInt($('input[name="multi_open_count"]').val())
|
||||
};
|
||||
|
||||
// 验证数据
|
||||
if (isNaN(formData.login_type) || formData.login_type < 0 || formData.login_type > 1) {
|
||||
layer.msg('请选择登录方式', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if (isNaN(formData.multi_open_scope) || formData.multi_open_scope < 0 || formData.multi_open_scope > 2) {
|
||||
layer.msg('请选择多开范围', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if (isNaN(formData.clean_interval) || formData.clean_interval < 1) {
|
||||
layer.msg('清理间隔必须大于0', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if (isNaN(formData.check_interval) || formData.check_interval < 1) {
|
||||
layer.msg('校验间隔必须大于0', {icon: 2});
|
||||
return;
|
||||
}
|
||||
if (isNaN(formData.multi_open_count) || formData.multi_open_count < 1) {
|
||||
layer.msg('多开数量必须大于0', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送更新请求
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/update_multi_config',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function(res) {
|
||||
if (res.message) {
|
||||
layer.msg('多开配置更新成功', {icon: 1});
|
||||
layer.close(index);
|
||||
table.reload('appsTable');
|
||||
} else {
|
||||
layer.msg(res.msg || '更新多开配置失败', {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('网络错误,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
},
|
||||
btn2: function(index) {
|
||||
layer.close(index);
|
||||
},
|
||||
success: function() {
|
||||
// 重新渲染表单
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('获取多开配置失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
} else if (menudata.id === 'reset_secret') {
|
||||
// 重置密钥
|
||||
layer.confirm('确定重置该应用的密钥吗?重置后原密钥将失效!', {icon: 3, title: '提示'}, function(index) {
|
||||
// 发送重置密钥请求
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/reset_secret',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
uuid: obj.data.uuid
|
||||
}),
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('密钥重置成功!', {
|
||||
icon: 1,
|
||||
time: 2000 // 显示2秒
|
||||
});
|
||||
// 刷新表格数据
|
||||
table.reload('appsTable');
|
||||
} else {
|
||||
layer.msg(res.msg || '重置密钥失败', {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMsg = '重置密钥失败';
|
||||
if (xhr.responseText) {
|
||||
try {
|
||||
const errorRes = JSON.parse(xhr.responseText);
|
||||
errorMsg = errorRes.msg || errorMsg;
|
||||
} catch (e) {
|
||||
errorMsg = xhr.responseText;
|
||||
}
|
||||
}
|
||||
layer.msg(errorMsg, {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
},
|
||||
align: 'right', // 右对齐弹出
|
||||
style: 'box-shadow: 1px 1px 10px rgb(0 0 0 / 12%);' // 设置额外样式
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
{{ define "card_types.html" }}
|
||||
<section>
|
||||
<h2>卡密类型管理</h2>
|
||||
<div class="layui-btn-container" style="margin:12px 0">
|
||||
<button class="layui-btn" id="btnAddCardType"><i class="layui-icon layui-icon-add-1"></i> 新增类型</button>
|
||||
<button class="layui-btn layui-btn-normal" id="btnBatchEnableCardTypes"><i class="layui-icon layui-icon-ok-circle"></i> 批量启用</button>
|
||||
<button class="layui-btn layui-btn-warm" id="btnBatchDisableCardTypes"><i class="layui-icon layui-icon-close-fill"></i> 批量禁用</button>
|
||||
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteCardTypes"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">筛选</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form layui-form-pane" id="cardTypeFilterForm" lay-filter="cardTypeFilterForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keyword" placeholder="卡密类型名称" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnSearchCardTypes">查询</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnResetCardTypes">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">卡密类型列表</div>
|
||||
<div class="layui-card-body">
|
||||
<table id="cardTypesTable" lay-filter="cardTypesTableFilter"></table>
|
||||
<script type="text/html" id="tpl-cardtypes-ops">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的表单弹层内容 -->
|
||||
<div id="cardTypeFormModal" style="display:none;padding:16px">
|
||||
<!-- 参考demo表单2样式,添加layui-form-pane类实现方框风格 -->
|
||||
<form class="layui-form layui-form-pane" id="cardTypeForm">
|
||||
<input type="hidden" name="id" />
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" required lay-verify="required" placeholder="请输入类型名称" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">登录</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="login_types" placeholder="请输入登录方式名称,多个用逗号分隔" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作按钮已移除,统一由 layer.open 的 btn 控制“提交/取消” -->
|
||||
<!-- 操作按钮移除:统一由 layer.open 的 btn 控制“提交/取消” -->
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'layer'], () => {
|
||||
const { table, form, layer, $ } = layui;
|
||||
let currentFormLayerIndex; // 保存当前表单弹窗的索引
|
||||
|
||||
// 渲染表格
|
||||
const cardTypesTable = table.render({
|
||||
elem: '#cardTypesTable',
|
||||
url: '/admin/api/card_types/list',
|
||||
method: 'GET',
|
||||
page: true,
|
||||
limit: 20,
|
||||
limits: [10, 20, 50, 100],
|
||||
loading: true,
|
||||
cols: [[
|
||||
{ type: 'checkbox' },
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||||
{ field: 'name', title: '名称' },
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
templet: (d) => {
|
||||
return d.status === 1
|
||||
? '<span class="layui-badge layui-bg-green">启用</span>'
|
||||
: '<span class="layui-badge">禁用</span>';
|
||||
}
|
||||
},
|
||||
{ field: 'login_types', title: '登录方式' },
|
||||
{
|
||||
field: 'created_at',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
templet: (d) => {
|
||||
return formatDateTime(d.created_at);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
title: '更新时间',
|
||||
width: 180,
|
||||
templet: (d) => {
|
||||
return formatDateTime(d.updated_at);
|
||||
}
|
||||
},
|
||||
{ title: '操作', toolbar: '#tpl-cardtypes-ops', width: 150, fixed: 'right' }
|
||||
]],
|
||||
parseData: (res) => {
|
||||
// 后端已返回正确格式,直接使用
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.msg || '',
|
||||
"count": res.data ? res.data.total : 0,
|
||||
"data": res.data ? res.data.items : []
|
||||
};
|
||||
},
|
||||
request: {
|
||||
pageName: 'page',
|
||||
limitName: 'page_size'
|
||||
},
|
||||
where: {}
|
||||
});
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0') + ' ' +
|
||||
String(date.getHours()).padStart(2, '0') + ':' +
|
||||
String(date.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getSeconds()).padStart(2, '0');
|
||||
};
|
||||
|
||||
// 监听表格工具条
|
||||
table.on('tool(cardTypesTableFilter)', (obj) => {
|
||||
const { data, event } = obj;
|
||||
if (event === 'edit') {
|
||||
editCardType(data);
|
||||
} else if (event === 'del') {
|
||||
deleteCardType(data.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 新增卡密类型
|
||||
$('#btnAddCardType').on('click', () => {
|
||||
showCardTypeForm();
|
||||
});
|
||||
|
||||
// 显示表单弹窗(统一使用 layer.open 的按钮作为确认/取消)
|
||||
const showCardTypeForm = (data = null) => {
|
||||
const title = data ? '编辑卡密类型' : '新增卡密类型';
|
||||
currentFormLayerIndex = layer.open({
|
||||
type: 1,
|
||||
title: title,
|
||||
content: $('#cardTypeFormModal'),
|
||||
area: ['500px', '300px'],
|
||||
btn: ['提交', '取消'],
|
||||
btnAlign: 'c',
|
||||
yes: () => {
|
||||
// 点击“提交”时,执行统一的提交方法
|
||||
doCardTypeSubmit();
|
||||
},
|
||||
btn2: (index) => {
|
||||
// 点击“取消”时,关闭当前弹窗
|
||||
layer.close(index);
|
||||
},
|
||||
success: () => {
|
||||
// 成功打开弹窗后渲染表单,并根据是否为编辑模式进行回填
|
||||
form.render();
|
||||
if (data) {
|
||||
// 编辑模式,填充数据
|
||||
$('#cardTypeForm input[name="id"]').val(data.id);
|
||||
$('#cardTypeForm input[name="name"]').val(data.name);
|
||||
$('#cardTypeForm select[name="status"]').val(data.status);
|
||||
$('#cardTypeForm input[name="login_types"]').val(data.login_types);
|
||||
form.render('select');
|
||||
} else {
|
||||
// 新增模式,清空表单
|
||||
$('#cardTypeForm')[0].reset();
|
||||
$('#cardTypeForm input[name="id"]').val('');
|
||||
form.render();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑卡密类型
|
||||
const editCardType = (data) => {
|
||||
showCardTypeForm(data);
|
||||
};
|
||||
|
||||
// 提交表单(通过 layer.open 的“提交”按钮触发)
|
||||
const doCardTypeSubmit = () => {
|
||||
// 读取表单数据
|
||||
const idValue = $('#cardTypeForm input[name="id"]').val();
|
||||
const formData = {
|
||||
id: idValue ? parseInt(idValue) : 0,
|
||||
name: $('#cardTypeForm input[name="name"]').val().trim(),
|
||||
status: parseInt($('#cardTypeForm select[name="status"]').val()),
|
||||
login_types: $('#cardTypeForm input[name="login_types"]').val().trim()
|
||||
};
|
||||
|
||||
// 校验必填项
|
||||
if (!formData.name) {
|
||||
layer.msg('请输入类型名称', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据是否存在 id 判断是创建还是更新
|
||||
const url = formData.id ? '/admin/api/card_types/update' : '/admin/api/card_types/create';
|
||||
const loadIndex = layer.load(2);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
layer.close(loadIndex);
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg || (formData.id ? '更新成功' : '创建成功'), { icon: 1 });
|
||||
layer.close(currentFormLayerIndex);
|
||||
cardTypesTable.reload();
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败', { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
};
|
||||
|
||||
// 取消按钮已移除,统一由 layer.open 的“取消”按钮处理
|
||||
// 取消按钮已移除:统一由 layer.open 的“取消”按钮处理
|
||||
|
||||
// 删除卡密类型
|
||||
const deleteCardType = (id) => {
|
||||
layer.confirm('确定要删除这个卡密类型吗?', {
|
||||
icon: 3,
|
||||
title: '提示'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const loadIndex = layer.load(2);
|
||||
|
||||
fetch('/admin/api/card_types/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: id })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardTypesTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
$('#btnBatchDeleteCardTypes').on('click', () => {
|
||||
const checkStatus = table.checkStatus('cardTypesTable');
|
||||
if (checkStatus.data.length === 0) {
|
||||
layer.msg('请选择要删除的数据', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm(`确定要删除选中的 ${checkStatus.data.length} 条数据吗?`, {
|
||||
icon: 3,
|
||||
title: '提示'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const ids = checkStatus.data.map(item => item.id);
|
||||
const loadIndex = layer.load(2);
|
||||
|
||||
fetch('/admin/api/card_types/batch_delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: ids })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardTypesTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 批量启用
|
||||
$('#btnBatchEnableCardTypes').on('click', () => {
|
||||
batchUpdateStatus('/admin/api/card_types/batch_enable', '启用');
|
||||
});
|
||||
|
||||
// 批量禁用
|
||||
$('#btnBatchDisableCardTypes').on('click', () => {
|
||||
batchUpdateStatus('/admin/api/card_types/batch_disable', '禁用');
|
||||
});
|
||||
|
||||
// 批量更新状态的通用函数
|
||||
const batchUpdateStatus = (url, action) => {
|
||||
const checkStatus = table.checkStatus('cardTypesTable');
|
||||
if (checkStatus.data.length === 0) {
|
||||
layer.msg('请选择要操作的数据', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm(`确定要${action}选中的 ${checkStatus.data.length} 条数据吗?`, {
|
||||
icon: 3,
|
||||
title: '提示'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const ids = checkStatus.data.map(item => item.id);
|
||||
const loadIndex = layer.load(2);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: ids })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardTypesTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索功能
|
||||
$('#btnSearchCardTypes').on('click', () => {
|
||||
const keyword = $('#cardTypeFilterForm input[name="keyword"]').val();
|
||||
const status = $('#cardTypeFilterForm select[name="status"]').val();
|
||||
|
||||
cardTypesTable.reload({
|
||||
where: {
|
||||
keyword: keyword,
|
||||
status: status
|
||||
},
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 重置搜索
|
||||
$('#btnResetCardTypes').on('click', () => {
|
||||
$('#cardTypeFilterForm')[0].reset();
|
||||
form.render();
|
||||
cardTypesTable.reload({
|
||||
where: {},
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -1,771 +0,0 @@
|
||||
{{ define "cards.html" }}
|
||||
<section>
|
||||
<h2>卡密管理</h2>
|
||||
<div class="layui-btn-container" style="margin:12px 0">
|
||||
<button class="layui-btn" id="btnAddCard"><i class="layui-icon layui-icon-add-1"></i> 新增卡密</button>
|
||||
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteCards"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
|
||||
<button class="layui-btn layui-btn-normal" id="btnBatchEnableCards"><i class="layui-icon layui-icon-ok-circle"></i> 设为未用</button>
|
||||
<button class="layui-btn layui-btn-warm" id="btnBatchDisableCards"><i class="layui-icon layui-icon-close-fill"></i> 设为已用</button>
|
||||
<!-- 新增:导出卡密按钮 -->
|
||||
<button class="layui-btn layui-btn-primary" id="btnExportCards"><i class="layui-icon layui-icon-export"></i> 导出卡密</button>
|
||||
<!-- 新增:导出选中卡密按钮 -->
|
||||
<button class="layui-btn layui-btn-primary" id="btnExportSelectedCards"><i class="layui-icon layui-icon-export"></i> 导出选中</button>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">筛选</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form layui-form-pane" id="cardFilterForm" lay-filter="cardFilterForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">卡密</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keyword" placeholder="卡号/批次/备注/任务号" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="card_type" id="filterCardTypeSelect">
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="0">未使用</option>
|
||||
<option value="1">已使用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnSearchCards">查询</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnResetCards">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">卡密列表</div>
|
||||
<div class="layui-card-body">
|
||||
<table id="cardsTable" lay-filter="cardsTableFilter"></table>
|
||||
<script type="text/html" id="tpl-cards-ops">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的表单弹层内容:新增卡密 -->
|
||||
<div id="cardFormModal" style="display:none;padding:16px">
|
||||
<form class="layui-form layui-form-pane" id="cardForm">
|
||||
<input type="hidden" name="id" />
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">前缀</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="prefix" placeholder="可选,生成卡号时使用的前缀" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 新增:生成数量(默认1,最大500),位置在前缀之后、大小写之前 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数量</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="count" min="1" max="500" value="1" placeholder="一次生成的数量(默认1,最大500)" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 新增:生成大小写选项(默认小写),位置在长度之前 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">规则</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="uppercase">
|
||||
<option value="lower" selected>小写</option>
|
||||
<option value="upper">大写</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">长度</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="length" min="1" max="64" value="18" placeholder="生成卡号的总长度(包含前缀),默认18" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="card_type" id="cardTypeSelect">
|
||||
<option value="">请选择类型</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="0">未使用</option>
|
||||
<option value="1">已使用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="remark" placeholder="可填写备注信息" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移除:内置“操作/提交/取消”按钮,统一由 layer.open 的 btn 控制 -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的表单弹层内容:编辑卡密 -->
|
||||
<div id="cardEditFormModal" style="display:none;padding:16px">
|
||||
<form class="layui-form layui-form-pane" id="cardEditForm">
|
||||
<input type="hidden" name="id" />
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="card_type" id="cardEditTypeSelect">
|
||||
<option value="">请选择类型</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="0">未使用</option>
|
||||
<option value="1">已使用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">任务号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="task_no" placeholder="可选,支持填写/清空任务号" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="remark" placeholder="可填写备注信息" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移除:内置“操作/提交/取消”按钮,统一由 layer.open 的 btn 控制 -->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 新增:导出条件弹窗(隐藏) -->
|
||||
<div id="cardExportModal" style="display:none;padding:16px">
|
||||
<form class="layui-form layui-form-pane" id="cardExportForm" lay-filter="cardExportForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="0">未使用</option>
|
||||
<option value="1">已使用</option>
|
||||
<option value="2">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="card_type" id="exportCardTypeSelect">
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">批次</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="batch" placeholder="按批次模糊匹配" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="remark" placeholder="按备注模糊匹配" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'layer'], () => {
|
||||
const { table, form, layer, $ } = layui;
|
||||
let currentFormLayerIndex; // 保存当前表单弹窗的索引
|
||||
let cardTypes = []; // 存储卡密类型数据
|
||||
// 中文注释:以下三个标志用于协调类型列表和表格渲染的先后关系,避免出现“未知类型”
|
||||
let cardTypesLoaded = false; // 类型列表是否已加载完成
|
||||
let tableFirstRendered = false; // 表格是否已完成首次渲染
|
||||
let tableReloadedAfterTypes = false; // 类型加载后是否已触发表格的二次渲染
|
||||
|
||||
// 格式化时间的辅助函数
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return String(date.getFullYear()) + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0') + ' ' +
|
||||
String(date.getHours()).padStart(2, '0') + ':' +
|
||||
String(date.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getSeconds()).padStart(2, '0');
|
||||
};
|
||||
|
||||
// 获取卡密类型名称
|
||||
// 中文注释:根据 card_type_id 在缓存的 cardTypes 中查找对应的类型名称;
|
||||
// 为避免后端返回的 id 与前端数据在类型上不一致(字符串/数字)导致匹配失败,这里统一转换为数字再比较
|
||||
const getCardTypeName = (cardTypeId) => {
|
||||
const idNum = Number(cardTypeId);
|
||||
const cardType = cardTypes.find(type => Number(type.id) === idNum);
|
||||
return cardType ? cardType.name : '未知类型';
|
||||
};
|
||||
|
||||
// 渲染表格
|
||||
const cardsTable = table.render({
|
||||
elem: '#cardsTable',
|
||||
id: 'cardsTable',
|
||||
url: '/admin/api/cards/list',
|
||||
parseData: function(res) {
|
||||
// 后端返回的数据结构:{items, total, page, page_size, pages}
|
||||
return {
|
||||
code: res.code,
|
||||
msg: res.msg || '',
|
||||
count: res.data ? res.data.total : 0,
|
||||
data: res.data ? res.data.items : []
|
||||
};
|
||||
},
|
||||
request: {
|
||||
pageName: 'page', // 页码的参数名称,默认:page
|
||||
limitName: 'page_size' // 每页数据量的参数名称,默认:limit
|
||||
},
|
||||
method: 'GET',
|
||||
page: true,
|
||||
limit: 20,
|
||||
limits: [10, 20, 50, 100],
|
||||
loading: true,
|
||||
// 中文注释:表格首次渲染完成后,如果类型已经加载,则进行一次刷新以正确显示类型名称
|
||||
done: function() {
|
||||
if (!tableFirstRendered) {
|
||||
tableFirstRendered = true;
|
||||
if (cardTypesLoaded && !tableReloadedAfterTypes) {
|
||||
tableReloadedAfterTypes = true;
|
||||
cardsTable.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
cols: [[
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||||
{ field: 'card_number', title: '卡号', minWidth: 150 },
|
||||
{
|
||||
field: 'card_type_id',
|
||||
title: '类型',
|
||||
width: 100,
|
||||
templet: (d) => d.card_type_name || getCardTypeName(d.card_type_id)
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 80,
|
||||
templet: (d) => {
|
||||
if (d.status === 0) return '<span style="color: #5FB878;">未使用</span>';
|
||||
if (d.status === 1) return '<span style="color: #FF5722;">已使用</span>';
|
||||
return '<span style="color: #999;">禁用</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'task_no',
|
||||
title: '任务号',
|
||||
minWidth: 140,
|
||||
templet: (d) => d.task_no || '-'
|
||||
},
|
||||
{ field: 'batch', title: '批次', minWidth: 60 },
|
||||
{ field: 'remark', title: '备注', minWidth: 100 },
|
||||
{
|
||||
field: 'used_at',
|
||||
title: '使用时间',
|
||||
width: 180,
|
||||
templet: (d) => formatDateTime(d.used_at)
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
templet: (d) => formatDateTime(d.created_at)
|
||||
},
|
||||
{ fixed: 'right', title: '操作', toolbar: '#tpl-cards-ops', width: 120 }
|
||||
]]
|
||||
});
|
||||
|
||||
// 加载卡密类型数据
|
||||
const loadCardTypes = () => {
|
||||
fetch('/admin/api/cards/types?all=1')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.code === 0) {
|
||||
cardTypes = data.data || [];
|
||||
// 填充筛选下拉框
|
||||
const filterSelect = $('#filterCardTypeSelect');
|
||||
filterSelect.empty().append('<option value="">全部</option>');
|
||||
cardTypes.forEach(type => {
|
||||
if (type.status === 1) { // 只显示启用的类型
|
||||
filterSelect.append(`<option value="${type.id}">${type.name}</option>`);
|
||||
}
|
||||
});
|
||||
// 填充新增表单下拉框
|
||||
const cardTypeSelect = $('#cardTypeSelect');
|
||||
cardTypeSelect.empty().append('<option value="">请选择类型</option>');
|
||||
cardTypes.forEach(type => {
|
||||
if (type.status === 1) { // 只显示启用的类型
|
||||
cardTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
|
||||
}
|
||||
});
|
||||
// 填充编辑表单下拉框
|
||||
const cardEditTypeSelect = $('#cardEditTypeSelect');
|
||||
cardEditTypeSelect.empty().append('<option value="">请选择类型</option>');
|
||||
cardTypes.forEach(type => {
|
||||
if (type.status === 1) { // 只显示启用的类型
|
||||
cardEditTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
|
||||
}
|
||||
});
|
||||
// 新增:填充导出弹窗下拉框(显示全部状态的类型,方便条件筛选)
|
||||
const exportTypeSelect = $('#exportCardTypeSelect');
|
||||
exportTypeSelect.empty().append('<option value="">全部</option>');
|
||||
cardTypes.forEach(type => {
|
||||
exportTypeSelect.append(`<option value="${type.id}">${type.name}</option>`);
|
||||
});
|
||||
|
||||
form.render('select');
|
||||
|
||||
// 中文注释:标记类型加载完成;如表格已首次渲染,则进行一次性刷新以正确显示类型名称
|
||||
cardTypesLoaded = true;
|
||||
if (tableFirstRendered && !tableReloadedAfterTypes) {
|
||||
tableReloadedAfterTypes = true;
|
||||
cardsTable.reload();
|
||||
}
|
||||
// 卡密类型加载完成,表格会根据需要自动进行一次刷新
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载卡密类型失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化加载卡密类型
|
||||
loadCardTypes();
|
||||
|
||||
// 监听表格工具条
|
||||
table.on('tool(cardsTableFilter)', (obj) => {
|
||||
const { data, event } = obj;
|
||||
if (event === 'edit') {
|
||||
editCard(data);
|
||||
} else if (event === 'del') {
|
||||
deleteCard(data.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 新增卡密
|
||||
$('#btnAddCard').on('click', () => {
|
||||
showCardForm();
|
||||
});
|
||||
|
||||
// 显示新增卡密表单弹窗
|
||||
// 中文注释:弹出新增/编辑表单的公共方法,采用 layer.open + btn/yes/btn2 的“确认框风格”
|
||||
// data 为空表示新增;存在表示编辑。通过 yes 回调直接调用提交流程函数。
|
||||
const showCardForm = (data = null) => {
|
||||
const title = data ? '编辑卡密' : '新增卡密';
|
||||
const modalId = data ? '#cardEditFormModal' : '#cardFormModal';
|
||||
const formId = data ? '#cardEditForm' : '#cardForm';
|
||||
|
||||
const areaHeight = data ? '420px' : '600px';
|
||||
|
||||
currentFormLayerIndex = layer.open({
|
||||
type: 1,
|
||||
title: title,
|
||||
content: $(modalId),
|
||||
area: ['500px', areaHeight],
|
||||
btn: ['提交', '取消'],
|
||||
btnAlign: 'c',
|
||||
yes: () => {
|
||||
if (data) {
|
||||
doEditCardSubmit();
|
||||
} else {
|
||||
doCreateCardSubmit();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
btn2: (index) => {
|
||||
layer.close(index);
|
||||
},
|
||||
success: () => {
|
||||
form.render();
|
||||
if (data) {
|
||||
$(formId + ' input[name="id"]').val(data.id);
|
||||
$(formId + ' select[name="card_type"]').val(data.card_type_id);
|
||||
$(formId + ' select[name="status"]').val(data.status);
|
||||
$(formId + ' textarea[name="remark"]').val(data.remark || '');
|
||||
// 中文注释:编辑模式下,回填已有的任务号(若无则为空字符串)
|
||||
$(formId + ' input[name="task_no"]').val(data.task_no || '');
|
||||
form.render('select');
|
||||
} else {
|
||||
$(formId)[0].reset();
|
||||
$(formId + ' input[name="id"]').val('');
|
||||
// 中文注释:新增模式下显式清空任务号,避免出现上一次编辑残留
|
||||
$(formId + ' input[name="task_no"]').val('');
|
||||
form.render();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑卡密
|
||||
const editCard = (data) => {
|
||||
showCardForm(data);
|
||||
};
|
||||
|
||||
// 提交新增卡密表单
|
||||
// 提交逻辑函数化,供弹窗按钮直接调用,避免依赖模板内按钮
|
||||
// 中文注释:提交“新增卡密”表单,完成校验、请求与反馈
|
||||
const doCreateCardSubmit = () => {
|
||||
const uppercaseValue = $('#cardForm select[name="uppercase"]').val();
|
||||
const formData = {
|
||||
prefix: $('#cardForm input[name="prefix"]').val() || '',
|
||||
count: parseInt($('#cardForm input[name="count"]').val()) || 1,
|
||||
uppercase: uppercaseValue === 'upper',
|
||||
length: parseInt($('#cardForm input[name="length"]').val()) || 18,
|
||||
card_type_id: parseInt($('#cardForm select[name="card_type"]').val()),
|
||||
status: parseInt($('#cardForm select[name="status"]').val()),
|
||||
remark: $('#cardForm textarea[name="remark"]').val() || ''
|
||||
};
|
||||
|
||||
// 校验
|
||||
if (!formData.card_type_id) {
|
||||
layer.msg('请选择卡密类型', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
if (formData.count < 1 || formData.count > 500) {
|
||||
layer.msg('生成数量必须在1-500之间', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
if (formData.length < 1 || formData.length > 64) {
|
||||
layer.msg('卡号长度必须在1-64之间', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
const loadIndex = layer.load(2);
|
||||
fetch('/admin/api/cards/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
layer.close(currentFormLayerIndex);
|
||||
cardsTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('新增卡密失败:', error);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
};
|
||||
|
||||
// 中文注释:提交“编辑卡密”表单,完成校验、请求与反馈
|
||||
const doEditCardSubmit = () => {
|
||||
const idValue = $('#cardEditForm input[name="id"]').val();
|
||||
const taskNoRaw = $('#cardEditForm input[name="task_no"]').val();
|
||||
const hasTaskNoField = true; // 中文注释:该字段始终存在,通过值是否为空决定清空或设置
|
||||
const formData = {
|
||||
id: idValue ? parseInt(idValue) : 0,
|
||||
card_type_id: parseInt($('#cardEditForm select[name="card_type"]').val()),
|
||||
status: parseInt($('#cardEditForm select[name="status"]').val()),
|
||||
remark: $('#cardEditForm textarea[name="remark"]').val() || ''
|
||||
};
|
||||
|
||||
// 当任务号输入框有值或被清空时,也要传递 task_no 字段(允许清空)
|
||||
if (hasTaskNoField) {
|
||||
formData.task_no = (taskNoRaw || '').trim();
|
||||
}
|
||||
|
||||
// 校验
|
||||
if (!formData.id) {
|
||||
layer.msg('卡密ID不能为空', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
if (!formData.card_type_id) {
|
||||
layer.msg('请选择卡密类型', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
const loadIndex = layer.load(2);
|
||||
fetch('/admin/api/cards/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
layer.close(currentFormLayerIndex);
|
||||
cardsTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('编辑卡密失败:', error);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
};
|
||||
|
||||
// 删除卡密
|
||||
const deleteCard = (id) => {
|
||||
layer.confirm('确定要删除这个卡密吗?', {
|
||||
icon: 3,
|
||||
title: '提示'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const loadIndex = layer.load(2);
|
||||
|
||||
fetch('/admin/api/cards/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: parseInt(id) })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardsTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('删除卡密失败:', error);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除卡密
|
||||
$('#btnBatchDeleteCards').on('click', () => {
|
||||
const checkStatus = table.checkStatus('cardsTable');
|
||||
const data = checkStatus.data;
|
||||
|
||||
if (data.length === 0) {
|
||||
layer.msg('请选择要删除的卡密', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm(`确定要删除选中的 ${data.length} 个卡密吗?`, {
|
||||
icon: 3,
|
||||
title: '批量删除确认'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const loadIndex = layer.load(2);
|
||||
const ids = data.map(item => item.id);
|
||||
|
||||
fetch('/admin/api/cards/batch_delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: ids })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardsTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('批量删除卡密失败:', error);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 批量启用卡密
|
||||
$('#btnBatchEnableCards').on('click', () => {
|
||||
batchUpdateStatus(0, '设为未用');
|
||||
});
|
||||
|
||||
// 批量禁用卡密
|
||||
$('#btnBatchDisableCards').on('click', () => {
|
||||
batchUpdateStatus(1, '设为已用');
|
||||
});
|
||||
|
||||
// 批量更新状态
|
||||
const batchUpdateStatus = (status, statusText) => {
|
||||
const checkStatus = table.checkStatus('cardsTable');
|
||||
const data = checkStatus.data;
|
||||
|
||||
if (data.length === 0) {
|
||||
layer.msg('请选择要操作的卡密', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm(`确定要${statusText}选中的 ${data.length} 个卡密吗?`, {
|
||||
icon: 3,
|
||||
title: `批量${statusText}确认`
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const loadIndex = layer.load(2);
|
||||
const ids = data.map(item => item.id);
|
||||
|
||||
fetch('/admin/api/cards/batch_update_status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: ids, status: status })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
layer.close(loadIndex);
|
||||
if (data.code === 0) {
|
||||
layer.msg(data.msg, { icon: 1 });
|
||||
cardsTable.reload();
|
||||
} else {
|
||||
layer.msg(data.msg, { icon: 2 });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error(`批量${statusText}卡密失败:`, error);
|
||||
layer.msg('网络错误,请重试', { icon: 2 });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索功能
|
||||
$('#btnSearchCards').on('click', () => {
|
||||
const formData = form.val('cardFilterForm');
|
||||
const where = {};
|
||||
if (formData.card_type) {
|
||||
where.card_type_id = formData.card_type;
|
||||
}
|
||||
if (formData.status !== '') {
|
||||
where.status = formData.status;
|
||||
}
|
||||
if (formData.keyword && formData.keyword.trim() !== '') {
|
||||
// 中文注释:将关键字作为 keyword 传给后端,由后端在 card_number、remark、batch 三个字段中进行模糊匹配
|
||||
where.keyword = formData.keyword.trim();
|
||||
}
|
||||
table.reload('cardsTable', { where, page: { curr: 1 } });
|
||||
});
|
||||
|
||||
$('#btnResetCards').on('click', () => {
|
||||
$('#cardFilterForm')[0].reset();
|
||||
form.render();
|
||||
table.reload('cardsTable', { where: {}, page: { curr: 1 } });
|
||||
});
|
||||
|
||||
// =============== 导出卡密逻辑 ===============
|
||||
// 显示“导出卡密”弹窗
|
||||
// 中文注释:弹出导出条件选择弹窗,允许管理员按状态/类型/批次/备注筛选导出
|
||||
const showExportDialog = () => {
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '导出卡密',
|
||||
content: $('#cardExportModal'),
|
||||
area: ['520px', '360px'],
|
||||
btn: ['导出', '取消'],
|
||||
btnAlign: 'c',
|
||||
yes: (index) => {
|
||||
doExportCards();
|
||||
layer.close(index); // 关闭导出弹窗
|
||||
layer.msg('卡密导出中...', { icon: 1 });
|
||||
return false;
|
||||
},
|
||||
success: () => {
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 绑定按钮事件
|
||||
$('#btnExportCards').on('click', () => {
|
||||
showExportDialog();
|
||||
});
|
||||
|
||||
// 导出选中卡密
|
||||
// 中文注释:导出当前表格中选中的卡密,无需弹窗筛选,直接根据选中的卡密ID进行导出
|
||||
$('#btnExportSelectedCards').on('click', () => {
|
||||
const checkStatus = table.checkStatus('cardsTable');
|
||||
const data = checkStatus.data;
|
||||
|
||||
if (data.length === 0) {
|
||||
layer.msg('请选择要导出的卡密', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm(`确定要导出选中的 ${data.length} 个卡密吗?`, {
|
||||
icon: 3,
|
||||
title: '导出选中确认'
|
||||
}, (index) => {
|
||||
layer.close(index);
|
||||
const ids = data.map(item => item.id);
|
||||
const params = new URLSearchParams();
|
||||
params.set('ids', ids.join(','));
|
||||
|
||||
const url = '/admin/api/cards/export_selected?' + params.toString();
|
||||
triggerDownload(url);
|
||||
|
||||
layer.msg(`正在导出 ${data.length} 个卡密...`, { icon: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
// 执行导出
|
||||
// 中文注释:根据表单条件拼接导出URL,并以下载方式触发导出(CSV 文件)
|
||||
const doExportCards = () => {
|
||||
const formData = form.val('cardExportForm');
|
||||
const params = new URLSearchParams();
|
||||
if (formData.status !== '') params.set('status', formData.status);
|
||||
if (formData.card_type) params.set('card_type_id', formData.card_type);
|
||||
if (formData.batch && formData.batch.trim() !== '') params.set('batch', formData.batch.trim());
|
||||
if (formData.remark && formData.remark.trim() !== '') params.set('remark', formData.remark.trim());
|
||||
|
||||
const url = '/admin/api/cards/export' + (params.toString() ? ('?' + params.toString()) : '');
|
||||
triggerDownload(url);
|
||||
};
|
||||
|
||||
// 触发下载
|
||||
// 中文注释:通过创建临时 <a> 元素点击,保持当前页面不跳转,触发后端附件下载
|
||||
const triggerDownload = (url) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.target = '_blank';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -39,41 +39,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 卡密统计区域 -->
|
||||
<section style="margin-top:16px">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<!-- 当日卡密统计 -->
|
||||
<div class="layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">当日卡密统计 <span class="layui-badge layui-bg-blue" style="margin-left:8px">总数:<span id="today-total">-</span></span></div>
|
||||
<div class="layui-card-body">
|
||||
<div id="chart-today-by-status" style="width:100%;height:320px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 所有卡密统计 -->
|
||||
<div class="layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">所有卡密统计 <span class="layui-badge layui-bg-blue" style="margin-left:8px">总数:<span id="all-total">-</span></span></div>
|
||||
<div class="layui-card-body">
|
||||
<div id="chart-all-by-status" style="width:100%;height:320px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 30天走势图 -->
|
||||
<div class="layui-row layui-col-space15" style="margin-top:16px">
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">近30天卡密走势</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="chart-trend-30days" style="width:100%;height:360px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// 仪表盘统计脚本(采用箭头函数与中文注释)
|
||||
@@ -100,107 +66,7 @@ layui.use(['layer', 'util'], function(){
|
||||
}
|
||||
};
|
||||
|
||||
// 工具函数:状态码 -> 名称 映射
|
||||
// 说明:卡密状态映射,0=未使用,1=已使用,2=禁用
|
||||
const getStatusText = (s) => {
|
||||
const map = {0:'未使用',1:'已使用',2:'禁用'};
|
||||
const k = Number(s);
|
||||
return map[k] ?? String(s);
|
||||
};
|
||||
|
||||
// 工具函数:状态码 -> 颜色 映射(与徽章风格一致,尽量贴近 Layui 配色)
|
||||
const getStatusColor = (s) => {
|
||||
switch (Number(s)) {
|
||||
case 0: return '#1E9FFF'; // 蓝色 - 未使用
|
||||
case 1: return '#5FB878'; // 绿色 - 已使用
|
||||
case 2: return '#FF5722'; // 红色 - 禁用
|
||||
default: return '#909399'; // 灰色 - 默认
|
||||
}
|
||||
};
|
||||
|
||||
// 函数:渲染饼图
|
||||
// 说明:接收状态分布对象(键为状态码,值为数量),绘制环形图
|
||||
const renderPie = (domId, byStatus) => {
|
||||
const el = document.getElementById(domId);
|
||||
if (!el) return;
|
||||
const chart = echarts.init(el);
|
||||
|
||||
const codes = [0,1,2]; // 卡密状态:0=未使用,1=已使用,2=禁用
|
||||
const data = codes.map(code => ({
|
||||
name: getStatusText(code),
|
||||
value: Number((byStatus && byStatus[code]) || 0),
|
||||
itemStyle: { color: getStatusColor(code) }
|
||||
}));
|
||||
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { top: 'bottom' },
|
||||
series: [{
|
||||
name: '按状态分布',
|
||||
type: 'pie',
|
||||
radius: ['38%', '68%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: { formatter: '{b}: {c} ({d}%)' },
|
||||
data
|
||||
}]
|
||||
});
|
||||
|
||||
// 自适应
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
return chart;
|
||||
};
|
||||
|
||||
// 函数:渲染 30 天折线图
|
||||
// 说明:三条序列:total/used/unused,对应后台返回的数组
|
||||
const renderTrend = (domId, trend) => {
|
||||
const el = document.getElementById(domId);
|
||||
if (!el) return;
|
||||
const chart = echarts.init(el);
|
||||
|
||||
const dates = (trend && trend.dates) || [];
|
||||
const total = (trend && trend.total) || [];
|
||||
const used = (trend && trend.used) || [];
|
||||
const unused = (trend && trend.unused) || [];
|
||||
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['总数', '已使用', '未使用'] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: dates },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: '总数', type: 'line', smooth: true, data: total, itemStyle: { color: '#909399' } },
|
||||
{ name: '已使用', type: 'line', smooth: true, data: used, itemStyle: { color: getStatusColor(1) } },
|
||||
{ name: '未使用', type: 'line', smooth: true, data: unused, itemStyle: { color: getStatusColor(0) } }
|
||||
]
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
return chart;
|
||||
};
|
||||
|
||||
// 函数:拉取概览并渲染
|
||||
// 说明:请求 /admin/api/cards/stats_overview,更新总数文本并渲染两个饼图
|
||||
const loadAndRenderOverview = () => {
|
||||
$.get('/admin/api/cards/stats_overview', (res) => {
|
||||
if (!res || res.code !== 0) { layer.msg(res && res.msg ? res.msg : '获取统计概览失败'); return; }
|
||||
const data = res.data || {};
|
||||
$('#today-total').text((data.today && data.today.total) ?? '-');
|
||||
$('#all-total').text((data.all && data.all.total) ?? '-');
|
||||
// 渲染饼图
|
||||
renderPie('chart-today-by-status', data.today ? data.today.by_status : {});
|
||||
renderPie('chart-all-by-status', data.all ? data.all.by_status : {});
|
||||
});
|
||||
};
|
||||
|
||||
// 函数:拉取 30 天数据并渲染折线图
|
||||
// 说明:请求 /admin/api/cards/trend_30days,渲染趋势图
|
||||
const loadAndRenderTrend = () => {
|
||||
$.get('/admin/api/cards/trend_30days', (res) => {
|
||||
if (!res || res.code !== 0) { layer.msg(res && res.msg ? res.msg : '获取30天趋势失败'); return; }
|
||||
renderTrend('chart-trend-30days', res.data || {});
|
||||
});
|
||||
};
|
||||
|
||||
// 函数:刷新基本信息和运行状态
|
||||
// 说明:请求后台获取最新的系统信息并更新页面显示
|
||||
@@ -223,14 +89,8 @@ layui.use(['layer', 'util'], function(){
|
||||
});
|
||||
};
|
||||
|
||||
// 入口:确保 ECharts 已加载后开始渲染
|
||||
ensureECharts(() => {
|
||||
loadAndRenderOverview();
|
||||
loadAndRenderTrend();
|
||||
|
||||
// 立即刷新一次系统信息
|
||||
refreshSystemInfo();
|
||||
});
|
||||
// 立即刷新一次系统信息
|
||||
refreshSystemInfo();
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -54,14 +54,6 @@
|
||||
<dd><a data-path="apps" href="javascript:;">应用列表</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">卡密管理</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a data-path="logintypes" href="javascript:;">登录类型</a></dd>
|
||||
<dd><a data-path="cardtypes" href="javascript:;">卡密类型</a></dd>
|
||||
<dd><a data-path="cards" href="javascript:;">卡密列表</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
{{ define "login_types.html" }}
|
||||
<section>
|
||||
<h2>登录方式管理</h2>
|
||||
<div class="layui-btn-container" style="margin:12px 0">
|
||||
<button class="layui-btn" id="btnAddLoginType"><i class="layui-icon layui-icon-add-1"></i> 新增方式</button>
|
||||
<button class="layui-btn layui-btn-normal" id="btnBatchEnableLoginTypes"><i class="layui-icon layui-icon-ok-circle"></i> 批量启用</button>
|
||||
<button class="layui-btn layui-btn-warm" id="btnBatchDisableLoginTypes"><i class="layui-icon layui-icon-close-fill"></i> 批量禁用</button>
|
||||
<button class="layui-btn layui-btn-danger" id="btnBatchDeleteLoginTypes"><i class="layui-icon layui-icon-delete"></i> 批量删除</button>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">筛选</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form layui-form-pane" id="loginTypeFilterForm" lay-filter="loginTypeFilterForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keyword" placeholder="登录方式名称" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnSearchLoginTypes">查询</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnResetLoginTypes">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-card" style="margin-top:12px">
|
||||
<div class="layui-card-header">登录方式列表</div>
|
||||
<div class="layui-card-body">
|
||||
<table id="loginTypesTable" lay-filter="loginTypesTableFilter"></table>
|
||||
<script type="text/html" id="tpl-logintypes-ops">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的表单弹层内容 -->
|
||||
<div id="loginTypeFormModal" style="display:none;padding:16px">
|
||||
<!-- 参考demo表单2样式,添加layui-form-pane类实现方框风格 -->
|
||||
<form class="layui-form layui-form-pane" id="loginTypeForm" lay-filter="loginTypeForm">
|
||||
<input type="hidden" name="id" />
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" required lay-verify="required" placeholder="请输入登录方式名称" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="status">
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">验证类型</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="verify_types" placeholder="请输入验证类型,多个用逗号分隔" autocomplete="off" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作按钮移除:统一由 layer.open 的 btn 控制“提交/取消” -->
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// 登录类型管理页面的JavaScript脚本
|
||||
layui.use(['table', 'form', 'layer'], function(){
|
||||
const table = layui.table;
|
||||
const form = layui.form;
|
||||
const layer = layui.layer;
|
||||
const $ = layui.$;
|
||||
|
||||
// 表格实例
|
||||
let tableIns;
|
||||
|
||||
// 初始化表格
|
||||
const initTable = () => {
|
||||
tableIns = table.render({
|
||||
elem: '#loginTypesTable',
|
||||
url: '/admin/api/login_types/list',
|
||||
method: 'GET',
|
||||
page: true,
|
||||
limit: 20,
|
||||
limits: [10, 20, 50, 100],
|
||||
loading: true,
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'id', title: 'ID', width: 80, sort: true},
|
||||
{field: 'name', title: '名称'},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
return d.status === 1 ? '<span class="layui-badge layui-bg-green">启用</span>' : '<span class="layui-badge">禁用</span>';
|
||||
}},
|
||||
{field: 'verify_types', title: '验证类型'},
|
||||
{field: 'created_at', title: '创建时间', width: 180, templet: function(d){
|
||||
return formatDateTime(d.created_at);
|
||||
}},
|
||||
{field: 'updated_at', title: '更新时间', width: 180, templet: function(d){
|
||||
return formatDateTime(d.updated_at);
|
||||
}},
|
||||
{title: '操作', toolbar: '#tpl-logintypes-ops', width: 150, fixed: 'right'}
|
||||
]],
|
||||
parseData: function(res){
|
||||
// 后端已返回正确格式,直接使用
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.msg || '',
|
||||
"count": res.data ? res.data.total : 0,
|
||||
"data": res.data ? res.data.items : []
|
||||
};
|
||||
},
|
||||
request: {
|
||||
pageName: 'page',
|
||||
limitName: 'page_size'
|
||||
},
|
||||
where: {}
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0') + ' ' +
|
||||
String(date.getHours()).padStart(2, '0') + ':' +
|
||||
String(date.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getSeconds()).padStart(2, '0');
|
||||
};
|
||||
|
||||
// 重载表格数据
|
||||
const reloadTable = (where = {}) => {
|
||||
tableIns.reload({
|
||||
where: where,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取选中的行数据
|
||||
const getCheckData = () => {
|
||||
const checkStatus = table.checkStatus('loginTypesTable');
|
||||
return checkStatus.data;
|
||||
};
|
||||
|
||||
// 当前表单弹窗索引
|
||||
let currentFormLayerIndex = null;
|
||||
|
||||
// 显示表单弹层(统一使用 layer.open 的按钮作为确认/取消)
|
||||
const showFormModal = (title, data = {}) => {
|
||||
// 重置表单
|
||||
$('#loginTypeForm')[0].reset();
|
||||
|
||||
// 填充表单数据
|
||||
if (data.id) {
|
||||
$('input[name="id"]').val(data.id);
|
||||
$('input[name="name"]').val(data.name);
|
||||
$('select[name="status"]').val(data.status);
|
||||
$('input[name="verify_types"]').val(data.verify_types);
|
||||
}
|
||||
|
||||
// 刷新表单渲染
|
||||
form.render();
|
||||
|
||||
// 显示弹层并保存索引
|
||||
currentFormLayerIndex = layer.open({
|
||||
type: 1,
|
||||
title: title,
|
||||
content: $('#loginTypeFormModal'),
|
||||
area: ['500px', '300px'],
|
||||
btn: ['提交', '取消'],
|
||||
btnAlign: 'c',
|
||||
yes: () => {
|
||||
// 点击“提交”时执行提交
|
||||
doLoginTypeSubmit();
|
||||
},
|
||||
btn2: (index) => {
|
||||
// 点击“取消”时关闭弹层
|
||||
layer.close(index);
|
||||
},
|
||||
closeBtn: 1
|
||||
});
|
||||
};
|
||||
|
||||
// 提交表单(通过 layer.open 的“提交”按钮触发)
|
||||
const doLoginTypeSubmit = () => {
|
||||
// 读取表单数据并校验
|
||||
const idVal = $('input[name="id"]').val();
|
||||
const isEdit = idVal && idVal !== '';
|
||||
const name = $('input[name="name"]').val().trim();
|
||||
const status = parseInt($('select[name="status"]').val() || '0');
|
||||
const verifyTypes = $('input[name="verify_types"]').val().trim();
|
||||
|
||||
if (!name) {
|
||||
layer.msg('请输入登录方式名称', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
const url = isEdit ? '/admin/api/login_types/update' : '/admin/api/login_types/create';
|
||||
const requestData = {
|
||||
name: name,
|
||||
status: status,
|
||||
verify_types: verifyTypes
|
||||
};
|
||||
if (isEdit) requestData.id = parseInt(idVal);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData),
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(isEdit ? '更新成功' : '创建成功', { icon: 1, time: 1000 });
|
||||
layer.close(currentFormLayerIndex);
|
||||
reloadTable();
|
||||
} else {
|
||||
// 失败时对提示信息做截断,避免过长
|
||||
const raw = res.msg || '操作失败';
|
||||
const shortMsg = raw.length > 100 ? (raw.slice(0, 100) + '...') : raw;
|
||||
layer.msg(shortMsg, { icon: 2 });
|
||||
}
|
||||
},
|
||||
// 优先展示后端返回的业务错误信息,避免统一显示“网络错误”
|
||||
error: (xhr) => {
|
||||
// 失败时对提示信息做截断,避免过长
|
||||
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
|
||||
const shortMsg = raw.length > 100 ? (raw.slice(0, 100) + '...') : raw;
|
||||
layer.msg(shortMsg, { icon: 2 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除单个记录
|
||||
// 说明:删除前二次确认;后端返回400/500也能显示具体错误信息
|
||||
const deleteItem = (id) => {
|
||||
layer.confirm('确定要删除这条记录吗?', {icon: 3, title: '提示'}, function(index){
|
||||
$.ajax({
|
||||
url: '/admin/api/login_types/delete',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({id: id}),
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('删除成功', { icon: 1, time: 3000 });
|
||||
reloadTable();
|
||||
} else {
|
||||
// 删除失败:使用折行展示错误信息,便于阅读(不再截断)
|
||||
const raw = res.msg || '删除失败';
|
||||
const safe = String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
// 将常见分隔符替换为换行,结合 white-space: pre-wrap 生效
|
||||
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,,;;]/g, '\n')}</div>`;
|
||||
layer.msg(content, { icon: 2 });
|
||||
}
|
||||
},
|
||||
// 解析后端JSON错误响应,展示msg内容(支持折行)
|
||||
error: (xhr) => {
|
||||
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
|
||||
const safe = String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,,;;]/g, '\n')}</div>`;
|
||||
layer.msg(content, { icon: 2 });
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
};
|
||||
|
||||
// 批量操作
|
||||
// 参数:operation 用于提示文案,url 为接口地址,confirmMsg 为确认提示语
|
||||
const batchOperation = (operation, url, confirmMsg) => {
|
||||
const checkData = getCheckData();
|
||||
if (checkData.length === 0) {
|
||||
layer.msg('请选择要操作的数据', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = checkData.map(item => item.id);
|
||||
|
||||
layer.confirm(confirmMsg, {icon: 3, title: '提示'}, function(index){
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ids: ids}),
|
||||
// 统一成功/失败提示,移除残留的diff标记
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(operation + '成功', { icon: 1 });
|
||||
reloadTable();
|
||||
} else {
|
||||
// 批量失败:使用折行展示长信息(例如占用明细),便于阅读
|
||||
const raw = res.msg || operation + '失败';
|
||||
const safe = String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,,;;]/g, '\n')}</div>`;
|
||||
layer.msg(content, { icon: 2 });
|
||||
}
|
||||
},
|
||||
// 出错时同样尝试展示后端返回的msg(支持折行)
|
||||
error: (xhr) => {
|
||||
const raw = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '网络错误,请重试';
|
||||
const safe = String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
const content = `<div style="white-space:pre-wrap;word-break:break-word;">${safe.replace(/[,,;;]/g, '\n')}</div>`;
|
||||
layer.msg(content, { icon: 2 });
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
|
||||
// 新增按钮
|
||||
$('#btnAddLoginType').on('click', function(){
|
||||
showFormModal('新增登录方式');
|
||||
});
|
||||
|
||||
// 批量启用按钮
|
||||
$('#btnBatchEnableLoginTypes').on('click', function(){
|
||||
batchOperation('批量启用', '/admin/api/login_types/batch_enable', '确定要启用选中的登录方式吗?');
|
||||
});
|
||||
|
||||
// 批量禁用按钮
|
||||
$('#btnBatchDisableLoginTypes').on('click', function(){
|
||||
batchOperation('批量禁用', '/admin/api/login_types/batch_disable', '确定要禁用选中的登录方式吗?');
|
||||
});
|
||||
|
||||
// 批量删除按钮
|
||||
$('#btnBatchDeleteLoginTypes').on('click', function(){
|
||||
batchOperation('批量删除', '/admin/api/login_types/batch_delete', '确定要删除选中的登录方式吗?删除后不可恢复!');
|
||||
});
|
||||
|
||||
// 查询按钮
|
||||
$('#btnSearchLoginTypes').on('click', function(){
|
||||
const formData = form.val('loginTypeFilterForm');
|
||||
const where = {};
|
||||
|
||||
if (formData.keyword && formData.keyword.trim() !== '') {
|
||||
where.keyword = formData.keyword.trim();
|
||||
}
|
||||
if (formData.status && formData.status !== '') {
|
||||
where.status = formData.status;
|
||||
}
|
||||
|
||||
reloadTable(where);
|
||||
});
|
||||
|
||||
// 重置按钮
|
||||
$('#btnResetLoginTypes').on('click', function(){
|
||||
$('#loginTypeFilterForm')[0].reset();
|
||||
form.render();
|
||||
reloadTable();
|
||||
});
|
||||
|
||||
// Layui表单提交事件
|
||||
// 删除 Layui 表单提交监听(由 layer.open 的“提交”按钮统一触发)
|
||||
// form.on('submit(loginTypeSubmit)', function(data){
|
||||
// submitForm();
|
||||
// return false; // 阻止表单跳转
|
||||
// });
|
||||
|
||||
// 删除表单取消按钮事件(由 layer.open 的“取消”按钮统一处理)
|
||||
// $('#btnCancelLoginType').on('click', function(){
|
||||
// layer.close(currentFormLayerIndex);
|
||||
// });
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('tool(loginTypesTableFilter)', function(obj){
|
||||
const data = obj.data;
|
||||
const layEvent = obj.event;
|
||||
|
||||
if (layEvent === 'edit') {
|
||||
showFormModal('编辑登录方式', data);
|
||||
} else if (layEvent === 'del') {
|
||||
deleteItem(data.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化页面
|
||||
initTable();
|
||||
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user