From 8acbe732ffa61b9e80c920b7a660230f16a315e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Thu, 5 Nov 2026 01:08:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/stores/index.js | 11 +++ backend/src/views/home/index.vue | 90 +++++++++++++++++-- .../tools/reminder/components/detail.vue | 4 + .../views/tools/reminder/components/edit.vue | 7 +- backend/src/views/tools/reminder/index.vue | 4 + go/conf/app.conf | 2 +- go/controllers/backend_reminder.go | 14 +-- go/services/reminder_scheduler.go | 70 +++++++++++++-- 8 files changed, 181 insertions(+), 21 deletions(-) diff --git a/backend/src/stores/index.js b/backend/src/stores/index.js index 03189a0..afb9730 100644 --- a/backend/src/stores/index.js +++ b/backend/src/stores/index.js @@ -99,8 +99,19 @@ export const useTabsStore = defineTabsStore('tabs', () => { function setDefaultTab(tab) { const path = tab?.fullPath || ''; if (!path) return; + const prevDefault = defaultDashboardPath.value; defaultDashboardPath.value = path; + // 默认工作台路径变化 = 切换了模块:关闭旧模块的全部 tab, + // 只保留新模块的默认工作台。 + // prevDefault 为空(首次设置,如刷新后恢复)时不清空,保留恢复出来的 tabs。 + if (prevDefault && prevDefault !== path) { + tabList.value = [tab]; + activeTab.value = path; + saveTabsToStorage(tabList.value, activeTab.value); + return; + } + const idx = tabList.value.findIndex(t => t.fullPath === path); if (idx === -1) { tabList.value.unshift(tab); diff --git a/backend/src/views/home/index.vue b/backend/src/views/home/index.vue index 4bcd8ee..f1b6a20 100644 --- a/backend/src/views/home/index.vue +++ b/backend/src/views/home/index.vue @@ -1253,29 +1253,77 @@ onUnmounted(() => { border-color: #3d3d3d; } - // 暗色主题下未开通卡片 - &.disabled { - background: #222; - border-color: #333; + // 暗色主题 hover:用深灰浮起替代亮色主题的「蓝底白字」效果 + &:hover { + transform: translateY(-2px); + background: #222222; + border-color: #4d4d4d; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); .card-title { - color: #666 !important; + color: #ffffff !important; + } + + .card-desc { + color: #a8abb2 !important; } .card-icon-wrapper { - color: #555; + color: var(--yz-primary); + } + + .card-divider { + opacity: 1; + } + } + + // 暗色主题下未开通卡片 + &.disabled { + background: #222222; + border-color: #333333; + + .card-title { + color: #666666 !important; + } + + .card-icon-wrapper { + color: #555555; } .card-divider { border-color: #2e2e2e; } + // 未开通卡片 hover:保持置灰,不闪浅色底也不浮起 + &:hover { + transform: none; + background: #222222; + border-color: #333333; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + + .card-title { + color: #666666 !important; + } + + .card-desc { + color: #6b6b6b !important; + } + + .card-icon-wrapper { + color: #555555; + } + + .card-divider { + opacity: 1; + } + } + .card-locked .lock-icon { - color: #555; + color: #555555; } .card-locked .lock-tip { - color: #666; + color: #666666; } } } @@ -1304,6 +1352,32 @@ onUnmounted(() => { color: #8c8c8c !important; } + // 暗色主题 hover:深灰浮起,替代亮蓝底 + .modules-section .module-card:hover { + transform: translateY(-2px); + background: #222222; + border-color: #4d4d4d; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + } + + .modules-section .module-card:hover .card-desc { + color: #a8abb2 !important; + } + + // 未开通卡片 hover:保持置灰不浮起 + .modules-section .module-card.disabled:hover, + .modules-section .module-card.is-locked:hover { + transform: none; + background: #1f1f1f; + border-color: #333333; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + .modules-section .module-card.disabled:hover .card-desc, + .modules-section .module-card.is-locked:hover .card-desc { + color: #6b6b6b !important; + } + .modules-section .module-card.is-locked { background: #1f1f1f; diff --git a/backend/src/views/tools/reminder/components/detail.vue b/backend/src/views/tools/reminder/components/detail.vue index f1ba278..ffce4b6 100644 --- a/backend/src/views/tools/reminder/components/detail.vue +++ b/backend/src/views/tools/reminder/components/detail.vue @@ -75,6 +75,8 @@ const channelTextMap = { EMAIL: '邮件', BARK: 'Bark 推送', SITE_MSG: '站内信', + WECHAT_MP: '微信服务号', + WECHAT_MINI: '小程序订阅', } const channelColorMap = { @@ -82,6 +84,8 @@ const channelColorMap = { EMAIL: 'warning', BARK: 'danger', SITE_MSG: 'primary', + WECHAT_MP: 'success', + WECHAT_MINI: 'success', } function channelText(ch) { diff --git a/backend/src/views/tools/reminder/components/edit.vue b/backend/src/views/tools/reminder/components/edit.vue index d6f211b..881d8d6 100644 --- a/backend/src/views/tools/reminder/components/edit.vue +++ b/backend/src/views/tools/reminder/components/edit.vue @@ -43,7 +43,12 @@ 邮件 (EMAIL) Bark 推送 站内信 (SITE_MSG) + 微信服务号 + 小程序订阅 + + 微信服务号:需认证服务号且在个人中心完成账号绑定并关注;小程序订阅:需在小程序内授权过订阅消息(一次授权仅可发送一条)。两者均为一次性发送。 + @@ -200,7 +205,7 @@ async function handleTest() { if (res?.code === 200 && Array.isArray(res.data)) { const lines = res.data.map(item => { - const name = { SMS: '短信', EMAIL: '邮件', BARK: 'Bark推送', SITE_MSG: '站内信' }[item.channel] || item.channel + const name = { SMS: '短信', EMAIL: '邮件', BARK: 'Bark推送', SITE_MSG: '站内信', WECHAT_MP: '微信服务号', WECHAT_MINI: '小程序订阅' }[item.channel] || item.channel const status = item.success ? '发送成功' : `发送失败 (${item.msg})` diff --git a/backend/src/views/tools/reminder/index.vue b/backend/src/views/tools/reminder/index.vue index 81f1945..178c351 100644 --- a/backend/src/views/tools/reminder/index.vue +++ b/backend/src/views/tools/reminder/index.vue @@ -168,6 +168,8 @@ const channelTextMap = { EMAIL: '邮件', BARK: 'Bark', SITE_MSG: '站内信', + WECHAT_MP: '微信服务号', + WECHAT_MINI: '小程序订阅', } const channelColorMap = { @@ -175,6 +177,8 @@ const channelColorMap = { EMAIL: 'warning', BARK: 'danger', SITE_MSG: 'primary', + WECHAT_MP: 'success', + WECHAT_MINI: 'success', } function channelText(ch) { diff --git a/go/conf/app.conf b/go/conf/app.conf index f2e0e11..5a4da52 100644 --- a/go/conf/app.conf +++ b/go/conf/app.conf @@ -1,4 +1,4 @@ -appname = server +appname = server httpport = 9000 runmode = dev diff --git a/go/controllers/backend_reminder.go b/go/controllers/backend_reminder.go index c594d14..f859bcc 100644 --- a/go/controllers/backend_reminder.go +++ b/go/controllers/backend_reminder.go @@ -287,7 +287,7 @@ func (c *BackendReminderController) CreateReminder() { for _, ch := range p.RemindChannels { ch = strings.ToUpper(strings.TrimSpace(ch)) - if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" { + if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" && ch != "WECHAT_MP" && ch != "WECHAT_MINI" { continue } @@ -426,7 +426,7 @@ func (c *BackendReminderController) UpdateReminder() { for _, ch := range p.RemindChannels { ch = strings.ToUpper(strings.TrimSpace(ch)) - if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" { + if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" && ch != "WECHAT_MP" && ch != "WECHAT_MINI" { continue } @@ -691,10 +691,12 @@ func (c *BackendReminderController) TestReminder() { } senders := map[string]services.ReminderSender{ - "SMS": &services.SMSSender{}, - "EMAIL": &services.EmailSender{}, - "BARK": &services.BarkSender{}, - "SITE_MSG": &services.SiteMsgSender{}, + "SMS": &services.SMSSender{}, + "EMAIL": &services.EmailSender{}, + "BARK": &services.BarkSender{}, + "SITE_MSG": &services.SiteMsgSender{}, + "WECHAT_MP": &services.WechatMpSender{}, + "WECHAT_MINI": &services.WechatMiniSender{}, } type TestResult struct { diff --git a/go/services/reminder_scheduler.go b/go/services/reminder_scheduler.go index 5f75d04..3ec7048 100644 --- a/go/services/reminder_scheduler.go +++ b/go/services/reminder_scheduler.go @@ -14,6 +14,7 @@ import ( "server/models" "server/services/wechatmini" + "server/services/wechatmp" "github.com/beego/beego/v2/core/logs" ) @@ -312,6 +313,62 @@ func (s *SiteMsgSender) Send(ctx context.Context, reminder ReminderData, title, return true, nil } +// WechatMpSender 微信服务号模板消息发送实现 +// 要求:服务号已认证且在平台设置中启用;用户已在服务号绑定账号并关注。 +type WechatMpSender struct{} + +func (s *WechatMpSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) { + cfg, err := wechatmp.LoadEnabledConfig() + if err != nil { + return false, fmt.Errorf("服务号未配置或未启用: %w", err) + } + + // 接收目标优先级:手动指定的 openid → 服务号粉丝绑定表反查 + openid := "" + if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { + openid = *reminder.ReceiverTarget + } + if openid == "" && reminder.Tid > 0 { + if f, ferr := wechatmp.GetFollowerByUser("tenant_user", reminder.ReceiverUserID); ferr == nil && f != nil { + if f.Subscribe != 1 { + return false, fmt.Errorf("用户已取消关注服务号,无法推送模板消息") + } + openid = f.OpenID + } + } + if openid == "" { + return false, fmt.Errorf("用户未绑定服务号,无法推送(请在服务号内完成账号绑定)") + } + + perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{ + Title: title, + Content: content, + Remark: "来自云泽管理平台日程提醒", + }) + if perr != nil { + return false, perr + } + return true, nil +} + +// WechatMiniSender 微信小程序订阅消息发送实现 +// 要求:用户在小程序内授权过订阅消息(一次性授权,发一条少一条)。 +type WechatMiniSender struct{} + +func (s *WechatMiniSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) { + if reminder.ReceiverUserID == 0 { + return false, fmt.Errorf("无效的接收用户") + } + res := wechatmini.PushTenantNotice(reminder.ReceiverUserID, title, content) + if res.Sent > 0 { + return true, nil + } + if len(res.Errors) > 0 { + return false, res.Errors[0] + } + return false, fmt.Errorf("用户未授权小程序订阅消息(授权一次仅可发送一条)") +} + // generateUUID 生成一个安全的随机 UUID 字符 func generateUUID() string { b := make([]byte, 16) @@ -368,10 +425,12 @@ func scanAndSendReminders() { // 实例分发发送 senders := map[string]ReminderSender{ - "SMS": &SMSSender{}, - "EMAIL": &EmailSender{}, - "BARK": &BarkSender{}, - "SITE_MSG": &SiteMsgSender{}, + "SMS": &SMSSender{}, + "EMAIL": &EmailSender{}, + "BARK": &BarkSender{}, + "SITE_MSG": &SiteMsgSender{}, + "WECHAT_MP": &WechatMpSender{}, + "WECHAT_MINI": &WechatMiniSender{}, } for i := range list { @@ -437,7 +496,8 @@ func scanAndSendReminders() { newSendCount := reminder.SendCount + 1 newStatus := reminder.RemindStatus - if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" { + if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" || + reminder.RemindChannel == "WECHAT_MP" || reminder.RemindChannel == "WECHAT_MINI" { // 一次性发送:发送后直接置为结束 newStatus = 2 } else {