diff --git a/offer_backend/apps/__init__.py b/offer_backend/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/accounts/__init__.py b/offer_backend/apps/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/accounts/apps.py b/offer_backend/apps/accounts/apps.py new file mode 100644 index 0000000..d45b5ba --- /dev/null +++ b/offer_backend/apps/accounts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.accounts' + verbose_name = '账户管理' diff --git a/offer_backend/apps/accounts/migrations/0001_initial.py b/offer_backend/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..8928f4d --- /dev/null +++ b/offer_backend/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.20 on 2026-03-24 09:09 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('organizations', '__first__'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('role', models.CharField(choices=[('superadmin', '超级管理员'), ('admin', '公司管理员'), ('seeker', '求职者')], default='seeker', max_length=20)), + ('phone', models.CharField(blank=True, max_length=20)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admins', to='organizations.organization')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/offer_backend/apps/accounts/migrations/__init__.py b/offer_backend/apps/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/accounts/models.py b/offer_backend/apps/accounts/models.py new file mode 100644 index 0000000..ae0bdb9 --- /dev/null +++ b/offer_backend/apps/accounts/models.py @@ -0,0 +1,34 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + ROLE_CHOICES = [ + ('superadmin', '超级管理员'), + ('admin', '公司管理员'), + ('seeker', '求职者'), + ] + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='seeker') + phone = models.CharField(max_length=20, blank=True) + organization = models.ForeignKey( + 'organizations.Organization', + null=True, blank=True, + on_delete=models.SET_NULL, + related_name='admins' + ) + + @property + def is_superadmin(self): + return self.role == 'superadmin' + + @property + def is_admin(self): + return self.role == 'admin' + + @property + def is_seeker(self): + return self.role == 'seeker' + + class Meta: + verbose_name = '用户' + verbose_name_plural = '用户' diff --git a/offer_backend/apps/accounts/permissions.py b/offer_backend/apps/accounts/permissions.py new file mode 100644 index 0000000..d725044 --- /dev/null +++ b/offer_backend/apps/accounts/permissions.py @@ -0,0 +1,23 @@ +from rest_framework.permissions import BasePermission + + +class IsSuperAdmin(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and request.user.is_superadmin + + +class IsCompanyAdmin(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and request.user.is_admin + + +class IsAdminOrSuperAdmin(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and ( + request.user.is_admin or request.user.is_superadmin + ) + + +class IsSeeker(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and request.user.is_seeker diff --git a/offer_backend/apps/accounts/serializers.py b/offer_backend/apps/accounts/serializers.py new file mode 100644 index 0000000..96620f0 --- /dev/null +++ b/offer_backend/apps/accounts/serializers.py @@ -0,0 +1,38 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, min_length=6) + + class Meta: + model = User + fields = ['username', 'email', 'phone', 'password'] + + def create(self, validated_data): + return User.objects.create_user(**validated_data, role='seeker') + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'email', 'phone', 'role', 'organization'] + read_only_fields = ['role'] + + +class AdminUserSerializer(serializers.ModelSerializer): + """超管用于创建/管理公司管理员账号""" + password = serializers.CharField(write_only=True, min_length=6) + + class Meta: + model = User + fields = ['id', 'username', 'email', 'phone', 'role', 'organization', 'password', 'is_active'] + + def create(self, validated_data): + password = validated_data.pop('password') + user = User(**validated_data) + user.set_password(password) + user.save() + return user diff --git a/offer_backend/apps/accounts/tests/__init__.py b/offer_backend/apps/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/accounts/tests/test_auth.py b/offer_backend/apps/accounts/tests/test_auth.py new file mode 100644 index 0000000..8692f73 --- /dev/null +++ b/offer_backend/apps/accounts/tests/test_auth.py @@ -0,0 +1,27 @@ +import pytest +from django.contrib.auth import get_user_model + +User = get_user_model() + + +@pytest.mark.django_db +class TestUserModel: + def test_create_seeker(self): + user = User.objects.create_user( + username='seeker1', password='pass123', role='seeker' + ) + assert user.role == 'seeker' + assert user.is_seeker is True + assert user.is_admin is False + + def test_create_admin(self): + user = User.objects.create_user( + username='admin1', password='pass123', role='admin' + ) + assert user.is_admin is True + + def test_create_superadmin(self): + user = User.objects.create_user( + username='super1', password='pass123', role='superadmin' + ) + assert user.is_superadmin is True diff --git a/offer_backend/apps/accounts/urls.py b/offer_backend/apps/accounts/urls.py new file mode 100644 index 0000000..28c4ef6 --- /dev/null +++ b/offer_backend/apps/accounts/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from .views import RegisterView, MeView, UserManageViewSet, UserDetailView + +urlpatterns = [ + path('register/', RegisterView.as_view()), + path('login/', TokenObtainPairView.as_view()), + path('token/refresh/', TokenRefreshView.as_view()), + path('me/', MeView.as_view()), + path('users/', UserManageViewSet.as_view()), + path('users//', UserDetailView.as_view()), +] diff --git a/offer_backend/apps/accounts/views.py b/offer_backend/apps/accounts/views.py new file mode 100644 index 0000000..86da59b --- /dev/null +++ b/offer_backend/apps/accounts/views.py @@ -0,0 +1,40 @@ +from rest_framework import generics +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from django.contrib.auth import get_user_model +from .serializers import RegisterSerializer, UserSerializer, AdminUserSerializer +from .permissions import IsSuperAdmin + +User = get_user_model() + + +class RegisterView(generics.CreateAPIView): + serializer_class = RegisterSerializer + permission_classes = [AllowAny] + + +class MeView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + return Response(UserSerializer(request.user).data) + + def patch(self, request): + serializer = UserSerializer(request.user, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + +class UserManageViewSet(generics.ListCreateAPIView): + """超管:管理所有用户""" + serializer_class = AdminUserSerializer + permission_classes = [IsSuperAdmin] + queryset = User.objects.all() + + +class UserDetailView(generics.RetrieveUpdateDestroyAPIView): + serializer_class = AdminUserSerializer + permission_classes = [IsSuperAdmin] + queryset = User.objects.all() diff --git a/offer_backend/apps/applications/__init__.py b/offer_backend/apps/applications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/applications/apps.py b/offer_backend/apps/applications/apps.py new file mode 100644 index 0000000..eb5cd54 --- /dev/null +++ b/offer_backend/apps/applications/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class ApplicationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.applications' diff --git a/offer_backend/apps/applications/urls.py b/offer_backend/apps/applications/urls.py new file mode 100644 index 0000000..e39cb2c --- /dev/null +++ b/offer_backend/apps/applications/urls.py @@ -0,0 +1,3 @@ +from django.urls import path + +urlpatterns = [] diff --git a/offer_backend/apps/jobs/__init__.py b/offer_backend/apps/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/jobs/apps.py b/offer_backend/apps/jobs/apps.py new file mode 100644 index 0000000..fc7583a --- /dev/null +++ b/offer_backend/apps/jobs/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class JobsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.jobs' diff --git a/offer_backend/apps/jobs/urls.py b/offer_backend/apps/jobs/urls.py new file mode 100644 index 0000000..e39cb2c --- /dev/null +++ b/offer_backend/apps/jobs/urls.py @@ -0,0 +1,3 @@ +from django.urls import path + +urlpatterns = [] diff --git a/offer_backend/apps/organizations/__init__.py b/offer_backend/apps/organizations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/organizations/apps.py b/offer_backend/apps/organizations/apps.py new file mode 100644 index 0000000..b85c06d --- /dev/null +++ b/offer_backend/apps/organizations/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class OrganizationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.organizations' diff --git a/offer_backend/apps/organizations/models.py b/offer_backend/apps/organizations/models.py new file mode 100644 index 0000000..f54646a --- /dev/null +++ b/offer_backend/apps/organizations/models.py @@ -0,0 +1,13 @@ +from django.db import models + + +class Organization(models.Model): + name = models.CharField(max_length=255) + + class Meta: + app_label = 'organizations' + verbose_name = '组织' + verbose_name_plural = '组织' + + def __str__(self): + return self.name diff --git a/offer_backend/apps/organizations/urls.py b/offer_backend/apps/organizations/urls.py new file mode 100644 index 0000000..e39cb2c --- /dev/null +++ b/offer_backend/apps/organizations/urls.py @@ -0,0 +1,3 @@ +from django.urls import path + +urlpatterns = [] diff --git a/offer_backend/apps/resumes/__init__.py b/offer_backend/apps/resumes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/offer_backend/apps/resumes/apps.py b/offer_backend/apps/resumes/apps.py new file mode 100644 index 0000000..910b4e2 --- /dev/null +++ b/offer_backend/apps/resumes/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class ResumesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.resumes' diff --git a/offer_backend/apps/resumes/urls.py b/offer_backend/apps/resumes/urls.py new file mode 100644 index 0000000..e39cb2c --- /dev/null +++ b/offer_backend/apps/resumes/urls.py @@ -0,0 +1,3 @@ +from django.urls import path + +urlpatterns = []