# 集团招聘网站实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 构建集团招聘网站,支持多公司多角色管理、职位发布、简历投递全流程。 **Architecture:** Django 4.2 + DRF 后端提供 RESTful API,Vue3 单页应用前端分三个区域(公开门户/求职者中心/管理后台),JWT 认证,PostgreSQL 存储业务数据。 **Tech Stack:** Django 4.2, DRF, simplejwt, PostgreSQL, Redis, Vue3, Vite, Pinia, Element Plus --- ## 文件结构总览 ``` offer/ ├── offer_backend/ │ ├── config/ │ │ ├── settings/ │ │ │ ├── base.py │ │ │ ├── development.py │ │ │ └── production.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── apps/ │ │ ├── accounts/ │ │ │ ├── models.py # 自定义 User 模型 + 角色 │ │ │ ├── serializers.py # 登录/注册/用户序列化 │ │ │ ├── views.py # JWT 登录、注册、用户管理 │ │ │ ├── permissions.py # IsSuperAdmin, IsAdmin, IsSeeker │ │ │ ├── urls.py │ │ │ └── tests/ │ │ │ └── test_auth.py │ │ ├── organizations/ │ │ │ ├── models.py # Organization(自关联树) │ │ │ ├── serializers.py │ │ │ ├── views.py │ │ │ ├── urls.py │ │ │ └── tests/ │ │ │ └── test_organizations.py │ │ ├── jobs/ │ │ │ ├── models.py # Job(职位) │ │ │ ├── serializers.py │ │ │ ├── views.py │ │ │ ├── filters.py # django-filter 搜索过滤 │ │ │ ├── urls.py │ │ │ └── tests/ │ │ │ └── test_jobs.py │ │ ├── resumes/ │ │ │ ├── models.py # Resume(JSONB 教育/工作经历) │ │ │ ├── serializers.py │ │ │ ├── views.py │ │ │ ├── urls.py │ │ │ └── tests/ │ │ │ └── test_resumes.py │ │ └── applications/ │ │ ├── models.py # Application(resume_snapshot) │ │ ├── serializers.py │ │ ├── views.py │ │ ├── emails.py # 邮件通知发送 │ │ ├── urls.py │ │ └── tests/ │ │ └── test_applications.py │ ├── manage.py │ └── requirements.txt └── offer_frontend/ ├── src/ │ ├── api/ │ │ ├── client.js # axios 实例 + JWT 拦截器 │ │ ├── auth.js │ │ ├── organizations.js │ │ ├── jobs.js │ │ ├── resumes.js │ │ └── applications.js │ ├── router/ │ │ └── index.js # 路由 + 守卫 │ ├── stores/ │ │ ├── auth.js # 用户状态 + token │ │ └── job.js # 职位搜索状态 │ ├── layouts/ │ │ ├── PortalLayout.vue │ │ ├── SeekerLayout.vue │ │ └── AdminLayout.vue │ ├── views/ │ │ ├── portal/ │ │ │ ├── HomeView.vue │ │ │ ├── JobListView.vue │ │ │ ├── JobDetailView.vue │ │ │ ├── CompanyListView.vue │ │ │ └── CompanyDetailView.vue │ │ ├── auth/ │ │ │ ├── LoginView.vue │ │ │ └── RegisterView.vue │ │ ├── seeker/ │ │ │ ├── ResumeView.vue │ │ │ ├── ApplicationsView.vue │ │ │ └── ProfileView.vue │ │ └── admin/ │ │ ├── JobManageView.vue │ │ ├── ApplicationManageView.vue │ │ ├── OrganizationManageView.vue │ │ └── UserManageView.vue │ ├── components/ │ │ ├── JobCard.vue │ │ └── CompanyCard.vue │ ├── App.vue │ └── main.js ├── index.html ├── vite.config.js └── package.json ``` --- ## Task 1: 初始化 Django 后端项目 **Files:** - Create: `offer_backend/requirements.txt` - Create: `offer_backend/manage.py` - Create: `offer_backend/config/settings/base.py` - Create: `offer_backend/config/settings/development.py` - Create: `offer_backend/config/urls.py` - [ ] **Step 1: 创建目录结构** ```bash cd C:/code/offer mkdir -p offer_backend/config/settings mkdir -p offer_backend/apps touch offer_backend/config/__init__.py touch offer_backend/config/settings/__init__.py ``` - [ ] **Step 2: 写 requirements.txt** ``` # offer_backend/requirements.txt Django==4.2.20 djangorestframework==3.16.0 djangorestframework-simplejwt==5.3.1 django-cors-headers==4.3.1 django-filter==23.5 psycopg2-binary==2.9.9 redis==5.0.1 django-redis==5.4.0 Pillow==10.3.0 python-decouple==3.8 ``` - [ ] **Step 3: 初始化 Django 项目** ```bash cd offer_backend pip install -r requirements.txt django-admin startproject config . ``` - [ ] **Step 4: 写 config/settings/base.py** ```python # offer_backend/config/settings/base.py from pathlib import Path from decouple import config BASE_DIR = Path(__file__).resolve().parent.parent.parent SECRET_KEY = config('SECRET_KEY') INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third party 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', 'django_filters', # Local 'apps.accounts', 'apps.organizations', 'apps.jobs', 'apps.resumes', 'apps.applications', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] AUTH_USER_MODEL = 'accounts.User' REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, } from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=2), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), } STATIC_URL = '/static/' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_TZ = True ``` - [ ] **Step 5: 写 config/settings/development.py** ```python # offer_backend/config/settings/development.py from .base import * DEBUG = True ALLOWED_HOSTS = ['*'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': config('DB_NAME', default='offer_db'), 'USER': config('DB_USER', default='postgres'), 'PASSWORD': config('DB_PASSWORD', default='postgres'), 'HOST': config('DB_HOST', default='localhost'), 'PORT': config('DB_PORT', default='5432'), } } CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/1'), } } CORS_ALLOW_ALL_ORIGINS = True EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ``` - [ ] **Step 6: 写 config/urls.py** ```python # offer_backend/config/urls.py from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('api/auth/', include('apps.accounts.urls')), path('api/organizations/', include('apps.organizations.urls')), path('api/jobs/', include('apps.jobs.urls')), path('api/resumes/', include('apps.resumes.urls')), path('api/applications/', include('apps.applications.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ``` - [ ] **Step 7: 创建 .env 文件** ```bash # offer_backend/.env SECRET_KEY=your-secret-key-change-in-production DB_NAME=offer_db DB_USER=postgres DB_PASSWORD=postgres DB_HOST=localhost DB_PORT=5432 REDIS_URL=redis://127.0.0.1:6379/1 ``` - [ ] **Step 8: 验证启动** ```bash cd offer_backend DJANGO_SETTINGS_MODULE=config.settings.development python manage.py check ``` Expected: `System check identified no issues` - [ ] **Step 9: Commit** ```bash git add offer_backend/ git commit -m "feat: initialize Django backend project structure" ``` --- ## Task 2: 配置 pytest 测试基础设施(必须在 Task 3 之前完成) **Files:** - Create: `offer_backend/conftest.py` - Create: `offer_backend/pytest.ini` - [ ] **Step 1: 添加 pytest 依赖** 在 `offer_backend/requirements.txt` 末尾添加: ``` pytest==8.1.1 pytest-django==4.8.0 ``` ```bash cd offer_backend pip install pytest pytest-django ``` - [ ] **Step 2: 写 pytest.ini** ```ini # offer_backend/pytest.ini [pytest] DJANGO_SETTINGS_MODULE = config.settings.development python_files = tests/test_*.py python_classes = Test* python_functions = test_* ``` - [ ] **Step 3: 写 conftest.py** ```python # offer_backend/conftest.py import pytest from django.test import Client @pytest.fixture def client(): return Client() ``` - [ ] **Step 4: Commit** ```bash git add offer_backend/conftest.py offer_backend/pytest.ini offer_backend/requirements.txt git commit -m "chore: add pytest configuration for Django tests" ``` --- ## Task 3: accounts app — 自定义用户模型 **Files:** - Create: `offer_backend/apps/accounts/models.py` - Create: `offer_backend/apps/accounts/tests/test_auth.py` - Create: `offer_backend/apps/accounts/serializers.py` - Create: `offer_backend/apps/accounts/views.py` - Create: `offer_backend/apps/accounts/permissions.py` - Create: `offer_backend/apps/accounts/urls.py` - [ ] **Step 1: 创建 accounts app** ```bash cd offer_backend python manage.py startapp accounts apps/accounts ``` - [ ] **Step 2: 写失败测试** ```python # offer_backend/apps/accounts/tests/test_auth.py 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 ``` - [ ] **Step 3: 运行确认失败** ```bash cd offer_backend DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/accounts/tests/test_auth.py -v ``` Expected: `ImportError` 或 `ModuleNotFoundError` - [ ] **Step 4: 写 models.py** ```python # offer_backend/apps/accounts/models.py 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 = '用户' ``` - [ ] **Step 5: 写 permissions.py** ```python # offer_backend/apps/accounts/permissions.py 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 ``` - [ ] **Step 6: 写 serializers.py** ```python # offer_backend/apps/accounts/serializers.py 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 ``` - [ ] **Step 7: 写 views.py** ```python # offer_backend/apps/accounts/views.py from rest_framework import generics, status 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() ``` - [ ] **Step 8: 写 urls.py** ```python # offer_backend/apps/accounts/urls.py 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()), ] ``` - [ ] **Step 9: 运行迁移并测试** ```bash cd offer_backend DJANGO_SETTINGS_MODULE=config.settings.development python manage.py makemigrations accounts DJANGO_SETTINGS_MODULE=config.settings.development python manage.py migrate DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/accounts/tests/test_auth.py -v ``` Expected: 3 tests PASSED - [ ] **Step 10: Commit** ```bash git add offer_backend/apps/accounts/ git commit -m "feat: add custom User model with role-based permissions" ``` --- ## Task 4: organizations app **Files:** - Create: `offer_backend/apps/organizations/models.py` - Create: `offer_backend/apps/organizations/serializers.py` - Create: `offer_backend/apps/organizations/views.py` - Create: `offer_backend/apps/organizations/urls.py` - Create: `offer_backend/apps/organizations/tests/test_organizations.py` - [ ] **Step 1: 创建 app** ```bash cd offer_backend python manage.py startapp organizations apps/organizations ``` - [ ] **Step 2: 写失败测试** ```python # offer_backend/apps/organizations/tests/test_organizations.py 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 ``` - [ ] **Step 3: 写 models.py** ```python # offer_backend/apps/organizations/models.py from django.db import models class Organization(models.Model): 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: verbose_name = '组织架构' verbose_name_plural = '组织架构' def __str__(self): return self.name ``` - [ ] **Step 4: 写 serializers.py** ```python # offer_backend/apps/organizations/serializers.py 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 ``` - [ ] **Step 5: 写 views.py 和 urls.py** ```python # offer_backend/apps/organizations/views.py 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] ``` ```python # offer_backend/apps/organizations/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import OrganizationPublicViewSet, OrganizationManageViewSet router = DefaultRouter() router.register('public', OrganizationPublicViewSet, basename='org-public') router.register('manage', OrganizationManageViewSet, basename='org-manage') urlpatterns = [path('', include(router.urls))] ``` - [ ] **Step 6: 迁移并测试** ```bash DJANGO_SETTINGS_MODULE=config.settings.development python manage.py makemigrations organizations DJANGO_SETTINGS_MODULE=config.settings.development python manage.py migrate DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/organizations/tests/ -v ``` Expected: 3 tests PASSED - [ ] **Step 7: Commit** ```bash git add offer_backend/apps/organizations/ git commit -m "feat: add Organization model with tree structure" ``` --- ## Task 5: jobs app **Files:** - Create: `offer_backend/apps/jobs/models.py` - Create: `offer_backend/apps/jobs/filters.py` - Create: `offer_backend/apps/jobs/serializers.py` - Create: `offer_backend/apps/jobs/views.py` - Create: `offer_backend/apps/jobs/urls.py` - Create: `offer_backend/apps/jobs/tests/test_jobs.py` - [ ] **Step 1: 创建 app** ```bash python manage.py startapp jobs apps/jobs ``` - [ ] **Step 2: 写失败测试** ```python # offer_backend/apps/jobs/tests/test_jobs.py import pytest from django.contrib.auth import get_user_model from apps.organizations.models import Organization from apps.jobs.models import Job User = get_user_model() @pytest.fixture def org(): return Organization.objects.create(name='测试公司', email='test@test.com') @pytest.fixture def admin_user(org): return User.objects.create_user( username='admin1', password='pass123', role='admin', organization=org ) @pytest.mark.django_db class TestJobModel: def test_create_job(self, org): job = Job.objects.create( organization=org, title='Python工程师', category='技术', location='北京', salary='20k-30k', description='职位描述', status='published' ) assert job.status == 'published' assert str(job) == 'Python工程师' @pytest.mark.django_db class TestJobAPI: def test_public_can_list_published_jobs(self, client, org): Job.objects.create( organization=org, title='已发布职位', status='published', category='技术', location='北京', salary='10k' ) Job.objects.create( organization=org, title='草稿职位', status='draft', category='技术', location='北京', salary='10k' ) response = client.get('/api/jobs/public/') assert response.status_code == 200 assert response.data['count'] == 1 ``` - [ ] **Step 3: 写 models.py** ```python # offer_backend/apps/jobs/models.py from django.db import models class Job(models.Model): STATUS_CHOICES = [ ('draft', '草稿'), ('published', '已发布'), ('closed', '已关闭'), ] organization = models.ForeignKey( 'organizations.Organization', on_delete=models.CASCADE, related_name='jobs' ) title = models.CharField(max_length=100, verbose_name='职位名称') category = models.CharField(max_length=50, verbose_name='职位类别') location = models.CharField(max_length=100, verbose_name='工作地点') salary = models.CharField(max_length=50, verbose_name='薪资范围') description = models.TextField(verbose_name='职位描述') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] verbose_name = '职位' def __str__(self): return self.title ``` - [ ] **Step 4: 写 filters.py** ```python # offer_backend/apps/jobs/filters.py import django_filters from .models import Job class JobFilter(django_filters.FilterSet): title = django_filters.CharFilter(lookup_expr='icontains') category = django_filters.CharFilter(lookup_expr='exact') location = django_filters.CharFilter(lookup_expr='icontains') organization = django_filters.NumberFilter(field_name='organization__id') class Meta: model = Job fields = ['title', 'category', 'location', 'organization'] ``` - [ ] **Step 5: 写 serializers.py 和 views.py** ```python # offer_backend/apps/jobs/serializers.py from rest_framework import serializers from .models import Job from apps.organizations.serializers import OrganizationSerializer from apps.organizations.models import Organization class JobListSerializer(serializers.ModelSerializer): organization_name = serializers.CharField(source='organization.name', read_only=True) class Meta: model = Job fields = ['id', 'title', 'category', 'location', 'salary', 'organization', 'organization_name', 'status', 'created_at'] class JobDetailSerializer(serializers.ModelSerializer): organization = OrganizationSerializer(read_only=True) organization_id = serializers.PrimaryKeyRelatedField( source='organization', queryset=Organization.objects.all(), write_only=True ) class Meta: model = Job fields = ['id', 'title', 'category', 'location', 'salary', 'description', 'organization', 'organization_id', 'status', 'created_at'] ``` ```python # offer_backend/apps/jobs/views.py from rest_framework import viewsets, permissions from rest_framework.decorators import action from rest_framework.response import Response from .models import Job from .serializers import JobListSerializer, JobDetailSerializer from .filters import JobFilter from apps.accounts.permissions import IsAdminOrSuperAdmin class JobPublicViewSet(viewsets.ReadOnlyModelViewSet): """公开只读,仅返回已发布职位""" queryset = Job.objects.filter(status='published').select_related('organization') filterset_class = JobFilter search_fields = ['title', 'description', 'location'] permission_classes = [permissions.AllowAny] def get_serializer_class(self): if self.action == 'retrieve': return JobDetailSerializer return JobListSerializer class JobManageViewSet(viewsets.ModelViewSet): """管理端:公司管理员管理本公司职位""" permission_classes = [IsAdminOrSuperAdmin] def get_serializer_class(self): if self.action in ['retrieve', 'create', 'update', 'partial_update']: return JobDetailSerializer return JobListSerializer def get_queryset(self): user = self.request.user if user.is_superadmin: return Job.objects.all().select_related('organization') return Job.objects.filter(organization=user.organization).select_related('organization') def perform_create(self, serializer): if self.request.user.is_admin: serializer.save(organization=self.request.user.organization) else: serializer.save() ``` - [ ] **Step 6: 写 urls.py** ```python # offer_backend/apps/jobs/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import JobPublicViewSet, JobManageViewSet router = DefaultRouter() router.register('public', JobPublicViewSet, basename='job-public') router.register('manage', JobManageViewSet, basename='job-manage') urlpatterns = [path('', include(router.urls))] ``` - [ ] **Step 7: 迁移并测试** ```bash DJANGO_SETTINGS_MODULE=config.settings.development python manage.py makemigrations jobs DJANGO_SETTINGS_MODULE=config.settings.development python manage.py migrate DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/jobs/tests/ -v ``` Expected: 2 tests PASSED - [ ] **Step 8: Commit** ```bash git add offer_backend/apps/jobs/ git commit -m "feat: add Job model with search/filter and role-based access" ``` --- ## Task 6: resumes app **Files:** - Create: `offer_backend/apps/resumes/models.py` - Create: `offer_backend/apps/resumes/serializers.py` - Create: `offer_backend/apps/resumes/views.py` - Create: `offer_backend/apps/resumes/urls.py` - Create: `offer_backend/apps/resumes/tests/test_resumes.py` - [ ] **Step 1: 创建 app** ```bash python manage.py startapp resumes apps/resumes ``` - [ ] **Step 2: 写失败测试** ```python # offer_backend/apps/resumes/tests/test_resumes.py import pytest from django.contrib.auth import get_user_model from apps.resumes.models import Resume User = get_user_model() @pytest.fixture def seeker(): return User.objects.create_user(username='seeker1', password='pass', role='seeker') @pytest.mark.django_db class TestResumeModel: def test_create_resume(self, seeker): resume = Resume.objects.create( user=seeker, name='张三', gender='male', education=[{'school': '北京大学', 'degree': '本科', 'major': '计算机'}], experience=[{'company': 'ABC公司', 'position': '工程师', 'duration': '2年'}], ) assert resume.name == '张三' assert len(resume.education) == 1 assert len(resume.experience) == 1 def test_seeker_has_one_resume(self, seeker): Resume.objects.create(user=seeker, name='张三') assert Resume.objects.filter(user=seeker).count() == 1 ``` - [ ] **Step 3: 写 models.py** ```python # offer_backend/apps/resumes/models.py from django.db import models from django.conf import settings class Resume(models.Model): GENDER_CHOICES = [('male', '男'), ('female', '女'), ('other', '其他')] user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='resume' ) name = models.CharField(max_length=50, verbose_name='姓名') gender = models.CharField(max_length=10, choices=GENDER_CHOICES, blank=True) birthday = models.DateField(null=True, blank=True) education = models.JSONField(default=list, verbose_name='教育经历') experience = models.JSONField(default=list, verbose_name='工作经历') attachment = models.FileField(upload_to='resumes/', null=True, blank=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = '简历' def to_snapshot(self): """序列化为投递快照,与主表解耦""" return { 'name': self.name, 'gender': self.gender, 'birthday': str(self.birthday) if self.birthday else None, 'education': self.education, 'experience': self.experience, 'attachment_url': self.attachment.url if self.attachment else None, } ``` - [ ] **Step 4: 写 serializers.py、views.py、urls.py** ```python # offer_backend/apps/resumes/serializers.py from rest_framework import serializers from .models import Resume class ResumeSerializer(serializers.ModelSerializer): class Meta: model = Resume fields = ['id', 'name', 'gender', 'birthday', 'education', 'experience', 'attachment', 'updated_at'] read_only_fields = ['updated_at'] ``` ```python # offer_backend/apps/resumes/views.py from rest_framework import generics from rest_framework.permissions import IsAuthenticated from .models import Resume from .serializers import ResumeSerializer from apps.accounts.permissions import IsSeeker class MyResumeView(generics.RetrieveUpdateAPIView): """求职者获取/更新自己的简历(不存在则自动创建)""" serializer_class = ResumeSerializer permission_classes = [IsSeeker] def get_object(self): resume, _ = Resume.objects.get_or_create( user=self.request.user, defaults={'name': self.request.user.username} ) return resume ``` ```python # offer_backend/apps/resumes/urls.py from django.urls import path from .views import MyResumeView urlpatterns = [ path('me/', MyResumeView.as_view()), ] ``` - [ ] **Step 5: 迁移并测试** ```bash DJANGO_SETTINGS_MODULE=config.settings.development python manage.py makemigrations resumes DJANGO_SETTINGS_MODULE=config.settings.development python manage.py migrate DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/resumes/tests/ -v ``` Expected: 2 tests PASSED - [ ] **Step 6: Commit** ```bash git add offer_backend/apps/resumes/ git commit -m "feat: add Resume model with JSONB fields and snapshot support" ``` --- ## Task 7: applications app **Files:** - Create: `offer_backend/apps/applications/models.py` - Create: `offer_backend/apps/applications/emails.py` - Create: `offer_backend/apps/applications/serializers.py` - Create: `offer_backend/apps/applications/views.py` - Create: `offer_backend/apps/applications/urls.py` - Create: `offer_backend/apps/applications/tests/test_applications.py` - [ ] **Step 1: 创建 app** ```bash python manage.py startapp applications apps/applications ``` - [ ] **Step 2: 写失败测试** ```python # offer_backend/apps/applications/tests/test_applications.py import pytest from django.contrib.auth import get_user_model from apps.organizations.models import Organization from apps.jobs.models import Job from apps.resumes.models import Resume from apps.applications.models import Application User = get_user_model() @pytest.fixture def setup(db): org = Organization.objects.create(name='公司', email='co@test.com') seeker = User.objects.create_user(username='seeker1', password='pass', role='seeker') job = Job.objects.create( organization=org, title='测试职位', category='技术', location='北京', salary='15k', status='published' ) resume = Resume.objects.create(user=seeker, name='张三') return {'org': org, 'seeker': seeker, 'job': job, 'resume': resume} @pytest.mark.django_db class TestApplicationModel: def test_create_application(self, setup): app = Application.objects.create( job=setup['job'], applicant=setup['seeker'], resume_snapshot=setup['resume'].to_snapshot(), ) assert app.status == 'pending' assert app.resume_snapshot['name'] == '张三' def test_cannot_apply_twice(self, setup): Application.objects.create( job=setup['job'], applicant=setup['seeker'], resume_snapshot=setup['resume'].to_snapshot(), ) with pytest.raises(Exception): Application.objects.create( job=setup['job'], applicant=setup['seeker'], resume_snapshot={}, ) ``` - [ ] **Step 3: 写 models.py** ```python # offer_backend/apps/applications/models.py from django.db import models from django.conf import settings class Application(models.Model): STATUS_CHOICES = [ ('pending', '待查看'), ('viewed', '已查看'), ('interviewing', '面试中'), ('hired', '已录用'), ('rejected', '已拒绝'), ] job = models.ForeignKey('jobs.Job', on_delete=models.CASCADE, related_name='applications') applicant = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='applications' ) resume_snapshot = models.JSONField(verbose_name='简历快照') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') note = models.TextField(blank=True, verbose_name='HR备注') applied_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = [['job', 'applicant']] ordering = ['-applied_at'] verbose_name = '投递记录' ``` - [ ] **Step 4: 写 emails.py** ```python # offer_backend/apps/applications/emails.py from django.core.mail import send_mail from django.conf import settings STATUS_LABELS = { 'viewed': '已查看', 'interviewing': '面试邀请', 'hired': '恭喜录用', 'rejected': '很遗憾未通过', } def notify_status_change(application): label = STATUS_LABELS.get(application.status) if not label or not application.applicant.email: return send_mail( subject=f'【招聘通知】您投递的"{application.job.title}"状态更新:{label}', message=f'您好 {application.applicant.username},\n\n您投递的职位"{application.job.title}"状态已更新为:{label}。\n\n请登录平台查看详情。', from_email=settings.DEFAULT_FROM_EMAIL if hasattr(settings, 'DEFAULT_FROM_EMAIL') else 'noreply@offer.com', recipient_list=[application.applicant.email], fail_silently=True, ) ``` - [ ] **Step 5: 写 serializers.py、views.py、urls.py** ```python # offer_backend/apps/applications/serializers.py from rest_framework import serializers from .models import Application class ApplicationCreateSerializer(serializers.ModelSerializer): class Meta: model = Application fields = ['job'] def create(self, validated_data): request = self.context['request'] try: resume = request.user.resume except Exception: raise serializers.ValidationError( {'detail': '请先完善简历后再投递'} ) if not resume.name: raise serializers.ValidationError( {'detail': '请先完善简历后再投递'} ) return Application.objects.create( job=validated_data['job'], applicant=request.user, resume_snapshot=resume.to_snapshot(), ) class ApplicationSerializer(serializers.ModelSerializer): job_title = serializers.CharField(source='job.title', read_only=True) company_name = serializers.CharField(source='job.organization.name', read_only=True) class Meta: model = Application fields = ['id', 'job', 'job_title', 'company_name', 'resume_snapshot', 'status', 'note', 'applied_at'] read_only_fields = ['resume_snapshot', 'applied_at'] class ApplicationStatusSerializer(serializers.ModelSerializer): """HR 更新状态""" class Meta: model = Application fields = ['status', 'note'] ``` ```python # offer_backend/apps/applications/views.py from rest_framework import generics, viewsets from rest_framework.permissions import IsAuthenticated from .models import Application from .serializers import ApplicationCreateSerializer, ApplicationSerializer, ApplicationStatusSerializer from .emails import notify_status_change from apps.accounts.permissions import IsSeeker, IsAdminOrSuperAdmin class ApplyView(generics.CreateAPIView): serializer_class = ApplicationCreateSerializer permission_classes = [IsSeeker] class MyApplicationsView(generics.ListAPIView): serializer_class = ApplicationSerializer permission_classes = [IsSeeker] def get_queryset(self): return Application.objects.filter(applicant=self.request.user).select_related('job__organization') class ApplicationManageViewSet(viewsets.ReadOnlyModelViewSet): """HR 查看本公司投递""" serializer_class = ApplicationSerializer permission_classes = [IsAdminOrSuperAdmin] def get_queryset(self): user = self.request.user if user.is_superadmin: return Application.objects.all().select_related('job__organization', 'applicant') return Application.objects.filter( job__organization=user.organization ).select_related('job__organization', 'applicant') class ApplicationStatusUpdateView(generics.UpdateAPIView): serializer_class = ApplicationStatusSerializer permission_classes = [IsAdminOrSuperAdmin] queryset = Application.objects.all() def perform_update(self, serializer): instance = serializer.save() notify_status_change(instance) ``` ```python # offer_backend/apps/applications/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import ApplyView, MyApplicationsView, ApplicationManageViewSet, ApplicationStatusUpdateView router = DefaultRouter() router.register('manage', ApplicationManageViewSet, basename='application-manage') urlpatterns = [ path('apply/', ApplyView.as_view()), path('mine/', MyApplicationsView.as_view()), path('manage//status/', ApplicationStatusUpdateView.as_view()), path('', include(router.urls)), ] ``` - [ ] **Step 6: 迁移并测试** ```bash DJANGO_SETTINGS_MODULE=config.settings.development python manage.py makemigrations applications DJANGO_SETTINGS_MODULE=config.settings.development python manage.py migrate DJANGO_SETTINGS_MODULE=config.settings.development pytest apps/applications/tests/ -v ``` Expected: 2 tests PASSED - [ ] **Step 7: 全量后端测试** ```bash DJANGO_SETTINGS_MODULE=config.settings.development pytest --tb=short ``` Expected: All tests PASSED - [ ] **Step 8: Commit** ```bash git add offer_backend/apps/applications/ git commit -m "feat: add Application model with resume snapshot and email notification" ``` --- ## Task 8: 初始化 Vue3 前端项目 **Files:** - Create: `offer_frontend/package.json` - Create: `offer_frontend/vite.config.js` - Create: `offer_frontend/src/main.js` - Create: `offer_frontend/src/App.vue` - Create: `offer_frontend/src/api/client.js` - [ ] **Step 1: 创建 Vite + Vue3 项目** ```bash cd C:/code/offer npm create vite@latest offer_frontend -- --template vue cd offer_frontend npm install ``` - [ ] **Step 2: 安装依赖** ```bash npm install vue-router@4 pinia axios element-plus @element-plus/icons-vue npm install -D unplugin-vue-components unplugin-auto-import ``` - [ ] **Step 3: 配置 vite.config.js** ```js // offer_frontend/vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import path from 'path' export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }), ], resolve: { alias: { '@': path.resolve(__dirname, './src') } }, server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } }) ``` - [ ] **Step 4: 写 src/api/client.js(axios + JWT 拦截器)** ```js // offer_frontend/src/api/client.js import axios from 'axios' const client = axios.create({ baseURL: '/api' }) client.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) client.interceptors.response.use( res => res, async err => { const original = err.config if (err.response?.status === 401 && !original._retry) { original._retry = true try { const refresh = localStorage.getItem('refresh_token') const { data } = await axios.post('/api/auth/token/refresh/', { refresh }) localStorage.setItem('access_token', data.access) original.headers.Authorization = `Bearer ${data.access}` return client(original) } catch { localStorage.clear() window.location.href = '/login' } } return Promise.reject(err) } ) export default client ``` - [ ] **Step 5: 写各模块 API 文件** ```js // offer_frontend/src/api/auth.js import client from './client' import axios from 'axios' export const login = (data) => axios.post('/api/auth/login/', data) export const register = (data) => client.post('/auth/register/', data) export const getMe = () => client.get('/auth/me/') export const updateMe = (data) => client.patch('/auth/me/', data) // offer_frontend/src/api/jobs.js import client from './client' export const getJobs = (params) => client.get('/jobs/public/', { params }) export const getJob = (id) => client.get(`/jobs/public/${id}/`) export const manageJobs = (params) => client.get('/jobs/manage/', { params }) export const createJob = (data) => client.post('/jobs/manage/', data) export const updateJob = (id, data) => client.patch(`/jobs/manage/${id}/`, data) export const deleteJob = (id) => client.delete(`/jobs/manage/${id}/`) // offer_frontend/src/api/organizations.js import client from './client' export const getOrganizations = () => client.get('/organizations/public/') export const getOrganization = (id) => client.get(`/organizations/public/${id}/`) export const manageOrganizations = () => client.get('/organizations/manage/') export const createOrganization = (data) => client.post('/organizations/manage/', data) export const updateOrganization = (id, data) => client.patch(`/organizations/manage/${id}/`, data) export const deleteOrganization = (id) => client.delete(`/organizations/manage/${id}/`) // offer_frontend/src/api/resumes.js import client from './client' export const getMyResume = () => client.get('/resumes/me/') export const updateMyResume = (data) => client.patch('/resumes/me/', data) // offer_frontend/src/api/applications.js import client from './client' export const applyJob = (jobId) => client.post('/applications/apply/', { job: jobId }) export const getMyApplications = () => client.get('/applications/mine/') export const getManageApplications = (params) => client.get('/applications/manage/', { params }) export const updateApplicationStatus = (id, data) => client.patch(`/applications/manage/${id}/status/`, data) ``` - [ ] **Step 6: 写 Pinia stores** ```js // offer_frontend/src/stores/auth.js import { defineStore } from 'pinia' import { login as loginApi, getMe } from '@/api/auth' export const useAuthStore = defineStore('auth', { state: () => ({ user: null, loading: false, }), getters: { isLoggedIn: s => !!s.user, isSuperAdmin: s => s.user?.role === 'superadmin', isAdmin: s => s.user?.role === 'admin', isSeeker: s => s.user?.role === 'seeker', }, actions: { async login(username, password) { const { data } = await loginApi({ username, password }) localStorage.setItem('access_token', data.access) localStorage.setItem('refresh_token', data.refresh) await this.fetchMe() }, async fetchMe() { const { data } = await getMe() this.user = data }, logout() { localStorage.clear() this.user = null }, }, }) ``` ```js // offer_frontend/src/stores/job.js import { defineStore } from 'pinia' import { getJobs } from '@/api/jobs' export const useJobStore = defineStore('job', { state: () => ({ jobs: [], total: 0, loading: false }), actions: { async fetchJobs(params) { this.loading = true const { data } = await getJobs(params) this.jobs = data.results this.total = data.count this.loading = false }, }, }) ``` - [ ] **Step 7: 写 main.js** ```js // offer_frontend/src/main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount('#app') ``` - [ ] **Step 8: 验证启动** ```bash cd offer_frontend npm run dev ``` Expected: Vite 启动成功,浏览器打开无报错 - [ ] **Step 9: Commit** ```bash git add offer_frontend/ git commit -m "feat: initialize Vue3 frontend with router, pinia, element-plus" ``` --- ## Task 9: 路由与布局 **Files:** - Create: `offer_frontend/src/router/index.js` - Create: `offer_frontend/src/layouts/PortalLayout.vue` - Create: `offer_frontend/src/layouts/SeekerLayout.vue` - Create: `offer_frontend/src/layouts/AdminLayout.vue` - Create: `offer_frontend/src/App.vue` - [ ] **Step 1: 写路由配置(含守卫)** ```js // offer_frontend/src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import { useAuthStore } from '@/stores/auth' const routes = [ // 公开门户 { path: '/', component: () => import('@/layouts/PortalLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/portal/HomeView.vue') }, { path: 'jobs', name: 'JobList', component: () => import('@/views/portal/JobListView.vue') }, { path: 'jobs/:id', name: 'JobDetail', component: () => import('@/views/portal/JobDetailView.vue') }, { path: 'companies', name: 'CompanyList', component: () => import('@/views/portal/CompanyListView.vue') }, { path: 'companies/:id', name: 'CompanyDetail', component: () => import('@/views/portal/CompanyDetailView.vue') }, ] }, { path: '/login', name: 'Login', component: () => import('@/views/auth/LoginView.vue') }, { path: '/register', name: 'Register', component: () => import('@/views/auth/RegisterView.vue') }, // 求职者中心 { path: '/seeker', component: () => import('@/layouts/SeekerLayout.vue'), meta: { requireAuth: true, role: 'seeker' }, children: [ { path: 'resume', name: 'SeekerResume', component: () => import('@/views/seeker/ResumeView.vue') }, { path: 'applications', name: 'SeekerApplications', component: () => import('@/views/seeker/ApplicationsView.vue') }, { path: 'profile', name: 'SeekerProfile', component: () => import('@/views/seeker/ProfileView.vue') }, ] }, // 管理后台 { path: '/admin', component: () => import('@/layouts/AdminLayout.vue'), meta: { requireAuth: true, role: 'admin' }, children: [ { path: 'jobs', name: 'AdminJobs', component: () => import('@/views/admin/JobManageView.vue') }, { path: 'applications', name: 'AdminApplications', component: () => import('@/views/admin/ApplicationManageView.vue') }, { path: 'organizations', name: 'AdminOrganizations', component: () => import('@/views/admin/OrganizationManageView.vue'), meta: { role: 'superadmin' } }, { path: 'users', name: 'AdminUsers', component: () => import('@/views/admin/UserManageView.vue'), meta: { role: 'superadmin' } }, ] }, { path: '/:pathMatch(.*)*', redirect: '/' }, ] const router = createRouter({ history: createWebHistory(), routes, }) router.beforeEach(async (to, from, next) => { const auth = useAuthStore() if (to.meta.requireAuth) { if (!localStorage.getItem('access_token')) { return next({ name: 'Login', query: { redirect: to.fullPath } }) } if (!auth.user) { try { await auth.fetchMe() } catch { return next({ name: 'Login' }) } } const requiredRole = to.meta.role if (requiredRole === 'seeker' && !auth.isSeeker) return next({ path: '/' }) if (requiredRole === 'admin' && !(auth.isAdmin || auth.isSuperAdmin)) return next({ path: '/' }) if (requiredRole === 'superadmin' && !auth.isSuperAdmin) return next({ path: '/admin/jobs' }) } next() }) export default router ``` - [ ] **Step 2: 写 PortalLayout.vue** ```vue ``` - [ ] **Step 3: 写 SeekerLayout.vue 和 AdminLayout.vue** ```vue ``` ```vue ``` - [ ] **Step 4: Commit** ```bash git add offer_frontend/src/router/ offer_frontend/src/layouts/ git commit -m "feat: add router with guards and layout components" ``` --- ## Task 10: 公开门户页面 **Files:** - Create: `offer_frontend/src/views/portal/HomeView.vue` - Create: `offer_frontend/src/views/portal/JobListView.vue` - Create: `offer_frontend/src/views/portal/JobDetailView.vue` - Create: `offer_frontend/src/views/portal/CompanyListView.vue` - Create: `offer_frontend/src/views/portal/CompanyDetailView.vue` - Create: `offer_frontend/src/components/JobCard.vue` - Create: `offer_frontend/src/components/CompanyCard.vue` - [ ] **Step 1: 写 JobCard.vue** ```vue ``` - [ ] **Step 2: 写 JobListView.vue(搜索+列表)** ```vue ``` - [ ] **Step 3: 写 JobDetailView.vue(职位详情+投递)** ```vue ``` - [ ] **Step 4: 写 HomeView.vue、CompanyListView.vue、CompanyDetailView.vue(简版)** ```vue ``` ```vue ``` ```vue ``` - [ ] **Step 5: Commit** ```bash git add offer_frontend/src/views/portal/ offer_frontend/src/components/ git commit -m "feat: add public portal pages (home, job list, job detail, companies)" ``` --- ## Task 11: 登录/注册页 **Files:** - Create: `offer_frontend/src/views/auth/LoginView.vue` - Create: `offer_frontend/src/views/auth/RegisterView.vue` - [ ] **Step 1: 写 LoginView.vue** ```vue ``` - [ ] **Step 2: 写 RegisterView.vue** ```vue ``` - [ ] **Step 3: Commit** ```bash git add offer_frontend/src/views/auth/ git commit -m "feat: add login and register views with role-based redirect" ``` --- ## Task 12: 求职者中心页面 **Files:** - Create: `offer_frontend/src/views/seeker/ResumeView.vue` - Create: `offer_frontend/src/views/seeker/ApplicationsView.vue` - Create: `offer_frontend/src/views/seeker/ProfileView.vue` - [ ] **Step 1: 写 ResumeView.vue(在线简历编辑)** ```vue ``` - [ ] **Step 2: 写 ApplicationsView.vue** ```vue ``` - [ ] **Step 3: 写 ProfileView.vue** ```vue ``` - [ ] **Step 4: Commit** ```bash git add offer_frontend/src/views/seeker/ git commit -m "feat: add seeker center (resume editor, applications, profile)" ``` --- ## Task 13: 管理后台页面 **Files:** - Create: `offer_frontend/src/views/admin/JobManageView.vue` - Create: `offer_frontend/src/views/admin/ApplicationManageView.vue` - Create: `offer_frontend/src/views/admin/OrganizationManageView.vue` - Create: `offer_frontend/src/views/admin/UserManageView.vue` - [ ] **Step 1: 写 JobManageView.vue** ```vue ``` - [ ] **Step 2: 写 ApplicationManageView.vue(查看简历/更新状态)** ```vue ``` - [ ] **Step 3: 写 OrganizationManageView.vue(超管专属)** ```vue ``` - [ ] **Step 4: 写 UserManageView.vue(超管专属)** ```vue ``` - [ ] **Step 5: Commit** ```bash git add offer_frontend/src/views/admin/ git commit -m "feat: add admin management views (jobs, applications, organizations, users)" ``` --- ## Task 14: 收尾 — App.vue + 整体联调 **Files:** - Modify: `offer_frontend/src/App.vue` - [ ] **Step 1: 写 App.vue** ```vue ``` - [ ] **Step 2: 创建超管账号** ```bash cd offer_backend DJANGO_SETTINGS_MODULE=config.settings.development python manage.py shell -c " from apps.accounts.models import User User.objects.create_user(username='superadmin', password='admin123', role='superadmin', email='admin@offer.com') print('超管账号创建成功') " ``` - [ ] **Step 3: 同时启动前后端,联调验证** ```bash # 终端1:后端 cd offer_backend DJANGO_SETTINGS_MODULE=config.settings.development python manage.py runserver # 终端2:前端 cd offer_frontend npm run dev ``` 联调检查清单: - [ ] 求职者注册 → 登录 → 跳转到 `/seeker/resume` - [ ] 超管登录 → 跳转到 `/admin/jobs` - [ ] 超管创建组织架构 → 创建公司管理员账号 - [ ] 公司管理员登录 → 发布职位 - [ ] 公开页面搜索到已发布职位 - [ ] 求职者填写简历 → 投递职位 → 状态变为"待查看" - [ ] 公司管理员查看投递 → 更新状态 → 求职者收到邮件(console 后端可见) - [ ] **Step 4: 最终 Commit** ```bash git add . git commit -m "feat: complete recruitment website MVP" ```