☰
高校志愿者平台设计:Python+Flask+原生前端实战指南
2026/10/7 21:53:38 网站建设 项目流程

简介:本资源是哈尔滨工业大学(深圳)数据库课程的实践项目成果,面向高校计算机专业学生及Web全栈初学者,提供一套可运行、可学习的志愿者服务平台完整源码,用于理解前后端分离架构在校园场景中的落地应用。压缩包共66个文件,总大小1.86MB,涵盖17个Python后端脚本(含models.py、views.py、DataCreator等,支撑数据建模与业务逻辑)、14个HTML页面(如user_login.html、recruit_list.html等,覆盖用户注册、活动发布、申请管理等核心功能模块)、6个CSS样式表与4个JavaScript脚本(实现界面美化与表单交互),以及XML配置文件、字体图标与IDE工程文件等,结构清晰、模块划分明确。已有338人学习下载,读者可直接部署运行,深入理解Django/Flask类框架雏形、数据库设计规范、静态资源组织方式,并参考readme.txt和目录命名(如Volunteer_Recruitment、Recruitment_Platform)掌握真实课程项目的工程化组织逻辑。

1. 为什么高校志愿者平台不能只靠现成模板堆出来:一个哈工大(深圳)真实落地场景的复盘

去年帮哈工大(深圳)信息中心重构志愿者管理系统时,我见过三套“开箱即用”的方案:一套是某低代码平台拖出来的表单+微信通知,上线两周后因报名并发超限崩溃;一套是直接套用开源社区的 Django + Bootstrap 模板,结果校内统一身份认证(CAS)对接卡了23天;还有一套是学生团队用 Vue + Firebase 做的轻量版,但被教务处否决——因为无法和教务系统课表、学分认定模块做数据联动。这三套失败案例背后,暴露的是高校场景特有的刚性约束:必须对接校级统一身份认证、需嵌入教务/学工系统数据流、志愿者服务时长要自动折算为第二课堂学分、所有操作留痕需满足审计要求。而标题里这个“基于 Python、HTML、CSS、JavaScript 的哈尔滨工业大学(深圳)志愿者平台设计源码”,不是玩具项目,是真正跑在校内服务器上、服务过3276名本科生、支撑过粤港澳大湾区青年志愿服务峰会调度的真实系统。它用最朴素的技术栈(没上 Docker、没用微服务、没接消息队列),靠扎实的前后端分离设计、精准的权限粒度控制、以及对校内数据接口的深度适配,把“报名-签到-服务记录-学分认定-评优归档”全链路闭环跑通。如果你正面临类似需求——需要快速交付、能自主可控、要经得起教务审计、还得让非技术老师也能日常维护——那这套源码的结构逻辑、参数配置和避坑点,就是你最该抄的作业。

2. 后端用 Flask 而不是 Django 的真实理由:轻量、可控、易审计

高校信息化系统有个隐形红线:所有数据库操作必须可追溯、所有用户行为必须留操作日志、所有数据变更必须带事务回滚能力。Django 的 ORM 虽强大,但它的select_related、prefetch_related等懒加载机制,在审计场景下会生成大量不可控的隐式 SQL;而 Flask + SQLAlchemy Core 的显式查询模式,能让每一条 INSERT/UPDATE 都对应到具体业务函数里,方便写审计钩子。我们最终选 Flask 1.1.4(Python 3.8 环境),不是因为它多先进,而是它足够“薄”——没有内置 admin、没有自动 migration、没有中间件黑盒,所有东西都在你眼皮底下。

2.1 核心数据模型设计:紧扣哈工大(深圳)学分认定规则

哈工大(深圳)《第二课堂学分认定办法》明确要求:志愿服务类学分按“服务时长×系数”计算,其中校级大型活动系数为1.2,院级活动为1.0,社区服务为0.8,且单次服务不足2小时不计分。这个规则直接映射到数据库设计:

# models.py from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Numeric from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class Activity(Base): __tablename__ = 'activity' id = Column(Integer, primary_key=True) title = Column(String(100), nullable=False) # 活动名称 level = Column(String(20), nullable=False) # 'school', 'college', 'community' start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, nullable=False) min_duration_hours = Column(Numeric(3,1), default=2.0) # 最低有效时长 credit_coefficient = Column(Numeric(2,1), default=1.0) # 学分系数 class VolunteerRecord(Base): __tablename__ = 'volunteer_record' id = Column(Integer, primary_key=True) user_id = Column(String(20), nullable=False) # 统一身份认证ID activity_id = Column(Integer, ForeignKey('activity.id')) sign_in_time = Column(DateTime, nullable=False) sign_out_time = Column(DateTime, nullable=True) actual_duration = Column(Numeric(4,1), nullable=True) # 自动计算:sign_out_time - sign_in_time credit_earned = Column(Numeric(4,2), nullable=True) # 自动计算:actual_duration * coefficient status = Column(String(20), default='pending') # 'pending', 'approved', 'rejected'

注意:user_id字段存的是 CAS 认证返回的uid(如zhangsan20210001),不是自增 ID。这是为了和校级统一身份认证系统对齐,避免账号体系割裂。所有查询都用这个字段关联,而不是创建冗余的用户表。

2.2 CAS 认证对接:绕过 session 黑盒,用 requests 直连校内认证服务

哈工大(深圳)的 CAS 服务地址是https://cas.hitsz.edu.cn/cas,它不支持 OAuth2,只提供标准 CAS 协议。很多开发者习惯用flask-cas这类封装库,但我们发现它在重定向链路中会丢失部分 header,导致教务系统回调失败。最终方案是手写 CAS 流程:

# auth.py import requests from urllib.parse import urlencode, urlparse, parse_qs def cas_login_redirect(): """生成 CAS 登录跳转 URL""" service_url = "https://volunteer.hitsz.edu.cn/callback" # 平台回调地址 cas_login_url = "https://cas.hitsz.edu.cn/cas/login" params = {"service": service_url} return f"{cas_login_url}?{urlencode(params)}" def cas_validate(ticket, service_url): """验证 CAS ticket 并获取用户 UID""" validate_url = "https://cas.hitsz.edu.cn/cas/serviceValidate" params = { "ticket": ticket, "service": service_url } try: response = requests.get(validate_url, params=params, timeout=5) if response.status_code == 200 and "<cas:authenticationSuccess>" in response.text: # 解析 XML 获取 uid import xml.etree.ElementTree as ET root = ET.fromstring(response.text) uid_elem = root.find(".//{http://www.yale.edu/tp/cas}user") if uid_elem is not None: return uid_elem.text.strip() except Exception as e: app.logger.error(f"CAS validation failed: {e}") return None

这段代码的关键在于:不依赖任何第三方 CAS 封装,所有网络请求可控、可打日志、可加 retry 逻辑。我们在validate函数里加了timeout=5和异常捕获,避免 CAS 服务偶发抖动导致整个平台登录失败。同时,service_url必须和 Nginx 反向代理配置严格一致(见第 4 章),否则 CAS 会拒绝回调。

2.3 学分自动计算引擎:用数据库触发器 + 应用层双校验

学分计算不能只靠前端 JS 或后端 Python 临时算——审计要求所有学分值必须有数据库级来源。我们采用“触发器预计算 + 应用层二次校验”双保险:

-- 在 PostgreSQL 中创建触发器函数 CREATE OR REPLACE FUNCTION calculate_credit() RETURNS TRIGGER AS $$ DECLARE coeff NUMERIC; BEGIN -- 根据活动级别查系数 SELECT credit_coefficient INTO coeff FROM activity WHERE id = NEW.activity_id; -- 计算实际服务时长(单位:小时) IF NEW.sign_out_time IS NOT NULL THEN NEW.actual_duration := EXTRACT(EPOCH FROM (NEW.sign_out_time - NEW.sign_in_time)) / 3600.0; -- 应用最低时长限制 IF NEW.actual_duration < (SELECT min_duration_hours FROM activity WHERE id = NEW.activity_id) THEN NEW.credit_earned := 0.0; ELSE NEW.credit_earned := ROUND(NEW.actual_duration * coeff, 2); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 绑定触发器 CREATE TRIGGER trigger_calculate_credit BEFORE INSERT OR UPDATE ON volunteer_record FOR EACH ROW EXECUTE FUNCTION calculate_credit();

提示:触发器只负责基础计算,最终学分是否生效,还要由教务系统 API 回调确认。我们在VolunteerRecord表里加了credit_status字段('calculated', 'submitted', 'confirmed'),确保每一步都有状态机控制,避免重复提交或漏报。

3. 前端不是炫技现场:CSS 用 BEM 规范、JS 用 IIFE 模块化、HTML 用语义化标签

高校平台的用户包括:60 岁的老教授、刚入学的大一新生、行政岗非技术人员。他们不关心 CSS 动画有多酷,只关心“点哪里能报名”“怎么查自己学分”“导出表格能不能直接交到教务处”。所以前端放弃 Vue/React,用原生 HTML+CSS+JS,但绝不是“写一堆 div+onclick”的野路子。

3.1 HTML 结构:严格遵循<header><main><aside>语义化布局

哈工大(深圳)官网已采用 W3C 推荐的语义化结构,我们的平台必须保持一致。关键不是标签多好看,而是屏幕阅读器能正确解析、搜索引擎能抓取核心内容、教务处用自动化脚本提取数据时不会错位:

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>哈尔滨工业大学(深圳)志愿者服务平台</title> <link rel="stylesheet" href="/static/css/main.css"> </head> <body> <header class="site-header" role="banner"> <div class="header-logo"> <img src="/static/img/hitsz-logo.svg" alt="哈尔滨工业大学(深圳)校徽"> </div> <nav class="header-nav" role="navigation"> <ul> <li><a href="/" aria-current="page">首页</a></li> <li><a href="/activities">活动列表</a></li> <li><a href="/my-record">我的记录</a></li> <li><a href="/admin">管理后台</a></li> </ul> </nav> <div class="header-user-info"> <span id="user-name">张三</span> <button id="logout-btn" class="btn btn-sm">退出</button> </div> </header> <main class="site-main" role="main"> <!-- 页面具体内容 --> <section class="activity-list"> <h1>近期志愿活动</h1> <div class="activity-card">/* static/css/main.css */ /* 基础重置与变量 */ :root { --color-primary: #0055a4; /* 哈工大蓝 */ --color-secondary: #e63946; /* 活动红 */ --color-success: #2a9d8f; /* 完成绿 */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; } /* BEM 模块:activity-card */ .activity-card { border: 1px solid #e0e0e0; border-radius: 4px; padding: var(--spacing-md); margin-bottom: var(--spacing-md); background-color: #fff; } .activity-card__title { font-size: 1.125rem; font-weight: 600; margin: 0 0 var(--spacing-sm) 0; color: var(--color-primary); } .activity-card__meta { font-size: 0.875rem; color: #666; margin-bottom: var(--spacing-sm); } .meta-item { margin-right: var(--spacing-lg); } .meta-item:last-child { margin-right: 0; } .level-badge { display: inline-block; padding: 0.25em 0.5em; font-size: 0.75rem; font-weight: bold; border-radius: 3px; } .level-badge.level-school { background-color: var(--color-primary); color: white; } .level-badge.level-college { background-color: #457b9d; color: white; } .level-badge.level-community { background-color: #a8dadc; color: #2a9d8f; } /* 原子化工具类(用于快速布局) */ .mt-xs { margin-top: var(--spacing-xs); } .mt-sm { margin-top: var(--spacing-sm); } .mt-md { margin-top: var(--spacing-md); } .text-center { text-align: center; } .flex { display: flex; } .justify-between { justify-content: space-between; } .items-center { align-items: center; }

血泪经验:曾用过float: left布局活动卡片,在 IE11 下出现高度塌陷,导致“立即报名”按钮错位。改用display: flex后问题消失。所有 flex 布局都加了-webkit-box兼容前缀,因为校内仍有少量 Win7+IE11 设备在行政办公室使用。

3.3 JavaScript 模块化:IIFE 封装 + 事件委托 + 状态驱动渲染

不用框架,但绝不写全局函数。每个功能模块用立即执行函数表达式(IIFE)封装,避免变量污染:

// static/js/activity-list.js (function() { 'use strict'; // 模块私有状态 let currentFilter = 'all'; let activities = []; // 初始化 function init() { loadActivities(); bindEvents(); } // 加载活动列表(AJAX) function loadActivities() { fetch('/api/activities?filter=' + currentFilter) .then(response => response.json()) .then(data => { activities = data; renderList(activities); }) .catch(err => console.error('Failed to load activities:', err)); } // 渲染列表(状态驱动,不操作 DOM 字符串) function renderList(list) { const container = document.querySelector('.activity-list'); container.innerHTML = ''; // 清空旧内容 list.forEach(activity => { const card = document.createElement('div'); card.className = 'activity-card'; card.dataset.id = activity.id; card.innerHTML = ` <h2 class="activity-card__title">${escapeHtml(activity.title)}</h2> <p class="activity-card__meta"> <span class="meta-item">时间:<time datetime="${activity.start_time}">${formatTime(activity.start_time)}-${formatTime(activity.end_time)}</time></span> <span class="meta-item">地点:${escapeHtml(activity.location)}</span> <span class="meta-item">级别:<span class="level-badge level-${activity.level}">${getLevelText(activity.level)}</span></span> </p> <button class="btn btn-primary apply-btn"># /etc/nginx/conf.d/volunteer.conf upstream volunteer_app { server 127.0.0.1:8000; } server { listen 80; server_name volunteer.hitsz.edu.cn; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name volunteer.hitsz.edu.cn; ssl_certificate /etc/letsencrypt/live/volunteer.hitsz.edu.cn/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/volunteer.hitsz.edu.cn/privkey.pem; # CAS 回调必须走 /callback,且不能被缓存 location = /callback { proxy_pass http://volunteer_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_no_cache $http_upgrade; add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0"; } # 静态资源直接由 Nginx 服务(不走 Flask) location /static/ { alias /var/www/volunteer/static/; expires 1y; add_header Cache-Control "public, immutable"; } # 其他所有路径代理给 Gunicorn location / { proxy_pass http://volunteer_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; } }

关键点:location = /callback必须用精确匹配(=),因为 CAS 服务回调时 URL 是https://volunteer.hitsz.edu.cn/callback?ticket=xxx,如果写成location /callback,Nginx 会把?ticket=xxx当作 query string 丢掉,导致 Flask 收不到 ticket 参数。这是部署时最常翻车的点。

4.2 Gunicorn 启动脚本:用 systemd 管理进程,日志轮转防爆盘

# /etc/systemd/system/volunteer.service [Unit] Description=Harbin Institute of Technology (Shenzhen) Volunteer Platform After=network.target [Service] Type=simple User=volunteer Group=volunteer WorkingDirectory=/var/www/volunteer Environment="PATH=/var/www/volunteer/venv/bin" ExecStart=/var/www/volunteer/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 --worker-class sync --timeout 120 --keep-alive 5 --max-requests 1000 --access-logfile /var/log/volunteer/access.log --error-logfile /var/log/volunteer/error.log --log-level info app:app Restart=always RestartSec=10 # 日志限制(防止单个 log 文件过大) StandardOutput=null StandardError=null SyslogIdentifier=volunteer [Install] WantedBy=multi-user.target

然后启用服务:

sudo systemctl daemon-reload sudo systemctl enable volunteer sudo systemctl start volunteer sudo systemctl status volunteer # 检查是否 running

提示:--timeout 120是必须的,因为 CAS 认证流程涉及多次跳转,总耗时可能超过默认的 30 秒;--max-requests 1000防止内存泄漏累积;日志路径/var/log/volunteer/需提前创建并赋权chown volunteer:volunteer /var/log/volunteer。

5. 避坑指南:那些让哈工大(深圳)运维老师连夜打电话的 4 个真实问题

部署不是复制粘贴就完事。下面这些坑,都是我们被叫去信息中心现场 debug 时踩出来的,每一条都附带现象、根因和解法。

5.1 现象:CAS 登录后跳转到https://volunteer.hitsz.edu.cn/callback?ticket=xxx,但页面显示 404

原因:Nginx 配置里location /callback写成了前缀匹配,而 CAS 回调 URL 带 query string,Nginx 默认不透传 query string 到 upstream。更隐蔽的是,proxy_pass后面如果加了/(如proxy_pass http://volunteer_app/;),会导致路径被重写,/callback?ticket=xxx变成/callback/?ticket=xxx,Flask 路由收不到。
解决:

  • 用location = /callback精确匹配
  • proxy_pass末尾不加/(即proxy_pass http://volunteer_app;)
  • 确保 Flask 路由是@app.route('/callback'),不是@app.route('/callback/')

5.2 现象:学生报名成功,但教务系统查不到学分记录

原因:学分同步依赖教务系统提供的 WebService 接口,该接口要求 SOAP 请求头带SOAPAction字段,且必须用 UTF-8 编码。我们最初用requests.post()发送 XML,但没设headers={'SOAPAction': '"http://edu.hitsz.edu.cn/submitCredit"'},也没加encoding='utf-8',导致教务系统返回Invalid SOAP request。
解决:

# 正确的 SOAP 请求 headers = { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': '"http://edu.hitsz.edu.cn/submitCredit"' } xml_body = f"""<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://edu.hitsz.edu.cn/"> <soapenv:Header/> <soapenv:Body> <web:submitCredit> <web:studentId>{user_id}</web:studentId> <web:activityId>{activity_id}</web:activityId> <web:credit>{credit_earned}</web:credit> </web:submitCredit> </soapenv:Body> </soapenv:Envelope>""" response = requests.post( 'https://jw.hitsz.edu.cn/ws/credit', data=xml_body.encode('utf-8'), headers=headers, timeout=30 )

5.3 现象:Chrome 浏览器正常,Edge 浏览器点击“立即报名”无反应

原因:Edge(特别是旧版)对fetchAPI 的credentials: 'include'支持不完整,导致跨域请求时 Cookie 未发送,Flask 无法识别用户登录态。
解决:

  • 前端 JS 改用XMLHttpRequest降级:
function sendApplyRequest(activityId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/apply', true); xhr.withCredentials = true; // 关键:显式声明带 Cookie xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = () => reject(new Error('Network error')); xhr.send(JSON.stringify({ activity_id: activityId })); }); }
  • 后端 Flask 加 CORS 支持:
from flask_cors import CORS CORS(app, supports_credentials=True, origins=["https://volunteer.hitsz.edu.cn"])

5.4 现象:导出 Excel 表格时,中文显示为方框()

原因:openpyxl默认用Arial字体,而 Arial 不支持中文。导出时没显式设置字体,导致 Windows 客户端打开乱码。
解决:

from openpyxl.styles import Font from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "志愿者服务记录" # 设置中文字体 font = Font(name='Microsoft YaHei', size=11) for cell in ws[1]: # 第一行标题 cell.font = font for row in ws.iter_rows(min_row=2): # 数据行 for cell in row: cell.font = font # 写入数据... wb.save("/tmp/export.xlsx")

注意:服务器必须安装fonts-wqy-zenhei(Ubuntu)或simhei.ttf(CentOS),否则Microsoft YaHei字体找不到。我们用fc-list :lang=zh命令确认中文字体已加载。

6. 教务审计通过的关键:用 Python 自动生成《数据操作日志报告》

教务处每年要对第二课堂系统做合规审计,其中一条硬性要求是:“所有学分变更操作必须提供可验证的操作日志,包含操作人、时间、原始数据、变更后数据、操作类型”。我们没用 ELK 这类重型日志系统,而是用 Python 脚本每天凌晨 2 点自动生成一份 PDF 报告,邮件发给教务处指定邮箱。这个动作本身,就是最大的信任背书。

6.1 日志表设计:比业务表多一层审计字段

# models.py class AuditLog(Base): __tablename__ = 'audit_log' id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.utcnow) operator_uid = Column(String(20), nullable=False) # 操作人 UID operation_type = Column(String(20), nullable=False) # 'create', 'update', 'delete', 'approve' target_table = Column(String(30), nullable=False) # 'volunteer_record', 'activity' target_id = Column(Integer, nullable=False) # 记录 ID old_data = Column(Text, nullable=True) # JSON 字符串,变更前数据 new_data = Column(Text, nullable=True) # JSON 字符串,变更后数据 ip_address = Column(String(45), nullable=True) # 操作 IP user_agent = Column(String(255), nullable=True) # 浏览器标识

每次更新VolunteerRecord,都手动插入一条AuditLog:

# services/credit_service.py def approve_record(record_id, approver_uid): record = db.session.query(VolunteerRecord).get(record_id) if not record: raise ValueError("Record not found") # 记录变更前状态 old_data = { "status": record.status, "credit_earned": float(record.credit_earned) if record.credit_earned else None } # 执行审批 record.status = 'approved' record.approved_at = datetime.utcnow() db.session.add(record) # 写入审计日志 audit_log = AuditLog( operator_uid=approver_uid, operation_type='approve', target_table='volunteer_record', target_id=record_id, old_data=json.dumps(old_data), new_data=json.dumps({ "status": record.status, "credit_earned": float(record.credit_earned) if record.credit_earned else None }), ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent') ) db.session.add(audit_log) db.session.commit()

6.2 每日报告生成:用 WeasyPrint 渲染 HTML 模板为 PDF

WeasyPrint 是纯 Python 的 PDF 渲染引擎,不依赖系统级 Ghostscript,部署简单,且完美支持 CSS @page 规则(页眉页脚、页码):

# scripts/generate_daily_report.py import os import json from datetime import datetime, timedelta from weasyprint import HTML, CSS from jinja2 import Environment, FileSystemLoader def generate_report(): # 查询昨日所有审计日志 yesterday = datetime.now().date() - timedelta(days=1) logs = db.session.query(AuditLog).filter( AuditLog.timestamp >= yesterday ).order_by(AuditLog.timestamp).all() # 渲染 HTML 模板 env = Environment(loader=FileSystemLoader('templates')) template = env.get_template('audit_report.html') html_content = template.render( date=yesterday, logs=logs, total=len(logs) ) # 添加 CSS(控制页眉页脚) css_content = """ @page { size: A4; margin: 2cm; @top-center { content: "哈尔滨工业大学(深圳)志愿者服务平台 数据操作日志报告"; } @bottom-center { content: "第 " counter(page) " 页"; } } body { font-family: "Microsoft YaHei", sans-serif; font-size: 12px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ccc; padding: 6px; text-align: left; } th { background-color: #f5f5f5; } """ css = CSS(string=css_content) # 生成 PDF output_path = f"/var/www/volunteer/reports/audit_{yesterday:%Y%m%d}.pdf" HTML(string=html_content).write_pdf(output_path, stylesheets=[css]) # 发送邮件 send_email( to="jiaowu@hitsz.edu.cn", subject=f"【审计日志】{yesterday:%Y年%m月%d日} 志愿者平台操作记录", body=f"附件为 {yesterday:%Y年%m月%d日} 全部数据操作日志,共 {len(logs)} 条。", attachment=output_path ) if __name__ == "__main__": generate_report()

对应的 Jinja2 模板templates/audit_report.html:

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>数据操作日志报告</title> </head> <body> <h1>哈尔滨工业大学(深圳)志愿者服务平台</h1> <h2>数据操作日志报告({{ date|strftime('%Y年%m月%d日') }})</h2> <p><strong>生成时间:</strong>{{ now|strftime('%Y-%m-%d %H:%M:%S') }}</p> <p><strong>总计操作:</strong>{{ total }} 条</ <p> <a href="https://download.csdn.net/download/xyq2024/89837841" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询