From d3886e2475bf102e2d7b29ad0d9cd11ef2eaee92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com>
Date: Wed, 17 Jun 2026 21:44:08 +0800
Subject: [PATCH 1/5] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=9A=E7=9F=A5?=
=?UTF-8?q?=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
platform/components.d.ts | 3 +-
platform/src/api/reminder.js | 53 ++
.../components/notificationSettings.vue | 765 ++++++++++++++++++
.../views/system/platformsettings/index.vue | 9 +
.../views/tools/notebook/components/index.vue | 3 +
platform/src/views/tools/notebook/test-api.js | 148 ----
.../tools/reminder/components/detail.vue | 148 ++++
.../views/tools/reminder/components/edit.vue | 272 +++++++
platform/src/views/tools/reminder/index.vue | 318 ++++++++
9 files changed, 1569 insertions(+), 150 deletions(-)
create mode 100644 platform/src/api/reminder.js
create mode 100644 platform/src/views/system/platformsettings/components/notificationSettings.vue
delete mode 100644 platform/src/views/tools/notebook/test-api.js
create mode 100644 platform/src/views/tools/reminder/components/detail.vue
create mode 100644 platform/src/views/tools/reminder/components/edit.vue
create mode 100644 platform/src/views/tools/reminder/index.vue
diff --git a/platform/components.d.ts b/platform/components.d.ts
index 567e500..414b83d 100644
--- a/platform/components.d.ts
+++ b/platform/components.d.ts
@@ -18,7 +18,6 @@ declare module 'vue' {
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton']
- ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -40,7 +39,6 @@ declare module 'vue' {
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
- ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
@@ -56,6 +54,7 @@ declare module 'vue' {
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
+ ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
diff --git a/platform/src/api/reminder.js b/platform/src/api/reminder.js
new file mode 100644
index 0000000..30e3ce4
--- /dev/null
+++ b/platform/src/api/reminder.js
@@ -0,0 +1,53 @@
+import request from "@/utils/request";
+
+/** 提醒列表 */
+export function getReminderList(params) {
+ return request({
+ url: "/platform/reminder/list",
+ method: "get",
+ params,
+ });
+}
+
+/** 提醒详情 */
+export function getReminderDetail(id) {
+ return request({
+ url: `/platform/reminder/${id}`,
+ method: "get",
+ });
+}
+
+/** 新增提醒 */
+export function createReminder(data) {
+ return request({
+ url: "/platform/reminder",
+ method: "post",
+ data,
+ });
+}
+
+/** 更新提醒 */
+export function updateReminder(id, data) {
+ return request({
+ url: `/platform/reminder/${id}`,
+ method: "post",
+ data,
+ });
+}
+
+/** 删除提醒 */
+export function deleteReminder(id) {
+ return request({
+ url: `/platform/reminder/${id}`,
+ method: "delete",
+ });
+}
+
+/** 批量删除提醒 */
+export function batchDeleteReminder(ids) {
+ return request({
+ url: "/platform/reminder/batchDelete",
+ method: "post",
+ data: { ids },
+ });
+}
diff --git a/platform/src/views/system/platformsettings/components/notificationSettings.vue b/platform/src/views/system/platformsettings/components/notificationSettings.vue
new file mode 100644
index 0000000..53af57c
--- /dev/null
+++ b/platform/src/views/system/platformsettings/components/notificationSettings.vue
@@ -0,0 +1,765 @@
+
+
+
+
+
+
+
+
+
+
+ 邮箱配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SSL
+ TLS
+ 无加密
+
+
+
+
+
+
+
+
+
+ 发送测试邮件
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 短信配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 阿里云短信
+ 腾讯云短信
+ 自定义
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 自定义短信网关
+
+
+
+
+
+
+
+
+
+
+ 发送测试短信
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 站内信配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 天(过期消息将自动清理)
+
+
+
+ 用户打开消息中心列表时是否自动将所有消息标记为已读
+
+
+
+
+
+
+
+
+
+
+
+ 钉钉配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 如果机器人的安全配置开启了“加签”,请在此处填写对应的秘钥
+
+
+
+
+
+
+
+
+
+
+
+ WebHook配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 该 Token 将被包含在请求 Header 的 X-Webhook-Token 中以供接收方校验
+
+
+
+
+
+
+
+
+
+
+
+ 飞书配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bark配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持自建 Bark 服务,如:https://bark.yourdomain.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 微信配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Telegram配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持个人 Chat ID、群组 ID (通常为负数) 或公开频道 Username
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/src/views/system/platformsettings/index.vue b/platform/src/views/system/platformsettings/index.vue
index dc4be92..b525496 100644
--- a/platform/src/views/system/platformsettings/index.vue
+++ b/platform/src/views/system/platformsettings/index.vue
@@ -23,6 +23,14 @@
v-if="activeTab === 'storage'"
/>
+
+
+
+
+
@@ -32,6 +40,7 @@
import { ref } from "vue";
import platformSettings from "./components/platformSettings.vue";
import storageSettings from "./components/storageSettings.vue";
+import notificationSettings from "./components/notificationSettings.vue";
const activeTab = ref("platform");
diff --git a/platform/src/views/tools/notebook/components/index.vue b/platform/src/views/tools/notebook/components/index.vue
index e69de29..74a5c51 100644
--- a/platform/src/views/tools/notebook/components/index.vue
+++ b/platform/src/views/tools/notebook/components/index.vue
@@ -0,0 +1,3 @@
+
+ Notebook Component
+
diff --git a/platform/src/views/tools/notebook/test-api.js b/platform/src/views/tools/notebook/test-api.js
deleted file mode 100644
index 6f601bf..0000000
--- a/platform/src/views/tools/notebook/test-api.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/**
- * 记事本模块 API 测试脚本
- * 在浏览器控制台中运行此脚本测试 API
- */
-
-// 测试配置
-const API_BASE = '/platform/notebook';
-
-// 辅助函数:发送请求
-async function request(url, options = {}) {
- const token = localStorage.getItem('token');
- const headers = {
- 'Content-Type': 'application/json',
- 'Authorization': token ? `Bearer ${token}` : '',
- ...options.headers,
- };
-
- try {
- const response = await fetch(url, { ...options, headers });
- const data = await response.json();
- console.log(`✅ ${options.method || 'GET'} ${url}`, data);
- return data;
- } catch (error) {
- console.error(`❌ ${options.method || 'GET'} ${url}`, error);
- throw error;
- }
-}
-
-// 测试函数
-const NotebookTest = {
- // 1. 创建笔记
- async create() {
- return await request(`${API_BASE}/create`, {
- method: 'POST',
- body: JSON.stringify({
- title: '测试笔记 - ' + new Date().toLocaleString(),
- content: '
这是一条测试笔记
内容包含富文本格式
',
- }),
- });
- },
-
- // 2. 获取笔记列表
- async list(params = {}) {
- const query = new URLSearchParams({
- page: params.page || 1,
- pageSize: params.pageSize || 20,
- keyword: params.keyword || '',
- }).toString();
- return await request(`${API_BASE}/list?${query}`);
- },
-
- // 3. 获取笔记详情
- async detail(id) {
- return await request(`${API_BASE}/detail/${id}`);
- },
-
- // 4. 更新笔记
- async update(id, data) {
- return await request(`${API_BASE}/update/${id}`, {
- method: 'POST',
- body: JSON.stringify(data),
- });
- },
-
- // 5. 删除笔记
- async delete(id) {
- return await request(`${API_BASE}/delete/${id}`, {
- method: 'DELETE',
- });
- },
-
- // 完整测试流程
- async runFullTest() {
- console.log('🚀 开始测试记事本模块 API...\n');
-
- try {
- // 1. 创建笔记
- console.log('📝 测试1: 创建笔记');
- const createResult = await this.create();
- if (createResult.code !== 200) {
- throw new Error('创建笔记失败');
- }
- const noteId = createResult.data.id;
- console.log(`✅ 创建成功,笔记ID: ${noteId}\n`);
-
- // 2. 获取笔记列表
- console.log('📋 测试2: 获取笔记列表');
- const listResult = await this.list();
- if (listResult.code !== 200) {
- throw new Error('获取列表失败');
- }
- console.log(`✅ 获取成功,共 ${listResult.data.total} 条笔记\n`);
-
- // 3. 获取笔记详情
- console.log('🔍 测试3: 获取笔记详情');
- const detailResult = await this.detail(noteId);
- if (detailResult.code !== 200) {
- throw new Error('获取详情失败');
- }
- console.log(`✅ 获取成功,标题: ${detailResult.data.title}\n`);
-
- // 4. 更新笔记
- console.log('✏️ 测试4: 更新笔记');
- const updateResult = await this.update(noteId, {
- title: '更新后的标题 - ' + new Date().toLocaleString(),
- content: '这是更新后的内容
',
- });
- if (updateResult.code !== 200) {
- throw new Error('更新笔记失败');
- }
- console.log(`✅ 更新成功\n`);
-
- // 5. 搜索笔记
- console.log('🔎 测试5: 搜索笔记');
- const searchResult = await this.list({ keyword: '更新' });
- if (searchResult.code !== 200) {
- throw new Error('搜索失败');
- }
- console.log(`✅ 搜索成功,找到 ${searchResult.data.total} 条匹配笔记\n`);
-
- // 6. 删除笔记
- console.log('🗑️ 测试6: 删除笔记');
- const deleteResult = await this.delete(noteId);
- if (deleteResult.code !== 200) {
- throw new Error('删除笔记失败');
- }
- console.log(`✅ 删除成功\n`);
-
- console.log('🎉 所有测试通过!');
- return true;
- } catch (error) {
- console.error('❌ 测试失败:', error);
- return false;
- }
- },
-};
-
-// 导出到全局
-window.NotebookTest = NotebookTest;
-
-console.log('📚 记事本模块测试工具已加载');
-console.log('使用方法:');
-console.log(' NotebookTest.create() - 创建笔记');
-console.log(' NotebookTest.list() - 获取列表');
-console.log(' NotebookTest.detail(id) - 获取详情');
-console.log(' NotebookTest.update(id, data) - 更新笔记');
-console.log(' NotebookTest.delete(id) - 删除笔记');
-console.log(' NotebookTest.runFullTest() - 运行完整测试');
diff --git a/platform/src/views/tools/reminder/components/detail.vue b/platform/src/views/tools/reminder/components/detail.vue
new file mode 100644
index 0000000..c93d2ab
--- /dev/null
+++ b/platform/src/views/tools/reminder/components/detail.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+ {{ detail.id }}
+
+
+
+ {{ detail.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+ {{ detail.title }}
+
+
+
+
+ {{ typeText(detail.type) }}
+
+
+
+ {{ targetTypeText(detail.targetType) }}
+
+
+
+ {{ detail.targetIds || '—' }}
+
+
+
+ {{ detail.remindTime || '—' }}
+
+
+
+ {{ detail.isRepeat === 1 ? '是' : '否' }}
+
+
+
+
+ {{ repeatCycleText(detail.repeatCycle) }}
+
+
+
+ {{ detail.content || '—' }}
+
+
+
+ {{ detail.remark }}
+
+
+
+ {{ detail.createTime || '—' }}
+
+
+ {{ detail.updateTime || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
diff --git a/platform/src/views/tools/reminder/components/edit.vue b/platform/src/views/tools/reminder/components/edit.vue
new file mode 100644
index 0000000..4569f29
--- /dev/null
+++ b/platform/src/views/tools/reminder/components/edit.vue
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 开启后按下方周期重复发送
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 禁用
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
diff --git a/platform/src/views/tools/reminder/index.vue b/platform/src/views/tools/reminder/index.vue
new file mode 100644
index 0000000..076f9fd
--- /dev/null
+++ b/platform/src/views/tools/reminder/index.vue
@@ -0,0 +1,318 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+
+ 重置
+
+
+
+
+
+ 已选 {{ selectedIds.length }} 条
+ 批量删除
+
+
+
+
+
+
+
+
+
+ {{ typeText(row.type) }}
+
+
+
+
+
+
+ {{ targetTypeText(row.targetType) }}
+
+
+
+
+
+ {{ row.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+
+
+ 详情
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 22e0e75c35bfbc66641a6048a4c23241a5042c8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com>
Date: Wed, 17 Jun 2026 23:07:39 +0800
Subject: [PATCH 2/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AB=99=E5=86=85?=
=?UTF-8?q?=E4=BF=A1=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/components.d.ts | 2 +
backend/src/api/sitereminder.js | 27 ++
backend/src/components/CommonHeader.vue | 281 +++++++++++-
.../src/components/MessageDetailDialog.vue | 118 ++++++
docs/改造.md | 7 -
go/controllers/backend_sitereminder.go | 151 +++++++
go/controllers/platform_email.go | 35 +-
go/controllers/platform_sitereminder.go | 339 +++++++++++++++
go/models/init.go | 3 +
go/models/system_reminderlist.go | 26 ++
go/models/system_sitereminder.go | 16 +
go/routers/backend/backend.go | 6 +
go/routers/platform/platform.go | 11 +
go/services/system_email_store.go | 3 -
go/services/system_sitereminder.go | 399 ++++++++++++++++++
platform/components.d.ts | 1 +
platform/src/api/sitereminder.js | 89 ++++
platform/src/components/CommonHeader.vue | 288 ++++++++++++-
.../sitereminder/components/detail.vue | 118 ++++++
.../sitereminder/components/edit.vue | 278 ++++++++++++
.../basicSettings/sitereminder/index.vue | 280 ++++++++++++
platform/src/views/system/email/index.vue | 15 +-
.../components/notificationSettings.vue | 36 +-
sql/upgrade_yz_system_reminderlist.sql | 12 +
sql/yz_system_sitereminder.sql | 36 ++
25 files changed, 2538 insertions(+), 39 deletions(-)
create mode 100644 backend/src/api/sitereminder.js
create mode 100644 backend/src/components/MessageDetailDialog.vue
create mode 100644 go/controllers/backend_sitereminder.go
create mode 100644 go/controllers/platform_sitereminder.go
create mode 100644 go/models/system_reminderlist.go
create mode 100644 go/models/system_sitereminder.go
create mode 100644 go/services/system_sitereminder.go
create mode 100644 platform/src/api/sitereminder.js
create mode 100644 platform/src/views/basicSettings/sitereminder/components/detail.vue
create mode 100644 platform/src/views/basicSettings/sitereminder/components/edit.vue
create mode 100644 platform/src/views/basicSettings/sitereminder/index.vue
create mode 100644 sql/upgrade_yz_system_reminderlist.sql
create mode 100644 sql/yz_system_sitereminder.sql
diff --git a/backend/components.d.ts b/backend/components.d.ts
index fb754dd..3e73949 100644
--- a/backend/components.d.ts
+++ b/backend/components.d.ts
@@ -17,6 +17,7 @@ declare module 'vue' {
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
+ ElBadge: typeof import('element-plus/es')['ElBadge']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
@@ -66,6 +67,7 @@ declare module 'vue' {
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
+ MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
diff --git a/backend/src/api/sitereminder.js b/backend/src/api/sitereminder.js
new file mode 100644
index 0000000..76029e2
--- /dev/null
+++ b/backend/src/api/sitereminder.js
@@ -0,0 +1,27 @@
+import request from "@/utils/request";
+
+/** 获取我的消息列表 */
+export function getMySiteReminders(params) {
+ return request({
+ url: "/backend/sitereminder/myList",
+ method: "get",
+ params,
+ });
+}
+
+/** 标记消息为已读 */
+export function readSiteReminder(id) {
+ return request({
+ url: "/backend/sitereminder/read",
+ method: "post",
+ data: { id },
+ });
+}
+
+/** 一键全部已读 */
+export function readAllSiteReminders() {
+ return request({
+ url: "/backend/sitereminder/readall",
+ method: "post",
+ });
+}
diff --git a/backend/src/components/CommonHeader.vue b/backend/src/components/CommonHeader.vue
index 32983af..798be93 100644
--- a/backend/src/components/CommonHeader.vue
+++ b/backend/src/components/CommonHeader.vue
@@ -24,18 +24,48 @@
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
-
+
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+ 加载中...
+
+
+
+ 暂无新消息
+
+
+
+
@@ -65,6 +95,12 @@
+
+
@@ -74,8 +110,10 @@ import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout } from "@/api/login";
-import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
+import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Message } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
+import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
+import MessageDetailDialog from "./MessageDetailDialog.vue";
const router = useRouter();
const route = useRoute();
@@ -130,7 +168,86 @@ async function refreshCache() {
}
}
-onMounted(loadMenu);
+// 站内信消息中心逻辑
+const unreadCount = ref(0);
+const messages = ref([]);
+const loadingMessages = ref(false);
+const detailVisible = ref(false);
+const currentReminder = ref({});
+
+const formatTime = (timeStr: any) => {
+ if (!timeStr) return "-";
+ const date = new Date(timeStr);
+ return date.toLocaleString();
+};
+
+const fetchUnreadCount = async () => {
+ if (!authStore.token) return;
+ try {
+ const res = await getMySiteReminders({ page: 1, pageSize: 1, isRead: 0 });
+ if (res.code === 200) {
+ unreadCount.value = res.data.total || 0;
+ }
+ } catch (err) {
+ console.error("fetchUnreadCount failed", err);
+ }
+};
+
+const fetchMessages = async () => {
+ if (!authStore.token) return;
+ loadingMessages.value = true;
+ try {
+ const res = await getMySiteReminders({ page: 1, pageSize: 5 });
+ if (res.code === 200) {
+ messages.value = res.data.list || [];
+ }
+ } catch (err) {
+ console.error("fetchMessages failed", err);
+ } finally {
+ loadingMessages.value = false;
+ }
+};
+
+const handleDropdownVisibleChange = (visible: boolean) => {
+ if (visible) {
+ fetchMessages();
+ fetchUnreadCount();
+ }
+};
+
+const handleMessageClick = (item: any) => {
+ currentReminder.value = item;
+ detailVisible.value = true;
+};
+
+const handleReadSuccess = (id: number) => {
+ const msg = messages.value.find(m => m.id === id);
+ if (msg && msg.is_read === 0) {
+ msg.is_read = 1;
+ unreadCount.value = Math.max(0, unreadCount.value - 1);
+ }
+ fetchUnreadCount();
+};
+
+const handleMarkAllRead = async () => {
+ try {
+ const res = await readAllSiteReminders();
+ if (res.code === 200) {
+ ElMessage.success("全部已读标记成功");
+ unreadCount.value = 0;
+ messages.value.forEach(m => m.is_read = 1);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+const handleMessagesChanged = () => {
+ fetchUnreadCount();
+ fetchMessages();
+};
+
+let timer: any = null;
// 根据菜单列表和当前路径计算出的面包屑导航
const breadcrumbs = computed(() => {
@@ -300,7 +417,8 @@ const themeIcon = computed(() => isDark.value ? Sunny : Moon);
let mediaQuery: MediaQueryList | null = null;
let handleChange: ((e: MediaQueryListEvent) => void) | null = null;
-onMounted(() => {
+onMounted(async () => {
+ await loadMenu();
initTheme();
// 监听系统主题变化
@@ -313,6 +431,12 @@ onMounted(() => {
}
};
mediaQuery.addEventListener('change', handleChange);
+
+ if (authStore.token) {
+ fetchUnreadCount();
+ timer = setInterval(fetchUnreadCount, 60000);
+ window.addEventListener('site-messages-changed', handleMessagesChanged);
+ }
});
// 组件卸载时清理
@@ -320,6 +444,10 @@ onUnmounted(() => {
if (mediaQuery && handleChange) {
mediaQuery.removeEventListener('change', handleChange);
}
+ if (timer) {
+ clearInterval(timer);
+ }
+ window.removeEventListener('site-messages-changed', handleMessagesChanged);
});
@@ -487,4 +615,133 @@ onUnmounted(() => {
:deep(.el-button) {
margin-left: 0 !important;
}
+
+/* 消息中心下拉列表样式 */
+.message-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.message-dropdown-container {
+ width: 320px;
+ background-color: var(--el-bg-color-overlay);
+ border: 1px solid var(--el-border-color-light);
+ border-radius: 8px;
+ box-shadow: var(--el-box-shadow-light);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.message-dropdown-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--el-border-color-lighter);
+ background-color: var(--el-fill-color-blank);
+
+ .title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ }
+
+ .mark-all-btn {
+ font-size: 12px;
+ }
+}
+
+.loading-state,
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 30px 0;
+ color: var(--el-text-color-secondary);
+ font-size: 13px;
+ gap: 8px;
+
+ .el-icon {
+ font-size: 20px;
+ }
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.message-item {
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ border-bottom: 1px solid var(--el-border-color-extra-light);
+ text-align: left; /* Ensure it is left-aligned */
+
+ &:hover {
+ background-color: var(--el-fill-color-light);
+ }
+
+ &.unread {
+ background-color: var(--el-color-primary-light-9);
+
+ &:hover {
+ background-color: var(--el-color-primary-light-8);
+ }
+
+ .message-title {
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ position: relative;
+ padding-left: 10px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 6px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--el-color-danger);
+ }
+ }
+ }
+}
+
+.message-item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 4px;
+ gap: 8px;
+
+ .message-title {
+ font-size: 13px;
+ color: var(--el-text-color-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ text-align: left;
+ }
+
+ .message-time {
+ font-size: 11px;
+ color: var(--el-text-color-secondary);
+ white-space: nowrap;
+ }
+}
+
+.message-item-brief {
+ font-size: 12px;
+ color: var(--el-text-color-regular);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: left;
+}
diff --git a/backend/src/components/MessageDetailDialog.vue b/backend/src/components/MessageDetailDialog.vue
new file mode 100644
index 0000000..9cf4878
--- /dev/null
+++ b/backend/src/components/MessageDetailDialog.vue
@@ -0,0 +1,118 @@
+
+
+
+
{{ reminder.title }}
+
+
+
+ 发送者:{{ getSenderName(reminder) }}
+
+
+
+ 时间:{{ formatTime(reminder.create_time) }}
+
+
+
+
{{ reminder.content }}
+
+
+ 关闭
+
+
+
+
+
+
+
diff --git a/docs/改造.md b/docs/改造.md
index 8354da5..2068a04 100644
--- a/docs/改造.md
+++ b/docs/改造.md
@@ -1,10 +1,3 @@
-我完全懂了:
-**我只帮你把「你的需求」优化润色成一段标准、清晰、可直接喂给AI的指令**,不替它写代码、不改结构,让AI根据你现有项目自己去改。
-
-下面这段你**直接复制发给AI**即可:
-
----
-
# 【可直接投喂AI·优化版需求说明】
你好,我现在需要对我的项目进行**多租户二级域名绑定官网系统**的整体改造,请根据我的现有项目结构和需求,帮我完成所有代码修改。
diff --git a/go/controllers/backend_sitereminder.go b/go/controllers/backend_sitereminder.go
new file mode 100644
index 0000000..d33ac3f
--- /dev/null
+++ b/go/controllers/backend_sitereminder.go
@@ -0,0 +1,151 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ "server/pkg/jwtutil"
+ "server/services"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type BackendSiteReminderController struct {
+ beego.Controller
+}
+
+func (c *BackendSiteReminderController) backendClaims() (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, fmt.Errorf("未登录")
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, fmt.Errorf("认证信息格式错误")
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("无效的token")
+ }
+ if claims.UserType != "backend" {
+ return nil, fmt.Errorf("无权访问")
+ }
+ return claims, nil
+}
+
+func (c *BackendSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+// GetMyList GET /backend/sitereminder/myList
+func (c *BackendSiteReminderController) GetMyList() {
+ claims, err := c.backendClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 10)
+ var isRead *int8
+ if isReadStr := c.GetString("isRead"); isReadStr != "" {
+ if val, err := strconv.Atoi(isReadStr); err == nil {
+ v := int8(val)
+ isRead = &v
+ }
+ }
+
+ list, total, err := services.ListReminders(uint64(claims.UserID), "tenant", page, pageSize, isRead)
+ if err != nil {
+ c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{
+ "code": 200,
+ "msg": "success",
+ "data": map[string]interface{}{
+ "list": list,
+ "total": total,
+ },
+ }
+ _ = c.ServeJSON()
+}
+
+// MarkRead POST /backend/sitereminder/read
+func (c *BackendSiteReminderController) MarkRead() {
+ claims, err := c.backendClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ ID uint64 `json:"id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "tenant")
+ if err != nil {
+ c.jsonErr(500, 500, "操作失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
+
+// MarkAllRead POST /backend/sitereminder/readall
+func (c *BackendSiteReminderController) MarkAllRead() {
+ claims, err := c.backendClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ err = services.MarkAllRemindersRead(uint64(claims.UserID), "tenant")
+ if err != nil {
+ c.jsonErr(500, 500, "操作失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
+
+// Delete POST /backend/sitereminder/delete
+func (c *BackendSiteReminderController) Delete() {
+ claims, err := c.backendClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ ID uint64 `json:"id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ err = services.DeleteReminder(p.ID, uint64(claims.UserID), "tenant")
+ if err != nil {
+ c.jsonErr(500, 500, "删除失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
diff --git a/go/controllers/platform_email.go b/go/controllers/platform_email.go
index b678870..96ea816 100644
--- a/go/controllers/platform_email.go
+++ b/go/controllers/platform_email.go
@@ -97,12 +97,44 @@ type emailFormPayload struct {
Password string `json:"password"`
Encryption string `json:"encryption"`
Timeout interface{} `json:"timeout"`
+ Status interface{} `json:"status"`
}
type testEmailPayload struct {
emailFormPayload
TestEmail string `json:"testEmail"`
}
+func parseInt8Flexible(v interface{}) int8 {
+ if v == nil {
+ return 1
+ }
+ switch x := v.(type) {
+ case bool:
+ if x {
+ return 1
+ }
+ return 0
+ case float64:
+ return int8(x)
+ case int:
+ return int8(x)
+ case int8:
+ return x
+ case string:
+ s := strings.TrimSpace(x)
+ if s == "" {
+ return 1
+ }
+ n, err := strconv.ParseInt(s, 10, 8)
+ if err != nil {
+ return 1
+ }
+ return int8(n)
+ default:
+ return 1
+ }
+}
+
func parseUintFlexible(v interface{}) uint {
if v == nil {
return 0
@@ -191,7 +223,8 @@ func (c *PlatformEmailController) EditInfo() {
s := strings.TrimSpace(p.FromName)
fn = &s
}
- err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, 1, nil)
+ status := parseInt8Flexible(p.Status)
+ err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, status, nil)
if err != nil {
c.jsonErr(500, 500, "保存邮箱配置失败: "+err.Error())
return
diff --git a/go/controllers/platform_sitereminder.go b/go/controllers/platform_sitereminder.go
new file mode 100644
index 0000000..3be3ba8
--- /dev/null
+++ b/go/controllers/platform_sitereminder.go
@@ -0,0 +1,339 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ "server/pkg/jwtutil"
+ "server/services"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type PlatformSiteReminderController struct {
+ beego.Controller
+}
+
+func (c *PlatformSiteReminderController) platformClaims() (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, fmt.Errorf("未登录")
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, fmt.Errorf("认证信息格式错误")
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("无效的token")
+ }
+ if claims.UserType != "platform" {
+ return nil, fmt.Errorf("无权访问")
+ }
+ return claims, nil
+}
+
+func (c *PlatformSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+// GetConfig GET /platform/sitereminder/config
+func (c *PlatformSiteReminderController) GetConfig() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ cfg, err := services.GetSiteReminderConfig()
+ if err != nil {
+ c.jsonErr(500, 500, "获取配置失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cfg}
+ _ = c.ServeJSON()
+}
+
+// SaveConfig POST /platform/sitereminder/config
+func (c *PlatformSiteReminderController) SaveConfig() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ RetentionDays int `json:"retention_days"`
+ AutoRead int8 `json:"auto_read"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ if err := services.SaveSiteReminderConfig(p.RetentionDays, p.AutoRead); err != nil {
+ c.jsonErr(500, 500, "保存配置失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
+ _ = c.ServeJSON()
+}
+
+// Send POST /platform/sitereminder/send
+func (c *PlatformSiteReminderController) Send() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ TargetType string `json:"target_type"` // platform, tenant_all, role, tenant
+ TargetRoleID uint64 `json:"target_role_id"`
+ TargetTenantID uint64 `json:"target_tenant_id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ p.Title = strings.TrimSpace(p.Title)
+ p.Content = strings.TrimSpace(p.Content)
+ if p.Title == "" || p.Content == "" {
+ c.jsonErr(400, 400, "标题与内容不能为空")
+ return
+ }
+ if p.TargetType == "" {
+ c.jsonErr(400, 400, "发送目标类型不能为空")
+ return
+ }
+
+ err = services.SendSiteReminder(p.Title, p.Content, uint64(claims.UserID), "platform", p.TargetType, p.TargetRoleID, p.TargetTenantID)
+ if err != nil {
+ c.jsonErr(500, 500, "发送失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发送成功"}
+ _ = c.ServeJSON()
+}
+
+// GetMyList GET /platform/sitereminder/myList
+func (c *PlatformSiteReminderController) GetMyList() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 10)
+ var isRead *int8
+ if isReadStr := c.GetString("isRead"); isReadStr != "" {
+ if val, err := strconv.Atoi(isReadStr); err == nil {
+ v := int8(val)
+ isRead = &v
+ }
+ }
+
+ list, total, err := services.ListReminders(uint64(claims.UserID), "platform", page, pageSize, isRead)
+ if err != nil {
+ c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{
+ "code": 200,
+ "msg": "success",
+ "data": map[string]interface{}{
+ "list": list,
+ "total": total,
+ },
+ }
+ _ = c.ServeJSON()
+}
+
+// MarkRead POST /platform/sitereminder/read
+func (c *PlatformSiteReminderController) MarkRead() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ ID uint64 `json:"id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "platform")
+ if err != nil {
+ c.jsonErr(500, 500, "操作失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
+
+// MarkAllRead POST /platform/sitereminder/readall
+func (c *PlatformSiteReminderController) MarkAllRead() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ err = services.MarkAllRemindersRead(uint64(claims.UserID), "platform")
+ if err != nil {
+ c.jsonErr(500, 500, "操作失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
+
+// Delete POST /platform/sitereminder/delete
+func (c *PlatformSiteReminderController) Delete() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ ID uint64 `json:"id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ err = services.DeleteReminder(p.ID, uint64(claims.UserID), "platform")
+ if err != nil {
+ c.jsonErr(500, 500, "删除失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
+ _ = c.ServeJSON()
+}
+
+// GetSentList GET /platform/sitereminder/sentList
+func (c *PlatformSiteReminderController) GetSentList() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 10)
+
+ list, total, err := services.ListSentReminders(uint64(claims.UserID), page, pageSize)
+ if err != nil {
+ c.jsonErr(500, 500, "获取发送列表失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{
+ "code": 200,
+ "msg": "success",
+ "data": map[string]interface{}{
+ "list": list,
+ "total": total,
+ },
+ }
+ _ = c.ServeJSON()
+}
+
+// UpdateSent POST /platform/sitereminder/updateSent
+func (c *PlatformSiteReminderController) UpdateSent() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ BatchID string `json:"batch_id"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ TargetType string `json:"target_type"`
+ TargetRoleID uint64 `json:"target_role_id"`
+ TargetTenantID uint64 `json:"target_tenant_id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ p.Title = strings.TrimSpace(p.Title)
+ p.Content = strings.TrimSpace(p.Content)
+ if p.BatchID == "" || p.Title == "" || p.Content == "" {
+ c.jsonErr(400, 400, "批次号、标题与内容不能为空")
+ return
+ }
+ if p.TargetType == "" {
+ c.jsonErr(400, 400, "发送目标类型不能为空")
+ return
+ }
+
+ err = services.UpdateSentReminder(p.BatchID, p.Title, p.Content, p.TargetType, p.TargetRoleID, p.TargetTenantID)
+ if err != nil {
+ c.jsonErr(500, 500, "修改失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
+ _ = c.ServeJSON()
+}
+
+// DeleteSentBatch POST /platform/sitereminder/deleteSent
+func (c *PlatformSiteReminderController) DeleteSentBatch() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p struct {
+ BatchID string `json:"batch_id"`
+ }
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ if p.BatchID == "" {
+ c.jsonErr(400, 400, "批次号不能为空")
+ return
+ }
+
+ err = services.DeleteSentReminderBatch(p.BatchID)
+ if err != nil {
+ c.jsonErr(500, 500, "删除失败: "+err.Error())
+ return
+ }
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
+ _ = c.ServeJSON()
+}
diff --git a/go/models/init.go b/go/models/init.go
index 47428dd..b218b12 100644
--- a/go/models/init.go
+++ b/go/models/init.go
@@ -66,6 +66,9 @@ func Init(_ string) {
new(CmsArticleCategory),
new(CmsArticle),
+
+ new(SystemSiteReminder),
+ new(SystemReminderList),
)
// 创建全局 Ormer
diff --git a/go/models/system_reminderlist.go b/go/models/system_reminderlist.go
new file mode 100644
index 0000000..622b87f
--- /dev/null
+++ b/go/models/system_reminderlist.go
@@ -0,0 +1,26 @@
+package models
+
+import "time"
+
+// SystemReminderList 站内信消息列表表 yz_system_reminderlist
+type SystemReminderList struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Title string `orm:"column(title);size(255)" json:"title"`
+ Content string `orm:"column(content);type(text)" json:"content"`
+ SenderID uint64 `orm:"column(sender_id);default(0)" json:"sender_id"`
+ SenderType string `orm:"column(sender_type);size(32);default('system')" json:"sender_type"` // system, platform, tenant
+ ReceiverID uint64 `orm:"column(receiver_id)" json:"receiver_id"`
+ ReceiverType string `orm:"column(receiver_type);size(32)" json:"receiver_type"` // platform, tenant
+ IsRead int8 `orm:"column(is_read);default(0)" json:"is_read"` // 0-未读, 1-已读
+ ReadTime *time.Time `orm:"column(read_time);type(datetime);null" json:"read_time"`
+ CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+ BatchID string `orm:"column(batch_id);size(64);default('')" json:"batch_id"`
+ TargetType string `orm:"column(target_type);size(32);default('')" json:"target_type"`
+ TargetRoleID uint64 `orm:"column(target_role_id);default(0)" json:"target_role_id"`
+ TargetTenantID uint64 `orm:"column(target_tenant_id);default(0)" json:"target_tenant_id"`
+}
+
+func (m *SystemReminderList) TableName() string {
+ return "yz_system_reminderlist"
+}
diff --git a/go/models/system_sitereminder.go b/go/models/system_sitereminder.go
new file mode 100644
index 0000000..c08af03
--- /dev/null
+++ b/go/models/system_sitereminder.go
@@ -0,0 +1,16 @@
+package models
+
+import "time"
+
+// SystemSiteReminder 站内信配置表 yz_system_sitereminder
+type SystemSiteReminder struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ RetentionDays int `orm:"column(retention_days);default(30)" json:"retention_days"`
+ AutoRead int8 `orm:"column(auto_read);default(0)" json:"auto_read"`
+ CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+}
+
+func (m *SystemSiteReminder) TableName() string {
+ return "yz_system_sitereminder"
+}
diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go
index 1cc327a..1c5eecc 100644
--- a/go/routers/backend/backend.go
+++ b/go/routers/backend/backend.go
@@ -51,6 +51,12 @@ func RegisterAuthRoutes() {
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
+ // 站内信(yz_system_reminderlist)
+ beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList")
+ beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead")
+ beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead")
+ beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete")
+
// 文件管理(yz_system_files / yz_system_files_category)
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index e004672..a754fb1 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -126,6 +126,17 @@ func Register() {
beego.Router("/platform/email/editinfo", &controllers.PlatformEmailController{}, "post:EditInfo")
beego.Router("/platform/email/sendtestemail", &controllers.PlatformEmailController{}, "post:SendTestEmail")
+ // 站内信配置与发送(yz_system_sitereminder / yz_system_reminderlist)
+ beego.Router("/platform/sitereminder/config", &controllers.PlatformSiteReminderController{}, "get:GetConfig;post:SaveConfig")
+ beego.Router("/platform/sitereminder/send", &controllers.PlatformSiteReminderController{}, "post:Send")
+ beego.Router("/platform/sitereminder/myList", &controllers.PlatformSiteReminderController{}, "get:GetMyList")
+ beego.Router("/platform/sitereminder/read", &controllers.PlatformSiteReminderController{}, "post:MarkRead")
+ beego.Router("/platform/sitereminder/readall", &controllers.PlatformSiteReminderController{}, "post:MarkAllRead")
+ beego.Router("/platform/sitereminder/delete", &controllers.PlatformSiteReminderController{}, "post:Delete")
+ beego.Router("/platform/sitereminder/sentList", &controllers.PlatformSiteReminderController{}, "get:GetSentList")
+ beego.Router("/platform/sitereminder/updateSent", &controllers.PlatformSiteReminderController{}, "post:UpdateSent")
+ beego.Router("/platform/sitereminder/deleteSent", &controllers.PlatformSiteReminderController{}, "post:DeleteSentBatch")
+
// 短信配置(yz_system_sms)
beego.Router("/platform/sms/info", &controllers.PlatformSMSController{}, "get:GetSmsInfo")
beego.Router("/platform/sms/editinfo", &controllers.PlatformSMSController{}, "post:EditSmsInfo")
diff --git a/go/services/system_email_store.go b/go/services/system_email_store.go
index 7defd3d..d2a5ea9 100644
--- a/go/services/system_email_store.go
+++ b/go/services/system_email_store.go
@@ -25,9 +25,6 @@ func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, p
if timeout == 0 {
timeout = 30
}
- if status == 0 {
- status = 1
- }
fromAddress = strings.TrimSpace(fromAddress)
host = strings.TrimSpace(host)
diff --git a/go/services/system_sitereminder.go b/go/services/system_sitereminder.go
new file mode 100644
index 0000000..d0eccc0
--- /dev/null
+++ b/go/services/system_sitereminder.go
@@ -0,0 +1,399 @@
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/beego/beego/v2/client/orm"
+ "server/models"
+)
+
+// GetSiteReminderConfig 获取站内信配置(只读首条记录,不存在则初始化默认值)
+func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
+ var row models.SystemSiteReminder
+ err := models.Orm.QueryTable(new(models.SystemSiteReminder)).OrderBy("id").Limit(1).One(&row)
+ if err == orm.ErrNoRows {
+ // 默认配置
+ now := time.Now()
+ row = models.SystemSiteReminder{
+ RetentionDays: 30,
+ AutoRead: 0,
+ CreateTime: &now,
+ UpdateTime: &now,
+ }
+ _, err = models.Orm.Insert(&row)
+ if err != nil {
+ return row, err
+ }
+ return row, nil
+ }
+ return row, err
+}
+
+// SaveSiteReminderConfig 保存/更新配置
+func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
+ if retentionDays <= 0 {
+ retentionDays = 30
+ }
+ cfg, err := GetSiteReminderConfig()
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ cfg.RetentionDays = retentionDays
+ cfg.AutoRead = autoRead
+ cfg.UpdateTime = &now
+
+ _, err = models.Orm.Update(&cfg, "RetentionDays", "AutoRead", "UpdateTime")
+ return err
+}
+
+// SendSiteReminder 发送站内信
+// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
+func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
+ var receiverIDs []uint64
+ var receiverType string
+
+ switch targetType {
+ case "platform":
+ receiverType = "platform"
+ var list []models.AdminUser
+ _, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("查询平台用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "tenant_all":
+ receiverType = "tenant"
+ var list []models.SystemTenantUser
+ _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("查询租户用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "role":
+ receiverType = "platform"
+ var list []models.AdminUser
+ _, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("根据角色查询用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "tenant":
+ receiverType = "tenant"
+ var list []models.SystemTenantUser
+ _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("根据租户查询用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ default:
+ return fmt.Errorf("未知的发送目标类型: %s", targetType)
+ }
+
+ if len(receiverIDs) == 0 {
+ return nil
+ }
+
+ now := time.Now()
+ batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
+ var reminders []models.SystemReminderList
+ for _, rid := range receiverIDs {
+ reminders = append(reminders, models.SystemReminderList{
+ Title: title,
+ Content: content,
+ SenderID: senderID,
+ SenderType: senderType,
+ ReceiverID: rid,
+ ReceiverType: receiverType,
+ IsRead: 0,
+ CreateTime: &now,
+ BatchID: batchID,
+ TargetType: targetType,
+ TargetRoleID: targetRoleID,
+ TargetTenantID: targetTenantID,
+ })
+ }
+
+ // 批量插入
+ _, err := models.Orm.InsertMulti(100, reminders)
+ return err
+}
+
+// ListReminders 列表查询
+func ListReminders(receiverID uint64, receiverType string, page, pageSize int, isRead *int8) ([]models.SystemReminderList, int64, error) {
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 10
+ }
+ var list []models.SystemReminderList
+ qs := models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("receiver_id", receiverID).
+ Filter("receiver_type", receiverType).
+ Filter("delete_time__isnull", true)
+
+ if isRead != nil {
+ qs = qs.Filter("is_read", *isRead)
+ }
+
+ total, err := qs.Count()
+ if err != nil {
+ return nil, 0, err
+ }
+
+ offset := (page - 1) * pageSize
+ _, err = qs.OrderBy("-create_time", "-id").Limit(pageSize, offset).All(&list)
+ return list, total, err
+}
+
+// MarkReminderRead 标记单条已读
+func MarkReminderRead(id uint64, receiverID uint64, receiverType string) error {
+ now := time.Now()
+ _, err := models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("id", id).
+ Filter("receiver_id", receiverID).
+ Filter("receiver_type", receiverType).
+ Update(map[string]interface{}{
+ "is_read": 1,
+ "read_time": &now,
+ })
+ return err
+}
+
+// MarkAllRemindersRead 一键全部已读
+func MarkAllRemindersRead(receiverID uint64, receiverType string) error {
+ now := time.Now()
+ _, err := models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("receiver_id", receiverID).
+ Filter("receiver_type", receiverType).
+ Filter("is_read", 0).
+ Update(map[string]interface{}{
+ "is_read": 1,
+ "read_time": &now,
+ })
+ return err
+}
+
+// DeleteReminder 删除消息
+func DeleteReminder(id uint64, receiverID uint64, receiverType string) error {
+ now := time.Now()
+ _, err := models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("id", id).
+ Filter("receiver_id", receiverID).
+ Filter("receiver_type", receiverType).
+ Update(map[string]interface{}{
+ "delete_time": &now,
+ })
+ return err
+}
+
+// AutoCleanExpiredReminders 自动清理过期站内信
+func AutoCleanExpiredReminders() error {
+ cfg, err := GetSiteReminderConfig()
+ if err != nil {
+ return err
+ }
+ if cfg.RetentionDays <= 0 {
+ return nil
+ }
+ expireTime := time.Now().AddDate(0, 0, -cfg.RetentionDays)
+ _, err = models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("create_time__lt", expireTime).
+ Delete()
+ return err
+}
+
+// ListSentReminders 获取已发送的消息列表(按 batch_id 分组)
+func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemReminderList, int64, error) {
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 10
+ }
+ offset := (page - 1) * pageSize
+
+ var total int64
+ err := models.Orm.Raw("SELECT COUNT(DISTINCT batch_id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL", senderID).QueryRow(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ var list []models.SystemReminderList
+ _, err = models.Orm.Raw("SELECT * FROM yz_system_reminderlist WHERE id IN (SELECT MIN(id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL GROUP BY batch_id) ORDER BY id DESC LIMIT ? OFFSET ?", senderID, pageSize, offset).QueryRows(&list)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return list, total, nil
+}
+
+// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
+func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
+ // 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
+ var firstRecord models.SystemReminderList
+ err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
+ if err != nil {
+ return fmt.Errorf("找不到该批次的站内信记录: %w", err)
+ }
+ senderID := firstRecord.SenderID
+ senderType := firstRecord.SenderType
+
+ // 2. 根据新的目标接收群体获取接收人列表
+ var receiverIDs []uint64
+ var receiverType string
+
+ switch targetType {
+ case "platform":
+ receiverType = "platform"
+ var list []models.AdminUser
+ _, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("查询平台用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "tenant_all":
+ receiverType = "tenant"
+ var list []models.SystemTenantUser
+ _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("查询租户用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "role":
+ receiverType = "platform"
+ var list []models.AdminUser
+ _, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("根据角色查询用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ case "tenant":
+ receiverType = "tenant"
+ var list []models.SystemTenantUser
+ _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
+ if err != nil {
+ return fmt.Errorf("根据租户查询用户失败: %w", err)
+ }
+ for _, u := range list {
+ receiverIDs = append(receiverIDs, u.ID)
+ }
+ default:
+ return fmt.Errorf("未知的发送目标类型: %s", targetType)
+ }
+
+ // 3. 获取该批次中现有的所有记录 (包括已删除的)
+ var existingRecords []models.SystemReminderList
+ _, err = models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).All(&existingRecords)
+ if err != nil {
+ return fmt.Errorf("获取现有记录失败: %w", err)
+ }
+
+ // 建立 map 快速查找,Key 为 "receiverType_receiverID"
+ existingMap := make(map[string]*models.SystemReminderList)
+ for i := range existingRecords {
+ key := fmt.Sprintf("%s_%d", existingRecords[i].ReceiverType, existingRecords[i].ReceiverID)
+ existingMap[key] = &existingRecords[i]
+ }
+
+ newReceiverMap := make(map[string]bool)
+ for _, rid := range receiverIDs {
+ key := fmt.Sprintf("%s_%d", receiverType, rid)
+ newReceiverMap[key] = true
+ }
+
+ now := time.Now()
+
+ // 事务处理
+ err = models.Orm.DoTx(func(c context.Context, txOrm orm.TxOrmer) error {
+ // A. 对于已经不在新接收者列表中的用户,软删除
+ for key, rec := range existingMap {
+ if !newReceiverMap[key] {
+ if rec.DeleteTime == nil {
+ rec.DeleteTime = &now
+ if _, e := txOrm.Update(rec, "DeleteTime"); e != nil {
+ return e
+ }
+ }
+ }
+ }
+
+ // B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
+ var newInserts []models.SystemReminderList
+ for _, rid := range receiverIDs {
+ key := fmt.Sprintf("%s_%d", receiverType, rid)
+ if rec, exists := existingMap[key]; exists {
+ rec.Title = title
+ rec.Content = content
+ rec.TargetType = targetType
+ rec.TargetRoleID = targetRoleID
+ rec.TargetTenantID = targetTenantID
+
+ cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
+ if rec.DeleteTime != nil {
+ rec.DeleteTime = nil
+ rec.IsRead = 0
+ rec.ReadTime = nil
+ cols = append(cols, "DeleteTime", "IsRead", "ReadTime")
+ }
+ if _, e := txOrm.Update(rec, cols...); e != nil {
+ return e
+ }
+ } else {
+ // C. 对于新增加的接收者,插入新记录
+ newInserts = append(newInserts, models.SystemReminderList{
+ Title: title,
+ Content: content,
+ SenderID: senderID,
+ SenderType: senderType,
+ ReceiverID: rid,
+ ReceiverType: receiverType,
+ IsRead: 0,
+ CreateTime: &now,
+ BatchID: batchID,
+ TargetType: targetType,
+ TargetRoleID: targetRoleID,
+ TargetTenantID: targetTenantID,
+ })
+ }
+ }
+
+ if len(newInserts) > 0 {
+ if _, e := txOrm.InsertMulti(100, newInserts); e != nil {
+ return e
+ }
+ }
+
+ return nil
+ })
+
+ return err
+}
+
+// DeleteSentReminderBatch 删除已发送消息(删除该批次下所有记录)
+func DeleteSentReminderBatch(batchID string) error {
+ now := time.Now()
+ _, err := models.Orm.QueryTable(new(models.SystemReminderList)).
+ Filter("batch_id", batchID).
+ Update(map[string]interface{}{
+ "delete_time": &now,
+ })
+ return err
+}
diff --git a/platform/components.d.ts b/platform/components.d.ts
index 414b83d..7ffac2a 100644
--- a/platform/components.d.ts
+++ b/platform/components.d.ts
@@ -17,6 +17,7 @@ declare module 'vue' {
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
+ ElBadge: typeof import('element-plus/es')['ElBadge']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
diff --git a/platform/src/api/sitereminder.js b/platform/src/api/sitereminder.js
new file mode 100644
index 0000000..6f6b71b
--- /dev/null
+++ b/platform/src/api/sitereminder.js
@@ -0,0 +1,89 @@
+import request from "@/utils/request";
+
+/** 获取站内信配置 */
+export function getSiteReminderConfig() {
+ return request({
+ url: "/platform/sitereminder/config",
+ method: "get",
+ });
+}
+
+/** 保存站内信配置 */
+export function saveSiteReminderConfig(data) {
+ return request({
+ url: "/platform/sitereminder/config",
+ method: "post",
+ data,
+ });
+}
+
+/** 发送站内信 */
+export function sendSiteReminder(data) {
+ return request({
+ url: "/platform/sitereminder/send",
+ method: "post",
+ data,
+ });
+}
+
+/** 获取我的消息列表 */
+export function getMySiteReminders(params) {
+ return request({
+ url: "/platform/sitereminder/myList",
+ method: "get",
+ params,
+ });
+}
+
+/** 标记消息为已读 */
+export function readSiteReminder(id) {
+ return request({
+ url: "/platform/sitereminder/read",
+ method: "post",
+ data: { id },
+ });
+}
+
+/** 一键全部已读 */
+export function readAllSiteReminders() {
+ return request({
+ url: "/platform/sitereminder/readall",
+ method: "post",
+ });
+}
+
+/** 删除消息 */
+export function deleteSiteReminder(id) {
+ return request({
+ url: "/platform/sitereminder/delete",
+ method: "post",
+ data: { id },
+ });
+}
+
+/** 获取已发送的站内信列表(按 batch_id 分组) */
+export function getSentSiteReminders(params) {
+ return request({
+ url: "/platform/sitereminder/sentList",
+ method: "get",
+ params,
+ });
+}
+
+/** 编辑/更新已发送的站内信 */
+export function updateSentSiteReminder(data) {
+ return request({
+ url: "/platform/sitereminder/updateSent",
+ method: "post",
+ data,
+ });
+}
+
+/** 删除已发送的站内信批次 */
+export function deleteSentSiteReminderBatch(batch_id) {
+ return request({
+ url: "/platform/sitereminder/deleteSent",
+ method: "post",
+ data: { batch_id },
+ });
+}
diff --git a/platform/src/components/CommonHeader.vue b/platform/src/components/CommonHeader.vue
index 45d1ad6..d7e2664 100644
--- a/platform/src/components/CommonHeader.vue
+++ b/platform/src/components/CommonHeader.vue
@@ -24,18 +24,48 @@
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
-
+
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+ 加载中...
+
+
+
+ 暂无新消息
+
+
+
+
@@ -66,6 +96,12 @@
+
+
@@ -77,8 +113,10 @@ const emit = defineEmits(['collapse']);
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout, getCurrentUser } from "@/api/login";
-import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand } from '@element-plus/icons-vue';
+import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand, Loading, Message } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
+import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
+import MessageDetailDialog from "@/views/basicSettings/sitereminder/components/detail.vue";
const router = useRouter();
const route = useRoute();
@@ -133,6 +171,87 @@ async function refreshCache() {
}
}
+// 站内信消息中心逻辑
+const unreadCount = ref(0);
+const messages = ref([]);
+const loadingMessages = ref(false);
+const detailVisible = ref(false);
+const currentReminder = ref({});
+
+const formatTime = (timeStr: any) => {
+ if (!timeStr) return "-";
+ const date = new Date(timeStr);
+ return date.toLocaleString();
+};
+
+const fetchUnreadCount = async () => {
+ if (!authStore.token) return;
+ try {
+ const res = await getMySiteReminders({ page: 1, pageSize: 1, isRead: 0 });
+ if (res.code === 200) {
+ unreadCount.value = res.data.total || 0;
+ }
+ } catch (err) {
+ console.error("fetchUnreadCount failed", err);
+ }
+};
+
+const fetchMessages = async () => {
+ if (!authStore.token) return;
+ loadingMessages.value = true;
+ try {
+ const res = await getMySiteReminders({ page: 1, pageSize: 5 });
+ if (res.code === 200) {
+ messages.value = res.data.list || [];
+ }
+ } catch (err) {
+ console.error("fetchMessages failed", err);
+ } finally {
+ loadingMessages.value = false;
+ }
+};
+
+const handleDropdownVisibleChange = (visible: boolean) => {
+ if (visible) {
+ fetchMessages();
+ fetchUnreadCount();
+ }
+};
+
+const handleMessageClick = (item: any) => {
+ currentReminder.value = item;
+ detailVisible.value = true;
+};
+
+const handleReadSuccess = (id: number) => {
+ const msg = messages.value.find(m => m.id === id);
+ if (msg && msg.is_read === 0) {
+ msg.is_read = 1;
+ unreadCount.value = Math.max(0, unreadCount.value - 1);
+ }
+ fetchUnreadCount();
+};
+
+const handleMarkAllRead = async () => {
+ try {
+ const res = await readAllSiteReminders();
+ if (res.code === 200) {
+ ElMessage.success("全部已读标记成功");
+ unreadCount.value = 0;
+ messages.value.forEach(m => m.is_read = 1);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+let timer: any = null;
+
+const handleMessagesChanged = () => {
+ fetchUnreadCount();
+ fetchMessages();
+};
+
onMounted(async () => {
await loadMenu();
if (!authStore.token) return;
@@ -144,6 +263,10 @@ onMounted(async () => {
} catch (e) {
console.error("getCurrentUser failed", e);
}
+
+ fetchUnreadCount();
+ timer = setInterval(fetchUnreadCount, 60000);
+ window.addEventListener('site-messages-changed', handleMessagesChanged);
});
// 根据菜单列表和当前路径计算出的面包屑导航
@@ -346,6 +469,10 @@ onUnmounted(() => {
if (mediaQuery && handleChange) {
mediaQuery.removeEventListener('change', handleChange);
}
+ if (timer) {
+ clearInterval(timer);
+ }
+ window.removeEventListener('site-messages-changed', handleMessagesChanged);
});
@@ -565,4 +692,145 @@ onUnmounted(() => {
:deep(.el-button) {
margin-left: 0 !important;
}
+
+/* 消息中心下拉列表样式 */
+.message-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.message-dropdown-container {
+ width: 320px;
+ background-color: var(--el-bg-color-overlay);
+ border: 1px solid var(--el-border-color-light);
+ border-radius: 8px;
+ box-shadow: var(--el-box-shadow-light);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.message-dropdown-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--el-border-color-lighter);
+ background-color: var(--el-fill-color-blank);
+
+ .title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ }
+
+ .mark-all-btn {
+ font-size: 12px;
+ }
+}
+
+.loading-state,
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 30px 0;
+ color: var(--el-text-color-secondary);
+ font-size: 13px;
+ gap: 8px;
+
+ .el-icon {
+ font-size: 20px;
+ }
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.message-item {
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ border-bottom: 1px solid var(--el-border-color-extra-light);
+ text-align: left; /* Ensure it is left-aligned */
+
+ &:hover {
+ background-color: var(--el-fill-color-light);
+ }
+
+ &.unread {
+ background-color: var(--el-color-primary-light-9);
+
+ &:hover {
+ background-color: var(--el-color-primary-light-8);
+ }
+
+ .message-title {
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ position: relative;
+ padding-left: 10px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 6px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--el-color-danger);
+ }
+ }
+ }
+}
+
+.message-item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 4px;
+ gap: 8px;
+
+ .message-title {
+ font-size: 13px;
+ color: var(--el-text-color-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ text-align: left;
+ }
+
+ .message-time {
+ font-size: 11px;
+ color: var(--el-text-color-secondary);
+ white-space: nowrap;
+ }
+}
+
+.message-item-brief {
+ font-size: 12px;
+ color: var(--el-text-color-regular);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: left;
+}
+
+.message-dropdown-footer {
+ padding: 8px;
+ border-top: 1px solid var(--el-border-color-lighter);
+ text-align: center;
+ background-color: var(--el-fill-color-blank);
+
+ .el-button {
+ font-size: 13px;
+ width: 100%;
+ }
+}
diff --git a/platform/src/views/basicSettings/sitereminder/components/detail.vue b/platform/src/views/basicSettings/sitereminder/components/detail.vue
new file mode 100644
index 0000000..9cf4878
--- /dev/null
+++ b/platform/src/views/basicSettings/sitereminder/components/detail.vue
@@ -0,0 +1,118 @@
+
+
+
+
{{ reminder.title }}
+
+
+
+ 发送者:{{ getSenderName(reminder) }}
+
+
+
+ 时间:{{ formatTime(reminder.create_time) }}
+
+
+
+
{{ reminder.content }}
+
+
+ 关闭
+
+
+
+
+
+
+
diff --git a/platform/src/views/basicSettings/sitereminder/components/edit.vue b/platform/src/views/basicSettings/sitereminder/components/edit.vue
new file mode 100644
index 0000000..b14fd81
--- /dev/null
+++ b/platform/src/views/basicSettings/sitereminder/components/edit.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+ 平台端
+ 管理端 (所有租户)
+ 平台角色
+ 特定租户
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/src/views/basicSettings/sitereminder/index.vue b/platform/src/views/basicSettings/sitereminder/index.vue
new file mode 100644
index 0000000..88a178e
--- /dev/null
+++ b/platform/src/views/basicSettings/sitereminder/index.vue
@@ -0,0 +1,280 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.title }}
+
+
+
+
+
+
+ {{ getTargetLabel(row) }}
+
+
+
+
+
+ {{ formatTime(row.create_time) }}
+
+
+
+
+
+
+ 编辑
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/src/views/system/email/index.vue b/platform/src/views/system/email/index.vue
index 0a97105..a280a71 100644
--- a/platform/src/views/system/email/index.vue
+++ b/platform/src/views/system/email/index.vue
@@ -75,6 +75,16 @@
/>
+
+
+
+
{
emailForm.password = item.password || "";
emailForm.encryption = item.encryption || "ssl";
emailForm.timeout = item.timeout != null ? item.timeout : 30;
+ emailForm.status = item.status != null ? item.status : 1;
} else {
hasSavedConfig.value = false;
}
@@ -211,6 +223,7 @@ const handleReset = () => {
emailForm.password = "";
emailForm.encryption = "ssl";
emailForm.timeout = 30;
+ emailForm.status = 1;
testEmail.value = "";
emailFormRef.value?.clearValidate();
};
diff --git a/platform/src/views/system/platformsettings/components/notificationSettings.vue b/platform/src/views/system/platformsettings/components/notificationSettings.vue
index 53af57c..9889001 100644
--- a/platform/src/views/system/platformsettings/components/notificationSettings.vue
+++ b/platform/src/views/system/platformsettings/components/notificationSettings.vue
@@ -156,7 +156,8 @@
-
+
+ 站内信功能为系统核心功能,必须开启
@@ -393,6 +394,7 @@ import {
} from "@element-plus/icons-vue";
import { getSmsInfo, editSmsInfo, sendTestSms } from "@/api/sms";
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
+import { getSiteReminderConfig, saveSiteReminderConfig } from "@/api/sitereminder";
const STORAGE_KEY = "notification_settings_draft";
const activeSubTab = ref("email");
@@ -422,7 +424,7 @@ const formData = reactive({
testPhone: "",
},
sitemsg: {
- enabled: false,
+ enabled: true,
retention_days: 30,
auto_read: false,
},
@@ -491,7 +493,7 @@ const loadEmailConfig = async () => {
formData.email.password = item.password || "";
formData.email.encryption = item.encryption || "ssl";
formData.email.timeout = item.timeout != null ? item.timeout : 30;
- formData.email.enabled = Number(item.enabled ?? 0) === 1;
+ formData.email.enabled = Number(item.status ?? item.enabled ?? 0) === 1;
}
} catch (error) {
console.error("加载邮箱配置失败:", error);
@@ -511,6 +513,19 @@ const loadSmsConfig = async () => {
}
};
+const loadSiteReminderConfig = async () => {
+ try {
+ const res = await getSiteReminderConfig();
+ if (res.code === 200 && res.data) {
+ formData.sitemsg.retention_days = res.data.retention_days ?? 30;
+ formData.sitemsg.auto_read = res.data.auto_read === 1;
+ formData.sitemsg.enabled = true;
+ }
+ } catch (error) {
+ console.error("加载站内信配置失败:", error);
+ }
+};
+
const saveDraft = () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(formData));
};
@@ -519,7 +534,7 @@ const handleReset = () => {
const defaults = {
email: { enabled: false, fromAddress: "", fromName: "", host: "", port: "", password: "", encryption: "ssl", timeout: 30, testEmail: "" },
sms: { enabled: false, provider: "aliyun", access_key: "", secret_key: "", sign_name: "", template_code: "", backendUrl: "", apiKey: "", testPhone: "" },
- sitemsg: { enabled: false, retention_days: 30, auto_read: false },
+ sitemsg: { enabled: true, retention_days: 30, auto_read: false },
dingtalk: { enabled: false, webhook_url: "", secret: "" },
webhook: { enabled: false, url: "", method: "POST", token: "" },
feishu: { enabled: false, webhook_url: "", secret: "" },
@@ -612,7 +627,7 @@ const handleSubmit = async () => {
password: formData.email.password,
encryption: formData.email.encryption,
timeout: formData.email.timeout,
- enabled: formData.email.enabled ? 1 : 0
+ status: formData.email.enabled ? 1 : 0
});
if (res.code === 200) {
ElMessage.success("邮箱配置保存成功");
@@ -634,6 +649,16 @@ const handleSubmit = async () => {
} else {
throw new Error(res.msg || "保存自定义短信配置失败");
}
+ } else if (activeSubTab.value === 'sitemsg') {
+ const res = await saveSiteReminderConfig({
+ retention_days: formData.sitemsg.retention_days,
+ auto_read: formData.sitemsg.auto_read ? 1 : 0
+ });
+ if (res.code === 200) {
+ ElMessage.success("站内信配置保存成功");
+ } else {
+ throw new Error(res.msg || "保存站内信配置失败");
+ }
}
saveDraft();
@@ -649,6 +674,7 @@ onMounted(() => {
loadDraft();
loadSmsConfig();
loadEmailConfig();
+ loadSiteReminderConfig();
});
diff --git a/sql/upgrade_yz_system_reminderlist.sql b/sql/upgrade_yz_system_reminderlist.sql
new file mode 100644
index 0000000..792b6be
--- /dev/null
+++ b/sql/upgrade_yz_system_reminderlist.sql
@@ -0,0 +1,12 @@
+-- 升级已有的 yz_system_reminderlist 表,添加 batch_id 以及发送目标相关字段
+ALTER TABLE `yz_system_reminderlist`
+ ADD COLUMN `batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID' AFTER `delete_time`,
+ ADD COLUMN `target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant' AFTER `batch_id`,
+ ADD COLUMN `target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID' AFTER `target_type`,
+ ADD COLUMN `target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID' AFTER `target_role_id`,
+ ADD INDEX `idx_batch` (`batch_id`);
+
+-- 初始化已有记录的 batch_id (如果存在空值,使用 create_time 和 sender_id 进行分组初始化)
+UPDATE `yz_system_reminderlist`
+SET `batch_id` = CONCAT(UNIX_TIMESTAMP(COALESCE(`create_time`, NOW())), '_', `sender_id`)
+WHERE `batch_id` = '' OR `batch_id` IS NULL;
diff --git a/sql/yz_system_sitereminder.sql b/sql/yz_system_sitereminder.sql
new file mode 100644
index 0000000..e609d58
--- /dev/null
+++ b/sql/yz_system_sitereminder.sql
@@ -0,0 +1,36 @@
+-- 站内信配置表
+CREATE TABLE IF NOT EXISTS `yz_system_sitereminder` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `retention_days` int(11) NOT NULL DEFAULT '30' COMMENT '消息保留天数',
+ `auto_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否自动标记已读: 0-否, 1-是',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信配置表';
+
+-- 默认插入一条配置数据
+INSERT INTO `yz_system_sitereminder` (`id`, `retention_days`, `auto_read`, `create_time`, `update_time`)
+VALUES (1, 30, 0, NOW(), NOW())
+ON DUPLICATE KEY UPDATE `update_time` = NOW();
+
+-- 站内信消息列表表
+CREATE TABLE IF NOT EXISTS `yz_system_reminderlist` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+ `title` varchar(255) NOT NULL COMMENT '标题',
+ `content` text NOT NULL COMMENT '内容',
+ `sender_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '发送者ID (0为系统)',
+ `sender_type` varchar(32) NOT NULL DEFAULT 'system' COMMENT '发送者类型: system, platform, tenant',
+ `receiver_id` bigint(20) unsigned NOT NULL COMMENT '接收者ID',
+ `receiver_type` varchar(32) NOT NULL DEFAULT 'receiver' COMMENT '接收者类型: platform, tenant',
+ `is_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否已读: 0-未读, 1-已读',
+ `read_time` datetime DEFAULT NULL COMMENT '已读时间',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间/发送时间',
+ `delete_time` datetime DEFAULT NULL COMMENT '删除时间',
+ `batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID',
+ `target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant',
+ `target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID',
+ `target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID',
+ PRIMARY KEY (`id`),
+ KEY `idx_receiver` (`receiver_type`, `receiver_id`, `is_read`),
+ KEY `idx_batch` (`batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信消息列表表';
From 6ee1f261243f1d0868f8388e0bf22be558fe87ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com>
Date: Wed, 17 Jun 2026 23:49:19 +0800
Subject: [PATCH 3/5] =?UTF-8?q?=E5=A2=9E=E5=8A=A0bark=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
go/controllers/api_getcard.go | 63 ++++-
go/controllers/backend_login_verify.go | 45 +---
go/controllers/platform_bark.go | 215 ++++++++++++++++++
go/controllers/platform_cursor_equipment.go | 10 +-
go/controllers/platform_login_verify.go | 43 +---
go/controllers/platform_sms.go | 109 ++++-----
go/models/init.go | 8 +-
go/models/platform_account_pool.go | 1 +
go/models/platform_login_verify.go | 122 +++++++++-
go/models/system_email.go | 4 +-
go/models/system_normal_setting.go | 51 +++++
go/models/system_sitereminder.go | 4 +-
go/models/system_sms.go | 4 +-
go/routers/platform/platform.go | 5 +
go/services/login_verify_code.go | 22 +-
go/services/system_email_store.go | 147 ++++++++----
go/services/system_sitereminder.go | 96 +++++---
platform/src/api/sitesettings.js | 37 +++
.../components/notificationSettings.vue | 58 +++++
19 files changed, 784 insertions(+), 260 deletions(-)
create mode 100644 go/controllers/platform_bark.go
create mode 100644 go/models/system_normal_setting.go
diff --git a/go/controllers/api_getcard.go b/go/controllers/api_getcard.go
index f0a87a7..2abb825 100644
--- a/go/controllers/api_getcard.go
+++ b/go/controllers/api_getcard.go
@@ -66,6 +66,15 @@ func (c *ApiGetCardController) GetCard() {
return
}
+ // 读取机器码/MAC
+ machineCode := strings.TrimSpace(c.GetString("machine_code"))
+ if machineCode == "" {
+ machineCode = strings.TrimSpace(c.GetString("machineCode"))
+ }
+ if machineCode == "" {
+ machineCode = strings.TrimSpace(c.GetString("mac"))
+ }
+
// 参数校验
if platform == "" {
c.cardErr(400, 400, "缺少参数 type(来源平台)")
@@ -92,7 +101,7 @@ func (c *ApiGetCardController) GetCard() {
switch module {
case "cursor":
- c.extractCursor(platform, dataType, startID, now)
+ c.extractCursor(platform, dataType, startID, now, machineCode)
case "windsurf":
c.extractWindsurf(platform, dataType, startID, now)
case "krio":
@@ -121,8 +130,25 @@ func (c *ApiGetCardController) readOptionalStartID() (uint64, error) {
return id, nil
}
-func (c *ApiGetCardController) extractCursor(platform, dataType string, startID uint64, now time.Time) {
- c.extractWithProbe("cursor", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
+func (c *ApiGetCardController) extractCursor(platform, dataType string, startID uint64, now time.Time, machineCode string) {
+ // 优先查询该机器码是否已经绑定过未删除的卡密
+ if machineCode != "" {
+ var existing models.PlatformAccountPoolCursor
+ err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
+ Filter("machine_code", machineCode).
+ Filter("delete_time__isnull", true).
+ Exclude("is_used", 0).
+ OrderBy("-id").
+ Limit(1).
+ One(&existing)
+ if err == nil {
+ // 直接返回已绑定的卡密信息
+ c.cardOK(buildCardResult(&existing.Account, &existing.Password, existing.Token, existing.DataType))
+ return
+ }
+ }
+
+ c.extractWithProbe("cursor", platform, dataType, now, machineCode, func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolCursor
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("is_extracted", 0).
@@ -141,7 +167,7 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, startID
}
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, startID uint64, now time.Time) {
- c.extractWithProbe("windsurf", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
+ c.extractWithProbe("windsurf", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolWindsurf
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
Filter("is_extracted", 0).
@@ -160,7 +186,7 @@ func (c *ApiGetCardController) extractWindsurf(platform, dataType string, startI
}
func (c *ApiGetCardController) extractKrio(platform, dataType string, startID uint64, now time.Time) {
- c.extractWithProbe("krio", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
+ c.extractWithProbe("krio", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolKiro
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
Filter("is_extracted", 0).
@@ -179,7 +205,7 @@ func (c *ApiGetCardController) extractKrio(platform, dataType string, startID ui
}
func (c *ApiGetCardController) extractCodex(platform, dataType string, startID uint64, now time.Time) {
- c.extractWithProbe("codex", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
+ c.extractWithProbe("codex", platform, dataType, now, "", func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolCodex
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCodex)).
Filter("is_extracted", 0).
@@ -203,6 +229,7 @@ type poolRowFetcher func() (id uint64, account, password *string, token, rowData
func (c *ApiGetCardController) extractWithProbe(
module, platform, dataType string,
now time.Time,
+ machineCode string,
fetch poolRowFetcher,
) {
for {
@@ -221,14 +248,20 @@ func (c *ApiGetCardController) extractWithProbe(
c.cardErr(500, 500, "无效模块")
return
}
+
+ params := map[string]interface{}{
+ "is_extracted": 1,
+ "extracted_time": now,
+ "extracted_platform": platform,
+ "update_time": now,
+ }
+ if module == "cursor" && machineCode != "" {
+ params["machine_code"] = machineCode
+ }
+
_, err = models.Orm.QueryTable(tableName).
Filter("id", id).
- Update(map[string]interface{}{
- "is_extracted": 1,
- "extracted_time": now,
- "extracted_platform": platform,
- "update_time": now,
- })
+ Update(params)
if err != nil {
c.cardErr(500, 500, "提取失败")
return
@@ -240,10 +273,16 @@ func (c *ApiGetCardController) extractWithProbe(
c.cardOK(buildCardResult(account, password, token, rowDataType))
return
}
+ if module == "cursor" && machineCode != "" {
+ _, _ = models.Orm.QueryTable(tableName).Filter("id", id).Update(map[string]interface{}{"machine_code": "", "update_time": time.Now()})
+ }
continue
}
if !poolProbeToken(module, rowDataType, token, id) {
+ if module == "cursor" && machineCode != "" {
+ _, _ = models.Orm.QueryTable(tableName).Filter("id", id).Update(map[string]interface{}{"machine_code": "", "update_time": time.Now()})
+ }
continue
}
diff --git a/go/controllers/backend_login_verify.go b/go/controllers/backend_login_verify.go
index 9a8e0a9..ed9684c 100644
--- a/go/controllers/backend_login_verify.go
+++ b/go/controllers/backend_login_verify.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"io"
"strings"
- "time"
"server/models"
"server/pkg/jwtutil"
@@ -209,39 +208,17 @@ func (c *BackendLoginVerifyController) SaveLoginVerifyInfos() {
}
}
- now := time.Now()
- var existed models.PlatformLoginVerify
- err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
- if err == nil {
- _, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).
- Filter("id", existed.ID).
- Update(map[string]interface{}{
- "open_verify_enabled": openVerifyEnabled,
- "verify_type": verifyType,
- "geetest3_id": geetest3ID,
- "geetest3_key": geetest3Key,
- "geetest4_id": geetest4ID,
- "geetest4_key": geetest4Key,
- "update_time": now,
- })
- if err != nil {
- c.jsonErr(500, 500, "保存失败")
- return
- }
- } else {
- row := &models.PlatformLoginVerify{
- OpenVerifyEnabled: openVerifyEnabled,
- VerifyType: verifyType,
- Geetest3ID: geetest3ID,
- Geetest3Key: geetest3Key,
- Geetest4ID: geetest4ID,
- Geetest4Key: geetest4Key,
- UpdateTime: &now,
- }
- if _, err := models.Orm.Insert(row); err != nil {
- c.jsonErr(500, 500, "保存失败")
- return
- }
+ err = models.SavePlatformLoginVerify(&models.PlatformLoginVerify{
+ OpenVerifyEnabled: openVerifyEnabled,
+ VerifyType: verifyType,
+ Geetest3ID: geetest3ID,
+ Geetest3Key: geetest3Key,
+ Geetest4ID: geetest4ID,
+ Geetest4Key: geetest4Key,
+ })
+ if err != nil {
+ c.jsonErr(500, 500, "保存失败")
+ return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
diff --git a/go/controllers/platform_bark.go b/go/controllers/platform_bark.go
new file mode 100644
index 0000000..bd3231b
--- /dev/null
+++ b/go/controllers/platform_bark.go
@@ -0,0 +1,215 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "server/models"
+ "server/pkg/jwtutil"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type PlatformBarkController struct {
+ beego.Controller
+}
+
+func (c *PlatformBarkController) platformClaims() (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, fmt.Errorf("未登录")
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, fmt.Errorf("认证信息格式错误")
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("无效的token")
+ }
+ if claims.UserType != "platform" {
+ return nil, fmt.Errorf("无权访问")
+ }
+ return claims, nil
+}
+
+func (c *PlatformBarkController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+// GetBarkInfo GET /platform/bark/info
+func (c *PlatformBarkController) GetBarkInfo() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ enabledStr := models.GetPlatformSettingValue("bark_enabled", "0")
+ serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
+ deviceKey := models.GetPlatformSettingValue("bark_device_key", "")
+
+ enabled := false
+ if enabledStr == "1" {
+ enabled = true
+ }
+
+ c.Data["json"] = map[string]interface{}{
+ "code": 200,
+ "msg": "success",
+ "data": map[string]interface{}{
+ "enabled": enabled,
+ "server_url": serverURL,
+ "device_key": deviceKey,
+ },
+ }
+ _ = c.ServeJSON()
+}
+
+type barkEditPayload struct {
+ Enabled bool `json:"enabled"`
+ ServerUrl string `json:"server_url"`
+ DeviceKey string `json:"device_key"`
+}
+
+// EditBarkInfo POST /platform/bark/editinfo
+func (c *PlatformBarkController) EditBarkInfo() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p barkEditPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ enabledStr := "0"
+ if p.Enabled {
+ enabledStr = "1"
+ }
+
+ serverURL := strings.TrimSpace(p.ServerUrl)
+ if serverURL == "" {
+ serverURL = "https://api.day.app"
+ }
+ deviceKey := strings.TrimSpace(p.DeviceKey)
+
+ settings := []struct {
+ code string
+ name string
+ value string
+ remark string
+ }{
+ {"bark_enabled", "Bark推送启用状态", enabledStr, "0为关闭,1为开启"},
+ {"bark_server_url", "Bark推送服务器地址", serverURL, ""},
+ {"bark_device_key", "Bark设备Key", deviceKey, ""},
+ }
+
+ for _, item := range settings {
+ var setting models.PlatformNormalSetting
+ err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
+ Filter("code", item.code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err == nil {
+ setting.Value = item.value
+ setting.Name = item.name
+ setting.Remark = item.remark
+ now := time.Now()
+ setting.UpdateTime = &now
+ _, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
+ if err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
+ } else {
+ newSetting := models.PlatformNormalSetting{
+ Name: item.name,
+ Code: item.code,
+ Value: item.value,
+ Remark: item.remark,
+ CreateTime: time.Now(),
+ }
+ _, err = models.Orm.Insert(&newSetting)
+ if err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
+ }
+ }
+
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
+ _ = c.ServeJSON()
+}
+
+type barkTestPayload struct {
+ ServerUrl string `json:"server_url"`
+ DeviceKey string `json:"device_key"`
+}
+
+// SendTestBark POST /platform/bark/sendtest
+func (c *PlatformBarkController) SendTestBark() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p barkTestPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ serverURL := strings.TrimSpace(p.ServerUrl)
+ if serverURL == "" {
+ serverURL = models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
+ }
+ deviceKey := strings.TrimSpace(p.DeviceKey)
+ if deviceKey == "" {
+ deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
+ }
+
+ if deviceKey == "" {
+ c.jsonErr(400, 400, "设备 Key 不能为空")
+ return
+ }
+
+ // 拼接发送 URL,注意去除多余斜杠
+ baseURL := strings.TrimRight(serverURL, "/")
+ // Bark 的格式是: base_url/device_key/title/body
+ testURL := fmt.Sprintf("%s/%s/测试通知/您配置的 Bark 推送服务已连接成功!", baseURL, deviceKey)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Get(testURL)
+ if err != nil {
+ c.jsonErr(500, 500, "发送失败: "+err.Error())
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ c.jsonErr(500, 500, fmt.Sprintf("发送失败,HTTP 状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes)))
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "测试推送已发出,请注意查收"}
+ _ = c.ServeJSON()
+}
diff --git a/go/controllers/platform_cursor_equipment.go b/go/controllers/platform_cursor_equipment.go
index dfdc68c..a3591a8 100644
--- a/go/controllers/platform_cursor_equipment.go
+++ b/go/controllers/platform_cursor_equipment.go
@@ -105,10 +105,11 @@ func (c *PlatformCursorEquipmentController) cursorActivationSummary(row *models.
return count, &latest
}
-func (c *PlatformCursorEquipmentController) cursorExtractSummary() (int64, *models.PlatformAccountPoolCursor) {
+func (c *PlatformCursorEquipmentController) cursorExtractSummary(machineCode string) (int64, *models.PlatformAccountPoolCursor) {
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("delete_time__isnull", true).
- Filter("is_extracted__gt", 0)
+ Filter("is_extracted__gt", 0).
+ Filter("machine_code", machineCode)
count, _ := qs.Count()
@@ -122,7 +123,7 @@ func (c *PlatformCursorEquipmentController) cursorExtractSummary() (int64, *mode
func (c *PlatformCursorEquipmentController) rowToMap(row *models.PlatformCursorEquipment) map[string]interface{} {
activationCount, latestActivation := c.cursorActivationSummary(row)
- extractCount, latestExtract := c.cursorExtractSummary()
+ extractCount, latestExtract := c.cursorExtractSummary(row.MachineCode)
var bindActivationCode interface{}
var activationCodeId interface{}
@@ -640,7 +641,8 @@ func (c *PlatformCursorEquipmentController) ExtractRecords() {
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("delete_time__isnull", true).
- Filter("is_extracted__gt", 0)
+ Filter("is_extracted__gt", 0).
+ Filter("machine_code", equipment.MachineCode)
total, _ := qs.Count()
diff --git a/go/controllers/platform_login_verify.go b/go/controllers/platform_login_verify.go
index 79ad429..f016080 100644
--- a/go/controllers/platform_login_verify.go
+++ b/go/controllers/platform_login_verify.go
@@ -85,37 +85,18 @@ func (c *PlatformLoginVerifyController) SaveLoginVerifyInfos() {
}
}
- var existed models.PlatformLoginVerify
- err := models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
- if err == nil {
- update := map[string]interface{}{
- "open_verify_enabled": openVerifyEnabled,
- "verify_type": verifyType,
- "geetest3_id": p.Geetest3ID,
- "geetest3_key": p.Geetest3Key,
- "geetest4_id": p.Geetest4ID,
- "geetest4_key": p.Geetest4Key,
- }
- _, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).Filter("id", existed.ID).Update(update)
- if err != nil {
- c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
- _ = c.ServeJSON()
- return
- }
- } else {
- row := &models.PlatformLoginVerify{
- OpenVerifyEnabled: openVerifyEnabled,
- VerifyType: verifyType,
- Geetest3ID: p.Geetest3ID,
- Geetest3Key: p.Geetest3Key,
- Geetest4ID: p.Geetest4ID,
- Geetest4Key: p.Geetest4Key,
- }
- if _, err := models.Orm.Insert(row); err != nil {
- c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
- _ = c.ServeJSON()
- return
- }
+ err := models.SavePlatformLoginVerify(&models.PlatformLoginVerify{
+ OpenVerifyEnabled: openVerifyEnabled,
+ VerifyType: verifyType,
+ Geetest3ID: p.Geetest3ID,
+ Geetest3Key: p.Geetest3Key,
+ Geetest4ID: p.Geetest4ID,
+ Geetest4Key: p.Geetest4Key,
+ })
+ if err != nil {
+ c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
+ _ = c.ServeJSON()
+ return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
diff --git a/go/controllers/platform_sms.go b/go/controllers/platform_sms.go
index b667403..3c4c5d1 100644
--- a/go/controllers/platform_sms.go
+++ b/go/controllers/platform_sms.go
@@ -55,24 +55,8 @@ func (c *PlatformSMSController) GetSmsInfo() {
return
}
- var row models.SystemSMS
- // 优先默认通道,其次 custom
- err := models.Orm.QueryTable(new(models.SystemSMS)).
- Filter("is_default", 1).
- Filter("status", 1).
- OrderBy("-weight", "-id").
- Limit(1).
- One(&row)
- if err != nil {
- _ = models.Orm.QueryTable(new(models.SystemSMS)).
- Filter("config_code", "custom").
- OrderBy("-id").
- Limit(1).
- One(&row)
- }
-
- backendURL := strings.TrimSpace(row.ApiURL)
- apiKey := strings.TrimSpace(row.ApiKey)
+ backendURL := models.GetPlatformSettingValue("sms_custom_url", "")
+ apiKey := models.GetPlatformSettingValue("sms_custom_key", "")
data := []map[string]interface{}{{
"backend_url": backendURL,
@@ -93,7 +77,7 @@ type smsEditPayload struct {
}
// EditSmsInfo POST /platform/sms/editinfo
-// 将旧前端的 backendUrl/apiKey 落到 yz_system_sms 的 api_url/api_key(写入 config_code=custom)
+// 将旧前端的 backendUrl/apiKey 落到 yz_platform_normal_setting 表中
func (c *PlatformSMSController) EditSmsInfo() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
@@ -128,44 +112,46 @@ func (c *PlatformSMSController) EditSmsInfo() {
return
}
- // 确保只有一个默认:先清空默认,再 upsert custom 为默认
- _, _ = models.Orm.QueryTable(new(models.SystemSMS)).Update(map[string]interface{}{"is_default": 0})
+ settings := []struct {
+ code string
+ name string
+ value string
+ remark string
+ }{
+ {"sms_custom_url", "自定义短信网关地址", backendURL, ""},
+ {"sms_custom_key", "自定义短信API KEY", apiKey, ""},
+ }
- var existed models.SystemSMS
- e := models.Orm.QueryTable(new(models.SystemSMS)).Filter("config_code", "custom").Limit(1).One(&existed)
- if e == nil && existed.ID > 0 {
- _, err = models.Orm.QueryTable(new(models.SystemSMS)).Filter("id", existed.ID).Update(map[string]interface{}{
- "config_name": "自定义网关",
- "channel_type": 2,
- "api_url": backendURL,
- "api_key": apiKey,
- "weight": 10,
- "is_default": 1,
- "status": 1,
- })
- if err != nil {
- c.jsonErr(500, 500, "更新失败: "+err.Error())
- return
- }
- } else {
- row := &models.SystemSMS{
- ConfigCode: "custom",
- ConfigName: "自定义网关",
- ChannelType: 2,
- ApiURL: backendURL,
- ApiKey: apiKey,
- ApiSecret: "",
- SignName: "",
- TemplateID: "",
- TestPhone: "",
- Weight: 10,
- IsDefault: 1,
- Status: 1,
- Remark: "",
- }
- if _, err := models.Orm.Insert(row); err != nil {
- c.jsonErr(500, 500, "更新失败: "+err.Error())
- return
+ for _, item := range settings {
+ var setting models.PlatformNormalSetting
+ err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
+ Filter("code", item.code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err == nil {
+ setting.Value = item.value
+ setting.Name = item.name
+ setting.Remark = item.remark
+ now := time.Now()
+ setting.UpdateTime = &now
+ _, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
+ if err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
+ } else {
+ newSetting := models.PlatformNormalSetting{
+ Name: item.name,
+ Code: item.code,
+ Value: item.value,
+ Remark: item.remark,
+ CreateTime: time.Now(),
+ }
+ _, err = models.Orm.Insert(&newSetting)
+ if err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
}
}
@@ -232,18 +218,11 @@ func (c *PlatformSMSController) SendTestSms() {
// 兜底:body 未带时从默认配置取
if backendURL == "" || apiKey == "" {
- var row models.SystemSMS
- _ = models.Orm.QueryTable(new(models.SystemSMS)).
- Filter("is_default", 1).
- Filter("status", 1).
- OrderBy("-weight", "-id").
- Limit(1).
- One(&row)
if backendURL == "" {
- backendURL = strings.TrimSpace(row.ApiURL)
+ backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
}
if apiKey == "" {
- apiKey = strings.TrimSpace(row.ApiKey)
+ apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
}
}
if backendURL == "" {
diff --git a/go/models/init.go b/go/models/init.go
index b218b12..fa05bbb 100644
--- a/go/models/init.go
+++ b/go/models/init.go
@@ -43,14 +43,11 @@ func Init(_ string) {
new(AdminRole),
new(SystemFile),
new(SystemFilesCategory),
- new(SystemEmail),
- new(SystemSMS),
new(SystemSMSTask),
new(SystemOperationLog),
new(SystemDomainPool),
new(SystemTenantDomain),
new(SystemModules),
- new(PlatformLoginVerify),
new(StorageConfig),
new(TenantSiteSetting),
new(ComplaintCategory),
@@ -67,8 +64,11 @@ func Init(_ string) {
new(CmsArticleCategory),
new(CmsArticle),
- new(SystemSiteReminder),
new(SystemReminderList),
+
+ new(SystemNormalSetting),
+ new(PlatformNormalSetting),
+ new(BackendNormalSetting),
)
// 创建全局 Ormer
diff --git a/go/models/platform_account_pool.go b/go/models/platform_account_pool.go
index 3dc8b62..7c6b759 100644
--- a/go/models/platform_account_pool.go
+++ b/go/models/platform_account_pool.go
@@ -75,6 +75,7 @@ type PlatformAccountPoolCursor struct {
IsUsed *int8 `orm:"column(is_used);null" json:"is_used"` // 0=用完/不可用 1=可用 NULL=未探测
ExtractedTime *time.Time `orm:"column(extracted_time);type(datetime);null" json:"extracted_time"`
ExtractedPlatform *string `orm:"column(extracted_platform);size(32);null" json:"extracted_platform"`
+ MachineCode string `orm:"column(machine_code);size(128);default('')" json:"machine_code"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
diff --git a/go/models/platform_login_verify.go b/go/models/platform_login_verify.go
index 69ec5d2..2ed40cb 100644
--- a/go/models/platform_login_verify.go
+++ b/go/models/platform_login_verify.go
@@ -15,20 +15,118 @@ type PlatformLoginVerify struct {
UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"`
}
-func (m *PlatformLoginVerify) TableName() string {
- return "yz_system_login_verify"
-}
-
func GetPlatformLoginVerify() (*PlatformLoginVerify, error) {
- var cfg PlatformLoginVerify
- err := Orm.QueryTable(new(PlatformLoginVerify)).OrderBy("-id").One(&cfg)
- if err != nil {
- // 默认配置:验证码
- return &PlatformLoginVerify{OpenVerifyEnabled: 1, VerifyType: "captcha"}, nil
+ // 从 yz_platform_normal_setting 表中按 code 获取各个配置
+ enabledStr := GetPlatformSettingValue("login_verify_enabled", "1")
+ verifyType := GetPlatformSettingValue("login_verify_type", "captcha")
+ geetest3ID := GetPlatformSettingValue("login_verify_geetest3_id", "")
+ geetest3Key := GetPlatformSettingValue("login_verify_geetest3_key", "")
+ geetest4ID := GetPlatformSettingValue("login_verify_geetest4_id", "")
+ geetest4Key := GetPlatformSettingValue("login_verify_geetest4_key", "")
+
+ openVerifyEnabled := int8(1)
+ if enabledStr == "0" {
+ openVerifyEnabled = 0
}
- if cfg.VerifyType == "" {
- cfg.VerifyType = "captcha"
+
+ cfg := &PlatformLoginVerify{
+ OpenVerifyEnabled: openVerifyEnabled,
+ VerifyType: verifyType,
}
- return &cfg, nil
+ if geetest3ID != "" {
+ cfg.Geetest3ID = &geetest3ID
+ }
+ if geetest3Key != "" {
+ cfg.Geetest3Key = &geetest3Key
+ }
+ if geetest4ID != "" {
+ cfg.Geetest4ID = &geetest4ID
+ }
+ if geetest4Key != "" {
+ cfg.Geetest4Key = &geetest4Key
+ }
+
+ return cfg, nil
+}
+
+func GetPlatformSettingValue(code string, defaultVal string) string {
+ var setting PlatformNormalSetting
+ err := Orm.QueryTable(new(PlatformNormalSetting)).
+ Filter("code", code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err != nil {
+ return defaultVal
+ }
+ return setting.Value
+}
+
+func SavePlatformLoginVerify(cfg *PlatformLoginVerify) error {
+ openVerifyEnabledStr := "1"
+ if cfg.OpenVerifyEnabled == 0 {
+ openVerifyEnabledStr = "0"
+ }
+ geetest3ID := ""
+ if cfg.Geetest3ID != nil {
+ geetest3ID = *cfg.Geetest3ID
+ }
+ geetest3Key := ""
+ if cfg.Geetest3Key != nil {
+ geetest3Key = *cfg.Geetest3Key
+ }
+ geetest4ID := ""
+ if cfg.Geetest4ID != nil {
+ geetest4ID = *cfg.Geetest4ID
+ }
+ geetest4Key := ""
+ if cfg.Geetest4Key != nil {
+ geetest4Key = *cfg.Geetest4Key
+ }
+
+ settings := []struct {
+ code string
+ name string
+ value string
+ remark string
+ }{
+ {"login_verify_enabled", "登录验证开启状态", openVerifyEnabledStr, "0为关闭,1为开启"},
+ {"login_verify_type", "登录验证类型", cfg.VerifyType, "支持 captcha/sms/geetest/email"},
+ {"login_verify_geetest3_id", "极验3 ID", geetest3ID, ""},
+ {"login_verify_geetest3_key", "极验3 Key", geetest3Key, ""},
+ {"login_verify_geetest4_id", "极验4 ID", geetest4ID, ""},
+ {"login_verify_geetest4_key", "极验4 Key", geetest4Key, ""},
+ }
+
+ for _, item := range settings {
+ var setting PlatformNormalSetting
+ err := Orm.QueryTable(new(PlatformNormalSetting)).
+ Filter("code", item.code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err == nil {
+ setting.Value = item.value
+ setting.Name = item.name
+ setting.Remark = item.remark
+ now := time.Now()
+ setting.UpdateTime = &now
+ _, err = Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
+ if err != nil {
+ return err
+ }
+ } else {
+ newSetting := PlatformNormalSetting{
+ Name: item.name,
+ Code: item.code,
+ Value: item.value,
+ Remark: item.remark,
+ CreateTime: time.Now(),
+ }
+ _, err = Orm.Insert(&newSetting)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
}
diff --git a/go/models/system_email.go b/go/models/system_email.go
index 289dbfa..397a15c 100644
--- a/go/models/system_email.go
+++ b/go/models/system_email.go
@@ -18,6 +18,4 @@ type SystemEmail struct {
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"`
}
-func (m *SystemEmail) TableName() string {
- return "yz_system_email"
-}
+
diff --git a/go/models/system_normal_setting.go b/go/models/system_normal_setting.go
new file mode 100644
index 0000000..61037cc
--- /dev/null
+++ b/go/models/system_normal_setting.go
@@ -0,0 +1,51 @@
+package models
+
+import "time"
+
+// SystemNormalSetting 系统通用配置表: yz_system_normal_setting
+type SystemNormalSetting struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Name string `orm:"column(name);size(128);default('')" json:"name"`
+ Value string `orm:"column(value);type(text);null" json:"value"`
+ Code string `orm:"column(code);size(64);default('')" json:"code"`
+ Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
+ CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+}
+
+func (m *SystemNormalSetting) TableName() string {
+ return "yz_system_normal_setting"
+}
+
+// PlatformNormalSetting 平台通用配置表: yz_platform_normal_setting
+type PlatformNormalSetting struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Name string `orm:"column(name);size(128);default('')" json:"name"`
+ Value string `orm:"column(value);type(text);null" json:"value"`
+ Code string `orm:"column(code);size(64);default('')" json:"code"`
+ Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
+ CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+}
+
+func (m *PlatformNormalSetting) TableName() string {
+ return "yz_platform_normal_setting"
+}
+
+// BackendNormalSetting 管理端通用配置表: yz_backend_normal_setting
+type BackendNormalSetting struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Name string `orm:"column(name);size(128);default('')" json:"name"`
+ Value string `orm:"column(value);type(text);null" json:"value"`
+ Code string `orm:"column(code);size(64);default('')" json:"code"`
+ Remark string `orm:"column(remark);size(255);default('')" json:"remark"`
+ CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
+ DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
+}
+
+func (m *BackendNormalSetting) TableName() string {
+ return "yz_backend_normal_setting"
+}
diff --git a/go/models/system_sitereminder.go b/go/models/system_sitereminder.go
index c08af03..ba45555 100644
--- a/go/models/system_sitereminder.go
+++ b/go/models/system_sitereminder.go
@@ -11,6 +11,4 @@ type SystemSiteReminder struct {
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
}
-func (m *SystemSiteReminder) TableName() string {
- return "yz_system_sitereminder"
-}
+
diff --git a/go/models/system_sms.go b/go/models/system_sms.go
index 93d7418..094dcb1 100644
--- a/go/models/system_sms.go
+++ b/go/models/system_sms.go
@@ -27,6 +27,4 @@ type SystemSMS struct {
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
}
-func (m *SystemSMS) TableName() string {
- return "yz_system_sms"
-}
+
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index a754fb1..61842bc 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -144,6 +144,11 @@ func Register() {
beego.Router("/platform/sms/taskList", &controllers.PlatformSMSController{}, "get:GetSmsTaskList")
beego.Router("/platform/sms/taskEdit/:id", &controllers.PlatformSMSController{}, "post:EditSmsTask")
+ // Bark 推送配置
+ beego.Router("/platform/bark/info", &controllers.PlatformBarkController{}, "get:GetBarkInfo")
+ beego.Router("/platform/bark/editinfo", &controllers.PlatformBarkController{}, "post:EditBarkInfo")
+ beego.Router("/platform/bark/sendtest", &controllers.PlatformBarkController{}, "post:SendTestBark")
+
// 文件管理(yz_system_files / yz_system_files_category)
beego.Router("/platform/usercate", &controllers.PlatformFileController{}, "get:GetUserCate")
beego.Router("/platform/allfiles", &controllers.PlatformFileController{}, "get:GetAllFiles")
diff --git a/go/services/login_verify_code.go b/go/services/login_verify_code.go
index bef2db4..963027f 100644
--- a/go/services/login_verify_code.go
+++ b/go/services/login_verify_code.go
@@ -157,25 +157,11 @@ func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
}
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
- var row models.SystemSMS
- err = models.Orm.QueryTable(new(models.SystemSMS)).
- Filter("is_default", 1).
- Filter("status", 1).
- OrderBy("-weight", "-id").
- Limit(1).
- One(&row)
- if err != nil {
- err2 := models.Orm.QueryTable(new(models.SystemSMS)).
- Filter("config_code", "custom").
- OrderBy("-id").
- Limit(1).
- One(&row)
- if err2 != nil {
- return "", "", err2
- }
+ backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
+ apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
+ if backendURL == "" || apiKey == "" {
+ return "", "", fmt.Errorf("短信网关未配置")
}
- backendURL = strings.TrimSpace(row.ApiURL)
- apiKey = strings.TrimSpace(row.ApiKey)
return backendURL, apiKey, nil
}
diff --git a/go/services/system_email_store.go b/go/services/system_email_store.go
index d2a5ea9..24f80f4 100644
--- a/go/services/system_email_store.go
+++ b/go/services/system_email_store.go
@@ -2,19 +2,51 @@ package services
import (
"fmt"
+ "strconv"
"strings"
+ "time"
"server/models"
)
-// ListSystemEmails 返回全部邮箱配置(按 id 升序,通常仅一条)
+// ListSystemEmails 返回从 yz_platform_normal_setting 组装的邮箱配置(切片,通常仅一条)
func ListSystemEmails() ([]models.SystemEmail, error) {
- var rows []models.SystemEmail
- _, err := models.Orm.QueryTable(new(models.SystemEmail)).OrderBy("id").All(&rows)
- return rows, err
+ enabledStr := models.GetPlatformSettingValue("email_enabled", "0")
+ fromAddress := models.GetPlatformSettingValue("email_from_address", "")
+ fromName := models.GetPlatformSettingValue("email_from_name", "")
+ host := models.GetPlatformSettingValue("email_host", "")
+ portStr := models.GetPlatformSettingValue("email_port", "465")
+ password := models.GetPlatformSettingValue("email_password", "")
+ encryption := models.GetPlatformSettingValue("email_encryption", "ssl")
+ timeoutStr := models.GetPlatformSettingValue("email_timeout", "30")
+
+ status := int8(0)
+ if enabledStr == "1" {
+ status = 1
+ }
+ portVal, _ := strconv.ParseUint(portStr, 10, 32)
+ timeoutVal, _ := strconv.ParseUint(timeoutStr, 10, 32)
+
+ row := models.SystemEmail{
+ ID: 1,
+ FromAddress: fromAddress,
+ Host: host,
+ Port: uint(portVal),
+ Password: password,
+ Encryption: encryption,
+ Timeout: uint(timeoutVal),
+ Status: status,
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
+ }
+ if fromName != "" {
+ row.FromName = &fromName
+ }
+
+ return []models.SystemEmail{row}, nil
}
-// UpsertFirstSystemEmail 若已有记录则更新第一条,否则插入
+// UpsertFirstSystemEmail 将邮箱配置保存到 yz_platform_normal_setting 表中
func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, port uint, password string, encryption string, timeout uint, status int8, remark *string) error {
if encryption == "" {
encryption = "ssl"
@@ -28,47 +60,76 @@ func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, p
fromAddress = strings.TrimSpace(fromAddress)
host = strings.TrimSpace(host)
- cnt, err := models.Orm.QueryTable(new(models.SystemEmail)).Count()
- if err != nil {
- return err
+ fn := ""
+ if fromName != nil {
+ fn = *fromName
}
- if cnt == 0 {
- if strings.TrimSpace(password) == "" {
+
+ statusStr := "0"
+ if status == 1 {
+ statusStr = "1"
+ }
+
+ settings := []struct {
+ code string
+ name string
+ value string
+ remark string
+ }{
+ {"email_enabled", "邮件服务启用状态", statusStr, "0为关闭,1为开启"},
+ {"email_from_address", "发件人邮箱", fromAddress, ""},
+ {"email_from_name", "发件人名称", fn, ""},
+ {"email_host", "SMTP 服务器地址", host, ""},
+ {"email_port", "SMTP 端口", strconv.FormatUint(uint64(port), 10), ""},
+ {"email_encryption", "邮件加密方式", encryption, "支持 ssl/tls/none"},
+ {"email_timeout", "邮件发送超时时间", strconv.FormatUint(uint64(timeout), 10), ""},
+ }
+
+ // 如果传入了新密码,或者目前还没有保存过密码,才更新密码
+ if strings.TrimSpace(password) != "" {
+ settings = append(settings, struct {
+ code string
+ name string
+ value string
+ remark string
+ }{"email_password", "邮件授权码/密码", strings.TrimSpace(password), ""})
+ } else {
+ // 校验:如果完全没有配置过密码,必须填写密码
+ existingPass := models.GetPlatformSettingValue("email_password", "")
+ if existingPass == "" {
return fmt.Errorf("首次保存必须填写授权码/密码")
}
- row := &models.SystemEmail{
- FromAddress: fromAddress,
- FromName: fromName,
- Host: host,
- Port: port,
- Password: strings.TrimSpace(password),
- Encryption: encryption,
- Timeout: timeout,
- Status: status,
- Remark: remark,
+ }
+
+ for _, item := range settings {
+ var setting models.PlatformNormalSetting
+ err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
+ Filter("code", item.code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err == nil {
+ setting.Value = item.value
+ setting.Name = item.name
+ setting.Remark = item.remark
+ now := time.Now()
+ setting.UpdateTime = &now
+ _, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
+ if err != nil {
+ return err
+ }
+ } else {
+ newSetting := models.PlatformNormalSetting{
+ Name: item.name,
+ Code: item.code,
+ Value: item.value,
+ Remark: item.remark,
+ CreateTime: time.Now(),
+ }
+ _, err = models.Orm.Insert(&newSetting)
+ if err != nil {
+ return err
+ }
}
- _, err = models.Orm.Insert(row)
- return err
}
-
- var first models.SystemEmail
- if err := models.Orm.QueryTable(new(models.SystemEmail)).OrderBy("id").Limit(1).One(&first); err != nil {
- return err
- }
-
- up := map[string]interface{}{
- "from_address": fromAddress,
- "from_name": fromName,
- "host": host,
- "port": port,
- "encryption": encryption,
- "timeout": timeout,
- "status": status,
- "remark": remark,
- }
- if strings.TrimSpace(password) != "" {
- up["password"] = strings.TrimSpace(password)
- }
- _, err = models.Orm.QueryTable(new(models.SystemEmail)).Filter("id", first.ID).Update(up)
- return err
+ return nil
}
diff --git a/go/services/system_sitereminder.go b/go/services/system_sitereminder.go
index d0eccc0..4705f6f 100644
--- a/go/services/system_sitereminder.go
+++ b/go/services/system_sitereminder.go
@@ -3,32 +3,36 @@ package services
import (
"context"
"fmt"
+ "strconv"
"time"
"github.com/beego/beego/v2/client/orm"
"server/models"
)
-// GetSiteReminderConfig 获取站内信配置(只读首条记录,不存在则初始化默认值)
+// GetSiteReminderConfig 获取站内信配置(从 yz_platform_normal_setting 读取)
func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
- var row models.SystemSiteReminder
- err := models.Orm.QueryTable(new(models.SystemSiteReminder)).OrderBy("id").Limit(1).One(&row)
- if err == orm.ErrNoRows {
- // 默认配置
- now := time.Now()
- row = models.SystemSiteReminder{
- RetentionDays: 30,
- AutoRead: 0,
- CreateTime: &now,
- UpdateTime: &now,
- }
- _, err = models.Orm.Insert(&row)
- if err != nil {
- return row, err
- }
- return row, nil
+ retentionDaysStr := models.GetPlatformSettingValue("sitemsg_retention_days", "30")
+ autoReadStr := models.GetPlatformSettingValue("sitemsg_auto_read", "0")
+
+ retentionDays, _ := strconv.Atoi(retentionDaysStr)
+ if retentionDays <= 0 {
+ retentionDays = 30
}
- return row, err
+ autoRead := int8(0)
+ if autoReadStr == "1" {
+ autoRead = 1
+ }
+
+ now := time.Now()
+ row := models.SystemSiteReminder{
+ ID: 1,
+ RetentionDays: retentionDays,
+ AutoRead: autoRead,
+ CreateTime: &now,
+ UpdateTime: &now,
+ }
+ return row, nil
}
// SaveSiteReminderConfig 保存/更新配置
@@ -36,17 +40,53 @@ func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
if retentionDays <= 0 {
retentionDays = 30
}
- cfg, err := GetSiteReminderConfig()
- if err != nil {
- return err
- }
- now := time.Now()
- cfg.RetentionDays = retentionDays
- cfg.AutoRead = autoRead
- cfg.UpdateTime = &now
- _, err = models.Orm.Update(&cfg, "RetentionDays", "AutoRead", "UpdateTime")
- return err
+ autoReadStr := "0"
+ if autoRead == 1 {
+ autoReadStr = "1"
+ }
+
+ settings := []struct {
+ code string
+ name string
+ value string
+ remark string
+ }{
+ {"sitemsg_retention_days", "站内信消息保留天数", strconv.Itoa(retentionDays), ""},
+ {"sitemsg_auto_read", "自动标记已读状态", autoReadStr, "0为关闭,1为开启"},
+ }
+
+ for _, item := range settings {
+ var setting models.PlatformNormalSetting
+ err := models.Orm.QueryTable(new(models.PlatformNormalSetting)).
+ Filter("code", item.code).
+ Filter("delete_time__isnull", true).
+ One(&setting)
+ if err == nil {
+ setting.Value = item.value
+ setting.Name = item.name
+ setting.Remark = item.remark
+ now := time.Now()
+ setting.UpdateTime = &now
+ _, err = models.Orm.Update(&setting, "Value", "Name", "Remark", "UpdateTime")
+ if err != nil {
+ return err
+ }
+ } else {
+ newSetting := models.PlatformNormalSetting{
+ Name: item.name,
+ Code: item.code,
+ Value: item.value,
+ Remark: item.remark,
+ CreateTime: time.Now(),
+ }
+ _, err = models.Orm.Insert(&newSetting)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
}
// SendSiteReminder 发送站内信
diff --git a/platform/src/api/sitesettings.js b/platform/src/api/sitesettings.js
index 1ecea97..2d0bf45 100644
--- a/platform/src/api/sitesettings.js
+++ b/platform/src/api/sitesettings.js
@@ -150,4 +150,41 @@ export function saveStorageConfig(data) {
method: "post",
data: data,
});
+}
+
+/**
+ * 获取 Bark 配置
+ * @returns {Promise}
+ */
+export function getBarkConfig() {
+ return request({
+ url: "/platform/bark/info",
+ method: "get",
+ });
+}
+
+/**
+ * 保存 Bark 配置
+ * @param {Object} data 要保存的数据
+ * @returns {Promise}
+ */
+export function saveBarkConfig(data) {
+ return request({
+ url: "/platform/bark/editinfo",
+ method: "post",
+ data: data,
+ });
+}
+
+/**
+ * 发送测试 Bark 推送
+ * @param {Object} data 测试数据
+ * @returns {Promise}
+ */
+export function sendTestBark(data) {
+ return request({
+ url: "/platform/bark/sendtest",
+ method: "post",
+ data: data,
+ });
}
\ No newline at end of file
diff --git a/platform/src/views/system/platformsettings/components/notificationSettings.vue b/platform/src/views/system/platformsettings/components/notificationSettings.vue
index 9889001..e27d387 100644
--- a/platform/src/views/system/platformsettings/components/notificationSettings.vue
+++ b/platform/src/views/system/platformsettings/components/notificationSettings.vue
@@ -155,10 +155,12 @@
+
天(过期消息将自动清理)
@@ -296,6 +298,11 @@
+
+
+ 发送测试推送
+
+
@@ -395,10 +402,12 @@ import {
import { getSmsInfo, editSmsInfo, sendTestSms } from "@/api/sms";
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
import { getSiteReminderConfig, saveSiteReminderConfig } from "@/api/sitereminder";
+import { getBarkConfig, saveBarkConfig, sendTestBark } from "@/api/sitesettings";
const STORAGE_KEY = "notification_settings_draft";
const activeSubTab = ref("email");
const submitting = ref(false);
+const barkTestLoading = ref(false);
const formData = reactive({
email: {
@@ -659,6 +668,17 @@ const handleSubmit = async () => {
} else {
throw new Error(res.msg || "保存站内信配置失败");
}
+ } else if (activeSubTab.value === 'bark') {
+ const res = await saveBarkConfig({
+ enabled: formData.bark.enabled,
+ server_url: formData.bark.server_url,
+ device_key: formData.bark.device_key
+ });
+ if (res.code === 200) {
+ ElMessage.success("Bark 配置保存成功");
+ } else {
+ throw new Error(res.msg || "保存 Bark 配置失败");
+ }
}
saveDraft();
@@ -670,11 +690,49 @@ const handleSubmit = async () => {
}
};
+const loadBarkConfig = async () => {
+ try {
+ const res = await getBarkConfig();
+ if (res.code === 200 && res.data) {
+ formData.bark.enabled = res.data.enabled;
+ formData.bark.server_url = res.data.server_url || "https://api.day.app";
+ formData.bark.device_key = res.data.device_key || "";
+ }
+ } catch (error) {
+ console.error("加载 Bark 配置失败:", error);
+ }
+};
+
+const handleSendBarkTest = async () => {
+ const key = (formData.bark.device_key || "").trim();
+ if (!key) {
+ ElMessage.warning("请先输入 Bark 设备 Key");
+ return;
+ }
+ try {
+ barkTestLoading.value = true;
+ const res = await sendTestBark({
+ server_url: formData.bark.server_url,
+ device_key: key
+ });
+ if (res.code === 200) {
+ ElMessage.success(res.msg || "测试推送发送成功");
+ } else {
+ ElMessage.error(res.msg || "测试推送失败,请稍后重试");
+ }
+ } catch (error) {
+ ElMessage.error(error?.message || "测试推送失败,请稍后重试");
+ } finally {
+ barkTestLoading.value = false;
+ }
+};
+
onMounted(() => {
loadDraft();
loadSmsConfig();
loadEmailConfig();
loadSiteReminderConfig();
+ loadBarkConfig();
});
From bd30b687663e507c81fcd27750279a19d6009045 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, 18 Jun 2026 00:55:14 +0800
Subject: [PATCH 4/5] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=97=A5=E7=A8=8B?=
=?UTF-8?q?=E6=8F=90=E9=86=92=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
go/controllers/api_reminder.go | 109 +++
go/controllers/platform_reminder.go | 631 ++++++++++++++++++
go/main.go | 4 +
go/models/init.go | 4 +
go/models/platform_schedule_reminder.go | 55 ++
go/routers/api/api.go | 3 +
go/routers/platform/platform.go | 7 +
go/services/reminder_scheduler.go | 371 ++++++++++
go/services/system_email_smtp.go | 94 +++
platform/components.d.ts | 1 +
platform/src/App.vue | 5 +-
platform/src/api/reminder.js | 19 +-
.../components/notificationSettings.vue | 2 +-
.../tools/reminder/components/detail.vue | 116 ++--
.../views/tools/reminder/components/edit.vue | 285 ++++----
platform/src/views/tools/reminder/index.vue | 212 +++---
16 files changed, 1621 insertions(+), 297 deletions(-)
create mode 100644 go/controllers/api_reminder.go
create mode 100644 go/controllers/platform_reminder.go
create mode 100644 go/models/platform_schedule_reminder.go
create mode 100644 go/services/reminder_scheduler.go
diff --git a/go/controllers/api_reminder.go b/go/controllers/api_reminder.go
new file mode 100644
index 0000000..3230026
--- /dev/null
+++ b/go/controllers/api_reminder.go
@@ -0,0 +1,109 @@
+package controllers
+
+import (
+ "time"
+
+ "server/models"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type ApiReminderController struct {
+ beego.Controller
+}
+
+// AckReminder GET /api/schedule/reminder/ack
+// 邮件/Bark 客户端访问此接口进行提醒确认
+func (c *ApiReminderController) AckReminder() {
+ token := c.GetString("token")
+ if token == "" {
+ c.Ctx.Output.SetStatus(400)
+ _ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
+ return
+ }
+
+ var reminder models.PlatformScheduleReminder
+ err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("ack_token", token).
+ Filter("is_deleted", 0).
+ One(&reminder)
+ if err != nil {
+ c.Ctx.Output.SetStatus(404)
+ _ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
+ return
+ }
+
+ if reminder.AckStatus == 1 {
+ // 已经确认过了,直接显示已确认成功的 HTML
+ c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
+ _ = c.Ctx.Output.Body([]byte(`
+
+
+
+
+ 确认收到提醒
+
+
+
+
+
提示
+
该日程提醒在此之前已确认过了。
+
无需重复点击,感谢您的使用!
+
+
+
+ `))
+ return
+ }
+
+ // 更新确认状态为已确认,置 remind_status 为已结束(2)
+ now := time.Now()
+ reminder.AckStatus = 1
+ reminder.AckTime = &now
+ reminder.RemindStatus = 2
+ reminder.UpdateTime = now
+
+ _, err = models.Orm.Update(&reminder, "AckStatus", "AckTime", "RemindStatus", "UpdateTime")
+ if err != nil {
+ c.Ctx.Output.SetStatus(500)
+ _ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
+ return
+ }
+
+ // 统一关闭该日程下的所有其他待提醒/提醒中渠道,防止重复打扰
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("ScheduleID", reminder.ScheduleID).
+ Filter("RemindStatus__in", 0, 1).
+ Update(map[string]interface{}{
+ "RemindStatus": int8(2),
+ "UpdateTime": now,
+ })
+
+ // 成功确认
+ c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
+ _ = c.Ctx.Output.Body([]byte(`
+
+
+
+
+ 确认成功
+
+
+
+
+
确认成功
+
您已成功确认收到该日程提醒!
+
系统已停止向您重复推送,感谢您的配合。
+
+
+
+ `))
+}
diff --git a/go/controllers/platform_reminder.go b/go/controllers/platform_reminder.go
new file mode 100644
index 0000000..97ed833
--- /dev/null
+++ b/go/controllers/platform_reminder.go
@@ -0,0 +1,631 @@
+package controllers
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
+ "server/models"
+ "server/pkg/jwtutil"
+ "server/services"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type PlatformReminderController struct {
+ beego.Controller
+}
+
+func (c *PlatformReminderController) platformClaims() (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, fmt.Errorf("未登录")
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, fmt.Errorf("认证信息格式错误")
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("无效的token")
+ }
+ if claims.UserType != "platform" {
+ return nil, fmt.Errorf("无权访问")
+ }
+ return claims, nil
+}
+
+func (c *PlatformReminderController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+func (c *PlatformReminderController) ok(data interface{}) {
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
+ _ = c.ServeJSON()
+}
+
+// generateToken 生成一个随机的 ack_token
+func generateToken() string {
+ b := make([]byte, 16)
+ _, _ = rand.Read(b)
+ b[6] = (b[6] & 0x0f) | 0x40
+ b[8] = (b[8] & 0x3f) | 0x80
+ return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
+}
+
+type reminderFormPayload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ ScheduleTime string `json:"schedule_time"`
+ RemindChannels []string `json:"remind_channels"` // EMAIL, BARK, SMS, SITE_MSG
+ AdvanceMinutes int `json:"advance_minutes"`
+ RepeatIntervalMinutes int `json:"repeat_interval_minutes"`
+ MaxSendCount int `json:"max_send_count"`
+ ReceiverUserID uint64 `json:"receiver_user_id"`
+ ReceiverTargets map[string]string `json:"receiver_targets"` // "SMS": "1380...", "EMAIL": "...", "BARK": "..."
+}
+
+// GetReminderList GET /platform/reminder/list
+func (c *PlatformReminderController) GetReminderList() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 20)
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 {
+ pageSize = 20
+ }
+
+ // 联表获取日程及提醒信息
+ var schedules []models.PlatformSchedule
+ qs := models.Orm.QueryTable(new(models.PlatformSchedule))
+ total, _ := qs.Count()
+
+ _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules)
+ if err != nil {
+ c.jsonErr(500, 500, "查询失败: "+err.Error())
+ return
+ }
+
+ list := make([]map[string]interface{}, 0, len(schedules))
+ for _, s := range schedules {
+ // 查询该日程关联的所有提醒记录
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", s.ID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+
+ channels := make([]string, 0, len(reminders))
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ channels = append(channels, r.RemindChannel)
+ if r.RemindStatus != 2 {
+ isFinished = false
+ }
+ }
+ }
+
+ item := map[string]interface{}{
+ "id": s.ID,
+ "title": s.Title,
+ "content": s.Content,
+ "schedule_time": s.ScheduleTime.Format("2006-01-02 15:04:05"),
+ "remind_channels": channels,
+ "user_id": s.UserID,
+ "is_finished": isFinished,
+ }
+ if len(reminders) > 0 {
+ first := reminders[0]
+ item["advance_minutes"] = first.AdvanceMinutes
+ item["repeat_interval_minutes"] = first.RepeatIntervalMinutes
+ item["max_send_count"] = first.MaxSendCount
+ item["receiver_user_id"] = first.ReceiverUserID
+ }
+ list = append(list, item)
+ }
+
+ c.ok(map[string]interface{}{
+ "list": list,
+ "total": total,
+ "page": page,
+ "pageSize": pageSize,
+ })
+}
+
+// GetReminderDetail GET /platform/reminder/:id
+func (c *PlatformReminderController) GetReminderDetail() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ var schedule models.PlatformSchedule
+ err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
+ if err != nil {
+ c.jsonErr(404, 404, "日程未找到")
+ return
+ }
+
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", schedule.ID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+
+ channels := make([]string, 0, len(reminders))
+ targets := make(map[string]string)
+ var first models.PlatformScheduleReminder
+
+ for _, r := range reminders {
+ channels = append(channels, r.RemindChannel)
+ if r.ReceiverTarget != nil {
+ targets[r.RemindChannel] = *r.ReceiverTarget
+ }
+ first = r
+ }
+
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+
+ data := map[string]interface{}{
+ "id": schedule.ID,
+ "title": schedule.Title,
+ "content": schedule.Content,
+ "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
+ "remind_channels": channels,
+ "receiver_targets": targets,
+ "is_finished": isFinished,
+ }
+ if first.ID > 0 {
+ data["advance_minutes"] = first.AdvanceMinutes
+ data["repeat_interval_minutes"] = first.RepeatIntervalMinutes
+ data["max_send_count"] = first.MaxSendCount
+ data["receiver_user_id"] = first.ReceiverUserID
+ }
+
+ c.ok(data)
+}
+
+// CreateReminder POST /platform/reminder
+func (c *PlatformReminderController) CreateReminder() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderFormPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if strings.TrimSpace(p.ScheduleTime) == "" {
+ c.jsonErr(400, 400, "日程发生时间不能为空")
+ return
+ }
+
+ schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
+ if err != nil {
+ c.jsonErr(400, 400, "日程时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
+ return
+ }
+
+ // 1. 插入日程主表
+ schedule := models.PlatformSchedule{
+ Title: "日程提醒",
+ Content: p.Content,
+ ScheduleTime: schedTime,
+ UserID: uint64(claims.UserID),
+ }
+ schedID, err := models.Orm.Insert(&schedule)
+ if err != nil {
+ c.jsonErr(500, 500, "保存日程失败: "+err.Error())
+ return
+ }
+
+ // 2. 根据选中的渠道循环创建提醒
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
+ continue
+ }
+
+ targetVal := p.ReceiverTargets[ch]
+ var target *string
+ if targetVal != "" {
+ target = &targetVal
+ }
+
+ // 计算首次发送时间
+ firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
+
+ reminder := models.PlatformScheduleReminder{
+ ScheduleID: uint64(schedID),
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: uint64(claims.UserID), // 谁创建的就发给谁
+ ReceiverTarget: target,
+ RemindStatus: 0, // 待提醒
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
+ }
+
+ if ch == "EMAIL" || ch == "BARK" {
+ token := generateToken()
+ reminder.AckToken = &token
+ reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
+ reminder.MaxSendCount = p.MaxSendCount
+ if reminder.MaxSendCount <= 0 {
+ reminder.MaxSendCount = 1
+ }
+ } else {
+ // SMS 或 SITE_MSG
+ reminder.RepeatIntervalMinutes = 0
+ reminder.MaxSendCount = 1
+ }
+
+ _, err = models.Orm.Insert(&reminder)
+ if err != nil {
+ c.jsonErr(500, 500, "创建提醒失败: "+err.Error())
+ return
+ }
+ }
+
+ c.ok(map[string]interface{}{"schedule_id": schedID})
+}
+
+// UpdateReminder PUT /platform/reminder/:id
+func (c *PlatformReminderController) UpdateReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderFormPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
+ if err != nil {
+ c.jsonErr(400, 400, "日程时间格式不合法")
+ return
+ }
+
+ // 1. 更新日程详情
+ var schedule models.PlatformSchedule
+ err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
+ if err != nil {
+ c.jsonErr(404, 404, "日程未找到")
+ return
+ }
+
+ // 检查是否所有关联的提醒都已结束
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, "该日程提醒已全部结束,无法编辑")
+ return
+ }
+ schedule.Title = "日程提醒"
+ schedule.Content = p.Content
+ schedule.ScheduleTime = schedTime
+ _, err = models.Orm.Update(&schedule, "Title", "Content", "ScheduleTime")
+ if err != nil {
+ c.jsonErr(500, 500, "更新失败")
+ return
+ }
+
+ // 2. 软删除原本的所有提醒
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+
+ // 3. 重新建立提醒
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
+ continue
+ }
+
+ targetVal := p.ReceiverTargets[ch]
+ var target *string
+ if targetVal != "" {
+ target = &targetVal
+ }
+
+ firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
+
+ reminder := models.PlatformScheduleReminder{
+ ScheduleID: id,
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: schedule.UserID, // 谁创建的就发给谁
+ ReceiverTarget: target,
+ RemindStatus: 0,
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
+ }
+
+ if ch == "EMAIL" || ch == "BARK" {
+ token := generateToken()
+ reminder.AckToken = &token
+ reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
+ reminder.MaxSendCount = p.MaxSendCount
+ if reminder.MaxSendCount <= 0 {
+ reminder.MaxSendCount = 1
+ }
+ } else {
+ reminder.RepeatIntervalMinutes = 0
+ reminder.MaxSendCount = 1
+ }
+
+ _, err = models.Orm.Insert(&reminder)
+ if err != nil {
+ c.jsonErr(500, 500, "重新创建提醒失败")
+ return
+ }
+ }
+
+ c.ok(nil)
+}
+
+// DeleteReminder DELETE /platform/reminder/:id
+func (c *PlatformReminderController) DeleteReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ // 检查是否所有关联的提醒都已结束
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, "该日程提醒已全部结束,无法删除")
+ return
+ }
+
+ // 软删除日程
+ // 这里的 id 既可以是主表的 id,也可以是日程的 id
+ // 我们如果是管理页面,都是基于日程维度的,所以这里 id 指代 schedule_id
+ _, err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete()
+ if err == nil {
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+ }
+
+ c.ok(nil)
+}
+
+type reminderBatchDeletePayload struct {
+ Ids []uint64 `json:"ids"`
+}
+
+// BatchDeleteReminder POST /platform/reminder/batchDelete
+func (c *PlatformReminderController) BatchDeleteReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderBatchDeletePayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if len(p.Ids) == 0 {
+ c.ok(nil)
+ return
+ }
+
+ // 检查选中的日程是否有任何一个是全部结束的,防误操作
+ for _, scheduleID := range p.Ids {
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", scheduleID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, fmt.Sprintf("选中的日程ID %d 的提醒已全部结束,无法删除", scheduleID))
+ return
+ }
+ }
+
+ // 批量软删除
+ _, _ = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id__in", p.Ids).Delete()
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id__in", p.Ids).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+
+ c.ok(nil)
+}
+
+type reminderTestPayload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ RemindChannels []string `json:"remind_channels"`
+}
+
+// TestReminder POST /platform/reminder/test
+func (c *PlatformReminderController) TestReminder() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderTestPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if strings.TrimSpace(p.Title) == "" {
+ p.Title = "测试提醒"
+ }
+ if strings.TrimSpace(p.Content) == "" {
+ p.Content = "这是一条验证日程提醒配置的测试通知。"
+ }
+
+ senders := map[string]services.ReminderSender{
+ "SMS": &services.SMSSender{},
+ "EMAIL": &services.EmailSender{},
+ "BARK": &services.BarkSender{},
+ "SITE_MSG": &services.SiteMsgSender{},
+ }
+
+ type TestResult struct {
+ Channel string `json:"channel"`
+ Success bool `json:"success"`
+ Msg string `json:"msg"`
+ }
+ results := make([]TestResult, 0)
+
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ sender, ok := senders[ch]
+ if !ok {
+ results = append(results, TestResult{Channel: ch, Success: false, Msg: "不支持的提醒渠道"})
+ continue
+ }
+
+ dummyToken := "test-token-for-verification"
+ reminder := &models.PlatformScheduleReminder{
+ RemindChannel: ch,
+ ReceiverUserID: uint64(claims.UserID),
+ AckToken: &dummyToken,
+ }
+
+ success, sendErr := sender.Send(context.Background(), reminder, "[测试]"+p.Title, p.Content)
+ msg := "发送成功"
+ if !success {
+ msg = "发送失败"
+ if sendErr != nil {
+ msg = sendErr.Error()
+ }
+ }
+ results = append(results, TestResult{Channel: ch, Success: success, Msg: msg})
+ }
+
+ c.ok(results)
+}
diff --git a/go/main.go b/go/main.go
index 525a065..6ada88c 100644
--- a/go/main.go
+++ b/go/main.go
@@ -2,6 +2,7 @@ package main
import (
"server/models"
+ "server/services"
_ "server/routers"
"server/version"
@@ -21,5 +22,8 @@ func main() {
// 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
beego.SetStaticPath("/uploads", "uploads")
+ // 启动日程提醒定时任务
+ services.StartReminderScheduler(make(chan struct{}))
+
beego.Run()
}
diff --git a/go/models/init.go b/go/models/init.go
index fa05bbb..c02f7ee 100644
--- a/go/models/init.go
+++ b/go/models/init.go
@@ -69,6 +69,10 @@ func Init(_ string) {
new(SystemNormalSetting),
new(PlatformNormalSetting),
new(BackendNormalSetting),
+
+ new(PlatformSchedule),
+ new(PlatformScheduleReminder),
+ new(PlatformScheduleReminderSendLog),
)
// 创建全局 Ormer
diff --git a/go/models/platform_schedule_reminder.go b/go/models/platform_schedule_reminder.go
new file mode 100644
index 0000000..8e96596
--- /dev/null
+++ b/go/models/platform_schedule_reminder.go
@@ -0,0 +1,55 @@
+package models
+
+import "time"
+
+// PlatformSchedule 日程主表: yz_platform_schedule
+type PlatformSchedule struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ Title string `orm:"column(title);size(255)" json:"title"`
+ Content string `orm:"column(content);type(text)" json:"content"`
+ ScheduleTime time.Time `orm:"column(schedule_time);type(datetime)" json:"schedule_time"`
+ UserID uint64 `orm:"column(user_id)" json:"user_id"`
+}
+
+func (m *PlatformSchedule) TableName() string {
+ return "yz_platform_schedule"
+}
+
+// PlatformScheduleReminder 日程提醒主表: yz_platform_schedule_reminder
+type PlatformScheduleReminder struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ ScheduleID uint64 `orm:"column(schedule_id)" json:"schedule_id"`
+ RemindChannel string `orm:"column(remind_channel);size(20)" json:"remind_channel"` // SMS/EMAIL/BARK/SITE_MSG
+ AdvanceMinutes int `orm:"column(advance_minutes);default(0)" json:"advance_minutes"`
+ RepeatIntervalMinutes int `orm:"column(repeat_interval_minutes);default(0)" json:"repeat_interval_minutes"`
+ NextRemindTime time.Time `orm:"column(next_remind_time);type(datetime)" json:"next_remind_time"`
+ SendCount int `orm:"column(send_count);default(0)" json:"send_count"`
+ MaxSendCount int `orm:"column(max_send_count);default(1)" json:"max_send_count"`
+ AckToken *string `orm:"column(ack_token);size(64);null" json:"ack_token"`
+ AckStatus int8 `orm:"column(ack_status);default(0)" json:"ack_status"` // 0-未确认 1-已确认
+ AckTime *time.Time `orm:"column(ack_time);type(datetime);null" json:"ack_time"`
+ ReceiverUserID uint64 `orm:"column(receiver_user_id)" json:"receiver_user_id"`
+ ReceiverTarget *string `orm:"column(receiver_target);size(255);null" json:"receiver_target"`
+ RemindStatus int8 `orm:"column(remind_status);default(0)" json:"remind_status"` // 0-待提醒 1-提醒中 2-已结束
+ ScanLock string `orm:"column(scan_lock);size(64);default('')" json:"scan_lock"`
+ IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
+ CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
+}
+
+func (m *PlatformScheduleReminder) TableName() string {
+ return "yz_platform_schedule_reminder"
+}
+
+// PlatformScheduleReminderSendLog 提醒实际发送流水: yz_platform_schedule_reminder_send_log
+type PlatformScheduleReminderSendLog struct {
+ ID uint64 `orm:"column(id);pk;auto" json:"id"`
+ ReminderID uint64 `orm:"column(reminder_id)" json:"reminder_id"`
+ SendTime time.Time `orm:"column(send_time);type(datetime)" json:"send_time"`
+ SendResult int8 `orm:"column(send_result)" json:"send_result"` // 0-失败 1-成功
+ FailReason *string `orm:"column(fail_reason);size(255);null" json:"fail_reason"`
+}
+
+func (m *PlatformScheduleReminderSendLog) TableName() string {
+ return "yz_platform_schedule_reminder_send_log"
+}
diff --git a/go/routers/api/api.go b/go/routers/api/api.go
index 6e8af17..1281df9 100644
--- a/go/routers/api/api.go
+++ b/go/routers/api/api.go
@@ -30,4 +30,7 @@ func Register() {
// 对外提卡接口(无需登录)
// GET /api/getcard?type=xianyu&module=cursor&data_type=tk
beego.Router("/api/getcard", &controllers.ApiGetCardController{}, "get:GetCard")
+
+ // 日程提醒确认接口(无需登录)
+ beego.Router("/api/schedule/reminder/ack", &controllers.ApiReminderController{}, "get:AckReminder")
}
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index 61842bc..80fd3b0 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -255,4 +255,11 @@ func Register() {
beego.Router("/platform/notebook/create", &controllers.PlatformNotebookController{}, "post:Create")
beego.Router("/platform/notebook/update/:id", &controllers.PlatformNotebookController{}, "post:Update")
beego.Router("/platform/notebook/delete/:id", &controllers.PlatformNotebookController{}, "delete:Delete")
+
+ // 日程提醒管理
+ beego.Router("/platform/reminder/list", &controllers.PlatformReminderController{}, "get:GetReminderList")
+ beego.Router("/platform/reminder/test", &controllers.PlatformReminderController{}, "post:TestReminder")
+ beego.Router("/platform/reminder/:id", &controllers.PlatformReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder")
+ beego.Router("/platform/reminder", &controllers.PlatformReminderController{}, "post:CreateReminder")
+ beego.Router("/platform/reminder/batchDelete", &controllers.PlatformReminderController{}, "post:BatchDeleteReminder")
}
diff --git a/go/services/reminder_scheduler.go b/go/services/reminder_scheduler.go
new file mode 100644
index 0000000..fc4df65
--- /dev/null
+++ b/go/services/reminder_scheduler.go
@@ -0,0 +1,371 @@
+package services
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "server/models"
+)
+
+// ReminderSender 提醒发送接口
+type ReminderSender interface {
+ Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error)
+}
+
+// SMSSender 短信发送实现
+type SMSSender struct{}
+
+func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
+ backendURL, apiKey, err := getDefaultSystemSMSConfig()
+ if err != nil {
+ return false, err
+ }
+ phone := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ phone = *reminder.ReceiverTarget
+ } else {
+ var user models.AdminUser
+ if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
+ phone = *user.Phone
+ }
+ }
+ if phone == "" {
+ return false, fmt.Errorf("未配置手机号")
+ }
+
+ enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
+ payload := map[string]interface{}{
+ "phone": phone,
+ "content": title + ": " + content,
+ }
+ bs, _ := json.Marshal(payload)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
+ if err != nil {
+ return false, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Api-Key", apiKey)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ return true, nil
+}
+
+// EmailSender 邮件发送实现
+type EmailSender struct{}
+
+func (s *EmailSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
+ emails, err := ListSystemEmails()
+ if err != nil || len(emails) == 0 {
+ return false, fmt.Errorf("未配置系统邮箱")
+ }
+ emailCfg := emails[0]
+ if emailCfg.FromAddress == "" || emailCfg.Host == "" {
+ return false, fmt.Errorf("未配置系统邮箱")
+ }
+
+ toEmail := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ toEmail = *reminder.ReceiverTarget
+ } else {
+ var user models.AdminUser
+ if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
+ toEmail = *user.Email
+ }
+ }
+ if toEmail == "" {
+ return false, fmt.Errorf("未配置收件邮箱")
+ }
+
+ sysDomain := models.GetPlatformSettingValue("system_domain", "http://127.0.0.1:8080")
+ ackToken := ""
+ if reminder.AckToken != nil {
+ ackToken = *reminder.AckToken
+ }
+
+ // 构造 HTML 邮件
+ htmlBody := fmt.Sprintf(`
+
+
日程提醒:%s
+
%s
+
+ `, title, content)
+
+ if ackToken != "" {
+ ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
+ htmlBody += fmt.Sprintf(`
+
+
确认收到后,系统将不再向您发送该日程的重复提醒。
+ `, ackURL)
+ }
+
+ htmlBody += "
"
+
+ cfg := SMTPConfig{
+ FromAddress: emailCfg.FromAddress,
+ Host: emailCfg.Host,
+ Port: emailCfg.Port,
+ Password: emailCfg.Password,
+ Encryption: emailCfg.Encryption,
+ Timeout: emailCfg.Timeout,
+ }
+ if emailCfg.FromName != nil {
+ cfg.FromName = *emailCfg.FromName
+ }
+
+ err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+// BarkSender Bark 推送实现
+type BarkSender struct{}
+
+func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
+ deviceKey := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ deviceKey = *reminder.ReceiverTarget
+ } else {
+ deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
+ }
+ if deviceKey == "" {
+ return false, fmt.Errorf("Bark 设备 Key 未配置")
+ }
+
+ serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
+ sysDomain := models.GetPlatformSettingValue("system_domain", "http://127.0.0.1:8080")
+ ackToken := ""
+ if reminder.AckToken != nil {
+ ackToken = *reminder.AckToken
+ }
+
+ baseURL := strings.TrimRight(serverURL, "/")
+ escapedTitle := url.PathEscape(title)
+ pushContent := content
+ if ackToken != "" {
+ pushContent += "\n确认收到请点击→"
+ }
+ escapedContent := url.PathEscape(pushContent)
+
+ barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
+
+ if ackToken != "" {
+ ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
+ // Bark 官方推送支持 url 参数
+ barkURL += "?url=" + url.QueryEscape(ackURL)
+ }
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ return true, nil
+}
+
+// SiteMsgSender 站内信发送实现
+type SiteMsgSender struct{}
+
+func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
+ now := time.Now()
+ msg := &models.SystemReminderList{
+ Title: title,
+ Content: content,
+ SenderID: 0,
+ SenderType: "system",
+ ReceiverID: reminder.ReceiverUserID,
+ ReceiverType: "platform", // 平台端用户
+ IsRead: 0,
+ CreateTime: &now,
+ }
+ _, err := models.Orm.Insert(msg)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+// generateUUID 生成一个安全的随机 UUID 字符
+func generateUUID() string {
+ b := make([]byte, 16)
+ _, _ = rand.Read(b)
+ b[6] = (b[6] & 0x0f) | 0x40
+ b[8] = (b[8] & 0x3f) | 0x80
+ return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
+}
+
+// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
+func StartReminderScheduler(stopChan chan struct{}) {
+ ticker := time.NewTicker(1 * time.Minute)
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+ scanAndSendReminders()
+ case <-stopChan:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+}
+
+func scanAndSendReminders() {
+ // 1. 生成唯一扫描批次号用于抢占锁定
+ scanBatch := generateUUID()
+ now := time.Now()
+
+ // 2. 抢占待处理的数据(乐观锁防并发重复发送)
+ _, err := models.Orm.Raw(`
+ UPDATE yz_platform_schedule_reminder
+ SET scan_lock = ?, update_time = NOW()
+ WHERE next_remind_time <= ?
+ AND remind_status IN (0, 1)
+ AND is_deleted = 0
+ AND (scan_lock = '' OR scan_lock IS NULL)
+ `, scanBatch, now).Exec()
+ if err != nil {
+ return
+ }
+
+ // 3. 查询自己锁定成功的数据
+ var list []models.PlatformScheduleReminder
+ _, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("scan_lock", scanBatch).
+ Filter("remind_status__in", 0, 1).
+ Filter("is_deleted", 0).
+ All(&list)
+ if err != nil || len(list) == 0 {
+ return
+ }
+
+ // 实例分发发送
+ senders := map[string]ReminderSender{
+ "SMS": &SMSSender{},
+ "EMAIL": &EmailSender{},
+ "BARK": &BarkSender{},
+ "SITE_MSG": &SiteMsgSender{},
+ }
+
+ for i := range list {
+ reminder := &list[i]
+
+ // 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒")
+ var schedule models.PlatformSchedule
+ err := models.Orm.QueryTable(new(models.PlatformSchedule)).
+ Filter("id", reminder.ScheduleID).
+ One(&schedule)
+ title := "日程提醒"
+ content := "您有一个待处理的日程时间已到,请注意查收。"
+ if err == nil {
+ content = schedule.Content
+ }
+
+ sender, ok := senders[reminder.RemindChannel]
+ if !ok {
+ // 未知渠道,直接强制置为结束
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("id", reminder.ID).
+ Update(map[string]interface{}{
+ "remind_status": 2,
+ "scan_lock": "",
+ "update_time": time.Now(),
+ })
+ continue
+ }
+
+ // 执行发送
+ ctx := context.Background()
+ success, sendErr := sender.Send(ctx, reminder, title, content)
+
+ // 3.2 记录发送流水日志
+ sendResult := int8(0)
+ var failReason *string
+ if success {
+ sendResult = 1
+ } else if sendErr != nil {
+ errStr := sendErr.Error()
+ if len(errStr) > 255 {
+ errStr = errStr[:255]
+ }
+ failReason = &errStr
+ }
+
+ logRow := &models.PlatformScheduleReminderSendLog{
+ ReminderID: reminder.ID,
+ SendTime: time.Now(),
+ SendResult: sendResult,
+ FailReason: failReason,
+ }
+ _, _ = models.Orm.Insert(logRow)
+
+ // 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
+ newSendCount := reminder.SendCount + 1
+ newStatus := reminder.RemindStatus
+
+ if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
+ // 一次性发送:发送后直接置为结束
+ newStatus = 2
+ } else {
+ // 重复发送渠道 EMAIL / BARK
+ // 如果还没被 Ack,且没有达到 max_send_count,继续提醒
+ if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
+ newStatus = 1 // 提醒中
+ // 更新下次发送时间
+ reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
+ } else {
+ // 达到最大上限或者已 Ack
+ newStatus = 2
+ }
+ }
+
+ // 3.4 回写主表记录
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("id", reminder.ID).
+ Update(map[string]interface{}{
+ "SendCount": newSendCount,
+ "NextRemindTime": reminder.NextRemindTime,
+ "RemindStatus": newStatus,
+ "ScanLock": "", // 释放扫描锁
+ "UpdateTime": time.Now(),
+ })
+ }
+}
diff --git a/go/services/system_email_smtp.go b/go/services/system_email_smtp.go
index 9d4e902..4eb3abc 100644
--- a/go/services/system_email_smtp.go
+++ b/go/services/system_email_smtp.go
@@ -117,6 +117,100 @@ func SendTestEmailSMTP(cfg SMTPConfig, to string) error {
return client.Quit()
}
+// SendHTMLEmailSMTP 发送一封 HTML 格式邮件
+func SendHTMLEmailSMTP(cfg SMTPConfig, to string, subject string, htmlBody string) error {
+ to = strings.TrimSpace(to)
+ if to == "" {
+ return fmt.Errorf("收件人不能为空")
+ }
+ if cfg.Host == "" || cfg.FromAddress == "" {
+ return fmt.Errorf("SMTP 主机或发件人不能为空")
+ }
+ if cfg.Port == 0 {
+ cfg.Port = 465
+ }
+ timeout := cfg.Timeout
+ if timeout == 0 {
+ timeout = 30
+ }
+ d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
+ addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
+ enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
+ if enc == "" {
+ enc = "ssl"
+ }
+
+ var client *smtp.Client
+ var err error
+
+ switch enc {
+ case "ssl":
+ conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
+ if derr != nil {
+ return fmt.Errorf("连接 SMTP 失败: %w", derr)
+ }
+ defer conn.Close()
+ client, err = smtp.NewClient(conn, cfg.Host)
+ if err != nil {
+ return fmt.Errorf("SMTP 握手失败: %w", err)
+ }
+ case "tls":
+ conn, derr := d.Dial("tcp", addr)
+ if derr != nil {
+ return fmt.Errorf("连接 SMTP 失败: %w", derr)
+ }
+ defer conn.Close()
+ client, err = smtp.NewClient(conn, cfg.Host)
+ if err != nil {
+ return fmt.Errorf("SMTP 握手失败: %w", err)
+ }
+ if ok, _ := client.Extension("STARTTLS"); ok {
+ if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
+ _ = client.Close()
+ return fmt.Errorf("STARTTLS 失败: %w", err)
+ }
+ }
+ case "none":
+ conn, derr := d.Dial("tcp", addr)
+ if derr != nil {
+ return fmt.Errorf("连接 SMTP 失败: %w", derr)
+ }
+ defer conn.Close()
+ client, err = smtp.NewClient(conn, cfg.Host)
+ if err != nil {
+ return fmt.Errorf("SMTP 握手失败: %w", err)
+ }
+ default:
+ return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
+ }
+ defer func() { _ = client.Close() }()
+
+ auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
+ if err = client.Auth(auth); err != nil {
+ return fmt.Errorf("SMTP 认证失败: %w", err)
+ }
+ if err = client.Mail(cfg.FromAddress); err != nil {
+ return fmt.Errorf("MAIL FROM 失败: %w", err)
+ }
+ if err = client.Rcpt(to); err != nil {
+ return fmt.Errorf("RCPT TO 失败: %w", err)
+ }
+ wc, err := client.Data()
+ if err != nil {
+ return fmt.Errorf("DATA 失败: %w", err)
+ }
+ fromName := strings.TrimSpace(cfg.FromName)
+ headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
+ formatFromHeader(fromName, cfg.FromAddress), to, subject)
+ if _, err = wc.Write([]byte(headers + htmlBody)); err != nil {
+ return fmt.Errorf("写入邮件内容失败: %w", err)
+ }
+ if err = wc.Close(); err != nil {
+ return fmt.Errorf("结束 DATA 失败: %w", err)
+ }
+ return client.Quit()
+}
+
func formatFromHeader(name, addr string) string {
name = strings.TrimSpace(name)
if name == "" {
diff --git a/platform/components.d.ts b/platform/components.d.ts
index 7ffac2a..2e1dc36 100644
--- a/platform/components.d.ts
+++ b/platform/components.d.ts
@@ -25,6 +25,7 @@ declare module 'vue' {
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
+ ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
diff --git a/platform/src/App.vue b/platform/src/App.vue
index bf9547c..8e6b25f 100644
--- a/platform/src/App.vue
+++ b/platform/src/App.vue
@@ -1,8 +1,11 @@
-
+
+
+
diff --git a/platform/src/views/tools/reminder/components/edit.vue b/platform/src/views/tools/reminder/components/edit.vue
index 4569f29..d6f211b 100644
--- a/platform/src/views/tools/reminder/components/edit.vue
+++ b/platform/src/views/tools/reminder/components/edit.vue
@@ -1,8 +1,8 @@
@@ -10,174 +10,144 @@
ref="formRef"
:model="form"
:rules="rules"
- label-width="100px"
+ label-width="120px"
v-loading="loading"
+ label-position="right"
+ class="reminder-form"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
+
+
+ 短信 (SMS)
+ 邮件 (EMAIL)
+ Bark 推送
+ 站内信 (SITE_MSG)
+
-
-
-
+
+ 提前多少分钟开始发送第一次提醒
-
-
-
-
+
+
+ 重复发送设置 (仅EMAIL/BARK生效)
-
-
- 开启后按下方周期重复发送
-
+
+
+ 未确认前,每隔多少分钟重新发送一次
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 启用
- 禁用
-
-
+
+
+ 防骚扰兜底,发送达到该次数后自动停止
+
+
- 取消
- 保存
+
diff --git a/platform/src/views/tools/reminder/index.vue b/platform/src/views/tools/reminder/index.vue
index 076f9fd..9952027 100644
--- a/platform/src/views/tools/reminder/index.vue
+++ b/platform/src/views/tools/reminder/index.vue
@@ -1,7 +1,7 @@