From 07b47fbf3a1103bb4216accaf8287c42a76d6af0 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 10 Jun 2025 12:12:55 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=94=A7=20fix(api):=20enhance=20URL=20?= =?UTF-8?q?validation=20to=20support=20IP=20addresses=20and=20ports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update URL regex pattern to accept both domain names and IP addresses - Add support for IPv4 addresses with optional port numbers - Improve validation to handle formats like http://192.168.1.1:8080 - Add comprehensive comments explaining supported URL formats - Maintain backward compatibility with existing domain-based URLs Fixes issue where IP-based URLs were incorrectly rejected as invalid format. --- setting/api_info.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setting/api_info.go b/setting/api_info.go index 0d7ffcfd..552dabf5 100644 --- a/setting/api_info.go +++ b/setting/api_info.go @@ -33,8 +33,10 @@ func ValidateApiInfo(apiInfoStr string) error { "violet": true, "grey": true, } - // URL正则表达式 - urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`) + // URL正则表达式,支持域名和IP地址格式 + // 域名格式:https://example.com 或 https://sub.example.com:8080 + // IP地址格式:https://192.168.1.1 或 https://192.168.1.1:8080 + urlRegex := regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?::[0-9]{1,5})?(?:/.*)?$`) for i, apiInfo := range apiInfoList { // 检查必填字段 From d9461a477dfd557b30254c06f75431bb882efd6d Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 10 Jun 2025 12:20:26 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=A7=20refactor(console):=20enhance?= =?UTF-8?q?=20URL=20validation=20and=20restructure=20settings=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor api_info.go to console.go for broader console settings support - Update URL regex pattern to accept both domain names and IP addresses - Add support for IPv4 addresses with optional port numbers - Improve validation to handle formats like http://192.168.1.1:8080 - Add ValidateConsoleSettings function for extensible settings validation - Maintain backward compatibility with existing ValidateApiInfo function - Add comprehensive comments explaining supported URL formats Fixes issue where IP-based URLs were incorrectly rejected as invalid format. Prepares infrastructure for additional console settings validation. --- setting/{api_info.go => console.go} | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) rename setting/{api_info.go => console.go} (88%) diff --git a/setting/api_info.go b/setting/console.go similarity index 88% rename from setting/api_info.go rename to setting/console.go index 552dabf5..e7847213 100644 --- a/setting/api_info.go +++ b/setting/console.go @@ -9,12 +9,22 @@ import ( "strings" ) -// ValidateApiInfo 验证API信息格式 -func ValidateApiInfo(apiInfoStr string) error { - if apiInfoStr == "" { +// ValidateConsoleSettings 验证控制台设置信息格式 +func ValidateConsoleSettings(settingsStr string, settingType string) error { + if settingsStr == "" { return nil // 空字符串是合法的 } + switch settingType { + case "ApiInfo": + return validateApiInfo(settingsStr) + default: + return fmt.Errorf("未知的设置类型:%s", settingType) + } +} + +// validateApiInfo 验证API信息格式 +func validateApiInfo(apiInfoStr string) error { var apiInfoList []map[string]interface{} if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil { return fmt.Errorf("API信息格式错误:%s", err.Error()) @@ -103,6 +113,11 @@ func ValidateApiInfo(apiInfoStr string) error { return nil } +// ValidateApiInfo 保持向后兼容的函数 +func ValidateApiInfo(apiInfoStr string) error { + return validateApiInfo(apiInfoStr) +} + // GetApiInfo 获取API信息列表 func GetApiInfo() []map[string]interface{} { // 从OptionMap中获取API信息,如果不存在则返回空数组 From 56188c33b536ada705e7184d9ccb12efdc60cf34 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 10 Jun 2025 12:43:14 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8E=A8=20refactor(ui):=20replace=20Ic?= =?UTF-8?q?onSearch=20with=20semantic=20lucide=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace IconSearch with Server icon for API info card title to better represent server/API related content - Add Server imports from lucide-react This change improves the semantic meaning of icons and provides better visual representation of their respective functionalities. --- web/src/pages/Detail/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index 6c1b87b3..44919a87 100644 --- a/web/src/pages/Detail/index.js +++ b/web/src/pages/Detail/index.js @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { useNavigate } from 'react-router-dom'; -import { Wallet, Activity, Zap, Gauge, PieChart } from 'lucide-react'; +import { Wallet, Activity, Zap, Gauge, PieChart, Server } from 'lucide-react'; import { Card, @@ -970,7 +970,7 @@ const Detail = (props) => { className="bg-gray-50 border-0 !rounded-2xl" title={
- + {t('API信息')}
} @@ -1007,12 +1007,12 @@ const Detail = (props) => { {api.route}
handleCopyUrl(api.url)} > {api.url}
-
+
{api.description}
From 26b70d6a25468feb42f7edf38d565e3769c08e59 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 10 Jun 2025 20:10:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20console=20announc?= =?UTF-8?q?ements=20and=20FAQ=20management=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SettingsAnnouncements component with full CRUD operations for system announcements * Support multiple announcement types (default, ongoing, success, warning, error) * Include publish date, content, type classification and additional notes * Implement batch operations and pagination for better data management * Add real-time preview with relative time display and date formatting - Add SettingsFAQ component for comprehensive FAQ management * Support question-answer pairs with rich text content * Include full editing, deletion and creation capabilities * Implement batch delete operations and paginated display * Add validation for complete Q&A information - Integrate announcement and FAQ modules into DashboardSetting * Add unified configuration interface in admin console * Implement auto-refresh functionality for real-time updates * Add loading states and error handling for better UX - Enhance backend API support in controller and setting modules * Add validation functions for console settings * Include time and sorting utilities for announcement management * Extend API endpoints for announcement and FAQ data persistence - Improve frontend infrastructure * Add new translation keys for internationalization support * Update utility functions for date/time formatting * Enhance CSS styles for better component presentation * Add icons and visual improvements for announcements and FAQ sections This implementation provides administrators with comprehensive tools to manage system-wide announcements and user FAQ content through an intuitive console interface. --- controller/misc.go | 2 + controller/option.go | 18 + setting/console.go | 186 +++++++ .../components/settings/DashboardSetting.js | 14 + web/src/helpers/utils.js | 63 +++ web/src/i18n/locales/en.json | 39 +- web/src/index.css | 12 +- web/src/pages/Detail/index.js | 216 +++++++- .../Setting/Dashboard/SettingsAPIInfo.js | 75 ++- .../Dashboard/SettingsAnnouncements.js | 485 ++++++++++++++++++ .../pages/Setting/Dashboard/SettingsFAQ.js | 413 +++++++++++++++ 11 files changed, 1487 insertions(+), 36 deletions(-) create mode 100644 web/src/pages/Setting/Dashboard/SettingsAnnouncements.js create mode 100644 web/src/pages/Setting/Dashboard/SettingsFAQ.js diff --git a/controller/misc.go b/controller/misc.go index be76cab5..69398b11 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -75,6 +75,8 @@ func GetStatus(c *gin.Context) { "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, "setup": constant.Setup, "api_info": setting.GetApiInfo(), + "announcements": setting.GetAnnouncements(), + "faq": setting.GetFAQ(), }, }) return diff --git a/controller/option.go b/controller/option.go index f33b877a..b52012fd 100644 --- a/controller/option.go +++ b/controller/option.go @@ -128,6 +128,24 @@ func UpdateOption(c *gin.Context) { }) return } + case "Announcements": + err = setting.ValidateConsoleSettings(option.Value, "Announcements") + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + case "FAQ": + err = setting.ValidateConsoleSettings(option.Value, "FAQ") + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } } err = model.UpdateOption(option.Key, option.Value) if err != nil { diff --git a/setting/console.go b/setting/console.go index e7847213..94023666 100644 --- a/setting/console.go +++ b/setting/console.go @@ -6,7 +6,9 @@ import ( "net/url" "one-api/common" "regexp" + "sort" "strings" + "time" ) // ValidateConsoleSettings 验证控制台设置信息格式 @@ -18,6 +20,10 @@ func ValidateConsoleSettings(settingsStr string, settingType string) error { switch settingType { case "ApiInfo": return validateApiInfo(settingsStr) + case "Announcements": + return validateAnnouncements(settingsStr) + case "FAQ": + return validateFAQ(settingsStr) default: return fmt.Errorf("未知的设置类型:%s", settingType) } @@ -138,4 +144,184 @@ func GetApiInfo() []map[string]interface{} { } return apiInfo +} + +// validateAnnouncements 验证系统公告格式 +func validateAnnouncements(announcementsStr string) error { + var announcementsList []map[string]interface{} + if err := json.Unmarshal([]byte(announcementsStr), &announcementsList); err != nil { + return fmt.Errorf("系统公告格式错误:%s", err.Error()) + } + + // 验证数组长度 + if len(announcementsList) > 100 { + return fmt.Errorf("系统公告数量不能超过100个") + } + + // 允许的类型值 + validTypes := map[string]bool{ + "default": true, "ongoing": true, "success": true, "warning": true, "error": true, + } + + for i, announcement := range announcementsList { + // 检查必填字段 + content, ok := announcement["content"].(string) + if !ok || content == "" { + return fmt.Errorf("第%d个公告缺少内容字段", i+1) + } + + // 检查发布日期字段 + publishDate, exists := announcement["publishDate"] + if !exists { + return fmt.Errorf("第%d个公告缺少发布日期字段", i+1) + } + + publishDateStr, ok := publishDate.(string) + if !ok || publishDateStr == "" { + return fmt.Errorf("第%d个公告的发布日期不能为空", i+1) + } + + // 验证ISO日期格式 + if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil { + return fmt.Errorf("第%d个公告的发布日期格式错误", i+1) + } + + // 验证可选字段 + if announcementType, exists := announcement["type"]; exists { + if typeStr, ok := announcementType.(string); ok { + if !validTypes[typeStr] { + return fmt.Errorf("第%d个公告的类型值不合法", i+1) + } + } + } + + // 验证字段长度 + if len(content) > 500 { + return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1) + } + + if extra, exists := announcement["extra"]; exists { + if extraStr, ok := extra.(string); ok && len(extraStr) > 200 { + return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1) + } + } + + // 检查并过滤危险字符(防止XSS) + dangerousChars := []string{" 100 { + return fmt.Errorf("常见问答数量不能超过100个") + } + + for i, faq := range faqList { + // 检查必填字段 + title, ok := faq["title"].(string) + if !ok || title == "" { + return fmt.Errorf("第%d个问答缺少标题字段", i+1) + } + + content, ok := faq["content"].(string) + if !ok || content == "" { + return fmt.Errorf("第%d个问答缺少内容字段", i+1) + } + + // 验证字段长度 + if len(title) > 200 { + return fmt.Errorf("第%d个问答的标题长度不能超过200字符", i+1) + } + + if len(content) > 1000 { + return fmt.Errorf("第%d个问答的内容长度不能超过1000字符", i+1) + } + + // 检查并过滤危险字符(防止XSS) + dangerousChars := []string{" 20 { + announcements = announcements[:20] + } + + return announcements +} + +// GetFAQ 获取常见问答列表 +func GetFAQ() []map[string]interface{} { + common.OptionMapRWMutex.RLock() + faqStr, exists := common.OptionMap["FAQ"] + common.OptionMapRWMutex.RUnlock() + + if !exists || faqStr == "" { + return []map[string]interface{}{} + } + + var faq []map[string]interface{} + if err := json.Unmarshal([]byte(faqStr), &faq); err != nil { + return []map[string]interface{}{} + } + + return faq } \ No newline at end of file diff --git a/web/src/components/settings/DashboardSetting.js b/web/src/components/settings/DashboardSetting.js index b00a6476..25d2ff8c 100644 --- a/web/src/components/settings/DashboardSetting.js +++ b/web/src/components/settings/DashboardSetting.js @@ -2,10 +2,14 @@ import React, { useEffect, useState } from 'react'; import { Card, Spin } from '@douyinfe/semi-ui'; import { API, showError } from '../../helpers'; import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js'; +import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js'; +import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js'; const DashboardSetting = () => { let [inputs, setInputs] = useState({ ApiInfo: '', + Announcements: '', + FAQ: '', }); let [loading, setLoading] = useState(false); @@ -49,6 +53,16 @@ const DashboardSetting = () => { + + {/* 系统公告管理 */} + + + + + {/* 常见问答管理 */} + + + ); diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index cd05653e..56e1104d 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -446,3 +446,66 @@ export const getLastAssistantMessage = (messages) => { } return null; }; + +// 计算相对时间(几天前、几小时前等) +export const getRelativeTime = (publishDate) => { + if (!publishDate) return ''; + + const now = new Date(); + const pubDate = new Date(publishDate); + + // 如果日期无效,返回原始字符串 + if (isNaN(pubDate.getTime())) return publishDate; + + const diffMs = now.getTime() - pubDate.getTime(); + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = Math.floor(diffDays / 30); + const diffYears = Math.floor(diffDays / 365); + + // 如果是未来时间,显示具体日期 + if (diffMs < 0) { + return formatDateString(pubDate); + } + + // 根据时间差返回相应的描述 + if (diffSeconds < 60) { + return '刚刚'; + } else if (diffMinutes < 60) { + return `${diffMinutes} 分钟前`; + } else if (diffHours < 24) { + return `${diffHours} 小时前`; + } else if (diffDays < 7) { + return `${diffDays} 天前`; + } else if (diffWeeks < 4) { + return `${diffWeeks} 周前`; + } else if (diffMonths < 12) { + return `${diffMonths} 个月前`; + } else if (diffYears < 2) { + return '1 年前'; + } else { + // 超过2年显示具体日期 + return formatDateString(pubDate); + } +}; + +// 格式化日期字符串 +export const formatDateString = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + +// 格式化日期时间字符串(包含时间) +export const formatDateTimeString = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; +}; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 66a52035..ee90e65c 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -510,7 +510,7 @@ "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:", "模型映射": "Model mapping", "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", - "默认": "default", + "默认": "Default", "图片演示": "Image demo", "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment", @@ -882,7 +882,7 @@ "线路监控": "line monitoring", "查看全部": "View all", "高延迟": "high latency", - "异常": "abnormal", + "异常": "Abnormal", "的未命名令牌": "unnamed token", "令牌更新成功!": "Token updated successfully!", "(origin) Discord原链接": "(origin) Discord original link", @@ -1584,14 +1584,13 @@ "模型数据分析": "Model Data Analysis", "搜索无结果": "No results found", "仪表盘配置": "Dashboard Configuration", - "API信息管理,可以配置多个API地址用于状态展示和负载均衡": "API information management, you can configure multiple API addresses for status display and load balancing", + "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)", "线路描述": "Route description", "颜色": "Color", "标识颜色": "Identifier color", "添加API": "Add API", "保存配置": "Save Configuration", "API信息": "API Information", - "暂无API信息配置": "No API information configured", "暂无API信息": "No API information", "请输入API地址": "Please enter the API address", "请输入线路描述": "Please enter the route description", @@ -1599,6 +1598,36 @@ "请输入说明": "Please enter the description", "如:香港线路": "e.g. Hong Kong line", "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.", + "请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.", + "请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.", "确定要删除此API信息吗?": "Are you sure you want to delete this API information?", - "测速": "Speed Test" + "测速": "Speed Test", + "批量删除": "Batch Delete", + "常见问答": "FAQ", + "进行中": "Ongoing", + "警告": "Warning", + "添加公告": "Add Notice", + "编辑公告": "Edit Notice", + "公告内容": "Notice Content", + "请输入公告内容": "Please enter the notice content", + "发布日期": "Publish Date", + "请选择发布日期": "Please select the publish date", + "发布时间": "Publish Time", + "公告类型": "Notice Type", + "说明信息": "Description", + "可选,公告的补充说明": "Optional, additional information for the notice", + "确定要删除此公告吗?": "Are you sure you want to delete this notice?", + "系统公告管理,可以发布系统通知和重要消息": "System notice management, you can publish system notices and important messages", + "暂无系统公告": "No system notice", + "添加问答": "Add FAQ", + "编辑问答": "Edit FAQ", + "问题标题": "Question Title", + "请输入问题标题": "Please enter the question title", + "回答内容": "Answer Content", + "请输入回答内容": "Please enter the answer content", + "确定要删除此问答吗?": "Are you sure you want to delete this FAQ?", + "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)", + "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)", + "暂无常见问答": "No FAQ", + "显示最新20条": "Display latest 20" } \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css index a1c931fc..bb6c3b48 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -301,12 +301,12 @@ code { font-size: 1.1em; } -/* API信息卡片样式 */ -.api-info-container { +/* 卡片内容容器通用样式 */ +.card-content-container { position: relative; } -.api-info-fade-indicator { +.card-content-fade-indicator { position: absolute; bottom: 0; left: 0; @@ -374,8 +374,8 @@ code { background: transparent; } -/* 隐藏模型设置区域的滚动条 */ -.api-info-scroll::-webkit-scrollbar, +/* 隐藏卡片内容区域的滚动条 */ +.card-content-scroll::-webkit-scrollbar, .model-settings-scroll::-webkit-scrollbar, .thinking-content-scroll::-webkit-scrollbar, .custom-request-textarea .semi-input::-webkit-scrollbar, @@ -383,7 +383,7 @@ code { display: none; } -.api-info-scroll, +.card-content-scroll, .model-settings-scroll, .thinking-content-scroll, .custom-request-textarea .semi-input, diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index 44919a87..592f8d37 100644 --- a/web/src/pages/Detail/index.js +++ b/web/src/pages/Detail/index.js @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { useNavigate } from 'react-router-dom'; -import { Wallet, Activity, Zap, Gauge, PieChart, Server } from 'lucide-react'; +import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react'; import { Card, @@ -13,7 +13,9 @@ import { Tabs, TabPane, Empty, - Tag + Tag, + Timeline, + Collapse } from '@douyinfe/semi-ui'; import { IconRefresh, @@ -26,7 +28,9 @@ import { IconPulse, IconStopwatchStroked, IconTypograph, - IconPieChart2Stroked + IconPieChart2Stroked, + IconPlus, + IconMinus } from '@douyinfe/semi-icons'; import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations'; import { VChart } from '@visactor/react-vchart'; @@ -43,7 +47,8 @@ import { renderQuota, modelToColor, copy, - showSuccess + showSuccess, + getRelativeTime } from '../../helpers'; import { UserContext } from '../../context/User/index.js'; import { StatusContext } from '../../context/Status/index.js'; @@ -179,7 +184,7 @@ const Detail = (props) => { const [times, setTimes] = useState(0); const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]); const [lineData, setLineData] = useState([]); - const [apiInfoData, setApiInfoData] = useState([]); + const [modelColors, setModelColors] = useState({}); const [activeChartTab, setActiveChartTab] = useState('1'); const [showApiScrollHint, setShowApiScrollHint] = useState(false); @@ -578,6 +583,37 @@ const Detail = (props) => { checkApiScrollable(); }; + const checkCardScrollable = (ref, setHintFunction) => { + if (ref.current) { + const element = ref.current; + const isScrollable = element.scrollHeight > element.clientHeight; + const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5; + setHintFunction(isScrollable && !isAtBottom); + } + }; + + const handleCardScroll = (ref, setHintFunction) => { + checkCardScrollable(ref, setHintFunction); + }; + + // ========== Additional Refs for new cards ========== + const announcementScrollRef = useRef(null); + const faqScrollRef = useRef(null); + + // ========== Additional State for scroll hints ========== + const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false); + const [showFaqScrollHint, setShowFaqScrollHint] = useState(false); + + // ========== Effects for scroll detection ========== + useEffect(() => { + const timer = setTimeout(() => { + checkApiScrollable(); + checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint); + checkCardScrollable(faqScrollRef, setShowFaqScrollHint); + }, 100); + return () => clearTimeout(timer); + }, []); + const getUserData = async () => { let res = await API.get(`/api/user/self`); const { success, message, data } = res.data; @@ -775,6 +811,32 @@ const Detail = (props) => { generateChartTimePoints, updateChartSpec, updateMapValue, t ]); + // ========== Status Data Management ========== + const announcementLegendData = useMemo(() => [ + { color: 'grey', label: t('默认'), type: 'default' }, + { color: 'blue', label: t('进行中'), type: 'ongoing' }, + { color: 'green', label: t('成功'), type: 'success' }, + { color: 'orange', label: t('警告'), type: 'warning' }, + { color: 'red', label: t('异常'), type: 'error' } + ], [t]); + + const apiInfoData = useMemo(() => { + return statusState?.status?.api_info || []; + }, [statusState?.status?.api_info]); + + const announcementData = useMemo(() => { + const announcements = statusState?.status?.announcements || []; + // 处理后台配置的公告数据,自动生成相对时间 + return announcements.map(item => ({ + ...item, + time: getRelativeTime(item.publishDate) + })); + }, [statusState?.status?.announcements]); + + const faqData = useMemo(() => { + return statusState?.status?.faq || []; + }, [statusState?.status?.faq]); + // ========== Hooks - Effects ========== useEffect(() => { getUserData(); @@ -787,19 +849,6 @@ const Detail = (props) => { } }, []); - useEffect(() => { - if (statusState?.status?.api_info) { - setApiInfoData(statusState.status.api_info); - } - }, [statusState?.status?.api_info]); - - useEffect(() => { - const timer = setTimeout(() => { - checkApiScrollable(); - }, 100); - return () => clearTimeout(timer); - }, []); - return (
@@ -975,10 +1024,10 @@ const Detail = (props) => {
} > -
+
{apiInfoData.length > 0 ? ( @@ -1023,7 +1072,7 @@ const Detail = (props) => { } darkModeImage={} - title={t('暂无API信息配置')} + title={t('暂无API信息')} description={t('请联系管理员在系统设置中配置API信息')} style={{ padding: '12px' }} /> @@ -1031,7 +1080,7 @@ const Detail = (props) => { )}
@@ -1039,6 +1088,129 @@ const Detail = (props) => { )}
+ + {/* 系统公告和常见问答卡片 */} + {!statusState?.status?.self_use_mode_enabled && ( +
+
+ {/* 公告卡片 */} + +
+ + {t('系统公告')} + + {t('显示最新20条')} + +
+ {/* 图例 */} +
+ {announcementLegendData.map((legend, index) => ( +
+
+ {legend.label} +
+ ))} +
+
+ } + > +
+
handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} + > + {announcementData.length > 0 ? ( + + ) : ( +
+ } + darkModeImage={} + title={t('暂无系统公告')} + description={t('请联系管理员在系统设置中配置公告信息')} + style={{ padding: '12px' }} + /> +
+ )} +
+
+
+ + + {/* 常见问答卡片 */} + + + {t('常见问答')} +
+ } + > +
+
handleCardScroll(faqScrollRef, setShowFaqScrollHint)} + > + {faqData.length > 0 ? ( + } + collapseIcon={} + > + {faqData.map((item, index) => ( + +

{item.content}

+
+ ))} +
+ ) : ( +
+ } + darkModeImage={} + title={t('暂无常见问答')} + description={t('请联系管理员在系统设置中配置常见问答')} + style={{ padding: '12px' }} + /> +
+ )} +
+
+
+ +
+
+ )}
); diff --git a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js b/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js index f97a0302..f4340e6e 100644 --- a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js +++ b/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js @@ -44,6 +44,9 @@ const SettingsAPIInfo = ({ options, refresh }) => { route: '', color: 'blue' }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); const colorOptions = [ { value: 'blue', label: 'blue' }, @@ -237,6 +240,7 @@ const SettingsAPIInfo = ({ options, refresh }) => { { title: t('操作'), fixed: 'right', + width: 150, render: (_, record) => ( +
); + // 计算当前页显示的数据 + const getCurrentPageData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return apiInfoList.slice(startIndex, endIndex); + }; + + const rowSelection = { + selectedRowKeys, + onChange: (selectedRowKeys, selectedRows) => { + setSelectedRowKeys(selectedRowKeys); + }, + onSelect: (record, selected, selectedRows) => { + console.log(`选择行: ${selected}`, record); + }, + onSelectAll: (selected, selectedRows) => { + console.log(`全选: ${selected}`, selectedRows); + }, + getCheckboxProps: (record) => ({ + disabled: false, + name: record.id, + }), + }; + return ( <> t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`), + pageSizeOptions: ['5', '10', '20', '50'], + onChange: (page, size) => { + setCurrentPage(page); + setPageSize(size); + }, + onShowSizeChange: (current, size) => { + setCurrentPage(1); + setPageSize(size); + } + }} size='middle' loading={loading} empty={ diff --git a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js new file mode 100644 index 00000000..7e890ed7 --- /dev/null +++ b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js @@ -0,0 +1,485 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Space, + Table, + Form, + Typography, + Empty, + Divider, + Modal, + Tag +} from '@douyinfe/semi-ui'; +import { + IllustrationNoResult, + IllustrationNoResultDark +} from '@douyinfe/semi-illustrations'; +import { + Plus, + Edit, + Trash2, + Save, + Bell +} from 'lucide-react'; +import { API, showError, showSuccess, getRelativeTime, formatDateTimeString } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; + +const { Text } = Typography; + +const SettingsAnnouncements = ({ options, refresh }) => { + const { t } = useTranslation(); + + const [announcementsList, setAnnouncementsList] = useState([]); + const [showAnnouncementModal, setShowAnnouncementModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deletingAnnouncement, setDeletingAnnouncement] = useState(null); + const [editingAnnouncement, setEditingAnnouncement] = useState(null); + const [modalLoading, setModalLoading] = useState(false); + const [loading, setLoading] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [announcementForm, setAnnouncementForm] = useState({ + content: '', + publishDate: new Date(), + type: 'default', + extra: '' + }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const typeOptions = [ + { value: 'default', label: t('默认') }, + { value: 'ongoing', label: t('进行中') }, + { value: 'success', label: t('成功') }, + { value: 'warning', label: t('警告') }, + { value: 'error', label: t('错误') } + ]; + + const getTypeColor = (type) => { + const colorMap = { + default: 'grey', + ongoing: 'blue', + success: 'green', + warning: 'orange', + error: 'red' + }; + return colorMap[type] || 'grey'; + }; + + const columns = [ + { + title: t('内容'), + dataIndex: 'content', + key: 'content', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('发布时间'), + dataIndex: 'publishDate', + key: 'publishDate', + width: 180, + render: (publishDate) => ( +
+
+ {getRelativeTime(publishDate)} +
+
+ {publishDate ? formatDateTimeString(new Date(publishDate)) : '-'} +
+
+ ) + }, + { + title: t('类型'), + dataIndex: 'type', + key: 'type', + width: 100, + render: (type) => ( + + {typeOptions.find(opt => opt.value === type)?.label || type} + + ) + }, + { + title: t('说明'), + dataIndex: 'extra', + key: 'extra', + render: (text) => ( +
+ {text || '-'} +
+ ) + }, + { + title: t('操作'), + key: 'action', + fixed: 'right', + width: 150, + render: (text, record) => ( + + + + + ) + } + ]; + + const updateOption = async (key, value) => { + const res = await API.put('/api/option/', { + key, + value, + }); + const { success, message } = res.data; + if (success) { + showSuccess('系统公告已更新'); + if (refresh) refresh(); + } else { + showError(message); + } + }; + + const submitAnnouncements = async () => { + try { + setLoading(true); + const announcementsJson = JSON.stringify(announcementsList); + await updateOption('Announcements', announcementsJson); + setHasChanges(false); + } catch (error) { + console.error('系统公告更新失败', error); + showError('系统公告更新失败'); + } finally { + setLoading(false); + } + }; + + const handleAddAnnouncement = () => { + setEditingAnnouncement(null); + setAnnouncementForm({ + content: '', + publishDate: new Date(), + type: 'default', + extra: '' + }); + setShowAnnouncementModal(true); + }; + + const handleEditAnnouncement = (announcement) => { + setEditingAnnouncement(announcement); + setAnnouncementForm({ + content: announcement.content, + publishDate: announcement.publishDate ? new Date(announcement.publishDate) : new Date(), + type: announcement.type || 'default', + extra: announcement.extra || '' + }); + setShowAnnouncementModal(true); + }; + + const handleDeleteAnnouncement = (announcement) => { + setDeletingAnnouncement(announcement); + setShowDeleteModal(true); + }; + + const confirmDeleteAnnouncement = () => { + if (deletingAnnouncement) { + const newList = announcementsList.filter(item => item.id !== deletingAnnouncement.id); + setAnnouncementsList(newList); + setHasChanges(true); + showSuccess('公告已删除,请及时点击“保存配置”进行保存'); + } + setShowDeleteModal(false); + setDeletingAnnouncement(null); + }; + + const handleSaveAnnouncement = async () => { + if (!announcementForm.content || !announcementForm.publishDate) { + showError('请填写完整的公告信息'); + return; + } + + try { + setModalLoading(true); + + // 将publishDate转换为ISO字符串保存 + const formData = { + ...announcementForm, + publishDate: announcementForm.publishDate.toISOString() + }; + + let newList; + if (editingAnnouncement) { + newList = announcementsList.map(item => + item.id === editingAnnouncement.id + ? { ...item, ...formData } + : item + ); + } else { + const newId = Math.max(...announcementsList.map(item => item.id), 0) + 1; + const newAnnouncement = { + id: newId, + ...formData + }; + newList = [...announcementsList, newAnnouncement]; + } + + setAnnouncementsList(newList); + setHasChanges(true); + setShowAnnouncementModal(false); + showSuccess(editingAnnouncement ? '公告已更新,请及时点击“保存配置”进行保存' : '公告已添加,请及时点击“保存配置”进行保存'); + } catch (error) { + showError('操作失败: ' + error.message); + } finally { + setModalLoading(false); + } + }; + + const parseAnnouncements = (announcementsStr) => { + if (!announcementsStr) { + setAnnouncementsList([]); + return; + } + + try { + const parsed = JSON.parse(announcementsStr); + const list = Array.isArray(parsed) ? parsed : []; + // 确保每个项目都有id + const listWithIds = list.map((item, index) => ({ + ...item, + id: item.id || index + 1 + })); + setAnnouncementsList(listWithIds); + } catch (error) { + console.error('解析系统公告失败:', error); + setAnnouncementsList([]); + } + }; + + useEffect(() => { + if (options.Announcements !== undefined) { + parseAnnouncements(options.Announcements); + } + }, [options.Announcements]); + + const handleBatchDelete = () => { + if (selectedRowKeys.length === 0) { + showError('请先选择要删除的系统公告'); + return; + } + + const newList = announcementsList.filter(item => !selectedRowKeys.includes(item.id)); + setAnnouncementsList(newList); + setSelectedRowKeys([]); + setHasChanges(true); + showSuccess(`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存配置”进行保存`); + }; + + const renderHeader = () => ( +
+
+
+ + {t('系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)')} +
+
+ + + +
+
+ + + +
+
+
+ ); + + // 计算当前页显示的数据 + const getCurrentPageData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return announcementsList.slice(startIndex, endIndex); + }; + + const rowSelection = { + selectedRowKeys, + onChange: (selectedRowKeys, selectedRows) => { + setSelectedRowKeys(selectedRowKeys); + }, + onSelect: (record, selected, selectedRows) => { + console.log(`选择行: ${selected}`, record); + }, + onSelectAll: (selected, selectedRows) => { + console.log(`全选: ${selected}`, selectedRows); + }, + getCheckboxProps: (record) => ({ + disabled: false, + name: record.id, + }), + }; + + return ( + <> + +
t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`), + pageSizeOptions: ['5', '10', '20', '50'], + onChange: (page, size) => { + setCurrentPage(page); + setPageSize(size); + }, + onShowSizeChange: (current, size) => { + setCurrentPage(1); + setPageSize(size); + } + }} + size='middle' + loading={loading} + empty={ + } + darkModeImage={} + description={t('暂无系统公告')} + style={{ padding: 30 }} + /> + } + className="rounded-xl overflow-hidden" + /> + + + setShowAnnouncementModal(false)} + okText={t('保存')} + cancelText={t('取消')} + className="rounded-xl" + confirmLoading={modalLoading} + > +
+ setAnnouncementForm({ ...announcementForm, content: value })} + /> + setAnnouncementForm({ ...announcementForm, publishDate: value })} + /> + setAnnouncementForm({ ...announcementForm, type: value })} + /> + setAnnouncementForm({ ...announcementForm, extra: value })} + /> + +
+ + { + setShowDeleteModal(false); + setDeletingAnnouncement(null); + }} + okText={t('确认删除')} + cancelText={t('取消')} + type="warning" + className="rounded-xl" + okButtonProps={{ + type: 'danger', + theme: 'solid' + }} + > + {t('确定要删除此公告吗?')} + + + ); +}; + +export default SettingsAnnouncements; \ No newline at end of file diff --git a/web/src/pages/Setting/Dashboard/SettingsFAQ.js b/web/src/pages/Setting/Dashboard/SettingsFAQ.js new file mode 100644 index 00000000..f6cc8ac6 --- /dev/null +++ b/web/src/pages/Setting/Dashboard/SettingsFAQ.js @@ -0,0 +1,413 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Space, + Table, + Form, + Typography, + Empty, + Divider, + Modal +} from '@douyinfe/semi-ui'; +import { + IllustrationNoResult, + IllustrationNoResultDark +} from '@douyinfe/semi-illustrations'; +import { + Plus, + Edit, + Trash2, + Save, + HelpCircle +} from 'lucide-react'; +import { API, showError, showSuccess } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; + +const { Text } = Typography; + +const SettingsFAQ = ({ options, refresh }) => { + const { t } = useTranslation(); + + const [faqList, setFaqList] = useState([]); + const [showFaqModal, setShowFaqModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deletingFaq, setDeletingFaq] = useState(null); + const [editingFaq, setEditingFaq] = useState(null); + const [modalLoading, setModalLoading] = useState(false); + const [loading, setLoading] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [faqForm, setFaqForm] = useState({ + title: '', + content: '' + }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const columns = [ + { + title: t('问题标题'), + dataIndex: 'title', + key: 'title', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('回答内容'), + dataIndex: 'content', + key: 'content', + render: (text) => ( +
+ {text} +
+ ) + }, + { + title: t('操作'), + key: 'action', + fixed: 'right', + width: 150, + render: (text, record) => ( + + + + + ) + } + ]; + + const updateOption = async (key, value) => { + const res = await API.put('/api/option/', { + key, + value, + }); + const { success, message } = res.data; + if (success) { + showSuccess('常见问答已更新'); + if (refresh) refresh(); + } else { + showError(message); + } + }; + + const submitFAQ = async () => { + try { + setLoading(true); + const faqJson = JSON.stringify(faqList); + await updateOption('FAQ', faqJson); + setHasChanges(false); + } catch (error) { + console.error('常见问答更新失败', error); + showError('常见问答更新失败'); + } finally { + setLoading(false); + } + }; + + const handleAddFaq = () => { + setEditingFaq(null); + setFaqForm({ + title: '', + content: '' + }); + setShowFaqModal(true); + }; + + const handleEditFaq = (faq) => { + setEditingFaq(faq); + setFaqForm({ + title: faq.title, + content: faq.content + }); + setShowFaqModal(true); + }; + + const handleDeleteFaq = (faq) => { + setDeletingFaq(faq); + setShowDeleteModal(true); + }; + + const confirmDeleteFaq = () => { + if (deletingFaq) { + const newList = faqList.filter(item => item.id !== deletingFaq.id); + setFaqList(newList); + setHasChanges(true); + showSuccess('问答已删除,请及时点击“保存配置”进行保存'); + } + setShowDeleteModal(false); + setDeletingFaq(null); + }; + + const handleSaveFaq = async () => { + if (!faqForm.title || !faqForm.content) { + showError('请填写完整的问答信息'); + return; + } + + try { + setModalLoading(true); + + let newList; + if (editingFaq) { + newList = faqList.map(item => + item.id === editingFaq.id + ? { ...item, ...faqForm } + : item + ); + } else { + const newId = Math.max(...faqList.map(item => item.id), 0) + 1; + const newFaq = { + id: newId, + ...faqForm + }; + newList = [...faqList, newFaq]; + } + + setFaqList(newList); + setHasChanges(true); + setShowFaqModal(false); + showSuccess(editingFaq ? '问答已更新,请及时点击“保存配置”进行保存' : '问答已添加,请及时点击“保存配置”进行保存'); + } catch (error) { + showError('操作失败: ' + error.message); + } finally { + setModalLoading(false); + } + }; + + const parseFAQ = (faqStr) => { + if (!faqStr) { + setFaqList([]); + return; + } + + try { + const parsed = JSON.parse(faqStr); + const list = Array.isArray(parsed) ? parsed : []; + // 确保每个项目都有id + const listWithIds = list.map((item, index) => ({ + ...item, + id: item.id || index + 1 + })); + setFaqList(listWithIds); + } catch (error) { + console.error('解析常见问答失败:', error); + setFaqList([]); + } + }; + + useEffect(() => { + if (options.FAQ !== undefined) { + parseFAQ(options.FAQ); + } + }, [options.FAQ]); + + const handleBatchDelete = () => { + if (selectedRowKeys.length === 0) { + showError('请先选择要删除的常见问答'); + return; + } + + const newList = faqList.filter(item => !selectedRowKeys.includes(item.id)); + setFaqList(newList); + setSelectedRowKeys([]); + setHasChanges(true); + showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存配置”进行保存`); + }; + + const renderHeader = () => ( +
+
+
+ + {t('常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)')} +
+
+ + + +
+
+ + + +
+
+
+ ); + + // 计算当前页显示的数据 + const getCurrentPageData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return faqList.slice(startIndex, endIndex); + }; + + const rowSelection = { + selectedRowKeys, + onChange: (selectedRowKeys, selectedRows) => { + setSelectedRowKeys(selectedRowKeys); + }, + onSelect: (record, selected, selectedRows) => { + console.log(`选择行: ${selected}`, record); + }, + onSelectAll: (selected, selectedRows) => { + console.log(`全选: ${selected}`, selectedRows); + }, + getCheckboxProps: (record) => ({ + disabled: false, + name: record.id, + }), + }; + + return ( + <> + +
t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`), + pageSizeOptions: ['5', '10', '20', '50'], + onChange: (page, size) => { + setCurrentPage(page); + setPageSize(size); + }, + onShowSizeChange: (current, size) => { + setCurrentPage(1); + setPageSize(size); + } + }} + size='middle' + loading={loading} + empty={ + } + darkModeImage={} + description={t('暂无常见问答')} + style={{ padding: 30 }} + /> + } + className="rounded-xl overflow-hidden" + /> + + + setShowFaqModal(false)} + okText={t('保存')} + cancelText={t('取消')} + className="rounded-xl" + confirmLoading={modalLoading} + width={800} + > +
+ setFaqForm({ ...faqForm, title: value })} + /> + setFaqForm({ ...faqForm, content: value })} + /> + +
+ + { + setShowDeleteModal(false); + setDeletingFaq(null); + }} + okText={t('确认删除')} + cancelText={t('取消')} + type="warning" + className="rounded-xl" + okButtonProps={{ + type: 'danger', + theme: 'solid' + }} + > + {t('确定要删除此问答吗?')} + + + ); +}; + +export default SettingsFAQ; \ No newline at end of file From 3f89ee66e1861042567b9bcbe34482907fdacb53 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 10 Jun 2025 20:41:43 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Update=20payment=20ca?= =?UTF-8?q?llback=20return=20URL=20path=20from=20/log=20to=20/console/log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modified returnUrl configuration in RequestEpay function - Changed payment success redirect path to match updated frontend routing - Updated controller/topup.go line 116 to use correct callback path --- controller/topup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/topup.go b/controller/topup.go index 4654b6ea..951b2cf2 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -106,7 +106,7 @@ func RequestEpay(c *gin.Context) { payType = "wxpay" } callBackAddress := service.GetCallbackAddress() - returnUrl, _ := url.Parse(setting.ServerAddress + "/log") + returnUrl, _ := url.Parse(setting.ServerAddress + "/console/log") notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify") tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix()) tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)