提交 7b0e7dcd authored 作者: 陈泽健's avatar 陈泽健

feat(service-monitor): 导出 PDF/Excel 格式

阶段九 P2 功能增强:

Excel 导出(openpyxl):
- Sheet 1:汇总信息(目标、套件、时间、统计)
- Sheet 2:检测明细(全量表格)
- 状态列条件格式(正常=绿、警告=黄、严重=红)
- 表头冻结

PDF 导出(weasyprint):
- 使用 weasyprint 将 HTML 转 PDF
- 降级策略:不可用时返回可打印 HTML
- @page 分页规则 + 中文字体支持
- 打印按钮提示(降级模式)

路由改进:
- api_export_report() 重构避免重复代码
- 支持 format=excel/pdf 参数

前端:
- report.html 导出菜单新增两个选项

文档:
- PRD_需求文档_导出PDF_Excel格式.md
- PRD_计划执行_导出PDF_Excel格式.md

测试:167 个测试全绿 Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 069f9fa2
# PRD 计划执行:导出 PDF/Excel 格式
> 模块:服务监测(service_monitor)
> 优先级:P2
> 创建日期:2026-07-23
> 需求文档:`PRD_需求文档_导出PDF_Excel格式.md`
---
## 执行计划
### 阶段一:Excel 导出实现(预估 1h)
**任务清单**
- [ ] 1.1 新增 export_excel() 函数
- 文件:`skill/code/web/service_monitor/services/report_service.py`
- 使用 `openpyxl`(已在依赖中)
- Sheet 1:汇总信息
- Sheet 2:检测明细(全量表格 + 条件格式)
- [ ] 1.2 集成到导出 API
- 文件:`skill/code/web/service_monitor/routes.py`
- 修改:`api_export_report()` 增加 `format=excel` 支持
**验收标准**
- Excel 导出成功
- 两个 Sheet 正确
- 状态颜色标记生效
---
### 阶段二:PDF 导出实现(预估 1.5h)
**任务清单**
- [ ] 2.1 新增 export_pdf() 函数
- 文件:`skill/code/web/service_monitor/services/report_service.py`
- 生成带打印样式的 HTML
- 使用 weasyprint 转 PDF(如可用),否则降级返回 HTML
- [ ] 2.2 PDF HTML 模板
- 内联 CSS 样式
- @page 分页规则
- 中文字体支持
- 页眉页脚
- [ ] 2.3 集成到导出 API
- 修改:`api_export_report()` 增加 `format=pdf` 支持
**验收标准**
- PDF 导出成功
- 中文显示正常
- 样式清晰
---
### 阶段三:前端集成 + 测试(预估 0.5h)
**任务清单**
- [ ] 3.1 修改导出菜单
- 文件:`skill/code/web/templates/service_monitor/report.html`
- 新增:"导出 PDF"、"导出 Excel" 选项
- [ ] 3.2 更新文档 + Git commit
**验收标准**
- 导出菜单显示新选项
- 各格式导出正常
---
## 关键文件清单
### 修改文件
| 文件 | 改动 |
|------|------|
| `requirements.txt` | 新增 weasyprint 依赖 |
| `service_monitor/services/report_service.py` | 新增 export_pdf()、export_excel() |
| `service_monitor/routes.py` | api_export_report() 增加 pdf/excel |
| `templates/service_monitor/report.html` | 导出菜单新增选项 |
---
## 风险与注意事项
### 风险 1:weasyprint 依赖 GTK
**决策**:使用 try/except 降级策略,weasyprint 可用时生成 PDF,不可用时返回可打印 HTML
### 风险 2:大报告性能
**缓解**:Excel 使用 openpyxl 的 write_only 模式(如需),PDF 限制单页内容量
\ No newline at end of file
# PRD 需求文档:导出 PDF/Excel 格式
> 模块:服务监测(service_monitor)
> 优先级:P2
> 创建日期:2026-07-23
> 状态:待开发
---
## 1. 背景与目标
### 1.1 背景
服务监测模块已支持导出巡检报告为 Markdown 和 JSON 格式。运维人员需要更多导出选项:
- PDF 格式:保留样式,适合打印归档、邮件附件
- Excel 格式:检测项明细表格,适合数据分析和二次处理
### 1.2 目标
实现 PDF 和 Excel 格式导出:
- 复用现有报告数据结构
- 集成到现有导出菜单
- 保持与 Markdown 导出一致的 API 设计
---
## 2. 功能需求
### 2.1 导出入口
**位置**:报告详情页(`report.html`)导出菜单
**现有选项**
- 导出 Markdown
- 导出 JSON
**新增选项**
- 导出 PDF
- 导出 Excel
### 2.2 PDF 导出
**内容结构**
1. 报告头:目标名称、套件类型、时间范围
2. 汇总统计:总项数、正常/警告/严重数量
3. 异常项列表(置顶):模块、检测项、当前值、阈值、状态
4. 各模块检测明细(可折叠)
**样式要求**
- 保留颜色标记(正常=绿色、警告=黄色、严重=红色)
- 表格样式清晰,适合打印
- 中文支持(使用系统字体)
- 页眉:报告标题 + 目标名称
- 页脚:页码 + 生成时间
**技术选型**
- 使用 `weasyprint` 库(纯 Python,无需外部依赖)
- 复用 `compare_service.py` 的 HTML 导出模板样式
### 2.3 Excel 导出
**内容结构**
- Sheet 1:汇总(目标名称、套件、时间、统计)
- Sheet 2:检测明细(全量检测项表格)
**表格列**
| 列名 | 说明 |
|------|------|
| 模块 | 检测模块名称 |
| 检测项 KEY | 唯一标识 |
| 检测项名称 | 显示名称 |
| 当前值 | 实际检测值 |
| 阈值 | 阈值标准 |
| 状态 | 正常/警告/严重 |
| 类别 | system/service |
**样式要求**
- 表头加粗、冻结首行
- 状态列使用条件格式(正常=绿色背景、警告=黄色、严重=红色)
- 列宽自适应
**技术选型**
- 使用 `openpyxl` 库(已在依赖中)
---
## 3. API 设计
### 3.1 扩展现有导出接口
```
GET /api/service-monitor/reports/{report_id}/export?format={md|json|pdf|excel}
```
**新增 format 参数值**
- `pdf`:返回 PDF 文件
- `excel`:返回 Excel 文件
**响应头**
- PDF:`Content-Type: application/pdf`
- Excel:`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
**文件名**
- PDF:`report_{report_id}.pdf`
- Excel:`report_{report_id}.xlsx`
---
## 4. 技术方案
### 4.1 后端实现
**新增函数**
```python
# report_service.py
def export_pdf(report_id: str) -> Optional[bytes]:
"""导出 PDF 格式报告。
Returns:
PDF 二进制数据,报告不存在返回 None
"""
pass
def export_excel(report_id: str) -> Optional[bytes]:
"""导出 Excel 格式报告。
Returns:
Excel 二进制数据,报告不存在返回 None
"""
pass
```
**修改文件**
- `skill/code/web/service_monitor/services/report_service.py`:新增 `export_pdf()``export_excel()` 函数
- `skill/code/web/service_monitor/routes.py``api_export_report()` 增加 `pdf``excel` 格式支持
**新增依赖**
- `weasyprint>=60.0`:PDF 生成(需添加到 requirements.txt)
### 4.2 前端修改
**修改文件**
- `skill/code/web/templates/service_monitor/report.html`:导出菜单新增两个选项
---
## 5. 验收标准
### 5.1 功能验收
- [ ] 报告详情页导出菜单显示"导出 PDF"、"导出 Excel"
- [ ] PDF 导出成功,内容完整
- [ ] PDF 样式清晰,中文显示正常
- [ ] Excel 导出成功,包含两个 Sheet
- [ ] Excel 表格格式正确,状态列有颜色标记
- [ ] Token 免登访问时支持导出 PDF/Excel
### 5.2 边界情况
- [ ] 报告不存在时返回 404 错误
- [ ] 大报告(500+ 检测项)导出不超时
- [ ] 无异常项时 PDF 正常渲染
---
## 6. 时间规划
| 阶段 | 任务 | 预估时间 |
|------|------|---------|
| 阶段一 | PDF 导出实现 | 1.5h |
| 阶段二 | Excel 导出实现 | 1h |
| 阶段三 | 前端集成 + 测试 | 0.5h |
| **合计** | | **3h** |
---
## 7. 相关文档
- `HANDOFF_服务监测.md`:模块交接文档
- `PRD_需求文档_报告对比功能.md`:参考 HTML 导出样式
\ No newline at end of file
......@@ -658,6 +658,7 @@
- **移动端响应式深度优化完成**(新增 480px 断点 + 修正溢出问题)✅
- **报告对比功能完成**(对比服务 + API + 页面 + 导出)✅
- **异常项趋势告警完成**(连续异常检测 + 告警通知 + 配置页)✅
- **导出 PDF/Excel 格式完成**(Excel openpyxl + PDF weasyprint/HTML降级)✅
---
......@@ -677,7 +678,7 @@
| ~~P1~~ | ~~移动端响应式深度优化~~ | ✅ 已完成(新增 480px 断点 + 修正溢出) |
| ~~P1~~ | ~~报告对比功能~~ | ✅ 已完成(阶段九) |
| ~~P1~~ | ~~异常项趋势告警~~ | ✅ 已完成(阶段九) |
| P2 | 导出 PDF/Excel 格式 | 待开发 |
| ~~P2~~ | ~~导出 PDF/Excel 格式~~ | ✅ 已完成(阶段九) |
| 持续 | 更新 HANDOFF_服务监测文档 | 每次会话结束前更新进度 |
---
......@@ -707,19 +708,22 @@
- `notification.html`:新增"连续异常告警"配置区
- 配置字段:`alert_on_consecutive.enabled/threshold/alert_level/channels`
### P2:导出 PDF/Excel 格式
### ✅ P2:导出 PDF/Excel 格式(已完成)
**需求**:支持导出巡检报告为 PDF 和 Excel 格式
**功能点**
- 报告详情页导出菜单新增"导出 PDF"、"导出 Excel"
- PDF:保留样式,适合打印归档
- Excel:检测项明细,适合数据分析
**技术方案**
- PDF:`weasyprint``pdfkit`(wkhtmltopdf)
- Excel:`openpyxl``pandas`
- 复用现有 `report_service.py` 的数据获取逻辑
**实现内容**
- `report_service.py``export_excel()` 函数(使用 openpyxl)
- Sheet 1:汇总信息(目标、套件、时间、统计)
- Sheet 2:检测明细(全量表格 + 条件格式)
- 状态列颜色标记(正常=绿、警告=黄、严重=红)
- 表头冻结
- `report_service.py``export_pdf()` 函数
- 使用 weasyprint 将 HTML 转 PDF
- 降级策略:weasyprint 不可用时返回可打印 HTML
- `_build_report_html()`:打印友好 HTML 模板
- @page 分页规则 + 中文字体支持
- `routes.py``api_export_report()` 增加 `pdf``excel` 格式
- `report.html`:导出菜单新增"导出 PDF"、"导出 Excel"
- PDF 检测:通过 `%PDF` 魔数判断,降级时添加打印按钮
---
......@@ -948,6 +952,14 @@ feat(service-monitor): 新增安全合规/中间件检测模块并修复状态
| `templates/service_monitor/report.html` | 新增"对比历史报告"按钮 + 报告选择弹窗 |
| `templates/service_monitor/notification.html` | 新增"连续异常告警"配置区 |
### 本次修改文件(阶段九-P2:导出 PDF/Excel)
| 文件 | 改动 |
|------|------|
| `service_monitor/services/report_service.py` | 新增 export_excel()、export_pdf()、_build_report_html() |
| `service_monitor/routes.py` | api_export_report() 增加 pdf/excel 格式,重构避免重复代码 |
| `templates/service_monitor/report.html` | 导出菜单新增"导出 PDF"、"导出 Excel" |
### 新增文档
| 文件 | 说明 |
......
......@@ -410,29 +410,15 @@ def api_batch_delete_reports():
def api_export_report(report_id):
# token 免登导出(只读操作,允许访客)
token = request.args.get('token', '').strip()
if token and report_service.validate_access_token(report_id, token):
fmt = request.args.get('format', 'md')
if fmt == 'json':
data = report_service.export_json(report_id)
if not data:
return jsonify({"success": False, "error": {"code": 404, "message": "报告不存在"}}), 404
resp = Response(json.dumps(data, ensure_ascii=False, indent=2),
mimetype='application/json')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.json'
return resp
else:
md = report_service.export_markdown(report_id)
if md is None:
return jsonify({"success": False, "error": {"code": 404, "message": "报告不存在"}}), 404
resp = Response(md, mimetype='text/markdown')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.md'
return resp
# 常规登录访问
guard = _require_login_json()
if guard:
return guard
if not (token and report_service.validate_access_token(report_id, token)):
# 常规登录访问
guard = _require_login_json()
if guard:
return guard
fmt = request.args.get('format', 'md')
# JSON 格式
if fmt == 'json':
data = report_service.export_json(report_id)
if not data:
......@@ -441,14 +427,49 @@ def api_export_report(report_id):
mimetype='application/json')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.json'
return resp
else:
md = report_service.export_markdown(report_id)
if md is None:
# Excel 格式
if fmt == 'excel':
data = report_service.export_excel(report_id)
if not data:
return jsonify({"success": False, "error": {"code": 404, "message": "报告不存在"}}), 404
resp = Response(md, mimetype='text/markdown')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.md'
resp = Response(data,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.xlsx'
return resp
# PDF 格式
if fmt == 'pdf':
data = report_service.export_pdf(report_id)
if not data:
return jsonify({"success": False, "error": {"code": 404, "message": "报告不存在"}}), 404
# weasyprint 可用:返回 PDF;降级:返回可打印 HTML
if data[:4] == b'%PDF':
resp = Response(data, mimetype='application/pdf')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.pdf'
else:
# 降级 HTML(带打印按钮提示)
html_with_hint = (
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
"<style>body{margin:0}</style></head><body>"
"<div style='position:fixed;top:10px;right:10px;z-index:9999;"
"background:#1a73e8;color:#fff;padding:8px 16px;border-radius:6px;"
"font-size:13px;cursor:pointer;font-family:sans-serif'"
" onclick='window.print()'>🖨 打印 / 保存为 PDF</div>"
+ data.decode('utf-8')
+ "</body></html>"
)
resp = Response(html_with_hint, mimetype='text/html')
return resp
# Markdown 格式(默认)
md = report_service.export_markdown(report_id)
if md is None:
return jsonify({"success": False, "error": {"code": 404, "message": "报告不存在"}}), 404
resp = Response(md, mimetype='text/markdown')
resp.headers['Content-Disposition'] = f'attachment; filename=report_{report_id}.md'
return resp
# ============================================================
# API:报告对比
......
......@@ -175,6 +175,8 @@
<div class="export-menu" id="export-menu">
<a href="/api/service-monitor/reports/{{ report.id }}/export?format=md{% if token_auth %}&token={{ access_token }}{% endif %}">导出 Markdown</a>
<a href="/api/service-monitor/reports/{{ report.id }}/export?format=json{% if token_auth %}&token={{ access_token }}{% endif %}">导出 JSON</a>
<a href="/api/service-monitor/reports/{{ report.id }}/export?format=excel{% if token_auth %}&token={{ access_token }}{% endif %}">导出 Excel</a>
<a href="/api/service-monitor/reports/{{ report.id }}/export?format=pdf{% if token_auth %}&token={{ access_token }}{% endif %}">导出 PDF</a>
</div>
</div>
</div>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论