feat: add email html template
This commit is contained in:
@@ -3,9 +3,117 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 英文key到中文标签的映射
|
||||
LABEL_MAP = {
|
||||
# 通用
|
||||
'id': 'ID',
|
||||
'title': '标题',
|
||||
'name': '名称',
|
||||
'description': '描述',
|
||||
'status': '状态',
|
||||
'category': '类别',
|
||||
'owner': '所有者',
|
||||
'value': '价值',
|
||||
'location': '位置',
|
||||
'timestamp': '时间',
|
||||
'operation_type': '操作说明',
|
||||
'message': '说明',
|
||||
'created_at': '创建时间',
|
||||
'updated_at': '更新时间',
|
||||
|
||||
# 物品
|
||||
'serial_number': '序列号',
|
||||
'item_name': '物品名称',
|
||||
'item_serial': '物品序列号',
|
||||
'user': '使用者',
|
||||
'borrower_contact': '使用者联系方式',
|
||||
'start_time': '开始时间',
|
||||
'end_time': '结束时间',
|
||||
'purpose': '使用目的',
|
||||
'notes': '备注',
|
||||
'is_returned': '是否已归还',
|
||||
'condition_before': '使用前状况',
|
||||
'condition_after': '使用后状况',
|
||||
'purchase_date': '购买日期',
|
||||
'expected_return_time': '预计归还时间',
|
||||
|
||||
# 财务记录
|
||||
'amount': '金额',
|
||||
'transaction_type': '交易类型',
|
||||
'transaction_date': '交易日期',
|
||||
'record_type': '记录类型',
|
||||
'approver': '批准人',
|
||||
'department': '部门',
|
||||
'department_name': '部门',
|
||||
'category_name': '类别',
|
||||
|
||||
# 凭证
|
||||
'record_id': '记录ID',
|
||||
'record_title': '记录标题',
|
||||
'record_amount': '记录金额',
|
||||
'uploaded_images_count': '上传图片数量',
|
||||
'proof_id': '凭证ID',
|
||||
'image_description': '凭证说明',
|
||||
|
||||
# 人员
|
||||
'student_id': '学号',
|
||||
'gender': '性别',
|
||||
'grade_major': '年级专业',
|
||||
'project_group': '项目组',
|
||||
'project_group_name': '项目组',
|
||||
'position': '职位',
|
||||
'start_date': '开始日期',
|
||||
'end_date': '结束日期',
|
||||
'is_active': '是否在任',
|
||||
'phone': '电话',
|
||||
'qq': 'QQ',
|
||||
'email': '邮箱',
|
||||
'grader_major': '年级专业',
|
||||
}
|
||||
|
||||
# 这些key为元信息,不参与详情表格展示
|
||||
META_KEYS = {
|
||||
'timestamp', 'operation_path', 'operation_method'
|
||||
}
|
||||
|
||||
|
||||
def _format_value(value):
|
||||
"""将值格式化为字符串,布尔/数字/对象友好显示。"""
|
||||
if value is None:
|
||||
return ''
|
||||
# 保留数值0
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
if isinstance(value, bool):
|
||||
return '是' if value else '否'
|
||||
if isinstance(value, (list, dict)):
|
||||
try:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
except Exception:
|
||||
return str(value)
|
||||
# 其他转为字符串并去除首尾空白
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _build_data_items(instance_data: dict):
|
||||
"""从实例数据构建用于模板的条目列表,空值条目将被过滤。"""
|
||||
items = []
|
||||
for key, raw in (instance_data or {}).items():
|
||||
if key in META_KEYS:
|
||||
continue
|
||||
label = LABEL_MAP.get(key, key)
|
||||
value = _format_value(raw)
|
||||
if value == '':
|
||||
continue # 跳过空值
|
||||
items.append({'label': label, 'value': value})
|
||||
return items
|
||||
|
||||
|
||||
class EmailNotificationService:
|
||||
"""邮件通知服务"""
|
||||
|
||||
@@ -32,13 +140,7 @@ class EmailNotificationService:
|
||||
@staticmethod
|
||||
def send_operation_notification(operation_type, model_name, instance_data, user_info=None):
|
||||
"""
|
||||
发送操作通知邮件到多个邮箱
|
||||
|
||||
Args:
|
||||
operation_type: 操作类型 ('CREATE', 'UPDATE', 'DELETE')
|
||||
model_name: 模型名称 (如 'Item', 'FinanceRecord')
|
||||
instance_data: 实例数据
|
||||
user_info: 操作用户信息
|
||||
发送操作通知邮件到多个邮箱(带HTML模板,按label显示,空值隐藏)。
|
||||
"""
|
||||
try:
|
||||
settings_data = EmailNotificationService.get_notification_settings()
|
||||
@@ -51,42 +153,63 @@ class EmailNotificationService:
|
||||
logger.warning("没有配置启用的通知邮箱")
|
||||
return
|
||||
|
||||
# 构建邮件内容
|
||||
subject = f"[爱特工作室管理系统] {model_name}数据变更通知"
|
||||
|
||||
# 操作类型映射
|
||||
operation_map = {
|
||||
'CREATE': '创建',
|
||||
'UPDATE': '更新',
|
||||
'DELETE': '删除'
|
||||
}
|
||||
|
||||
operation_text = operation_map.get(operation_type, operation_type)
|
||||
|
||||
message = f"""
|
||||
系统数据变更通知
|
||||
# 可选的操作说明(如:凭证上传/凭证删除/记录更新等)
|
||||
operation_hint = _format_value((instance_data or {}).get('operation_type'))
|
||||
|
||||
操作类型: {operation_text}
|
||||
数据类型: {model_name}
|
||||
操作时间: {instance_data.get('timestamp', '未知')}
|
||||
操作用户: {user_info or '系统'}
|
||||
# 构建模板数据项(排除meta/空值并做label映射)
|
||||
data_items = _build_data_items(instance_data)
|
||||
|
||||
变更详情:
|
||||
{json.dumps(instance_data, ensure_ascii=False, indent=2)}
|
||||
# 标题
|
||||
subject = f"[爱特工作室管理系统] {model_name}{operation_text}通知"
|
||||
|
||||
---
|
||||
此邮件由爱特工作室物品管理及财务管理系统自动发送
|
||||
"""
|
||||
# 渲染HTML模板
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'operation_text': operation_text,
|
||||
'operation_hint': operation_hint,
|
||||
'timestamp': (instance_data or {}).get('timestamp', ''),
|
||||
'user_info': user_info or '系统',
|
||||
'data_items': data_items,
|
||||
}
|
||||
html_body = render_to_string('email_notification.html', context)
|
||||
|
||||
# 构建纯文本降级内容
|
||||
plain_lines = [
|
||||
f"【{model_name}】{operation_text}通知",
|
||||
f"数据类型: {model_name}",
|
||||
]
|
||||
if operation_hint:
|
||||
plain_lines.append(f"操作说明: {operation_hint}")
|
||||
plain_lines.extend([
|
||||
f"操作时间: {context['timestamp'] or '未知'}",
|
||||
f"操作用户: {context['user_info']}",
|
||||
"",
|
||||
"变更详情:",
|
||||
])
|
||||
for it in data_items:
|
||||
plain_lines.append(f"- {it['label']}: {it['value']}")
|
||||
plain_lines.append("\n此邮件由爱特工作室物品管理及财务管理系统自动发送")
|
||||
plain_message = "\n".join(plain_lines)
|
||||
|
||||
# 发送邮件到所有启用的通知邮箱
|
||||
for email in notification_emails:
|
||||
if email.strip(): # 确保邮箱不为空
|
||||
if email and str(email).strip(): # 确保邮箱不为空
|
||||
try:
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=message,
|
||||
from_email=settings.EMAIL_HOST_USER,
|
||||
recipient_list=[email.strip()],
|
||||
message=plain_message,
|
||||
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', settings.EMAIL_HOST_USER),
|
||||
recipient_list=[str(email).strip()],
|
||||
fail_silently=False,
|
||||
html_message=html_body,
|
||||
)
|
||||
logger.info(f"邮件通知发送成功到 {email}: {operation_type} {model_name}")
|
||||
except Exception as e:
|
||||
@@ -142,7 +265,7 @@ class EmailNotificationService:
|
||||
更新通知邮箱列表(使用数据库存储)
|
||||
|
||||
Args:
|
||||
emails_data: 邮箱数据列表,格式:[{'email': 'xxx@xxx.com', 'is_enabled': True, 'description': '描述'}]
|
||||
emails_data: 邮箱数据列表,格式:[{"email": "xxx@xxx.com", 'is_enabled': True, 'description': '描述'}]
|
||||
"""
|
||||
try:
|
||||
from .models import NotificationEmail
|
||||
@@ -225,7 +348,7 @@ class EmailNotificationService:
|
||||
|
||||
logger.info(f"邮箱 {email_obj.email} 状态已更新: {is_enabled}")
|
||||
return True
|
||||
except NotificationEmail.DoesNotExist:
|
||||
except ObjectDoesNotExist:
|
||||
logger.error(f"邮箱ID {email_id} 不存在")
|
||||
return False
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ model_name }}{{ operation_text }}通知</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; color: #333; margin: 0; padding: 0; background: #f5f7fa; }
|
||||
.container { max-width: 720px; margin: 24px auto; background: #ffffff; border: 1px solid #eaecef; border-radius: 8px; overflow: hidden; }
|
||||
.header { background: #409eff; color: #fff; padding: 16px 20px; }
|
||||
.header h1 { font-size: 18px; margin: 0; }
|
||||
.meta { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; background: #fafcff; }
|
||||
.meta-item { margin: 6px 0; font-size: 14px; }
|
||||
.meta-item strong { color: #606266; margin-right: 6px; }
|
||||
.content { padding: 12px 20px 20px; }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { text-align: left; padding: 10px 8px; font-size: 14px; }
|
||||
.table tr:nth-child(odd) { background: #fafafa; }
|
||||
.table th { width: 160px; color: #606266; font-weight: 600; background: #f8fafc; border-bottom: 1px solid #f0f0f0; }
|
||||
.footer { padding: 14px 20px; font-size: 12px; color: #909399; background: #fafafb; border-top: 1px solid #f0f0f0; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; color: #fff; }
|
||||
.badge.create { background: #67c23a; }
|
||||
.badge.update { background: #e6a23c; }
|
||||
.badge.delete { background: #f56c6c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>【{{ model_name }}】<span class="badge {% if operation_text == '创建' %}create{% elif operation_text == '更新' %}update{% elif operation_text == '删除' %}delete{% endif %}">{{ operation_text }}</span> 通知</h1>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div class="meta-item"><strong>数据类型</strong>{{ model_name }}</div>
|
||||
{% if operation_hint %}<div class="meta-item"><strong>操作说明</strong>{{ operation_hint }}</div>{% endif %}
|
||||
<div class="meta-item"><strong>操作时间</strong>{{ timestamp|default:'未知' }}</div>
|
||||
<div class="meta-item"><strong>操作用户</strong>{{ user_info|default:'系统' }}</div>
|
||||
</div>
|
||||
|
||||
{% if data_items and data_items|length %}
|
||||
<div class="content">
|
||||
<table class="table" role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tbody>
|
||||
{% for it in data_items %}
|
||||
<tr>
|
||||
<th>{{ it.label }}</th>
|
||||
<td>{{ it.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
此邮件由「爱特工作室物品管理及财务管理系统」自动发送,请勿直接回复。
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user