From 6dad3971de328345052006d9f05614c11580a732 Mon Sep 17 00:00:00 2001 From: skyle1995 Date: Sat, 25 Oct 2025 01:43:03 +0800 Subject: [PATCH] New interface management Optimize the pop-up interface --- .gitignore | 11 +- controllers/admin/api.go | 418 ++++++++++++++++++++++++++ controllers/admin/app.go | 325 +++++++++++++++++++- models/api.go | 170 ++++++++++- models/app.go | 3 + server/admin.go | 10 +- utils/encrypt/easy.go | 241 +++++++++++++++ web/static/js/admin.js | 8 +- web/template/admin/apis.html | 526 +++++++++++++++++++++++++++++++++ web/template/admin/apps.html | 236 +++++++++++---- web/template/admin/layout.html | 1 + 11 files changed, 1869 insertions(+), 80 deletions(-) create mode 100644 controllers/admin/api.go create mode 100644 utils/encrypt/easy.go create mode 100644 web/template/admin/apis.html diff --git a/.gitignore b/.gitignore index 556feda..49e1259 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +/config.json +/database.db +/recharge.db +logs +模板 .DS_Store networkDev -node.txt -/config.json -database.db -logs -模板 \ No newline at end of file +node.txt \ No newline at end of file diff --git a/controllers/admin/api.go b/controllers/admin/api.go new file mode 100644 index 0000000..65dbf71 --- /dev/null +++ b/controllers/admin/api.go @@ -0,0 +1,418 @@ +package admin + +import ( + "encoding/json" + "net/http" + "networkDev/database" + "networkDev/models" + "networkDev/utils" + "networkDev/utils/encrypt" + "strconv" + "strings" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + + "github.com/sirupsen/logrus" +) + +// APIFragmentHandler 接口列表页面片段处理器 +func APIFragmentHandler(w http.ResponseWriter, r *http.Request) { + utils.RenderTemplate(w, "apis.html", map[string]interface{}{ + "Title": "接口管理", + }) +} + +// APIListHandler 接口列表API处理器 +func APIListHandler(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")) + if page <= 0 { + page = 1 + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + if limit <= 0 { + limit = 10 + } + + // 获取应用UUID参数(用于按应用筛选接口) + appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid")) + + // 获取搜索参数 + search := strings.TrimSpace(r.URL.Query().Get("search")) + + // 构建查询 + db, err := database.GetDB() + if err != nil { + logrus.WithError(err).Error("Failed to get database connection") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 构建基础查询 + query := db.Model(&models.API{}) + + // 如果指定了应用UUID,则按应用筛选 + if appUUID != "" { + query = query.Where("app_uuid = ?", appUUID) + } + + // 如果有搜索条件,添加搜索 + if search != "" { + query = query.Where("api_key LIKE ? OR app_uuid LIKE ?", "%"+search+"%", "%"+search+"%") + } + + // 获取总数 + var total int64 + if err := query.Count(&total).Error; err != nil { + logrus.WithError(err).Error("Failed to count APIs") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 获取分页数据 + var apis []models.API + offset := (page - 1) * limit + if err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&apis).Error; err != nil { + logrus.WithError(err).Error("Failed to fetch APIs") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 获取关联的应用信息 + var appUUIDs []string + for _, api := range apis { + appUUIDs = append(appUUIDs, api.AppUUID) + } + + var apps []models.App + if len(appUUIDs) > 0 { + if err := db.Where("uuid IN ?", appUUIDs).Find(&apps).Error; err != nil { + logrus.WithError(err).Error("Failed to fetch related apps") + } + } + + // 创建应用UUID到应用名称的映射 + appMap := make(map[string]string) + for _, app := range apps { + appMap[app.UUID] = app.Name + } + + // 构建响应数据 + type APIResponse struct { + models.API + AppName string `json:"app_name"` + APITypeName string `json:"api_type_name"` + StatusName string `json:"status_name"` + AlgorithmNames struct { + Submit string `json:"submit"` + Return string `json:"return"` + } `json:"algorithm_names"` + } + + var responseAPIs []APIResponse + for _, api := range apis { + responseAPI := APIResponse{ + API: api, + AppName: appMap[api.AppUUID], + APITypeName: models.GetAPITypeName(api.APIType), + StatusName: getAPIStatusName(api.Status), + } + responseAPI.AlgorithmNames.Submit = models.GetAlgorithmName(api.SubmitAlgorithm) + responseAPI.AlgorithmNames.Return = models.GetAlgorithmName(api.ReturnAlgorithm) + responseAPIs = append(responseAPIs, responseAPI) + } + + // 计算分页信息 + totalPages := (total + int64(limit) - 1) / int64(limit) + + response := map[string]interface{}{ + "success": true, + "data": map[string]interface{}{ + "apis": responseAPIs, + "total": total, + "page": page, + "limit": limit, + "total_pages": totalPages, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// getAPIStatusName 获取API状态名称 +func getAPIStatusName(status int) string { + switch status { + case 1: + return "启用" + case 0: + return "禁用" + default: + return "未知" + } +} + +// APIUpdateHandler 更新接口处理器 +func APIUpdateHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + ID uint `json:"id"` + Status int `json:"status"` + SubmitAlgorithm int `json:"submit_algorithm"` + ReturnAlgorithm int `json:"return_algorithm"` + SubmitPublicKey string `json:"submit_public_key"` + SubmitPrivateKey string `json:"submit_private_key"` + ReturnPublicKey string `json:"return_public_key"` + ReturnPrivateKey string `json:"return_private_key"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // 验证必填字段 + if req.ID == 0 { + http.Error(w, "接口ID不能为空", http.StatusBadRequest) + return + } + + if req.Status != 0 && req.Status != 1 { + http.Error(w, "无效的状态值", http.StatusBadRequest) + return + } + + if !models.IsValidAlgorithm(req.SubmitAlgorithm) || !models.IsValidAlgorithm(req.ReturnAlgorithm) { + http.Error(w, "无效的算法类型", http.StatusBadRequest) + return + } + + // 获取数据库连接 + db, err := database.GetDB() + if err != nil { + logrus.WithError(err).Error("Failed to get database connection") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 查找并更新API记录 + var api models.API + if err := db.First(&api, req.ID).Error; err != nil { + http.Error(w, "接口不存在", http.StatusNotFound) + return + } + + // 更新字段(不允许修改 APIType) + api.Status = req.Status + api.SubmitAlgorithm = req.SubmitAlgorithm + api.ReturnAlgorithm = req.ReturnAlgorithm + + // 可选更新密钥/证书(当提供时) + if req.SubmitPublicKey != "" || req.SubmitPrivateKey != "" { + api.SubmitPublicKey = req.SubmitPublicKey + api.SubmitPrivateKey = req.SubmitPrivateKey + } + if req.ReturnPublicKey != "" || req.ReturnPrivateKey != "" { + api.ReturnPublicKey = req.ReturnPublicKey + api.ReturnPrivateKey = req.ReturnPrivateKey + } + + if err := db.Save(&api).Error; err != nil { + logrus.WithError(err).Error("Failed to update API") + http.Error(w, "更新接口失败", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "success": true, + "message": "接口更新成功", + "data": api, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// APIGetAppsHandler 获取应用列表(用于接口页面的应用选择器) +func APIGetAppsHandler(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 { + logrus.WithError(err).Error("Failed to get database connection") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // 获取所有应用 + var apps []models.App + if err := db.Select("uuid, name").Order("created_at ASC").Find(&apps).Error; err != nil { + logrus.WithError(err).Error("Failed to fetch apps") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "success": true, + "data": apps, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Side string `json:"side"` // submit | return + Algorithm int `json:"algorithm"` // 与 models.Algorithm* 对应 + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + if req.Side != "submit" && req.Side != "return" { + http.Error(w, "side参数必须为submit或return", http.StatusBadRequest) + return + } + if !models.IsValidAlgorithm(req.Algorithm) { + http.Error(w, "无效的算法类型", http.StatusBadRequest) + return + } + + // 根据算法生成密钥/证书 + result := map[string]interface{}{} + + switch req.Algorithm { + case models.AlgorithmNone: + // 不加密不生成任何密钥 + result["public_key"] = "" + result["private_key"] = "" + case models.AlgorithmRC4: + // 生成16字节随机密钥并返回16位十六进制(大写) + bytes := make([]byte, 8) + if _, err := rand.Read(bytes); err != nil { + logrus.WithError(err).Error("Failed to generate RC4 key") + http.Error(w, "生成RC4密钥失败", http.StatusInternalServerError) + return + } + result["public_key"] = "" + result["private_key"] = strings.ToUpper(hex.EncodeToString(bytes)) + case models.AlgorithmRSA, models.AlgorithmRSADynamic: + // 生成RSA 2048密钥对,返回PEM明文字符串 + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + logrus.WithError(err).Error("Failed to generate RSA key pair") + http.Error(w, "生成RSA密钥失败", http.StatusInternalServerError) + return + } + privBytes := x509.MarshalPKCS1PrivateKey(key) + pubBytes := x509.MarshalPKCS1PublicKey(&key.PublicKey) + privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) + pubPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubBytes}) + result["private_key"] = string(privPEM) + result["public_key"] = string(pubPEM) + case models.AlgorithmEasy: + // 生成易加密密钥对,返回逗号分隔的整数数组字符串 + encryptKey, _, err := encrypt.GenerateEasyKey() + if err != nil { + logrus.WithError(err).Error("Failed to generate Easy encryption key") + http.Error(w, "生成易加密密钥失败", http.StatusInternalServerError) + return + } + result["public_key"] = "" + result["private_key"] = encrypt.FormatKeyAsString(encryptKey) + default: + http.Error(w, "不支持的算法类型", http.StatusBadRequest) + return + } + + response := map[string]interface{}{ + "success": true, + "message": "生成成功", + "data": result, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func APIResetKeyHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + ID uint `json:"id"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if req.ID == 0 { + http.Error(w, "接口ID不能为空", http.StatusBadRequest) + return + } + + db, err := database.GetDB() + if err != nil { + logrus.WithError(err).Error("Failed to get database connection") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + var api models.API + if err := db.First(&api, req.ID).Error; err != nil { + http.Error(w, "接口不存在", http.StatusNotFound) + return + } + + // 生成新的16位大写十六进制密钥 + bytes := make([]byte, 8) + if _, err := rand.Read(bytes); err != nil { + logrus.WithError(err).Error("Failed to generate random API key") + http.Error(w, "生成密钥失败", http.StatusInternalServerError) + return + } + newKey := strings.ToUpper(hex.EncodeToString(bytes)) + + if err := db.Model(&api).Update("api_key", newKey).Error; err != nil { + logrus.WithError(err).Error("Failed to update API key") + http.Error(w, "更新密钥失败", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "success": true, + "message": "接口密钥重置成功", + "data": map[string]interface{}{ + "id": api.ID, + "api_key": newKey, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/controllers/admin/app.go b/controllers/admin/app.go index 19f3459..a8546f8 100644 --- a/controllers/admin/app.go +++ b/controllers/admin/app.go @@ -88,6 +88,76 @@ func AppsListHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// AppGetAppDataHandler 获取应用数据处理器 +func AppGetAppDataHandler(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 appData string + if app.AppData != "" { + decodedBytes, err := base64.StdEncoding.DecodeString(app.AppData) + if err != nil { + logrus.WithError(err).Error("Failed to decode app data") + // 如果解码失败,返回空字符串 + appData = "" + } else { + appData = string(decodedBytes) + } + } + + response := map[string]interface{}{ + "code": 0, + "msg": "获取成功", + "data": map[string]interface{}{ + "app_data": appData, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + // AppGetAnnouncementHandler 获取应用程序公告处理器 func AppGetAnnouncementHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -294,12 +364,73 @@ func AppCreateHandler(w http.ResponseWriter, r *http.Request) { return } - if err := db.Create(&app).Error; err != nil { + // 开始事务 + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 创建应用 + if err := tx.Create(&app).Error; err != nil { + tx.Rollback() logrus.WithError(err).Error("Failed to create app") http.Error(w, "创建应用失败", http.StatusInternalServerError) return } + // 为应用创建所有默认接口 + defaultAPITypes := []int{ + models.APITypeGetBulletin, // 获取程序公告 + models.APITypeGetUpdateUrl, // 获取更新地址 + models.APITypeCheckAppVersion, // 检测最新版本 + models.APITypeGetCardInfo, // 获取卡密信息 + models.APITypeSingleLogin, // 卡密登录 + models.APITypeUserLogin, // 用户登录 + models.APITypeUserRegin, // 用户注册 + models.APITypeUserRecharge, // 用户充值 + models.APITypeCardRegin, // 卡密注册 + models.APITypeLogOut, // 退出登录 + models.APITypeGetExpired, // 获取到期时间 + models.APITypeCheckUserStatus, // 检测账号状态 + models.APITypeGetAppData, // 获取程序数据 + models.APITypeGetVariable, // 获取变量数据 + models.APITypeUpdatePwd, // 修改账号密码 + models.APITypeMacChangeBind, // 机器码转绑 + models.APITypeIPChangeBind, // IP转绑 + models.APITypeDisableUser, // 封停用户 + models.APITypeBlackUser, // 添加黑名单 + models.APITypeUserDeductedTime, // 扣除时间 + } + + // 批量创建默认接口 + for _, apiType := range defaultAPITypes { + api := models.API{ + APIType: apiType, + AppUUID: app.UUID, + Status: 1, // 默认启用 + SubmitAlgorithm: models.AlgorithmNone, // 默认不加密 + ReturnAlgorithm: models.AlgorithmNone, // 默认不加密 + } + + if err := tx.Create(&api).Error; err != nil { + tx.Rollback() + logrus.WithError(err).WithField("api_type", apiType).Error("Failed to create default API") + http.Error(w, "创建默认接口失败", http.StatusInternalServerError) + return + } + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + logrus.WithError(err).Error("Failed to commit transaction") + http.Error(w, "提交事务失败", http.StatusInternalServerError) + return + } + + logrus.WithField("app_uuid", app.UUID).Info("Successfully created app with default APIs") + response := map[string]interface{}{ "code": 0, "msg": "创建成功", @@ -408,13 +539,51 @@ func AppDeleteHandler(w http.ResponseWriter, r *http.Request) { return } + // 开始事务 + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 首先获取应用信息以获取UUID + var app models.App + if err := tx.First(&app, req.ID).Error; err != nil { + tx.Rollback() + logrus.WithError(err).Error("Failed to find app") + http.Error(w, "应用不存在", http.StatusNotFound) + return + } + + // 删除该应用的所有相关接口 + if err := tx.Where("app_uuid = ?", app.UUID).Delete(&models.API{}).Error; err != nil { + tx.Rollback() + logrus.WithError(err).Error("Failed to delete related APIs") + http.Error(w, "删除相关接口失败", http.StatusInternalServerError) + return + } + // 删除应用 - if err := db.Delete(&models.App{}, req.ID).Error; err != nil { + if err := tx.Delete(&models.App{}, req.ID).Error; err != nil { + tx.Rollback() logrus.WithError(err).Error("Failed to delete app") http.Error(w, "删除应用失败", http.StatusInternalServerError) return } + // 提交事务 + if err := tx.Commit().Error; err != nil { + logrus.WithError(err).Error("Failed to commit transaction") + http.Error(w, "提交事务失败", http.StatusInternalServerError) + return + } + + logrus.WithFields(logrus.Fields{ + "app_id": req.ID, + "app_uuid": app.UUID, + }).Info("Successfully deleted app and related APIs") + response := map[string]interface{}{ "code": 0, "msg": "删除成功", @@ -452,13 +621,59 @@ func AppsBatchDeleteHandler(w http.ResponseWriter, r *http.Request) { return } - // 批量删除 - if err := db.Delete(&models.App{}, req.IDs).Error; err != nil { + // 开始事务 + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 首先获取要删除的应用的UUID列表 + var apps []models.App + if err := tx.Where("id IN ?", req.IDs).Find(&apps).Error; err != nil { + tx.Rollback() + logrus.WithError(err).Error("Failed to find apps") + http.Error(w, "查找应用失败", http.StatusInternalServerError) + return + } + + // 提取UUID列表 + var appUUIDs []string + for _, app := range apps { + appUUIDs = append(appUUIDs, app.UUID) + } + + // 删除这些应用的所有相关接口 + if len(appUUIDs) > 0 { + if err := tx.Where("app_uuid IN ?", appUUIDs).Delete(&models.API{}).Error; err != nil { + tx.Rollback() + logrus.WithError(err).Error("Failed to delete related APIs") + http.Error(w, "删除相关接口失败", http.StatusInternalServerError) + return + } + } + + // 批量删除应用 + if err := tx.Delete(&models.App{}, req.IDs).Error; err != nil { + tx.Rollback() logrus.WithError(err).Error("Failed to batch delete apps") http.Error(w, "批量删除失败", http.StatusInternalServerError) return } + // 提交事务 + if err := tx.Commit().Error; err != nil { + logrus.WithError(err).Error("Failed to commit transaction") + http.Error(w, "提交事务失败", http.StatusInternalServerError) + return + } + + logrus.WithFields(logrus.Fields{ + "app_ids": req.IDs, + "app_uuids": appUUIDs, + }).Info("Successfully batch deleted apps and related APIs") + response := map[string]interface{}{ "code": 0, "msg": "批量删除成功", @@ -523,6 +738,108 @@ func AppsBatchUpdateStatusHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// AppUpdateAppDataHandler 更新应用数据处理器 +func AppUpdateAppDataHandler(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"` + AppData string `json:"app_data"` + } + + 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编码 + encodedAppData := base64.StdEncoding.EncodeToString([]byte(req.AppData)) + + // 更新应用的数据内容 + if err := db.Model(&app).Update("app_data", encodedAppData).Error; err != nil { + logrus.WithError(err).Error("Failed to update app data") + 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 data updated successfully") + + response := map[string]interface{}{ + "code": 0, + "msg": "应用数据更新成功", + } + + 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 { diff --git a/models/api.go b/models/api.go index 319c18c..2fbbc0b 100644 --- a/models/api.go +++ b/models/api.go @@ -1,10 +1,11 @@ package models import ( + "crypto/rand" + "encoding/hex" "strings" "time" - "github.com/google/uuid" "gorm.io/gorm" ) @@ -30,24 +31,24 @@ type API struct { Status int `gorm:"default:1;not null;comment:接口状态,1=启用,0=禁用" json:"status"` // 接口提交算法 - // 支持的算法:0=不加密,1=RC4,2=RSA,3=RSA(动态) - SubmitAlgorithm int `gorm:"default:0;not null;comment:提交算法,0=不加密,1=RC4,2=RSA,3=RSA动态" json:"submit_algorithm"` + // 支持的算法:0=不加密,1=RC4,2=RSA,3=RSA(动态),4=易加密 + SubmitAlgorithm int `gorm:"default:0;not null;comment:提交算法,0=不加密,1=RC4,2=RSA,3=RSA动态,4=易加密" json:"submit_algorithm"` // 接口返回算法 - // 支持的算法:0=不加密,1=RC4,2=RSA,3=RSA(动态) - ReturnAlgorithm int `gorm:"default:0;not null;comment:返回算法,0=不加密,1=RC4,2=RSA,3=RSA动态" json:"return_algorithm"` + // 支持的算法:0=不加密,1=RC4,2=RSA,3=RSA(动态),4=易加密 + ReturnAlgorithm int `gorm:"default:0;not null;comment:返回算法,0=不加密,1=RC4,2=RSA,3=RSA动态,4=易加密" json:"return_algorithm"` - // 提交算法公钥(base64编码存储) - SubmitPublicKey string `gorm:"type:text;comment:提交算法公钥,base64编码" json:"submit_public_key"` + // 提交算法公钥(明文PEM存储) + SubmitPublicKey string `gorm:"type:text;comment:提交算法公钥,明文PEM" json:"submit_public_key"` - // 提交算法私钥(base64编码存储) - SubmitPrivateKey string `gorm:"type:text;comment:提交算法私钥,base64编码" json:"submit_private_key"` + // 提交算法私钥(明文PEM存储) + SubmitPrivateKey string `gorm:"type:text;comment:提交算法私钥,明文PEM" json:"submit_private_key"` - // 返回算法公钥(base64编码存储) - ReturnPublicKey string `gorm:"type:text;comment:返回算法公钥,base64编码" json:"return_public_key"` + // 返回算法公钥(明文PEM存储) + ReturnPublicKey string `gorm:"type:text;comment:返回算法公钥,明文PEM" json:"return_public_key"` - // 返回算法私钥(base64编码存储) - ReturnPrivateKey string `gorm:"type:text;comment:返回算法私钥,base64编码" json:"return_private_key"` + // 返回算法私钥(明文PEM存储) + ReturnPrivateKey string `gorm:"type:text;comment:返回算法私钥,明文PEM" json:"return_private_key"` // 时间字段 CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"` @@ -57,8 +58,10 @@ type API struct { // BeforeCreate 在创建记录前自动生成API密钥 func (api *API) BeforeCreate(tx *gorm.DB) error { if api.APIKey == "" { - // 生成唯一的API密钥 - api.APIKey = "api_" + strings.ToUpper(uuid.New().String()) + // 生成16位大写十六进制API密钥 + bytes := make([]byte, 8) // 8字节 = 16位十六进制字符 + rand.Read(bytes) + api.APIKey = strings.ToUpper(hex.EncodeToString(bytes)) } return nil } @@ -68,12 +71,50 @@ func (API) TableName() string { return "apis" } +// API类型常量定义 +const ( + // 基础信息获取类API + APITypeGetBulletin = 1 // 获取程序公告 + APITypeGetUpdateUrl = 2 // 获取更新地址 + APITypeCheckAppVersion = 3 // 检测最新版本 + APITypeGetCardInfo = 4 // 获取卡密信息 + + // 登录相关API + APITypeSingleLogin = 10 // 卡密登录 + + // 用户账号管理API + APITypeUserLogin = 20 // 用户登录 + APITypeUserRegin = 21 // 用户注册 + APITypeUserRecharge = 22 // 用户充值 + APITypeCardRegin = 23 // 卡密注册 + + // 登出API + APITypeLogOut = 30 // 退出登录 + + // 用户状态查询API + APITypeGetExpired = 40 // 获取到期时间 + APITypeCheckUserStatus = 41 // 检测账号状态 + APITypeGetAppData = 42 // 获取程序数据 + APITypeGetVariable = 43 // 获取变量数据 + + // 用户操作API + APITypeUpdatePwd = 50 // 修改账号密码 + APITypeMacChangeBind = 51 // 机器码转绑 + APITypeIPChangeBind = 52 // IP转绑 + + // 管理员操作API + APITypeDisableUser = 60 // 封停用户 + APITypeBlackUser = 61 // 添加黑名单 + APITypeUserDeductedTime = 62 // 扣除时间 +) + // 算法类型常量 const ( AlgorithmNone = 0 // 不加密 AlgorithmRC4 = 1 // RC4 AlgorithmRSA = 2 // RSA AlgorithmRSADynamic = 3 // RSA(动态) + AlgorithmEasy = 4 // 易加密 ) // GetAlgorithmName 获取算法名称 @@ -87,6 +128,8 @@ func GetAlgorithmName(algorithm int) string { return "RSA" case AlgorithmRSADynamic: return "RSA(动态)" + case AlgorithmEasy: + return "易加密" default: return "未知算法" } @@ -94,5 +137,100 @@ func GetAlgorithmName(algorithm int) string { // IsValidAlgorithm 验证算法类型是否有效 func IsValidAlgorithm(algorithm int) bool { - return algorithm >= AlgorithmNone && algorithm <= AlgorithmRSADynamic + return algorithm >= AlgorithmNone && algorithm <= AlgorithmEasy +} + +// GetAPITypeName 获取API类型名称 +func GetAPITypeName(apiType int) string { + switch apiType { + // 基础信息获取类API + case APITypeGetBulletin: + return "获取程序公告" + case APITypeGetUpdateUrl: + return "获取更新地址" + case APITypeCheckAppVersion: + return "检测最新版本" + case APITypeGetCardInfo: + return "获取卡密信息" + + // 登录相关API + case APITypeSingleLogin: + return "卡密登录" + + // 用户账号管理API + case APITypeUserLogin: + return "用户登录" + case APITypeUserRegin: + return "用户注册" + case APITypeUserRecharge: + return "用户充值" + case APITypeCardRegin: + return "卡密注册" + + // 登出API + case APITypeLogOut: + return "退出登录" + + // 用户状态查询API + case APITypeGetExpired: + return "获取到期时间" + case APITypeCheckUserStatus: + return "检测账号状态" + case APITypeGetAppData: + return "获取程序数据" + case APITypeGetVariable: + return "获取变量数据" + + // 用户操作API + case APITypeUpdatePwd: + return "修改账号密码" + case APITypeMacChangeBind: + return "机器码转绑" + case APITypeIPChangeBind: + return "IP转绑" + + // 管理员操作API + case APITypeDisableUser: + return "封停用户" + case APITypeBlackUser: + return "添加黑名单" + case APITypeUserDeductedTime: + return "扣除时间" + + default: + return "未知API类型" + } +} + +// IsValidAPIType 验证API类型是否有效 +func IsValidAPIType(apiType int) bool { + validTypes := []int{ + APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo, + APITypeSingleLogin, + APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin, + APITypeLogOut, + APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable, + APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind, + APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime, + } + + for _, validType := range validTypes { + if apiType == validType { + return true + } + } + return false +} + +// GetAPITypesByCategory 根据分类获取API类型列表 +func GetAPITypesByCategory() map[string][]int { + return map[string][]int{ + "基础信息获取": {APITypeGetBulletin, APITypeGetUpdateUrl, APITypeCheckAppVersion, APITypeGetCardInfo}, + "登录相关": {APITypeSingleLogin}, + "用户账号管理": {APITypeUserLogin, APITypeUserRegin, APITypeUserRecharge, APITypeCardRegin}, + "登出": {APITypeLogOut}, + "用户状态查询": {APITypeGetExpired, APITypeCheckUserStatus, APITypeGetAppData, APITypeGetVariable}, + "用户操作": {APITypeUpdatePwd, APITypeMacChangeBind, APITypeIPChangeBind}, + "管理员操作": {APITypeDisableUser, APITypeBlackUser, APITypeUserDeductedTime}, + } } diff --git a/models/app.go b/models/app.go index be99d71..a2537fe 100644 --- a/models/app.go +++ b/models/app.go @@ -39,6 +39,9 @@ type App struct { // DownloadURL:下载地址 DownloadURL string `gorm:"size:500;comment:下载地址" json:"download_url"` + // AppData:应用数据(base64编码存储) + AppData string `gorm:"type:text;comment:应用数据,base64编码存储" json:"app_data"` + // Announcement:程序公告内容(base64编码存储) Announcement string `gorm:"type:text;comment:程序公告内容,base64编码存储" json:"announcement"` diff --git a/server/admin.go b/server/admin.go index f050ce8..87538da 100644 --- a/server/admin.go +++ b/server/admin.go @@ -43,7 +43,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/apis", adminctl.AdminAuthRequired(adminctl.APIFragmentHandler)) // 个人资料API mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler)) @@ -61,6 +61,8 @@ func RegisterAdminRoutes(mux *http.ServeMux) { mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(adminctl.AppsBatchDeleteHandler)) mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(adminctl.AppsBatchUpdateStatusHandler)) mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(adminctl.AppResetSecretHandler)) + mux.HandleFunc("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(adminctl.AppGetAppDataHandler)) + mux.HandleFunc("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(adminctl.AppUpdateAppDataHandler)) 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)) @@ -70,6 +72,12 @@ func RegisterAdminRoutes(mux *http.ServeMux) { mux.HandleFunc("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(adminctl.AppGetRegisterConfigHandler)) mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(adminctl.AppUpdateRegisterConfigHandler)) + // API接口管理API + mux.HandleFunc("/admin/api/apis/list", adminctl.AdminAuthRequired(adminctl.APIListHandler)) + mux.HandleFunc("/admin/api/apis/update", adminctl.AdminAuthRequired(adminctl.APIUpdateHandler)) + mux.HandleFunc("/admin/api/apis/apps", adminctl.AdminAuthRequired(adminctl.APIGetAppsHandler)) + mux.HandleFunc("/admin/api/apis/reset_key", adminctl.AdminAuthRequired(adminctl.APIResetKeyHandler)) + mux.HandleFunc("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(adminctl.APIGenerateKeysHandler)) // 系统信息API(用于仪表盘定时刷新) mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler)) diff --git a/utils/encrypt/easy.go b/utils/encrypt/easy.go new file mode 100644 index 0000000..c8b3675 --- /dev/null +++ b/utils/encrypt/easy.go @@ -0,0 +1,241 @@ +package encrypt + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "strconv" + "strings" +) + +// EasyEncrypt 易加密算法结构体 +type EasyEncrypt struct { + encryptKey []int // 加密密钥 + decryptKey []int // 解密密钥 +} + +// NewEasyEncrypt 创建新的易加密实例 +func NewEasyEncrypt(encryptKey, decryptKey []int) *EasyEncrypt { + return &EasyEncrypt{ + encryptKey: encryptKey, + decryptKey: decryptKey, + } +} + +// GenerateEasyKey 生成易加密密钥对 +func GenerateEasyKey() ([]int, []int, error) { + // 使用crypto/rand生成随机长度(15-30位) + var lengthByte [1]byte + + // 生成加密密钥长度 + if _, err := rand.Read(lengthByte[:]); err != nil { + return nil, nil, err + } + encryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 + + encryptKey := make([]int, encryptKeyLen) + encryptBytes := make([]byte, encryptKeyLen) + if _, err := rand.Read(encryptBytes); err != nil { + return nil, nil, err + } + for i, b := range encryptBytes { + encryptKey[i] = int(b) // 0-255范围 + } + + // 生成解密密钥长度 + if _, err := rand.Read(lengthByte[:]); err != nil { + return nil, nil, err + } + decryptKeyLen := 15 + int(lengthByte[0])%16 // 15-30位随机长度 + + decryptKey := make([]int, decryptKeyLen) + decryptBytes := make([]byte, decryptKeyLen) + if _, err := rand.Read(decryptBytes); err != nil { + return nil, nil, err + } + for i, b := range decryptBytes { + decryptKey[i] = int(b) // 0-255范围 + } + + return encryptKey, decryptKey, nil +} + +// Encrypt 加密函数 - 对应 UserLogin_encrypt_Up_42510 +func (e *EasyEncrypt) Encrypt(input string) string { + if input == "" { + return "" + } + + mKeyLen := len(e.encryptKey) + inputLen := len(input) + var result strings.Builder + + for i := 0; i < inputLen; i++ { + mCode := int(input[i]) + mCode = (mCode - 207) ^ e.encryptKey[i%mKeyLen] + + if mCode < 0 { + mCode = -mCode + result.WriteString("-") + } + + // 转换为16进制字符串 + hexStr := strconv.FormatInt(int64(mCode), 16) + result.WriteString(hexStr) + result.WriteString(",") + } + + // Base64编码 + resultStr := result.String() + return base64.StdEncoding.EncodeToString([]byte(resultStr)) +} + +// Decrypt 解密函数 - 对应 UserLogin_decrypt_Down_42510 +func (e *EasyEncrypt) Decrypt(input string) string { + if input == "" { + return "" + } + + // Base64解码 + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "" + } + + decodedStr := string(decoded) + mKeyLen := len(e.encryptKey) + + // 按逗号分割 + parts := strings.Split(decodedStr, ",") + var result strings.Builder + + for i, part := range parts { + if part == "" { + continue + } + + var d int + if strings.HasPrefix(part, "-") { + // 处理负数 + hexStr := part[1:] + val, err := strconv.ParseInt(hexStr, 16, 32) + if err != nil { + continue + } + d = -int(val) + } else { + // 处理正数 + val, err := strconv.ParseInt(part, 16, 32) + if err != nil { + continue + } + d = int(val) + } + + // 解密计算 + decryptedChar := (d ^ e.encryptKey[i%mKeyLen]) + 207 + result.WriteByte(byte(decryptedChar)) + } + + return result.String() +} + +// EncryptWithKey 使用指定密钥加密 +func EncryptWithKey(input string, key []int) string { + if input == "" || len(key) == 0 { + return "" + } + + keyLen := len(key) + inputLen := len(input) + var result strings.Builder + + for i := 0; i < inputLen; i++ { + mCode := int(input[i]) + mCode = (mCode - 207) ^ key[i%keyLen] + + if mCode < 0 { + mCode = -mCode + result.WriteString("-") + } + + hexStr := strconv.FormatInt(int64(mCode), 16) + result.WriteString(hexStr) + result.WriteString(",") + } + + resultStr := result.String() + return base64.StdEncoding.EncodeToString([]byte(resultStr)) +} + +// DecryptWithKey 使用指定密钥解密 +func DecryptWithKey(input string, key []int) string { + if input == "" || len(key) == 0 { + return "" + } + + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "" + } + + decodedStr := string(decoded) + keyLen := len(key) + + parts := strings.Split(decodedStr, ",") + var result strings.Builder + + for i, part := range parts { + if part == "" { + continue + } + + var d int + if strings.HasPrefix(part, "-") { + hexStr := part[1:] + val, err := strconv.ParseInt(hexStr, 16, 32) + if err != nil { + continue + } + d = -int(val) + } else { + val, err := strconv.ParseInt(part, 16, 32) + if err != nil { + continue + } + d = int(val) + } + + decryptedChar := (d ^ key[i%keyLen]) + 40 + result.WriteByte(byte(decryptedChar)) + } + + return result.String() +} + +// FormatKeyAsString 将密钥数组格式化为字符串(用于存储) +func FormatKeyAsString(key []int) string { + var parts []string + for _, k := range key { + parts = append(parts, fmt.Sprintf("%d", k)) + } + return strings.Join(parts, ",") +} + +// ParseKeyFromString 从字符串解析密钥数组 +func ParseKeyFromString(keyStr string) []int { + if keyStr == "" { + return nil + } + + parts := strings.Split(keyStr, ",") + var key []int + + for _, part := range parts { + if val, err := strconv.Atoi(strings.TrimSpace(part)); err == nil { + key = append(key, val) + } + } + + return key +} diff --git a/web/static/js/admin.js b/web/static/js/admin.js index 151f252..cc94518 100755 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -291,7 +291,13 @@ loadScript(layuijs, function () { 'user-username': '用户名:用于登录的用户名,可以修改但需要保证唯一性', 'user-old-password': '旧密码:修改密码时需要输入当前密码进行验证,不修改密码时可留空', 'user-new-password': '新密码:要设置的新密码,长度至少6位,不修改密码时可留空', - 'user-confirm-password': '确认密码:再次输入新密码进行确认,必须与新密码一致' + 'user-confirm-password': '确认密码:再次输入新密码进行确认,必须与新密码一致', + // API接口管理相关 (apis.html) + 'submit-algorithm': '提交算法:客户端向服务器提交数据时使用的加密算法
• 不加密:数据明文传输,适用于内网环境
• RC4:对称加密,速度快,适用于一般场景
• RSA:非对称加密,安全性高,适用于敏感数据
• RSA(动态):动态生成密钥的RSA加密,安全性最高
• 易加密:自定义对称加密算法,使用15-30位整数密钥数组', + 'submit-keys': '提交密钥:用于加密客户端提交数据的密钥
• RC4:16位十六进制密钥,用于对称加密
• RSA:公钥用于客户端加密,私钥用于服务器解密
• 易加密:15-30位整数数组,逗号分隔
• 密钥由系统自动生成,确保安全性', + 'return-algorithm': '返回算法:服务器向客户端返回数据时使用的加密算法
• 不加密:数据明文传输,适用于内网环境
• RC4:对称加密,速度快,适用于一般场景
• RSA:非对称加密,安全性高,适用于敏感数据
• RSA(动态):动态生成密钥的RSA加密,安全性最高
• 易加密:自定义对称加密算法,使用15-30位整数密钥数组', + 'return-keys': '返回密钥:用于加密服务器返回数据的密钥
• RC4:16位十六进制密钥,用于对称加密
• RSA:公钥用于服务器加密,私钥用于客户端解密
• 易加密:15-30位整数数组,逗号分隔
• 密钥由系统自动生成,确保安全性', + 'api-status': '接口状态:控制当前API接口是否可用
• 启用:接口正常工作,客户端可以调用
• 禁用:接口暂停服务,客户端调用将返回错误' }; return tips[type] || '暂无说明'; } diff --git a/web/template/admin/apis.html b/web/template/admin/apis.html new file mode 100644 index 0000000..7349141 --- /dev/null +++ b/web/template/admin/apis.html @@ -0,0 +1,526 @@ +{{ define "apis.html" }} +
+

接口管理

+ + +
+
筛选
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+
+
+
+ +
+
接口列表
+
+
+ + +
+
+ + + + +
+ + + +{{ end }} \ No newline at end of file diff --git a/web/template/admin/apps.html b/web/template/admin/apps.html index 0fd0f4a..fef3db4 100644 --- a/web/template/admin/apps.html +++ b/web/template/admin/apps.html @@ -93,12 +93,7 @@ -
-
- - -
-
+ @@ -420,10 +415,66 @@ title: '新增应用', content: $('#appFormModal'), area: ['500px', '460px'], - btn: false, + btn: ['创建', '取消'], + yes: function(index, layero) { + // 手动触发表单提交验证 + var formData = {}; + $('#appForm').find('input, select, textarea').each(function() { + var $this = $(this); + var name = $this.attr('name'); + if (name) { + if ($this.attr('type') === 'checkbox') { + if ($this.attr('lay-skin') === 'switch') { + formData[name] = $this.prop('checked') ? 1 : 0; + } else { + formData[name] = $this.prop('checked') ? $this.val() : ''; + } + } else if ($this.attr('type') === 'radio') { + if ($this.prop('checked')) { + formData[name] = $this.val(); + } + } else { + formData[name] = $this.val(); + } + } + }); + + // 验证必填字段 + if (!formData.name || formData.name.trim() === '') { + layer.msg('请输入应用名称', {icon: 2}); + return; + } + + // 处理数据类型转换 + formData.download_type = parseInt(formData.download_type) || 0; + + $.ajax({ + url: '/admin/api/apps/create', + type: 'POST', + data: JSON.stringify(formData), + contentType: 'application/json', + success: function(res) { + if (res.code === 0) { + layer.msg(res.msg, {icon: 1}); + layer.close(index); + appsTable.reload(); + } else { + layer.msg(res.msg || '操作失败', {icon: 2}); + } + }, + error: function(xhr) { + layer.msg(xhr.responseText || '操作失败', {icon: 2}); + } + }); + }, + btn2: function(index) { + layer.close(index); + }, + success: function() { + form.render(); + }, shadeClose: false }); - form.render(); }); // 监听更新方式切换(保留事件监听器以备将来扩展) @@ -431,49 +482,7 @@ // 下载地址字段现在始终显示,无需切换显示状态 }); - // 表单提交 - form.on('submit(appFormSubmit)', function(data) { - const isEdit = data.field.id !== ''; - const url = isEdit ? '/admin/api/apps/update' : '/admin/api/apps/create'; - - // 转换字段类型为正确的数据类型 - const formData = { - ...data.field, - status: data.field.status === 'on' ? 1 : 0, // switch开关处理 - download_type: parseInt(data.field.download_type) || 0, - force_update: data.field.force_update === 'on' ? 1 : 0 // switch开关处理 - }; - - // 如果是编辑模式,确保id也是整数 - if (isEdit) { - formData.id = parseInt(data.field.id); - } - - $.ajax({ - url: url, - type: 'POST', - data: JSON.stringify(formData), - contentType: 'application/json', - success: function(res) { - if (res.code === 0) { - layer.msg(res.msg, {icon: 1}); - layer.closeAll(); - appsTable.reload(); - } else { - layer.msg(res.msg || '操作失败', {icon: 2}); - } - }, - error: function(xhr) { - layer.msg(xhr.responseText || '操作失败', {icon: 2}); - } - }); - return false; - }); - // 取消按钮 - $('#btnCancelApp').on('click', function() { - layer.closeAll(); - }); // 表格工具栏事件 table.on('tool(appsTableFilter)', function(obj) { @@ -498,10 +507,67 @@ title: '编辑应用', content: $('#appFormModal'), area: ['500px', '460px'], - btn: false, + btn: ['保存', '取消'], + yes: function(index, layero) { + // 手动触发表单提交验证 + var formData = {}; + $('#appForm').find('input, select, textarea').each(function() { + var $this = $(this); + var name = $this.attr('name'); + if (name) { + if ($this.attr('type') === 'checkbox') { + if ($this.attr('lay-skin') === 'switch') { + formData[name] = $this.prop('checked') ? 1 : 0; + } else { + formData[name] = $this.prop('checked') ? $this.val() : ''; + } + } else if ($this.attr('type') === 'radio') { + if ($this.prop('checked')) { + formData[name] = $this.val(); + } + } else { + formData[name] = $this.val(); + } + } + }); + + // 验证必填字段 + if (!formData.name || formData.name.trim() === '') { + layer.msg('请输入应用名称', {icon: 2}); + return; + } + + // 处理数据类型转换 + formData.download_type = parseInt(formData.download_type) || 0; + formData.id = parseInt(formData.id); + + $.ajax({ + url: '/admin/api/apps/update', + type: 'POST', + data: JSON.stringify(formData), + contentType: 'application/json', + success: function(res) { + if (res.code === 0) { + layer.msg(res.msg, {icon: 1}); + layer.close(index); + appsTable.reload(); + } else { + layer.msg(res.msg || '操作失败', {icon: 2}); + } + }, + error: function(xhr) { + layer.msg(xhr.responseText || '操作失败', {icon: 2}); + } + }); + }, + btn2: function(index) { + layer.close(index); + }, + success: function() { + form.render(); + }, shadeClose: false }); - form.render(); } else if (obj.event === 'del') { // 删除 @@ -531,6 +597,10 @@ elem: this, // 使用 this 而不是查找元素 show: true, // 外部事件触发即显示 data: [ + { + title: '应用数据', + id: 'app_data' + }, { title: '程序公告', id: 'announcement' @@ -553,7 +623,67 @@ } ], click: function(menudata, othis) { - if (menudata.id === 'announcement') { + if (menudata.id === 'app_data') { + // 应用数据 + // 先获取当前应用数据内容 + $.ajax({ + url: '/admin/api/apps/get_app_data?uuid=' + obj.data.uuid, + type: 'GET', + success: function(res) { + var currentAppData = ''; + if (res.code === 0 && res.data && res.data.app_data) { + currentAppData = res.data.app_data; + } + + // 显示编辑弹窗 + layer.open({ + type: 1, + title: '编辑应用数据 - ' + obj.data.name, + area: ['600px', '400px'], + content: '
' + + '' + + '
', + btn: ['保存', '取消'], + yes: function(index, layero) { + var appDataContent = $('#appDataEditor').val(); + + // 发送更新请求 + $.ajax({ + url: '/admin/api/apps/update_app_data', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + uuid: obj.data.uuid, + app_data: appDataContent + }), + 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 === 'announcement') { // 程序公告 // 先获取当前公告内容 $.ajax({ diff --git a/web/template/admin/layout.html b/web/template/admin/layout.html index de743da..eec5a68 100644 --- a/web/template/admin/layout.html +++ b/web/template/admin/layout.html @@ -55,6 +55,7 @@ 应用管理
应用列表
+
接口列表