
2024年年底已经到了,看到网易云音乐和QQ音乐的年度报告,突然觉得,我也可以给同事们安排一个。
实际上这个想法2023年就已经有了,并且也确实做了一些工作,但是因为不懂CSS和JavaScript,所以很难做出来漂亮美观的页面。
在大概8年前,我尝试过用WordPress来搭建博客和个人网站,虽然后来没有再使用,但是对WordPress + Elementor这种直接可视化拖动小部件来搭建网页的方式,印象非常深刻。它能让一个完全不懂前端的人,快速地搭建一个相对美观的网页。
本文介绍的技术方案,能够让你在对前端一无所知的情况下,也能构建出一个美观的、带有简单动画的年度报告页面。整体技术架构如下:
| 步骤 | 工具/技术 | 主要功能 |
|---|---|---|
| 1. 页面设计 | WordPress + Elementor Pro | 可视化拖拽构建美观页面 |
| 2. 静态化导出 | Simply Static 插件 | 将动态网站转换为静态资源 |
| 3. 模板化处理 | Jinja2 + Python | 将数据占位符转换为模板变量 |
| 4. 数据渲染 | Python + PostgreSQL | 从数据库提取数据并渲染页面 |
| 5. 部署上线 | 腾讯云COS + EdgeOne | 托管静态资源并提供CDN加速 |
| 6. 消息推送 | 钉钉工作通知 | 自动推送个性化报告链接 |
工作通知效果:


年度报告页面效果:

使用Docker Compose可以快速搭建WordPress开发环境,以下是完整的配置文件:
services:
wordpress:
image: wordpress:php8.3
restart: always
volumes:
- './html/plugins:/var/www/html/wp-content/plugins'
- './html/uploads:/var/www/html/wp-content/uploads'
- './html/public_static:/var/www/html/public_static'
- './php/php.ini:/usr/local/etc/php/php.ini'
ports:
- '60080:80'
environment:
TZ: "Asia/Shanghai"
WORDPRESS_DB_NAME: 'wordpress'
WORDPRESS_DB_USER: 'root'
WORDPRESS_DB_PASSWORD: '********'
WORDPRESS_DB_HOST: 'mysql'
WORDPRESS_CONFIG_EXTRA: |
define( 'WP_MEMORY_LIMIT', '1024M' );
define( 'WP_MAX_MEMORY_LIMIT', '2048M' );
define( 'WP_SITEURL', 'https://<your-domain>' );
define( 'WP_HOME', 'https://<your-domain>' );
define( 'WP_CACHE', true );
define( 'WP_ENVIRONMENT_TYPE', 'production' );
mysql:
image: mysql:latest
restart: always
volumes:
- 'mysql-data:/var/lib/mysql'
environment:
MYSQL_DATABASE: 'wordpress'
MYSQL_ROOT_PASSWORD: '********'
volumes:
mysql-data:
| 配置项 | 说明 | 重要性 |
|---|---|---|
plugins 目录挂载 |
持久化插件数据,方便备份和迁移 | ⭐⭐⭐ |
uploads 目录挂载 |
保存上传的媒体文件 | ⭐⭐⭐ |
public_static 目录挂载 |
Simply Static插件导出目录 | ⭐⭐⭐ |
php.ini 挂载 |
调整内存限制和上传大小 | ⭐⭐ |
WORDPRESS_CONFIG_EXTRA |
自动生成wp-config.php配置 | ⭐⭐⭐ |
💡 提示: 无需手动挂载
wp-config.php文件,容器启动时会根据环境变量自动生成。
将访问端口接入到腾讯云EdgeOne来获得公网访问域名并添加SSL证书,然后将域名配置到 WORDPRESS_CONFIG_EXTRA 环境变量中。
⚠️ 重要: 如果想要使用Elementor Pro构建器获得更多的页面动态效果、视差效果,则必须使用域名访问,否则无法激活Pro版本插件。
Elementor Pro是WordPress最强大的可视化页面构建器之一,提供了丰富的小部件和动画效果。
获取方式:
Simply Static可以将WordPress动态网站转换为纯静态HTML文件。
安装方式:
./html/plugins 目录
可以根据需要安装其他Elementor相关插件,获得更加丰富的页面小部件和功能扩展。

由于年度报告不需要导航栏和菜单栏,建议进行以下设置:

关键设置:
100vh📌 注意: 最小高度不等于实际高度,如果一个板块中的元素过多,可能会超出手机屏幕的高度。
在构建页面时,将需要动态替换的数据部分写成占位符格式:
<div class="user-name">{{name}}</div>
<div class="report-date">{{date}}</div>
<div class="data-value">{{some_data}}</div>
这样在转换成静态页面后,可以轻松地将这些占位符替换成Jinja2模板的实际字段名。
完成页面构建并预览效果满意后,使用Simply Static插件进行静态化导出。
步骤1:设置使用模式
进入插件的 Settings → General 页面,设置为 Offline Usage 模式:

步骤2:配置导出目录
进入插件的 Settings → Deploy 页面:
Local Directory/var/www/html/public_static
步骤3:执行导出
Generate Static Files 按钮Activity Log 标签查看日志Done! Finished in <time> 字样
⚠️ 导出前检查: 建议先到
Diagnostics诊断工具页面检查是否可以转换、是否存在不兼容插件等情况。
将 ./html/public_static 目录中生成的所有静态文件复制到一个新的项目文件夹。
由于我们将需要的页面设置成了网站主页,所以目录下的 index.html 就是我们需要的模板文件。
清理建议:
| 可删除内容 | 说明 |
|---|---|
| 其他页面HTML | 除index.html外的页面文件 |
| 文章和评论相关资源 | 不需要的博客功能资源 |
| 未使用的插件资源 | 页面中未引用的插件文件 |
| 未使用的主题资源 | 不需要的主题文件 |
💡 清理技巧:
- 先做好备份
- 在浏览器中打开index.html
- 逐步删除文件,观察页面是否异常
- 如有异常,立即恢复文件
创建Python脚本,实现从数据库查询数据并渲染到HTML模板的功能。
from datetime import datetime
import os
import psycopg
from pydantic import BaseModel
from jinja2 import Environment, FileSystemLoader
# 定义教师列表
teachers = ["张三", "李四", "王五"]
# 配置Jinja2模板环境
template_dir = os.path.join(os.path.dirname(__file__), 'static')
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template('index.html')
# 定义数据模型
class ReportData(BaseModel):
name: str
date: str = datetime.now().strftime("%Y-%m-%d %H:%M")
some_data: int = 0
# 连接数据库
conn = psycopg.connect("postgresql://<user>:<pwd>@<host>:5432/<database>")
cur = conn.cursor()
# 为每位教师生成报告
for teacher in teachers:
report_data = ReportData(name=teacher)
# 查询教师数据
cur.execute("""
SELECT .... AS some_data FROM table
WHERE "teacherName" = %s
AND "startTime" BETWEEN '2024-01-01' AND '2024-12-31';
""", [teacher])
record = cur.fetchone()
if record:
report_data.some_data = record[0]
# 渲染模板
output = template.render(report_data.model_dump())
# 保存HTML文件
with open(f'static/{teacher}.html', 'w', encoding="utf-8") as f:
f.write(output)
cur.close()
conn.close()
在运行脚本前,需要将 index.html 中的占位符替换为实际的模板变量:
| 原占位符 | 替换为 | 说明 |
|---|---|---|
{{data}} |
{{name}} |
教师姓名 |
{{data}} |
{{date}} |
报告生成日期 |
{{data}} |
{{some_data}} |
统计数据 |
运行脚本后,会生成 张三.html、李四.html、王五.html 三个年度报告文件。
测试流程:
index.html 模板在存储桶设置中找到「静态网站」功能并开启:

配置项说明:
index.html404.html(可选)使用腾讯云EdgeOne配置域名并接入存储桶:

配置步骤:
上传静态资源到存储桶根目录,访问 http://your-domain/index.html 验证配置是否正确。

from datetime import datetime
import os
import uuid
import psycopg
from pydantic import BaseModel
from jinja2 import Environment, FileSystemLoader
from qcloud_cos import CosConfig, CosS3Client
from dagster_dingtalk import DingTalkAppClient
# 初始化COS客户端
cos_client = CosS3Client(
CosConfig(
Region="ap-guangzhou",
SecretId="<your-secretid>",
SecretKey="<your-secretkey>"
)
)
# 初始化钉钉客户端
dt_client = DingTalkAppClient(
app_id="<your-app_id>",
client_id="<your-client_id>",
client_secret="<your-client_secret>"
)
# 配置模板和数据库
teachers = ["张三", "李四", "王五"]
template_dir = os.path.join(os.path.dirname(__file__), 'static')
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template('index.html')
class ReportData(BaseModel):
name: str
date: str = datetime.now().strftime("%Y-%m-%d %H:%M")
some_data: int = 0
conn = psycopg.connect("postgresql://<user>:<pwd>@<host>:5432/<database>")
cur = conn.cursor()
# 为每位教师生成并推送报告
for teacher in teachers:
# 创建报告数据对象
report_data = ReportData(name=teacher)
# 查询数据
cur.execute("""
SELECT .... AS some_data FROM table
WHERE "teacherName" = %s
AND "startTime" BETWEEN '2024-01-01' AND '2024-12-31';
""", [teacher])
record = cur.fetchone()
if record:
report_data.some_data = record[0]
# 渲染模板
output = template.render(report_data.model_dump())
# 生成唯一文件名(UUID后12位)
uuid_str = str(uuid.uuid4())[-12:]
# 上传到COS
cos_client.put_object(
Bucket='<your-bucket>',
Body=output.encode('utf-8'),
Key=f'{uuid_str}.html',
EnableMD5=False
)
# 构建钉钉消息
msg = {
"msgtype": "action_card",
"action_card": {
"title": "你的年度教学报告来啦!快来查看吧!",
"markdown": "\n"
f"\n### {teacher}老师,您的2024年度教学报告已生成!快来查看吧!",
"single_title": "点击查看",
"single_url": f"https://<your-domain>/{uuid_str}.html"
}
}
# 发送钉钉通知
dt_client.即时通信.工作通知.发送工作通知(
to_all_user=False,
user_list=[user_id],
msg=msg
)
cur.close()
conn.close()
为什么使用UUID作为文件名?
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 使用姓名 | 简单直观 | ❌ 任何人都能猜测URL访问他人报告 | 公开报告 |
| 使用UUID | ✅ 无法猜测 ✅ 保护隐私 |
需要维护映射关系 | 私密报告 |
通过截取UUID的后12位作为文件名,既保证了唯一性,又增加了URL的不可预测性,有效保护了用户隐私。
⚠️ 注意: 示例代码中使用的
dagster_dingtalk库是用于dagster环境的第三方库。在实际使用中,建议根据钉钉开放平台文档自行编写与钉钉OpenAPI交互的代码。
| 优势 | 说明 |
|---|---|
| 🎨 零前端门槛 | 无需CSS/JS知识,可视化拖拽即可完成 |
| ⚡ 快速上线 | 从设计到上线可在1-2天内完成 |
| 🔄 易于维护 | 样式调整只需在WordPress中修改 |
| 📊 数据驱动 | 支持从数据库动态生成个性化内容 |
| 🔒 安全可靠 | UUID文件名保护隐私,CDN加速访问 |
| 💰 成本低廉 | 主要成本为Elementor Pro授权和云服务 |
数据可视化增强
交互功能扩展
自动化优化
多渠道推送
至此,一个完整的年度报告生成和推送系统就搭建完成了!这套方案不仅适用于年度报告,也可以扩展到其他需要批量生成个性化页面的场景,如证书生成、成绩单发布等。希望这篇文章能帮助到有类似需求的朋友们!🎉