diff --git a/app b/app new file mode 100644 index 0000000..e491cee Binary files /dev/null and b/app differ diff --git a/internal/handler/config.go b/internal/handler/config.go index 65d3b7f..f3dda1f 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -43,7 +43,6 @@ func (h *ConfigHandler) SetConfig(c *gin.Context) { var config model.Config result := h.db.Where("`key` = ?", key).First(&config) if result.Error == gorm.ErrRecordNotFound { - // 创建新配置 config = model.Config{ Key: key, Value: req.Value, @@ -54,7 +53,6 @@ func (h *ConfigHandler) SetConfig(c *gin.Context) { return } } else if result.Error == nil { - // 更新已有配置 config.Value = req.Value if req.Remark != "" { config.Remark = req.Remark @@ -80,3 +78,86 @@ func (h *ConfigHandler) ListConfigs(c *gin.Context) { } Success(c, configs) } + +// BatchSetConfigs 批量设置配置 PUT /api/v1/admin/config +func (h *ConfigHandler) BatchSetConfigs(c *gin.Context) { + var req struct { + Items []ConfigItem `json:"items"` + } + if err := c.ShouldBindJSON(&req); err != nil { + FailBadRequest(c, "参数错误") + return + } + + for _, item := range req.Items { + var config model.Config + result := h.db.Where("`key` = ?", item.Key).First(&config) + if result.Error == gorm.ErrRecordNotFound { + config = model.Config{ + Key: item.Key, + Value: item.Value, + Remark: item.Remark, + } + h.db.Create(&config) + } else if result.Error == nil { + config.Value = item.Value + if item.Remark != "" { + config.Remark = item.Remark + } + h.db.Save(&config) + } + } + + SuccessMsg(c, "保存成功") +} + +// GetNavSettings 公开接口:获取导航页所有配置 GET /api/v1/nav-settings +func (h *ConfigHandler) GetNavSettings(c *gin.Context) { + // 导航页所有可配置项的 key 列表 + keys := []string{ + "nav_site_title", + "nav_hero_title", + "nav_hero_highlight", + "nav_hero_desc", + "nav_pill_templates", + "nav_pill_lines", + "nav_pill_industries", + "nav_pill_per_industry", + "nav_pill_responsive", + "nav_consult_title", + "nav_consult_desc", + "nav_consult_success", + "nav_footer_text", + "nav_company_name", + // 6 大行业标题 + "nav_sec_enterprise_title", + "nav_sec_enterprise_sub", + "nav_sec_shop_title", + "nav_sec_shop_sub", + "nav_sec_food_title", + "nav_sec_food_sub", + "nav_sec_game_title", + "nav_sec_game_sub", + "nav_sec_app_title", + "nav_sec_app_sub", + "nav_sec_music_title", + "nav_sec_music_sub", + } + + settings := map[string]string{} + for _, key := range keys { + var config model.Config + if err := h.db.Where("`key` = ?", key).First(&config).Error; err == nil { + settings[key] = config.Value + } + } + + Success(c, settings) +} + +// ConfigItem 配置项 +type ConfigItem struct { + Key string `json:"key"` + Value string `json:"value"` + Remark string `json:"remark"` +} diff --git a/internal/handler/customer.go b/internal/handler/customer.go index 47d91ab..355810a 100644 --- a/internal/handler/customer.go +++ b/internal/handler/customer.go @@ -8,19 +8,20 @@ import ( "net/http" "time" - "gotest/internal/config" + "gotest/internal/model" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) // CustomerHandler 客户信息收集处理器 type CustomerHandler struct { - cfg *config.Config + db *gorm.DB } // NewCustomerHandler 创建客户信息收集处理器 -func NewCustomerHandler(cfg *config.Config) *CustomerHandler { - return &CustomerHandler{cfg: cfg} +func NewCustomerHandler(db *gorm.DB) *CustomerHandler { + return &CustomerHandler{db: db} } // CustomerRequest 客户信息请求 @@ -54,7 +55,6 @@ func (h *CustomerHandler) Submit(c *gin.Context) { // 发送到企业微信webhook if err := h.sendToWechatWebhook(content); err != nil { - // 即使webhook失败,也记录下来并返回成功(避免暴露内部错误) fmt.Printf("发送企业微信webhook失败: %v\n", err) } @@ -63,13 +63,19 @@ func (h *CustomerHandler) Submit(c *gin.Context) { }) } +// getWebhookURL 从数据库获取webhook URL +func (h *CustomerHandler) getWebhookURL() string { + var cfg model.Config + if err := h.db.Where("`key` = ?", "wechat_webhook_url").First(&cfg).Error; err == nil && cfg.Value != "" { + return cfg.Value + } + return "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=71110fd5-9f51-4695-88dc-85ca34385d66" +} + // sendToWechatWebhook 发送消息到企业微信群机器人 func (h *CustomerHandler) sendToWechatWebhook(content string) error { - if h.cfg.WechatWebhook.URL == "" { - return fmt.Errorf("企业微信webhook URL未配置") - } + webhookURL := h.getWebhookURL() - // 构建消息体 message := map[string]interface{}{ "msgtype": "text", "text": map[string]string{ @@ -82,31 +88,26 @@ func (h *CustomerHandler) sendToWechatWebhook(content string) error { return fmt.Errorf("序列化消息失败: %w", err) } - // 发送POST请求 - resp, err := http.Post(h.cfg.WechatWebhook.URL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("发送请求失败: %w", err) } defer resp.Body.Close() - // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("读取响应失败: %w", err) } - // 检查响应状态 if resp.StatusCode != http.StatusOK { return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) } - // 解析响应JSON var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return fmt.Errorf("解析响应失败: %w", err) } - // 检查企业微信返回的错误码 if errcode, ok := result["errcode"].(float64); ok && errcode != 0 { errmsg, _ := result["errmsg"].(string) return fmt.Errorf("企业微信返回错误: %v - %s", errcode, errmsg) @@ -114,3 +115,43 @@ func (h *CustomerHandler) sendToWechatWebhook(content string) error { return nil } + +// TestWebhook 测试webhook POST /api/v1/admin/webhook/test +func (h *CustomerHandler) TestWebhook(c *gin.Context) { + webhookURL := h.getWebhookURL() + + content := fmt.Sprintf("✅ Webhook 测试成功\n⏰ 时间:%s", time.Now().Format("2006-01-02 15:04:05")) + + message := map[string]interface{}{ + "msgtype": "text", + "text": map[string]string{ + "content": content, + }, + } + + jsonData, err := json.Marshal(message) + if err != nil { + FailInternal(c, "序列化失败: "+err.Error()) + return + } + + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + FailInternal(c, "发送失败: "+err.Error()) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + var result map[string]interface{} + json.Unmarshal(body, &result) + + if errcode, ok := result["errcode"].(float64); ok && errcode != 0 { + errmsg, _ := result["errmsg"].(string) + FailInternal(c, fmt.Sprintf("企业微信返回错误: %v - %s", errcode, errmsg)) + return + } + + SuccessMsg(c, "测试消息发送成功!请检查企业微信群") +} diff --git a/internal/router/router.go b/internal/router/router.go index 3292f58..e263574 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -162,6 +162,9 @@ func Setup( customer.POST("/submit", customerH.Submit) } + // 导航页设置(公开,无需认证) + api.GET("/nav-settings", configH.GetNavSettings) + // 需要登录的路由 authed := api.Group("") authed.Use(middleware.JWTAuth(cfg.JWT.Secret)) @@ -205,6 +208,10 @@ func Setup( admin.GET("/config", configH.ListConfigs) admin.GET("/config/:key", configH.GetConfig) admin.PUT("/config/:key", configH.SetConfig) + admin.PUT("/config-batch", configH.BatchSetConfigs) + + // Webhook 测试 + admin.POST("/webhook/test", customerH.TestWebhook) } } diff --git a/main.go b/main.go index 85d91a7..4ac1a68 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,7 @@ func main() { appHandler := handler.NewAppHandler(appService) musicHandler := handler.NewMusicHandler(musicService) dashboardHandler := handler.NewDashboardHandler(db.DB) - customerHandler := handler.NewCustomerHandler(cfg) + customerHandler := handler.NewCustomerHandler(db.DB) accessLogHandler := handler.NewAccessLogHandler(db.DB) configHandler := handler.NewConfigHandler(db.DB) diff --git a/web/dist/admin/index.html b/web/dist/admin/index.html index 060f41c..450f86b 100644 --- a/web/dist/admin/index.html +++ b/web/dist/admin/index.html @@ -273,6 +273,9 @@ tbody tr:last-child td{border-bottom:none} 🔔 Webhook配置 + + 📝 导航页设置 + ⚙️ 系统设置 @@ -455,7 +458,7 @@ function navigate(page, params = {}) { document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active')); const link = document.querySelector(`[data-page="${page}"]`); if (link) link.classList.add('active'); - const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航', accesslog: '访问日志', webhook: 'Webhook配置' }; + const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航', accesslog: '访问日志', webhook: 'Webhook配置', navsettings: '导航页设置' }; document.getElementById('pageTitle').textContent = titles[page] || page; if (page === 'users') renderUsers(); else if (page === 'content') renderContent(params.table); @@ -463,6 +466,7 @@ function navigate(page, params = {}) { else if (page === 'sites') renderSites(); else if (page === 'accesslog') renderAccessLogs(); else if (page === 'webhook') renderWebhook(); + else if (page === 'navsettings') renderNavSettings(); if (window.innerWidth <= 768) document.getElementById('sidebar').classList.remove('open'); } @@ -1258,33 +1262,126 @@ async function testWebhook() { resultEl.style.display = 'block'; resultEl.innerHTML = '
发送中...
'; try { - await api('/admin/config/wechat_webhook_url', { - method: 'PUT', - body: JSON.stringify({ value: document.getElementById('webhookUrl').value.trim(), remark: '企业微信群机器人Webhook地址' }) - }); - // 通过客户咨询接口发送测试消息 - const resp = await fetch(API_BASE + '/customer/submit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: '测试用户', - phone: '13800000000', - company: '测试公司', - email: 'test@example.com', - needs: '这是一条测试消息,用于验证 Webhook 配置是否正确' - }) - }); - const data = await resp.json(); - if (data.code === 0) { - resultEl.innerHTML = '
✅ 测试消息已发送,请检查企业微信群
'; - } else { - resultEl.innerHTML = `
❌ 发送失败: ${data.message}
`; - } + await api('/admin/webhook/test', { method: 'POST' }); + resultEl.innerHTML = '
✅ 测试消息已发送,请检查企业微信群
'; } catch (e) { - resultEl.innerHTML = '
❌ 网络错误
'; + resultEl.innerHTML = `
❌ ${e.message || '发送失败'}
`; } } +// ===== 导航页设置 ===== +async function renderNavSettings() { + const el = document.getElementById('content'); + el.innerHTML = '
'; + try { + const data = await api('/admin/config'); + const configs = {}; + (data.data || []).forEach(c => { configs[c.key] = c.value; }); + el.innerHTML = ` +
+

📝 导航页文案设置

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+
+

📂 分类标题

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

💬 咨询区 & 页脚

+
+
+
+
+
+
+
+ +
+
+
`; + } catch (e) { el.innerHTML = '
加载失败
'; } +} + +async function saveNavSettings() { + const fields = [ + 'hero_title','hero_highlight','hero_desc','pill_templates','pill_lines','pill_per_industry','pill_responsive', + 'sec_enterprise_title','sec_enterprise_sub','sec_shop_title','sec_shop_sub', + 'sec_food_title','sec_food_sub','sec_game_title','sec_game_sub', + 'sec_app_title','sec_app_sub','sec_music_title','sec_music_sub', + 'consult_title','consult_desc','consult_success','footer_text','company_name' + ]; + const items = fields.map(f => ({ + key: 'nav_' + f, + value: (document.getElementById('ns_' + f)?.value || '').trim(), + remark: '导航页配置' + })); + try { + await api('/admin/config-batch', { + method: 'PUT', + body: JSON.stringify({ items }) + }); + toast('保存成功!刷新导航页即可看到效果', 'success'); + } catch (e) { toast('保存失败', 'error'); } +} + // ===== 通用工具 ===== function formatDate(d) { if (!d) return '-'; diff --git a/web/dist/index.html b/web/dist/index.html index 4137023..8b03592 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -105,19 +105,19 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

Gotest 模板导航

-

6 大行业,19 套精心设计的网站模板,一键切换,即刻预览

+

Gotest 模板导航

+

6 大行业,19 套精心设计的网站模板,一键切换,即刻预览

- 19 套模板 - 6 大行业 - 3 套 / 行业 - 响应式设计 + 19 套模板 + 6 大行业 + 3 套 / 行业 + 响应式设计
-

企业官网

3 套风格
+

企业官网

3 套风格
科技蓝
→
科技企业 · site1
现代科技公司风格,蓝色主调
制造金
→
制造企业 · site2
工业制造风格,深色金调
@@ -127,7 +127,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

跨境电商

4 套风格
+

跨境电商

4 套风格
红色经典
→
经典电商 · shop
经典电商红,大促风格
暗黑模式
→
暗黑电商 · shop2
黑色高端,极简暗色
@@ -138,7 +138,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

外卖平台

3 套风格
+

外卖平台

3 套风格
橙黄活力
→
美味达 · food1
暖橙食欲风格,活力满满
深色模式
→
GOURMET · food2
暗色高端外卖平台
@@ -148,7 +148,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

游戏平台

3 套风格
+

游戏平台

3 套风格
赛博朋克
→
NEON ARCADE · game1
霓虹未来,赛博朋克风格
像素复古
→
PIXEL GAMES · game2
8-bit 像素复古游戏风
@@ -158,7 +158,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

应用商店

3 套风格
+

应用商店

3 套风格
极简白
→
极简白 · app1
Apple 风格,简洁清爽
暗黑模式
→
暗黑应用 · app2
霓虹绿暗色,科技感十足
@@ -168,7 +168,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

音乐平台

3 套风格
+

音乐平台

3 套风格
暗黑播放器
→
云音 Music · music1
暗夜沉浸式音乐播放器
黑胶复古
→
Vinyl · music2
黑胶唱片复古风格
@@ -187,8 +187,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-

需要定制网站?

-

只需几百块,即可拥有同款精美站点!留下您的信息,我们尽快联系您

+

需要定制网站?

+

只需几百块,即可拥有同款精美站点!留下您的信息,我们尽快联系您

@@ -222,12 +222,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Ro
-
Gotest · 多行业网站模板集合 · 点击卡片进入对应模板
+ +