From 7e089bd5ec6344a158cfac89100ed86a4150b90b Mon Sep 17 00:00:00 2001 From: TianyangZhang Date: Tue, 24 Mar 2026 17:34:56 +0800 Subject: [PATCH] feat: add Organization model with tree structure Co-Authored-By: Claude Sonnet 4.6 --- .../organizations/migrations/0001_initial.py | 32 +++++++++++++++++++ .../apps/organizations/migrations/__init__.py | 0 offer_backend/apps/organizations/models.py | 17 ++++++++-- .../apps/organizations/serializers.py | 22 +++++++++++++ .../apps/organizations/tests/__init__.py | 0 .../organizations/tests/test_organizations.py | 23 +++++++++++++ offer_backend/apps/organizations/urls.py | 10 ++++-- offer_backend/apps/organizations/views.py | 19 +++++++++++ 8 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 offer_backend/apps/organizations/migrations/0001_initial.py create mode 100644 offer_backend/apps/organizations/migrations/__init__.py create mode 100644 offer_backend/apps/organizations/serializers.py create mode 100644 offer_backend/apps/organizations/tests/__init__.py create mode 100644 offer_backend/apps/organizations/tests/test_organizations.py create mode 100644 offer_backend/apps/organizations/views.py diff --git a/offer_backend/apps/organizations/migrations/0001_initial.py b/offer_backend/apps/organizations/migrations/0001_initial.py new file mode 100644 index 0000000..37ae8f1 --- /dev/null +++ b/offer_backend/apps/organizations/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.20 on 2026-03-24 09:34 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='公司名称')), + ('logo', models.ImageField(blank=True, null=True, upload_to='org_logos/')), + ('description', models.TextField(blank=True, verbose_name='公司简介')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='联系邮箱')), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='organizations.organization', verbose_name='上级公司')), + ], + options={ + 'verbose_name': '组织架构', + 'verbose_name_plural': '组织架构', + }, + ), + ] diff --git a/offer_backend/apps/organizations/migrations/__init__.py b/offer_backend/apps/organizations/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/organizations/models.py b/offer_backend/apps/organizations/models.py index f54646a..860d845 100644 --- a/offer_backend/apps/organizations/models.py +++ b/offer_backend/apps/organizations/models.py @@ -2,12 +2,23 @@ from django.db import models class Organization(models.Model): - name = models.CharField(max_length=255) + name = models.CharField(max_length=100, verbose_name='公司名称') + parent = models.ForeignKey( + 'self', null=True, blank=True, + on_delete=models.SET_NULL, + related_name='children', + verbose_name='上级公司' + ) + logo = models.ImageField(upload_to='org_logos/', null=True, blank=True) + description = models.TextField(blank=True, verbose_name='公司简介') + email = models.EmailField(blank=True, verbose_name='联系邮箱') + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) class Meta: app_label = 'organizations' - verbose_name = '组织' - verbose_name_plural = '组织' + verbose_name = '组织架构' + verbose_name_plural = '组织架构' def __str__(self): return self.name diff --git a/offer_backend/apps/organizations/serializers.py b/offer_backend/apps/organizations/serializers.py new file mode 100644 index 0000000..c3b2e32 --- /dev/null +++ b/offer_backend/apps/organizations/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers +from .models import Organization + + +class OrganizationSerializer(serializers.ModelSerializer): + class Meta: + model = Organization + fields = ['id', 'name', 'parent', 'logo', 'description', 'email', 'is_active'] + + +class OrganizationTreeSerializer(serializers.ModelSerializer): + """带子公司列表,用于门户展示""" + children = serializers.SerializerMethodField() + + class Meta: + model = Organization + fields = ['id', 'name', 'logo', 'description', 'email', 'children'] + + def get_children(self, obj): + return OrganizationSerializer( + obj.children.filter(is_active=True), many=True + ).data diff --git a/offer_backend/apps/organizations/tests/__init__.py b/offer_backend/apps/organizations/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/organizations/tests/test_organizations.py b/offer_backend/apps/organizations/tests/test_organizations.py new file mode 100644 index 0000000..9d1e4b1 --- /dev/null +++ b/offer_backend/apps/organizations/tests/test_organizations.py @@ -0,0 +1,23 @@ +import pytest +from apps.organizations.models import Organization + + +@pytest.mark.django_db +class TestOrganizationModel: + def test_create_group(self): + org = Organization.objects.create(name='示例集团', email='group@example.com') + assert org.parent is None + assert org.is_active is True + + def test_create_subsidiary(self): + parent = Organization.objects.create(name='示例集团', email='group@example.com') + child = Organization.objects.create( + name='子公司A', email='a@example.com', parent=parent + ) + assert child.parent == parent + + def test_list_subsidiaries(self): + parent = Organization.objects.create(name='集团', email='g@example.com') + Organization.objects.create(name='子A', email='a@example.com', parent=parent) + Organization.objects.create(name='子B', email='b@example.com', parent=parent) + assert parent.children.count() == 2 diff --git a/offer_backend/apps/organizations/urls.py b/offer_backend/apps/organizations/urls.py index e39cb2c..20bbaf9 100644 --- a/offer_backend/apps/organizations/urls.py +++ b/offer_backend/apps/organizations/urls.py @@ -1,3 +1,9 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import OrganizationPublicViewSet, OrganizationManageViewSet -urlpatterns = [] +router = DefaultRouter() +router.register('public', OrganizationPublicViewSet, basename='org-public') +router.register('manage', OrganizationManageViewSet, basename='org-manage') + +urlpatterns = [path('', include(router.urls))] diff --git a/offer_backend/apps/organizations/views.py b/offer_backend/apps/organizations/views.py new file mode 100644 index 0000000..0293d3f --- /dev/null +++ b/offer_backend/apps/organizations/views.py @@ -0,0 +1,19 @@ +from rest_framework import viewsets +from rest_framework.permissions import AllowAny +from .models import Organization +from .serializers import OrganizationSerializer, OrganizationTreeSerializer +from apps.accounts.permissions import IsSuperAdmin + + +class OrganizationPublicViewSet(viewsets.ReadOnlyModelViewSet): + """公开只读:门户展示用""" + queryset = Organization.objects.filter(is_active=True, parent__isnull=False) + serializer_class = OrganizationSerializer + permission_classes = [AllowAny] + + +class OrganizationManageViewSet(viewsets.ModelViewSet): + """超管:完整增删改查""" + queryset = Organization.objects.all() + serializer_class = OrganizationSerializer + permission_classes = [IsSuperAdmin]