diff --git a/src/backend/email_notice/admin.py b/src/backend/email_notice/admin.py index 7daa7d3..7d16e57 100644 --- a/src/backend/email_notice/admin.py +++ b/src/backend/email_notice/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin + from .models import NotificationEmail, NotificationSettings + @admin.register(NotificationEmail) class NotificationEmailAdmin(admin.ModelAdmin): list_display = ('email', 'is_enabled', 'description', 'created_at') diff --git a/src/backend/email_notice/middleware.py b/src/backend/email_notice/middleware.py index d16a644..25d1fad 100644 --- a/src/backend/email_notice/middleware.py +++ b/src/backend/email_notice/middleware.py @@ -1,10 +1,10 @@ -from django.utils.deprecation import MiddlewareMixin -from django.http import JsonResponse -from .services import EmailNotificationService import json import logging import threading -from django.core.exceptions import ObjectDoesNotExist + +from django.utils.deprecation import MiddlewareMixin + +from .services import EmailNotificationService logger = logging.getLogger(__name__) @@ -22,6 +22,10 @@ class EmailNotificationMiddleware(MiddlewareMixin): if not request.path.startswith('/api/'): return response + # 排除凭证相关的API路径,这些已经在视图中单独处理 + if '/proof-images/' in request.path : + return response + # 只处理数据新增修改操作,但排除DELETE操作,毕竟删除操作已经另外重写 if request.method not in ['POST', 'PUT', 'PATCH']: return response @@ -93,10 +97,25 @@ class EmailNotificationMiddleware(MiddlewareMixin): notification_data = { 'id': item_data.get('id', ''), 'name': item_data.get('name', ''), + 'item_name': item_data.get('item_name', ''), + 'item_serial': item_data.get('item_serial', ''), 'serial_number': item_data.get('serial_number', ''), - 'status': item_data.get('status', ''), 'category': item_data.get('category', ''), + 'status': item_data.get('status', ''), + 'location': item_data.get('location', ''), + 'description': item_data.get('description', ''), 'owner': item_data.get('owner', ''), + 'purchase_date': item_data.get('purchase_date', ''), + 'value': item_data.get('value', ''), + 'user': item_data.get('user', ''), + 'borrower_contact': item_data.get('borrower_contact', ''), + 'start_time': item_data.get('start_time', ''), + 'end_time': item_data.get('end_time', ''), + 'purpose': item_data.get('purpose', ''), + 'notes': item_data.get('notes', ''), + 'is_returned': item_data.get('is_returned', ''), + 'condition_before': item_data.get('condition_before', ''), + 'condition_after': item_data.get('condition_after', ''), 'timestamp': item_data.get('updated_at', ''), 'operation_path': request.path, 'operation_method': request.method @@ -181,8 +200,11 @@ class EmailNotificationMiddleware(MiddlewareMixin): personnel_data = {} # 构建通知数据,使用正确的字段名 - message = personnel_data['message', ''] - personnel_data = personnel_data['data', ''] + message = '' + if 'message' in personnel_data: + message = personnel_data['message', ''] + if 'data' in personnel_data: + personnel_data = personnel_data['data', ''] notification_data = { 'message': message, 'id': personnel_data.get('id', ''), @@ -192,6 +214,7 @@ class EmailNotificationMiddleware(MiddlewareMixin): 'grader_major': personnel_data.get('grade_major', ''), 'department': personnel_data.get('department', ''), 'project_group': personnel_data.get('project_group', ''), + 'project_group_name': personnel_data.get('project_group_name', ''), 'position': personnel_data.get('position', ''), 'start_date': personnel_data.get('start_date', ''), 'end_date': personnel_data.get('end_date', ''), diff --git a/src/backend/email_notice/models.py b/src/backend/email_notice/models.py index ef0185f..77c375b 100644 --- a/src/backend/email_notice/models.py +++ b/src/backend/email_notice/models.py @@ -1,5 +1,6 @@ -from django.db import models from django.core.validators import EmailValidator +from django.db import models + class NotificationEmail(models.Model): """通知邮箱模型""" diff --git a/src/backend/email_notice/services.py b/src/backend/email_notice/services.py index e9456dd..d388119 100644 --- a/src/backend/email_notice/services.py +++ b/src/backend/email_notice/services.py @@ -1,8 +1,9 @@ -from django.core.mail import send_mail -from django.conf import settings import json import logging +from django.conf import settings +from django.core.mail import send_mail + logger = logging.getLogger(__name__) class EmailNotificationService: diff --git a/src/backend/email_notice/tests.py b/src/backend/email_notice/tests.py index 83e29ec..6bc0772 100644 --- a/src/backend/email_notice/tests.py +++ b/src/backend/email_notice/tests.py @@ -1,8 +1,9 @@ from django.test import TestCase -from django.core import mail -from .models import NotificationEmail, NotificationSettings + +from .models import NotificationEmail from .services import EmailNotificationService + class EmailNotificationTestCase(TestCase): def setUp(self): """设置测试数据""" diff --git a/src/backend/email_notice/urls.py b/src/backend/email_notice/urls.py index b09b922..9ab0bc2 100644 --- a/src/backend/email_notice/urls.py +++ b/src/backend/email_notice/urls.py @@ -1,4 +1,5 @@ from django.urls import path + from . import views app_name = 'email_notice' diff --git a/src/backend/email_notice/views.py b/src/backend/email_notice/views.py index d403685..d4661d5 100644 --- a/src/backend/email_notice/views.py +++ b/src/backend/email_notice/views.py @@ -1,9 +1,12 @@ +import json +import re + from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods + from .services import EmailNotificationService -import json -import re + @csrf_exempt @require_http_methods(["POST", "GET"]) diff --git a/src/backend/finance/admin.py b/src/backend/finance/admin.py index c84e14b..eb9bbce 100644 --- a/src/backend/finance/admin.py +++ b/src/backend/finance/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Department, Category, FinancialRecord, ProofImage diff --git a/src/backend/finance/models.py b/src/backend/finance/models.py index 28c1c63..7ad61e8 100644 --- a/src/backend/finance/models.py +++ b/src/backend/finance/models.py @@ -1,5 +1,5 @@ -from django.db import models from django.contrib.auth.models import User +from django.db import models def proof_image_upload_path(instance, filename): diff --git a/src/backend/finance/serializers.py b/src/backend/finance/serializers.py index d91f4cb..fed0b28 100644 --- a/src/backend/finance/serializers.py +++ b/src/backend/finance/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from .models import FinancialRecord, Department, Category, ProofImage diff --git a/src/backend/finance/urls.py b/src/backend/finance/urls.py index 922cbb4..3c56683 100644 --- a/src/backend/finance/urls.py +++ b/src/backend/finance/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter + from .views import FinancialRecordViewSet, DepartmentViewSet, CategoryViewSet, ProofImageViewSet router = DefaultRouter() diff --git a/src/backend/finance/views.py b/src/backend/finance/views.py index 85153b2..96a0cb7 100644 --- a/src/backend/finance/views.py +++ b/src/backend/finance/views.py @@ -1,9 +1,11 @@ import os import threading + +from django.utils import timezone from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response -from django.utils import timezone + from .models import FinancialRecord, Department, Category, ProofImage from .serializers import ( FinancialRecordWriteSerializer, @@ -13,6 +15,7 @@ from .serializers import ( ProofImageSerializer ) + class FinancialRecordViewSet(viewsets.ModelViewSet): """ 获取财务记录 @@ -139,6 +142,39 @@ class FinancialRecordViewSet(viewsets.ModelViewSet): ) created_images.append(ProofImageSerializer(proof_image).data) + # 异步发送凭证上传通知邮件 + def send_proof_upload_notification(): + try: + from email_notice.services import EmailNotificationService + + user_info = self._get_user_info(request) + + # 构建凭证更新通知数据 + notification_data = { + 'record_id': record.id, + 'record_title': record.title, + 'record_amount': str(record.amount), + 'uploaded_images_count': len(created_images), + 'timestamp': timezone.now().isoformat(), + 'operation_type': '凭证上传', + 'operation_path': request.path, + 'operation_method': request.method + } + + # 发送凭证更新通知 + EmailNotificationService.send_operation_notification( + 'UPDATE', '财务凭证', notification_data, user_info + ) + print(f"凭证上传通知邮件已发送: 财务记录 ID:{record.id}, 上传{len(created_images)}张图片") + + except Exception as e: + print(f"发送凭证上传通知邮件失败: {e}") + + # 启动异步邮件发送线程 + email_thread = threading.Thread(target=send_proof_upload_notification) + email_thread.daemon = True + email_thread.start() + return Response({ 'message': f'成功上传 {len(created_images)} 张图片', 'images': created_images @@ -153,9 +189,19 @@ class ProofImageViewSet(viewsets.ModelViewSet): serializer_class = ProofImageSerializer def destroy(self, request, *args, **kwargs): - """重写删除方法,确保删除图片记录时同时删除物理文件""" + """删除方法,确保删除图片记录时同时删除物理文件,并发送邮件通知""" instance = self.get_object() + # 获取凭证信息用于邮件通知 + proof_info = { + 'id': instance.id, + 'record_id': instance.financial_record.id, + 'record_title': instance.financial_record.title, + 'record_amount': str(instance.financial_record.amount), + 'image_description': instance.description, + 'timestamp': timezone.now().isoformat(), + } + # 获取文件路径 file_path = None if instance.image: @@ -177,8 +223,61 @@ class ProofImageViewSet(viewsets.ModelViewSet): print(f"删除文件失败: {file_path}, 错误: {e}") # 即使文件删除失败,也不抛出异常,因为数据库记录已经删除 + # 异步发送凭证删除通知邮件 + def send_proof_delete_notification(): + try: + from email_notice.services import EmailNotificationService + + user_info = self._get_user_info(request) + + # 构建凭证删除通知数据 + notification_data = { + 'proof_id': proof_info['id'], + 'record_id': proof_info['record_id'], + 'record_title': proof_info['record_title'], + 'record_amount': proof_info['record_amount'], + 'image_description': proof_info['image_description'], + 'timestamp': proof_info['timestamp'], + 'operation_type': '凭证删除', + 'operation_path': request.path, + 'operation_method': request.method + } + + # 发送凭证删除通知 + EmailNotificationService.send_operation_notification( + 'DELETE', '财务凭证', notification_data, user_info + ) + print(f"凭证删除通知邮件已发送: 财务记录 ID:{proof_info['record_id']}, 凭证 ID:{proof_info['id']}") + + except Exception as e: + print(f"发送凭证删除通知邮件失败: {e}") + + # 启动异步邮件发送线程 + email_thread = threading.Thread(target=send_proof_delete_notification) + email_thread.daemon = True + email_thread.start() + return Response({'message': '图片删除成功'}, status=status.HTTP_200_OK) + def _get_user_info(self, request): + """获取用户信息""" + try: + if hasattr(request, 'user') and request.user.is_authenticated: + return f"{request.user.username} ({request.user.email})" + else: + return f"匿名用户 (IP: {self._get_client_ip(request)})" + except: + return "未知用户" + + def _get_client_ip(self, request): + """获取客户端IP""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + class DepartmentViewSet(viewsets.ModelViewSet): """ diff --git a/src/backend/item_manager/settings.py b/src/backend/item_manager/settings.py index 7711a1e..d19cf0e 100644 --- a/src/backend/item_manager/settings.py +++ b/src/backend/item_manager/settings.py @@ -10,9 +10,10 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.2/ref/settings/ """ -from pathlib import Path import json import os +from pathlib import Path + # SECURE 文件用来存储敏感信息,如 SECRET_KEY,SMTP信息 等 # Build paths inside the project like this: BASE_DIR / 'subdir'. diff --git a/src/backend/item_manager/urls.py b/src/backend/item_manager/urls.py index 8c8c934..b40d14a 100644 --- a/src/backend/item_manager/urls.py +++ b/src/backend/item_manager/urls.py @@ -15,10 +15,10 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin -from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), diff --git a/src/backend/items/admin.py b/src/backend/items/admin.py index 06b26b6..b4a05c6 100644 --- a/src/backend/items/admin.py +++ b/src/backend/items/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Item, ItemUsage, Category diff --git a/src/backend/items/models.py b/src/backend/items/models.py index 46204e1..e2e832b 100644 --- a/src/backend/items/models.py +++ b/src/backend/items/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.contrib.auth.models import User class Item(models.Model): diff --git a/src/backend/items/serializers.py b/src/backend/items/serializers.py index 8c91cc6..e9fc02c 100644 --- a/src/backend/items/serializers.py +++ b/src/backend/items/serializers.py @@ -1,5 +1,6 @@ -from rest_framework import serializers from django.contrib.auth.models import User +from rest_framework import serializers + from .models import Item, ItemUsage, Category diff --git a/src/backend/items/tests.py b/src/backend/items/tests.py index 7ce503c..a39b155 100644 --- a/src/backend/items/tests.py +++ b/src/backend/items/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/src/backend/items/urls.py b/src/backend/items/urls.py index b2808f0..1061e8f 100644 --- a/src/backend/items/urls.py +++ b/src/backend/items/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter + from . import views router = DefaultRouter() diff --git a/src/backend/items/views.py b/src/backend/items/views.py index 88f0220..10c02c4 100644 --- a/src/backend/items/views.py +++ b/src/backend/items/views.py @@ -1,9 +1,9 @@ -from django.shortcuts import render +from django.contrib.auth.models import User +from django.utils import timezone from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response -from django.contrib.auth.models import User -from django.utils import timezone + from .models import Item, ItemUsage, Category from .serializers import ( ItemSerializer, ItemDetailSerializer, ItemUsageSerializer, diff --git a/src/backend/personnel/admin.py b/src/backend/personnel/admin.py index 2811df0..b65f9ef 100644 --- a/src/backend/personnel/admin.py +++ b/src/backend/personnel/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Personnel, ProjectGroup diff --git a/src/backend/personnel/filters.py b/src/backend/personnel/filters.py index 13290a5..42d98fd 100644 --- a/src/backend/personnel/filters.py +++ b/src/backend/personnel/filters.py @@ -1,8 +1,8 @@ import django_filters -from django.db import models -from .models import Personnel, ProjectGroup from finance.models import Department +from .models import Personnel, ProjectGroup + class PersonnelFilter(django_filters.FilterSet): """人员信息过滤器""" diff --git a/src/backend/personnel/models.py b/src/backend/personnel/models.py index f85a76b..7d0a1f9 100644 --- a/src/backend/personnel/models.py +++ b/src/backend/personnel/models.py @@ -1,5 +1,5 @@ -from django.db import models from django.core.validators import RegexValidator +from django.db import models from finance.models import Department diff --git a/src/backend/personnel/serializers.py b/src/backend/personnel/serializers.py index 4a62df3..2739195 100644 --- a/src/backend/personnel/serializers.py +++ b/src/backend/personnel/serializers.py @@ -1,6 +1,7 @@ -from rest_framework import serializers -from .models import Personnel, ProjectGroup from finance.models import Department +from rest_framework import serializers + +from .models import Personnel, ProjectGroup class ProjectGroupSerializer(serializers.ModelSerializer): diff --git a/src/backend/personnel/urls.py b/src/backend/personnel/urls.py index 9e61c26..573f500 100644 --- a/src/backend/personnel/urls.py +++ b/src/backend/personnel/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter + from .views import PersonnelViewSet, ProjectGroupViewSet router = DefaultRouter() diff --git a/src/backend/personnel/views.py b/src/backend/personnel/views.py index 6f79da7..2c5dad3 100644 --- a/src/backend/personnel/views.py +++ b/src/backend/personnel/views.py @@ -1,17 +1,19 @@ -from rest_framework import viewsets, status -from rest_framework.decorators import action -from rest_framework.response import Response -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.filters import SearchFilter, OrderingFilter -from django.utils import timezone import logging + +from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.response import Response + +from .filters import PersonnelFilter from .models import Personnel, ProjectGroup from .serializers import ( PersonnelReadSerializer, PersonnelWriteSerializer, ProjectGroupSerializer ) -from .filters import PersonnelFilter logger = logging.getLogger(__name__) @@ -109,18 +111,23 @@ class PersonnelViewSet(viewsets.ModelViewSet): personnel = self.get_object() personnel_data = { 'id': personnel.id, - 'name': personnel.name, + 'name': "[已删除]" + personnel.name, 'student_id': personnel.student_id, - 'email': personnel.email, - 'phone': personnel.phone, - 'department_name': personnel.department.name if personnel.department else '', - 'project_group_name': personnel.project_group.name if personnel.project_group else '', + 'gender': personnel.gender, + 'grader_major': personnel.grade_major, + 'department': personnel.department.name, + 'project_group': personnel.project_group, 'position': personnel.position, + 'start_date': personnel.start_date, + 'end_date': personnel.end_date, 'is_active': personnel.is_active, - 'start_date': str(personnel.start_date), - 'end_date': str(personnel.end_date) if personnel.end_date else '', - 'timestamp': timezone.now().isoformat(), - + 'phone': personnel.phone, + 'qq': personnel.qq, + 'email': personnel.email, + 'description': personnel.description, + 'timestamp': personnel.updated_at, + 'operation_path': request.path, + 'operation_method': request.method } # 获取用户信息 @@ -210,7 +217,7 @@ class ProjectGroupViewSet(viewsets.ModelViewSet): project_group = self.get_object() project_group_data = { 'id': project_group.id, - 'name': project_group.name, + 'name': "[已删除]" + project_group.name, 'department_name': project_group.department.name if project_group.department else '', 'description': project_group.description, 'created_at': str(project_group.created_at), diff --git a/src/backend/scheduler/jobs.py b/src/backend/scheduler/jobs.py index ede3445..a599a48 100644 --- a/src/backend/scheduler/jobs.py +++ b/src/backend/scheduler/jobs.py @@ -1,8 +1,9 @@ import logging + from apscheduler.schedulers.background import BackgroundScheduler +from django_apscheduler import util from django_apscheduler.jobstores import DjangoJobStore from django_apscheduler.models import DjangoJobExecution -from django_apscheduler import util from personnel.models import Personnel logger = logging.getLogger(__name__) diff --git a/src/fronted/public/favicon.svg b/src/fronted/public/favicon.svg index d87ffb9..b573782 100644 --- a/src/fronted/public/favicon.svg +++ b/src/fronted/public/favicon.svg @@ -1,4 +1,4 @@ -使用历史 - +