From 24256f588cf34d3a9eddb9681c69974a7a90a2c1 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Mon, 22 Sep 2025 11:33:18 +0800 Subject: [PATCH] feat: project group can be belonged to many departments --- src/backend/email_notice/middleware.py | 7 +- src/backend/email_notice/services.py | 11 ++- src/backend/personnel/admin.py | 79 +++---------------- src/backend/personnel/models.py | 12 ++- src/backend/personnel/serializers.py | 17 ++-- src/backend/personnel/views.py | 18 +---- .../DepartmentProjectGroupManagement.vue | 27 +++++-- src/fronted/src/views/PersonnelForm.vue | 4 +- 8 files changed, 71 insertions(+), 104 deletions(-) diff --git a/src/backend/email_notice/middleware.py b/src/backend/email_notice/middleware.py index f63460a..580484a 100644 --- a/src/backend/email_notice/middleware.py +++ b/src/backend/email_notice/middleware.py @@ -312,10 +312,11 @@ class EmailNotificationMiddleware(MiddlewareMixin): notification_data = { 'id': project_group_data.get('id', ''), 'name': project_group_data.get('name', ''), - 'department': project_group_data.get('department', ''), - 'department_name': project_group_data.get('department_name', ''), + 'departments': project_group_data.get('departments', ''), + 'department_names': project_group_data.get('department_names', ''), + 'departments_info': project_group_data.get('departments_info', ''), 'description': project_group_data.get('description', ''), - 'timestamp': project_group_data.get('updated_at', ''), + 'timestamp': project_group_data.get('created_at', ''), 'operation_path': request.path, 'operation_method': request.method } diff --git a/src/backend/email_notice/services.py b/src/backend/email_notice/services.py index e1832a4..e287518 100644 --- a/src/backend/email_notice/services.py +++ b/src/backend/email_notice/services.py @@ -66,6 +66,7 @@ LABEL_MAP = { 'grade_major': '年级专业', 'project_group': '项目组', 'project_group_name': '项目组', + 'department_names': '所属部门', 'position': '职位', 'start_date': '开始日期', 'end_date': '结束日期', @@ -91,7 +92,15 @@ def _format_value(value): return str(value) if isinstance(value, bool): return '是' if value else '否' - if isinstance(value, (list, dict)): + if isinstance(value, list): + # 对于列表,优先显示为逗号分隔的字符串 + if all(isinstance(item, str) for item in value): + return ', '.join(value) + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + if isinstance(value, dict): try: return json.dumps(value, ensure_ascii=False) except Exception: diff --git a/src/backend/personnel/admin.py b/src/backend/personnel/admin.py index b65f9ef..bbaf238 100644 --- a/src/backend/personnel/admin.py +++ b/src/backend/personnel/admin.py @@ -6,12 +6,18 @@ from .models import Personnel, ProjectGroup @admin.register(ProjectGroup) class ProjectGroupAdmin(admin.ModelAdmin): """项目组管理""" - list_display = ['name', 'department', 'description', 'created_at'] - list_filter = ['department', 'created_at'] + list_display = ['name', 'get_departments', 'description', 'created_at'] + list_filter = ['departments', 'created_at'] search_fields = ['name', 'description'] - ordering = ['department', 'name'] + ordering = ['name'] + filter_horizontal = ['departments'] + + def get_departments(self, obj): + """获取部门列表显示""" + return ", ".join([dept.name for dept in obj.departments.all()]) + get_departments.short_description = '所属部门' + -import django_filters @admin.register(Personnel) class PersonnelAdmin(admin.ModelAdmin): """人员信息管理""" @@ -51,68 +57,3 @@ class PersonnelAdmin(admin.ModelAdmin): def get_queryset(self, request): """优化查询""" return super().get_queryset(request).select_related('department', 'project_group') -from .models import Personnel, ProjectGroup -from finance.models import Department - - -class PersonnelFilter(django_filters.FilterSet): - """人员信息过滤器""" - - # 基本筛选 - department = django_filters.ModelChoiceFilter( - queryset=Department.objects.all(), - field_name='department', - label='部门' - ) - - project_group = django_filters.ModelChoiceFilter( - queryset=ProjectGroup.objects.all(), - field_name='project_group', - label='项目组' - ) - - position = django_filters.CharFilter( - field_name='position', - lookup_expr='icontains', - label='职位' - ) - - gender = django_filters.ChoiceFilter( - choices=Personnel.GENDER_CHOICES, - field_name='gender', - label='性别' - ) - - is_active = django_filters.BooleanFilter( - field_name='is_active', - label='在职状态' - ) - - # 日期范围筛选 - start_date_from = django_filters.DateFilter( - field_name='start_date', - lookup_expr='gte', - label='任职开始时间(从)' - ) - - start_date_to = django_filters.DateFilter( - field_name='start_date', - lookup_expr='lte', - label='任职开始时间(到)' - ) - - end_date_from = django_filters.DateFilter( - field_name='end_date', - lookup_expr='gte', - label='任职结束时间(从)' - ) - - end_date_to = django_filters.DateFilter( - field_name='end_date', - lookup_expr='lte', - label='任职结束时间(到)' - ) - - class Meta: - model = Personnel - fields = ['department', 'project_group', 'position', 'gender', 'is_active'] diff --git a/src/backend/personnel/models.py b/src/backend/personnel/models.py index 82d4278..e543e22 100644 --- a/src/backend/personnel/models.py +++ b/src/backend/personnel/models.py @@ -6,17 +6,23 @@ from finance.models import Department class ProjectGroup(models.Model): """项目组模型""" name = models.CharField(max_length=100, verbose_name="项目组名称") - department = models.ForeignKey(Department, on_delete=models.CASCADE, verbose_name="所属部门") + departments = models.ManyToManyField(Department, verbose_name="所属部门", related_name="project_groups") description = models.TextField(blank=True, null=True, verbose_name="项目组描述") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: verbose_name = "项目组" verbose_name_plural = verbose_name - ordering = ['department', 'name'] + ordering = ['name'] def __str__(self): - return f"{self.department.name} - {self.name}" + department_names = ", ".join([dept.name for dept in self.departments.all()]) + return f"{self.name} ({department_names})" if department_names else self.name + + @property + def department_names(self): + """返回所属部门名称列表""" + return [dept.name for dept in self.departments.all()] class Personnel(models.Model): diff --git a/src/backend/personnel/serializers.py b/src/backend/personnel/serializers.py index 32656fa..6453a0c 100644 --- a/src/backend/personnel/serializers.py +++ b/src/backend/personnel/serializers.py @@ -6,13 +6,18 @@ from .models import Personnel, ProjectGroup class ProjectGroupSerializer(serializers.ModelSerializer): """项目组序列化器""" - department_name = serializers.CharField(source='department.name', read_only=True) + department_names = serializers.ListField(read_only=True) + departments_info = serializers.SerializerMethodField() class Meta: model = ProjectGroup - fields = ['id', 'name', 'department', 'department_name', 'description', 'created_at'] + fields = ['id', 'name', 'departments', 'department_names', 'departments_info', 'description', 'created_at'] read_only_fields = ['created_at'] + def get_departments_info(self, obj): + """获取部门详细信息""" + return [{'id': dept.id, 'name': dept.name} for dept in obj.departments.all()] + class PersonnelReadSerializer(serializers.ModelSerializer): """人员信息读取序列化器""" @@ -45,11 +50,13 @@ class PersonnelWriteSerializer(serializers.ModelSerializer): ] def validate_project_group(self, value): - """验证项目组是否属于选定的部门""" + """验证项目组是否包含选定的部门""" if value and hasattr(self, 'initial_data'): department_id = self.initial_data.get('department') - if department_id and value.department_id != int(department_id): - raise serializers.ValidationError("项目组必须属于选定的部门") + if department_id: + # 检查项目组的部门列表中是否包含选定的部门 + if not value.departments.filter(id=int(department_id)).exists(): + raise serializers.ValidationError("项目组必须包含选定的部门") return value def validate(self, attrs): diff --git a/src/backend/personnel/views.py b/src/backend/personnel/views.py index 3e40784..d3ea23f 100644 --- a/src/backend/personnel/views.py +++ b/src/backend/personnel/views.py @@ -197,16 +197,16 @@ class ProjectGroupViewSet(viewsets.ModelViewSet): queryset = ProjectGroup.objects.all() serializer_class = ProjectGroupSerializer filter_backends = [DjangoFilterBackend, SearchFilter] - filterset_fields = ['department'] + filterset_fields = ['departments'] search_fields = ['name', 'description'] - ordering = ['department', 'name'] + ordering = ['name'] @action(detail=False, methods=['get']) def by_department(self, request): """按部门获取项目组""" department_id = request.query_params.get('department_id') if department_id: - project_groups = ProjectGroup.objects.filter(department_id=department_id) + project_groups = ProjectGroup.objects.filter(departments__id=department_id).distinct() serializer = self.get_serializer(project_groups, many=True) return Response(serializer.data) return Response([]) @@ -221,7 +221,7 @@ class ProjectGroupViewSet(viewsets.ModelViewSet): project_group_data = { 'id': project_group.id, 'name': "[已删除]" + project_group.name, - 'department_name': project_group.department.name if project_group.department else '', + 'department_names': project_group.department_names, 'description': project_group.description, 'created_at': str(project_group.created_at), 'operation_path': request.path, @@ -267,13 +267,3 @@ class ProjectGroupViewSet(viewsets.ModelViewSet): else: ip = request.META.get('REMOTE_ADDR') return ip - - @action(detail=False, methods=['get']) - def by_department(self, request): - """按部门获取项目组""" - department_id = request.query_params.get('department_id') - if department_id: - project_groups = ProjectGroup.objects.filter(department_id=department_id) - serializer = self.get_serializer(project_groups, many=True) - return Response(serializer.data) - return Response([]) diff --git a/src/fronted/src/views/DepartmentProjectGroupManagement.vue b/src/fronted/src/views/DepartmentProjectGroupManagement.vue index c3de563..9a54565 100644 --- a/src/fronted/src/views/DepartmentProjectGroupManagement.vue +++ b/src/fronted/src/views/DepartmentProjectGroupManagement.vue @@ -54,7 +54,13 @@ - + + +