diff --git a/src/backend/email_notice/services.py b/src/backend/email_notice/services.py index d388119..49c969d 100644 --- a/src/backend/email_notice/services.py +++ b/src/backend/email_notice/services.py @@ -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_text} - 数据类型: {model_name} - 操作时间: {instance_data.get('timestamp', '未知')} - 操作用户: {user_info or '系统'} - - 变更详情: - {json.dumps(instance_data, ensure_ascii=False, indent=2)} - - --- - 此邮件由爱特工作室物品管理及财务管理系统自动发送 - """ + # 可选的操作说明(如:凭证上传/凭证删除/记录更新等) + operation_hint = _format_value((instance_data or {}).get('operation_type')) + + # 构建模板数据项(排除meta/空值并做label映射) + data_items = _build_data_items(instance_data) + + # 标题 + 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: diff --git a/src/backend/email_notice/templates/email_notification.html b/src/backend/email_notice/templates/email_notification.html new file mode 100644 index 0000000..30f359c --- /dev/null +++ b/src/backend/email_notice/templates/email_notification.html @@ -0,0 +1,60 @@ + + + + + + {{ model_name }}{{ operation_text }}通知 + + + +
+
+

【{{ model_name }}】{{ operation_text }} 通知

+
+ +
+
数据类型{{ model_name }}
+ {% if operation_hint %}
操作说明{{ operation_hint }}
{% endif %} +
操作时间{{ timestamp|default:'未知' }}
+
操作用户{{ user_info|default:'系统' }}
+
+ + {% if data_items and data_items|length %} +
+ + + {% for it in data_items %} + + + + + {% endfor %} + + +
+ {% endif %} + + +
+ +