提交 96d702f8 authored 作者: 陈泽健's avatar 陈泽健

feat(dingtalk): 定时任务钉钉报告通知(三形态消息 + 加签发送 + 配置管理)

- 新增 dingtalk_notify_service:build_sign(HMAC-SHA256 加签) / 三形态消息构建( 报告 / ️ 告警 / 🚨 异常)/ 失败明细折叠与超长保护 / send_markdown 发送
- 定时任务新增 dingtalk_notify 开关:执行完成后自动推送钉钉报告通知
- 系统设置提供报告钉钉通知配置管理(report_webhook_url / report_secret / report_at_mobiles,secret 脱敏)
- 新增 PLATFORM_BASE_URL 配置(消息中查看完整报告链接用)
- database._ensure_columns 旧库升级补列(scheduled_tasks.dingtalk_notify / dingtalk_configs.report_*)
- PRD + 执行计划文档归档 Docs/PRD/需求文档/执行中心/
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 ea2a90cb
# 执行计划 - 定时任务钉钉报告通知
> **文档类型**: 计划执行文档
> **创建日期**: 2026-09-07
> **作者**: czj
> **关联 PRD**: `_PRD_需求文档_定时任务钉钉报告通知.md`
> **状态**: 待执行
---
## Phase 1:数据模型与配置(P0)
**目标**:配置有地方存,链接有地址可拼。
### 1.1 `dingtalk_configs` 表加列(`models/dingtalk_config.py` + `database.py` 迁移)
- [ ] `report_webhook_url` String(512) nullable
- [ ] `report_secret` String(255) nullable
- [ ] `report_at_mobiles` JSON nullable(默认 list)
- [ ] `database.py` 启动迁移补列(SQLite/MySQL 双兼容,沿用既有模式)
### 1.2 `scheduled_tasks` 表加列
- [ ] `dingtalk_notify` Boolean default False
- [ ] 模型 `to_dict()` / docstring 同步
- [ ] `database.py` 迁移补列
### 1.3 `config.py` + `schemas/`
- [ ] `Settings.PLATFORM_BASE_URL: str = ""`(env 可覆盖)
- [ ] `ReportNotifyConfigResponse`(webhook_url / secret 脱敏 / at_mobiles / configured 标志)
- [ ] `ReportNotifyConfigUpdateRequest`(secret 为 `******` 表示保持原值)
- [ ] 定时任务 create/update schema 加 `dingtalk_notify: bool = False`
**验收**:应用启动后两张表新列存在,旧数据不丢。
---
## Phase 2:钉钉通知服务(P0 核心)
**目标**:新建 `backend/app/services/dingtalk_notify_service.py`
### 2.1 签名与发送
- [ ] `build_sign(secret, timestamp)`: `f"{ts}\n{secret}"` → HMAC-SHA256 → base64 → `quote_plus`
- [ ] `send_markdown(config, title, text, at_mobiles)`: 拼接 `&timestamp=&sign=` → POST JSON(msgtype=markdown + at)→ errcode==0 判成功
- [ ] `requests.post(timeout=10)``loop.run_in_executor` 线程池,不阻塞事件循环
### 2.2 消息构建
- [ ] `build_execution_message(execution, failed_results, task_name)``(title, text, at_mobiles)`
- [ ] 三形态选择:✅ 全通过 / ⚠️ 有失败 / 🚨 0 条用例结果
- [ ] 失败明细 ≤5 条 + 「还有 N 项,详见报告」折叠
- [ ] 错误摘要:error_message 优先 → 首个失败步骤 error 兜底;压平换行、截 80 字符
- [ ] 超长保护:UTF-8 > 2000 字节逐条丢明细,追加「(明细过长已折叠,详见报告)」
- [ ] 链接:`{PLATFORM_BASE_URL}/api/reports/generate/{id}`,base 为空省略链接行
### 2.3 业务入口
- [ ] `notify_execution_result(execution_id, task)`:查执行 + 失败用例 → 构建发送;异常只 log 不抛
- [ ] `send_test_message()`:读配置发测试 markdown,透出钉钉 errmsg
**验收**:单测覆盖签名对拍、三形态消息、5 条折叠、超长折叠。
---
## Phase 3:API 路由(P1)
### 3.1 `routers/system.py`
- [ ] `GET /api/system/report-notify-config`(secret 恒返 `******`,未设置返空)
- [ ] `PUT /api/system/report-notify-config``******` 不覆盖原值)
- [ ] `POST /api/system/report-notify-config/test`(返回 `{success, message}`
### 3.2 `routers/scheduled_tasks.py`
- [ ] create/update 透传 `dingtalk_notify`
- [ ] 任务列表/详情返回 `dingtalk_notify`
**验收**:curl 走通 GET/PUT/test 三接口;任务带开关保存后读回一致。
---
## Phase 4:调度挂钩(P0)
- [ ] `scheduler_service.py` UI 路径报告生成块之后:`if task.dingtalk_notify:``notify_execution_result`,独立 try/except
- [ ] webhook 未配置 → warning 日志跳过
- [ ] security 路径不挂钩(非目标)
**验收**:单测 mock 发送函数,断言开关开/关两种路径的调用与否。
---
## Phase 5:前端(P1)
### 5.1 `Settings.vue`
- [ ] 新增「报告钉钉通知(机器人)」卡片:webhook 地址(文本框)/ 加签密钥(密码框,`******` 占位)/ @手机号(标签输入)/ 保存 + 发送测试按钮
- [ ] `api/system.ts` 补三个接口封装
### 5.2 `ScheduledTasks.vue`
- [ ] 任务编辑表单加「执行完成后发送钉钉通知」开关
- [ ] 任务列表列标识(可选,若列宽允许可省)
**验收**:配置→测试发送→保存→任务开启开关,全流程页面操作走通;`npm run build` 通过。
---
## Phase 6:测试、部署与真实验证(P1)
### 6.1 单元测试 `tests/test_dingtalk_notify.py`
- [ ] 签名对拍(已知 secret+timestamp → 期望 sign)
- [ ] 三形态消息构建
- [ ] 5 条折叠 / 超长折叠 / 链接省略
- [ ] 调度挂钩开关行为
- [ ] 配置 API 脱敏与 `******` 保留语义
### 6.2 回归与部署
- [ ] `pytest tests/ -v` 全量回归
- [ ] `deploy_44_202.py --host 192.168.5.44 --no-frontend` 同步后端(前端改动视情况带上)
- [ ] 5.44 设置 `PLATFORM_BASE_URL`(确认实际访问地址,如 `http://192.168.5.44`)并重启容器
- [ ] 页面配置真实机器人 webhook → 测试发送 → 群内收到消息
- [ ] 触发一次小任务执行(含失败用例)→ 收到 ⚠️ 告警 + 链接手机可打开
### 6.3 收尾
- [ ] 更新 `HANDOFF_UI自动化.md`
- [ ] PRD/计划文档状态更新
---
## 风险与回滚
| 风险 | 缓解 |
|------|------|
| 钉钉机器人安全设置(关键字/加签/IP白名单)导致发送失败 | 测试发送透出 errmsg;消息标题含「UI自动化」可配合关键字设置 |
| 消息超 2048 字节被钉钉拒绝 | 超长保护逐条折叠明细(PRD 2.4.4-5),单测覆盖 |
| requests 阻塞事件循环 | run_in_executor 线程池 + timeout 10s |
| 误推大量失败消息刷屏 | 失败明细限 5 条折叠;一个任务一次执行只推一条 |
| 平台未配 PLATFORM_BASE_URL | 省略链接行仍发汇总,不阻断 |
---
*本文档由 Claude Code 生成,遵循项目计划执行文档规范。*
......@@ -189,6 +189,14 @@ async def _ensure_columns(conn) -> None:
("scheduled_tasks", "case_type", "VARCHAR(20) DEFAULT 'ui'"),
# 定时任务:安全测试配置ID(旧库升级)
("scheduled_tasks", "config_id", "VARCHAR(64) DEFAULT NULL"),
# 定时任务:执行完成后发送钉钉报告通知开关(旧库升级)
("scheduled_tasks", "dingtalk_notify", "BOOLEAN DEFAULT 0"),
# 钉钉配置:报告通知机器人 Webhook(旧库升级)
("dingtalk_configs", "report_webhook_url", "VARCHAR(512) DEFAULT ''"),
# 钉钉配置:报告通知机器人加签密钥(旧库升级)
("dingtalk_configs", "report_secret", "VARCHAR(255) DEFAULT ''"),
# 钉钉配置:报告通知被@手机号列表(旧库升级)
("dingtalk_configs", "report_at_mobiles", "JSON"),
# 性能测试:增强指标体系(P95/标准差/延迟/连接时间/带宽/峰值TPS/Apdex/错误分类,旧库升级)
("performance_tasks", "p95_response_time", "FLOAT DEFAULT 0.0"),
("performance_tasks", "std_dev", "FLOAT DEFAULT 0.0"),
......
......@@ -11,7 +11,7 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import String, DateTime, Integer
from sqlalchemy import String, DateTime, Integer, JSON
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
......@@ -45,6 +45,16 @@ class DingTalkConfig(Base):
captcha_path: Mapped[str] = mapped_column(
String(200), default="/platform/api/code", comment="验证码接口路径"
)
# 报告钉钉通知(群机器人 webhook 模式,与企业内部应用配置相互独立)
report_webhook_url: Mapped[Optional[str]] = mapped_column(
String(512), default="", comment="报告通知机器人 Webhook 地址"
)
report_secret: Mapped[Optional[str]] = mapped_column(
String(255), default="", comment="报告通知机器人加签密钥"
)
report_at_mobiles: Mapped[Optional[list]] = mapped_column(
JSON, default=list, comment="报告通知被@手机号列表(仅失败/异常时@)"
)
updated_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
......@@ -60,4 +70,7 @@ class DingTalkConfig(Base):
"target_url": self.target_url or "",
"login_path": self.login_path or "/platform/api/auth/login",
"captcha_path": self.captcha_path or "/platform/api/code",
"report_webhook_url": self.report_webhook_url or "",
"report_secret": self.report_secret or "",
"report_at_mobiles": self.report_at_mobiles or [],
}
\ No newline at end of file
......@@ -38,7 +38,8 @@ class ScheduledTask(Base):
interval_unit (str): 间隔单位 (minutes/hours/days)
run_time (str): 执行时间 HH:MM(schedule_type=daily/weekly)
weekdays (list): 星期 [0-6](schedule_type=weekly)
auto_report (bool): 执行完成后是否自动生成报告
auto_report (bool): 执行完成后是否自动生成报告
dingtalk_notify (bool): 执行完成后是否发送钉钉报告通知
last_run_at (datetime): 最近一次触发时间
next_run_at (datetime): 下次触发时间
run_count (int): 累计触发次数
......@@ -95,6 +96,9 @@ class ScheduledTask(Base):
auto_report: Mapped[bool] = mapped_column(
Boolean, default=True, comment="执行完成后自动生成报告"
)
dingtalk_notify: Mapped[bool] = mapped_column(
Boolean, default=False, comment="执行完成后发送钉钉报告通知"
)
last_run_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True, default=None, comment="最近一次触发时间"
)
......@@ -137,6 +141,7 @@ class ScheduledTask(Base):
"run_time": self.run_time or "",
"weekdays": self.weekdays or [],
"auto_report": self.auto_report,
"dingtalk_notify": self.dingtalk_notify,
"last_run_at": self.last_run_at.isoformat() if self.last_run_at else None,
"next_run_at": self.next_run_at.isoformat() if self.next_run_at else None,
"run_count": self.run_count,
......
......@@ -115,6 +115,7 @@ async def create_task(
run_time=data.run_time or "",
weekdays=data.weekdays or [],
auto_report=data.auto_report,
dingtalk_notify=data.dingtalk_notify,
)
task.next_run_at = compute_next_run(task)
db.add(task)
......
......@@ -30,6 +30,8 @@ from app.models.dingtalk_config import DingTalkConfig
from app.schemas.dingtalk_config import (
DingTalkConfigResponse,
DingTalkConfigUpdateRequest,
ReportNotifyConfigResponse,
ReportNotifyConfigUpdateRequest,
SutConfigResponse,
SutConfigUpdateRequest,
)
......@@ -256,6 +258,80 @@ async def update_dingtalk_config(request: DingTalkConfigUpdateRequest, db: Async
)
# ==================== 报告钉钉通知配置(群机器人 Webhook 模式) ====================
@router.get("/report-notify-config", response_model=ReportNotifyConfigResponse, summary="获取报告钉钉通知配置")
async def get_report_notify_config(db: AsyncSession = Depends(get_db)):
"""
获取报告钉钉通知配置(群机器人)
secret 已设置时脱敏返回 ******,前端密码框展示。
"""
from app.services.dingtalk_notify_service import get_notify_config, get_config_masked
config = get_config_masked(await get_notify_config(db))
return ReportNotifyConfigResponse(
webhook_url=config.get("webhook_url", ""),
secret=config.get("secret", ""),
at_mobiles=config.get("at_mobiles", []),
configured=bool(config.get("webhook_url")),
)
@router.put("/report-notify-config", response_model=ReportNotifyConfigResponse, summary="更新报告钉钉通知配置")
async def update_report_notify_config(
request: ReportNotifyConfigUpdateRequest,
db: AsyncSession = Depends(get_db),
):
"""
更新报告钉钉通知配置(持久化到数据库)
secret 传 ****** 表示保持原值不变(前端回显脱敏值后原样提交的场景)。
"""
result = await db.execute(select(DingTalkConfig).where(DingTalkConfig.id == 1))
config = result.scalar_one_or_none()
if not config:
config = DingTalkConfig(id=1)
db.add(config)
if request.webhook_url is not None:
config.report_webhook_url = request.webhook_url.strip()
if request.secret is not None and request.secret != "******":
config.report_secret = request.secret.strip()
if request.at_mobiles is not None:
# 去空白、去重、保留顺序
seen, mobiles = set(), []
for m in request.at_mobiles:
m = (m or "").strip()
if m and m not in seen:
seen.add(m)
mobiles.append(m)
config.report_at_mobiles = mobiles
await db.commit()
await db.refresh(config)
logger.info("报告钉钉通知配置已更新")
return ReportNotifyConfigResponse(
webhook_url=config.report_webhook_url or "",
secret="******" if config.report_secret else "",
at_mobiles=config.report_at_mobiles or [],
configured=bool(config.report_webhook_url),
)
@router.post("/report-notify-config/test", response_model=dict, summary="发送钉钉测试消息")
async def test_report_notify():
"""
用当前已保存的报告通知配置发送一条测试消息,验证机器人连通性。
"""
from app.services.dingtalk_notify_service import send_test_message
ok, message = await send_test_message()
return {"success": ok, "message": message}
# ==================== 被测系统配置 ====================
......
......@@ -8,7 +8,7 @@
创建日期:2026-08-13
"""
from typing import Optional
from typing import List, Optional
from pydantic import BaseModel, Field
......@@ -44,4 +44,19 @@ class SutConfigUpdateRequest(BaseModel):
)
captcha_path: Optional[str] = Field(
None, description="验证码接口路径,默认 /platform/api/code"
)
\ No newline at end of file
)
class ReportNotifyConfigResponse(BaseModel):
"""报告钉钉通知配置响应(secret 脱敏)"""
webhook_url: str = Field("", description="机器人 Webhook 地址")
secret: str = Field("", description="加签密钥(已设置时返回 ******)")
at_mobiles: List[str] = Field([], description="被@手机号列表")
configured: bool = Field(False, description="webhook 是否已配置")
class ReportNotifyConfigUpdateRequest(BaseModel):
"""报告钉钉通知配置更新请求(secret 传 ****** 表示保持原值)"""
webhook_url: Optional[str] = Field(None, description="机器人 Webhook 地址")
secret: Optional[str] = Field(None, description="加签密钥,****** 表示保持原值")
at_mobiles: Optional[List[str]] = Field(None, description="被@手机号列表")
\ No newline at end of file
......@@ -22,6 +22,7 @@ class ScheduledTaskCreate(BaseModel):
run_time: str = Field("", description="执行时间 HH:MM(daily/weekly)")
weekdays: List[int] = Field([], description="星期 [0-6](weekly)")
auto_report: bool = Field(True, description="执行完成后自动生成报告")
dingtalk_notify: bool = Field(False, description="执行完成后发送钉钉报告通知")
enabled: bool = Field(True, description="是否启用")
case_type: str = Field("ui", description="用例类型: ui/security")
config_id: Optional[str] = Field(None, description="安全测试配置ID(security时必填)")
......@@ -37,6 +38,7 @@ class ScheduledTaskUpdate(BaseModel):
run_time: Optional[str] = Field(None, description="执行时间 HH:MM")
weekdays: Optional[List[int]] = Field(None, description="星期 [0-6]")
auto_report: Optional[bool] = Field(None, description="自动生成报告")
dingtalk_notify: Optional[bool] = Field(None, description="发送钉钉报告通知")
enabled: Optional[bool] = Field(None, description="是否启用")
case_type: Optional[str] = Field(None, description="用例类型: ui/security")
config_id: Optional[str] = Field(None, description="安全测试配置ID")
......
此差异已折叠。
......@@ -390,6 +390,23 @@ async def run_task_once(task_id: str, manual: bool = False) -> Optional[str]:
f"[定时任务] 任务「{task.name}」生成报告失败: {e}"
)
# 5b. 钉钉报告通知(任务级开关;发送失败不影响调度推进)
if task.dingtalk_notify:
try:
from app.services.dingtalk_notify_service import notify_execution_result
ok, message = await notify_execution_result(execution.id, task.name, db=db)
if ok:
logger.info(f"[定时任务] 任务「{task.name}」钉钉通知已发送")
else:
logger.warning(
f"[定时任务] 任务「{task.name}」钉钉通知未发送: {message}"
)
except Exception as e:
logger.error(
f"[定时任务] 任务「{task.name}」钉钉通知异常: {e}"
)
# 6. 推进调度状态
task.last_run_at = datetime.now()
task.run_count = (task.run_count or 0) + 1
......
此差异已折叠。
......@@ -21,6 +21,7 @@ export interface ScheduledTask {
run_time: string
weekdays: number[]
auto_report: boolean
dingtalk_notify: boolean
schedule_description: string
last_run_at: string | null
next_run_at: string | null
......@@ -39,6 +40,7 @@ export interface ScheduledTaskPayload {
run_time?: string
weekdays?: number[]
auto_report?: boolean
dingtalk_notify?: boolean
enabled?: boolean
}
......
......@@ -83,4 +83,37 @@ export const systemApi = {
const response = await request.put('/api/system/sut-config', data)
return response as any
},
/**
* 获取报告钉钉通知配置(群机器人)
*/
async getReportNotifyConfig(): Promise<{
webhook_url: string
secret: string
at_mobiles: string[]
configured: boolean
}> {
const response = await request.get('/api/system/report-notify-config')
return response as any
},
/**
* 更新报告钉钉通知配置(secret 传 ****** 表示保持原值)
*/
async updateReportNotifyConfig(data: {
webhook_url?: string
secret?: string
at_mobiles?: string[]
}): Promise<any> {
const response = await request.put('/api/system/report-notify-config', data)
return response as any
},
/**
* 发送钉钉测试消息
*/
async testReportNotify(): Promise<{ success: boolean; message: string }> {
const response = await request.post('/api/system/report-notify-config/test')
return response as any
},
}
<!--
页面名称:ScheduledTasks.vue
页面描述:UI 自动化定时任务页面(任务列表 / 创建编辑 / 立即执行 / 运行历史)
页面描述:UI 自动化定时任务页面(任务列表 / 创建编辑 / 立即执行 / 运行历史 / 钉钉通知开关
路径:/scheduled-tasks
@author czj
@date 2026-08-19
@last-modified 2026-09-07
-->
<template>
<div class="scheduled-tasks-page">
......@@ -65,6 +66,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="钉钉通知" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.dingtalk_notify ? 'success' : 'info'" size="small">
{{ row.dingtalk_notify ? '开启' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-switch
......@@ -233,6 +241,11 @@
<span class="form-tip">执行完成后自动生成 HTML 报告到报告中心</span>
</el-form-item>
<el-form-item label="钉钉通知">
<el-switch v-model="form.dingtalk_notify" />
<span class="form-tip">执行完成后推送钉钉报告消息(机器人在「系统设置」配置)</span>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.enabled" />
<span class="form-tip">停用后不会按周期触发</span>
......@@ -345,6 +358,7 @@ interface TaskForm {
run_time: string
weekdays: number[]
auto_report: boolean
dingtalk_notify: boolean
enabled: boolean
}
......@@ -357,6 +371,7 @@ const createEmptyForm = (): TaskForm => ({
run_time: '',
weekdays: [1, 2, 3, 4, 5],
auto_report: true,
dingtalk_notify: false,
enabled: true,
})
......@@ -495,6 +510,7 @@ const openEdit = async (task: ScheduledTask) => {
run_time: task.run_time || '',
weekdays: [...(task.weekdays || [])],
auto_report: task.auto_report,
dingtalk_notify: task.dingtalk_notify,
enabled: task.enabled,
})
if (modules.value.length === 0) await loadModules()
......@@ -517,6 +533,7 @@ const handleSave = async () => {
run_time: form.schedule_type === 'interval' ? '' : form.run_time,
weekdays: form.schedule_type === 'weekly' ? form.weekdays : [],
auto_report: form.auto_report,
dingtalk_notify: form.dingtalk_notify,
enabled: form.enabled,
}
if (isEdit.value) {
......
<!--
页面名称:Settings.vue
页面描述:系统配置页面(被测系统配置 + 钉钉对接配置)
页面描述:系统配置页面(被测系统配置 + 钉钉对接配置 + 报告钉钉通知配置
路径:/system/settings
@author czj
@date 2026-08-13
@last-modified 2026-08-19
@last-modified 2026-09-07
-->
<template>
<div class="settings-page">
......@@ -123,6 +123,67 @@
</el-form-item>
</el-form>
</el-card>
<!-- 报告钉钉通知配置(群机器人) -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div class="card-header">
<span>报告钉钉通知(群机器人)</span>
<el-tag v-if="reportNotifyForm.configured" type="success" size="small">已配置</el-tag>
</div>
</template>
<el-form
ref="reportNotifyFormRef"
:model="reportNotifyForm"
:rules="reportNotifyRules"
label-width="140px"
label-position="right"
style="max-width: 600px"
>
<el-form-item label="Webhook 地址" prop="webhook_url">
<el-input
v-model="reportNotifyForm.webhook_url"
placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx"
clearable
/>
</el-form-item>
<el-form-item label="加签密钥" prop="secret">
<el-input
v-model="reportNotifyForm.secret"
type="password"
show-password
placeholder="机器人开启加签时填 SEC 开头密钥;未开启可留空"
clearable
/>
</el-form-item>
<el-form-item label="被@手机号" prop="at_mobiles_text">
<el-input
v-model="reportNotifyForm.at_mobiles_text"
placeholder="仅失败/异常消息时@,多个用逗号分隔,如 13800000001,13800000002"
clearable
/>
</el-form-item>
<el-alert
title="定时任务开启「执行完成后发送钉钉通知」后,按结果推送 ✅ 通过 / ⚠️ 失败(@手机号)/ 🚨 异常 三种消息,并附报告链接。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
/>
<el-form-item>
<el-button type="primary" :loading="reportNotifySaving" @click="handleReportNotifySave">
{{ reportNotifySaving ? '保存中...' : '保存配置' }}
</el-button>
<el-button :loading="reportNotifyTesting" @click="handleReportNotifyTest">发送测试消息</el-button>
<el-button @click="handleReportNotifyReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
......@@ -306,11 +367,121 @@ function handleReset() {
formRef.value?.clearValidate()
}
// ==================== 报告钉钉通知配置(群机器人) ====================
interface ReportNotifyForm {
webhook_url: string
secret: string
at_mobiles_text: string
configured: boolean
}
const reportNotifyFormRef = ref()
const reportNotifySaving = ref(false)
const reportNotifyTesting = ref(false)
const reportNotifyForm = reactive<ReportNotifyForm>({
webhook_url: '',
secret: '',
at_mobiles_text: '',
configured: false,
})
/** 原始快照,用于重置 */
let reportNotifySnapshot = ''
const reportNotifyRules = {
webhook_url: [{ required: true, message: '请输入机器人 Webhook 地址', trigger: 'blur' }],
}
/** 手机号文本 → 数组(去空白、去重、保留顺序) */
function parseMobiles(text: string): string[] {
const seen = new Set<string>()
const result: string[] = []
for (const m of (text || '').split(/[,,;;\s]+/)) {
const trimmed = m.trim()
if (trimmed && !seen.has(trimmed)) {
seen.add(trimmed)
result.push(trimmed)
}
}
return result
}
/** 加载报告通知配置 */
async function loadReportNotifyConfig() {
try {
const res = await systemApi.getReportNotifyConfig()
reportNotifyForm.webhook_url = res.webhook_url || ''
reportNotifyForm.secret = res.secret || ''
reportNotifyForm.at_mobiles_text = (res.at_mobiles || []).join(',')
reportNotifyForm.configured = !!res.configured
reportNotifySnapshot = JSON.stringify({
webhook_url: reportNotifyForm.webhook_url,
secret: reportNotifyForm.secret,
at_mobiles_text: reportNotifyForm.at_mobiles_text,
})
} catch (err: any) {
ElMessage.error('加载报告通知配置失败:' + (err?.message || '未知错误'))
}
}
/** 保存报告通知配置 */
async function handleReportNotifySave() {
const valid = await reportNotifyFormRef.value?.validate().catch(() => false)
if (!valid) return
reportNotifySaving.value = true
try {
await systemApi.updateReportNotifyConfig({
webhook_url: reportNotifyForm.webhook_url.trim(),
secret: reportNotifyForm.secret,
at_mobiles: parseMobiles(reportNotifyForm.at_mobiles_text),
})
ElMessage.success('报告通知配置已保存')
reportNotifyForm.configured = true
reportNotifySnapshot = JSON.stringify({
webhook_url: reportNotifyForm.webhook_url,
secret: reportNotifyForm.secret,
at_mobiles_text: reportNotifyForm.at_mobiles_text,
})
} catch (err: any) {
ElMessage.error('保存报告通知配置失败:' + (err?.message || '未知错误'))
} finally {
reportNotifySaving.value = false
}
}
/** 发送测试消息 */
async function handleReportNotifyTest() {
reportNotifyTesting.value = true
try {
const res = await systemApi.testReportNotify()
if (res.success) {
ElMessage.success('测试消息已发送成功')
} else {
ElMessage.error('测试消息发送失败:' + (res.message || '未知原因'))
}
} catch (err: any) {
ElMessage.error('发送测试消息异常:' + (err?.message || '未知错误'))
} finally {
reportNotifyTesting.value = false
}
}
/** 重置表单到加载时的值 */
function handleReportNotifyReset() {
const snapshot = JSON.parse(reportNotifySnapshot)
Object.assign(reportNotifyForm, snapshot)
reportNotifyFormRef.value?.clearValidate()
}
// ==================== 生命周期 ====================
onMounted(() => {
loadConfig()
loadSutConfig()
loadReportNotifyConfig()
})
</script>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论