v0.3.20: 每日汇报系统 — 每天8点自动收集前一日数据并推送汇总报告

新增:
- src/services/daily-report.service.ts (核心服务: 数据收集/报告生成/格式化/调度器)
- API: GET/PUT daily-report/config, GET daily-report/preview, POST daily-report/test, GET daily-report/last-run
- 前端: 侧边栏"📊 每日汇报"菜单 + SystemConfig.vue 配置面板(时间/内容开关/预览/测试发送)
- main.ts: 每60秒检查调度, 08:00-08:04 窗口内运行

报告内容: 搜索统计/转存统计(成功率)/各网盘容量和活跃状态/用户数
This commit is contained in:
2026-05-17 17:10:39 +08:00
parent 95df193e26
commit 0e0cad1271
45 changed files with 622 additions and 64 deletions

View File

@@ -38,6 +38,7 @@
<el-menu-item index="sys-strategy"> 性能配置</el-menu-item>
<el-menu-item index="sys-password">🔑 修改密码</el-menu-item>
<el-menu-item index="sys-notify">📬 消息推送</el-menu-item>
<el-menu-item index="sys-daily-report">📊 每日汇报</el-menu-item>
</el-sub-menu>
<el-menu-item index="save-records">
@@ -96,6 +97,7 @@ const pageTitles: Record<string, string> = {
'sys-strategy': '性能配置',
'sys-password': '修改管理员密码',
'sys-notify': '消息推送',
'sys-daily-report': '每日汇报',
'save-records': '转存日志',
}

View File

@@ -612,6 +612,51 @@
</el-table>
</el-card>
<!-- 📊 每日汇报 -->
<el-card id="section-sys-daily-report" v-show="!activeSection || activeSection === 'sys-daily-report'">
<template #header>
<div style="display:flex; align-items:center; justify-content:space-between;">
<span>📊 每日汇报</span>
<div>
<el-button size="small" :loading="dailyReportPreviewing" @click="handleDailyReportPreview">📋 预览</el-button>
<el-button size="small" type="primary" :loading="dailyReportSending" @click="handleDailyReportSendTest"> 发送测试</el-button>
</div>
</div>
</template>
<el-form label-width="140px" label-position="left">
<el-form-item label="启用每日汇报">
<el-switch v-model="dailyReportForm.enabled" active-text="每天8点自动发送" />
</el-form-item>
<el-form-item label="发送时间">
<el-time-picker
v-model="dailyReportForm.time"
format="HH:mm"
value-format="HH:mm"
placeholder="选择时间"
:disabled="!dailyReportForm.enabled"
/>
<div class="form-tip">默认每天 08:00 发送前一天的汇总报告</div>
</el-form-item>
<el-form-item label="报告内容">
<div style="display:flex; flex-wrap:wrap; gap:16px;">
<el-switch v-model="dailyReportForm.includeSearch" active-text="搜索统计" :disabled="!dailyReportForm.enabled" />
<el-switch v-model="dailyReportForm.includeSaves" active-text="转存统计" :disabled="!dailyReportForm.enabled" />
<el-switch v-model="dailyReportForm.includeStorage" active-text="网盘容量" :disabled="!dailyReportForm.enabled" />
<el-switch v-model="dailyReportForm.includeUsers" active-text="用户数" :disabled="!dailyReportForm.enabled" />
</div>
</el-form-item>
<el-form-item label="上次发送">
<span>{{ dailyReportLastRun || '从未发送' }}</span>
</el-form-item>
</el-form>
<!-- Preview modal -->
<el-dialog v-model="dailyReportPreviewVisible" title="📊 每日汇报预览" width="600px">
<div style="white-space: pre-wrap; font-family: monospace; background: var(--el-fill-color-light); padding: 16px; border-radius: 8px; max-height: 500px; overflow-y: auto;">{{ dailyReportPreview }}</div>
</el-dialog>
</el-card>
<!-- 🔄 系统维护 --> <el-card id="section-sys-maintenance" v-show="!activeSection || activeSection === 'sys-maintenance'"> <template #header> <span>🔄 系统维护</span> </template> <el-form label-width="180px" label-position="left"> <el-form-item label="自动更新镜像"> <el-switch v-model="autoUpdateEnabled" active-text="启用" inactive-text="禁用" /> <div class="form-tip">启用后 CloudSearch 将自动检测并更新到最新镜像版本</div> <div class="form-tip" style="color: var(--(--el-color-warning,#e6a23c));"> 当前需手动在服务器执行docker-compose -f /opt/CloudSearch/docker-compose.yml pull && docker-compose -f /opt/CloudSearch/docker-compose.yml up -d </div> </el-form-item> </el-form> </el-card>
<!-- 保存按钮 -->
@@ -694,6 +739,92 @@ const autoUpdateEnabled = computed({
})
// ======================== Push User Notifications ========================
// ======================== Daily Report ========================
const dailyReportForm = reactive({
enabled: true,
time: '08:00',
includeSearch: true,
includeSaves: true,
includeStorage: true,
includeUsers: true,
})
const dailyReportPreviewing = ref(false)
const dailyReportSending = ref(false)
const dailyReportPreview = ref('')
const dailyReportPreviewVisible = ref(false)
const dailyReportLastRun = ref('')
async function loadDailyReportConfig() {
try {
const res = await fetch('/api/admin/daily-report/config', {
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
})
if (res.ok) {
const cfg = await res.json()
Object.assign(dailyReportForm, cfg)
}
} catch {}
}
async function loadDailyReportLastRun() {
try {
const res = await fetch('/api/admin/daily-report/last-run', {
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
})
if (res.ok) {
const data = await res.json()
if (data.date) {
dailyReportLastRun.value = `${data.date} ${new Date(data.sentAt).toLocaleTimeString('zh-CN')}`
}
}
} catch {}
}
async function saveDailyReportConfig() {
try {
await fetch('/api/admin/daily-report/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
},
body: JSON.stringify({ ...dailyReportForm }),
})
} catch {}
}
async function handleDailyReportPreview() {
dailyReportPreviewing.value = true
try {
const res = await fetch('/api/admin/daily-report/preview', {
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
})
if (res.ok) {
const data = await res.json()
dailyReportPreview.value = data.content
dailyReportPreviewVisible.value = true
}
} finally {
dailyReportPreviewing.value = false
}
}
async function handleDailyReportSendTest() {
dailyReportSending.value = true
try {
const res = await fetch('/api/admin/daily-report/test', {
method: 'POST',
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
})
if (res.ok) {
ElMessage.success('测试报告已发送到全局通知通道')
} else {
ElMessage.error('发送失败')
}
} catch {
ElMessage.error('发送失败')
} finally {
dailyReportSending.value = false
}
}
const pushUsers = ref<any[]>([])
const notifyProviders = ref<Record<string, any>>({})
// const pushUserDialogVisible = ref(false) // removed - using inline form
@@ -1005,6 +1136,8 @@ onMounted(async () => {
initGlobalNotifyForm()
await loadGlobalNotifyConfig()
loadPushUsers()
loadDailyReportConfig()
loadDailyReportLastRun()
loadPushUserAccountOptions()
})
@@ -1320,6 +1453,7 @@ async function handleSave() {
key: cfg.key,
value: String(configs[cfg.key] ?? cfg.value),
}))
await saveDailyReportConfig()
// Add global_notify_config as JSON entry
entries.push({
key: 'global_notify_config',