from django.db import models from rest_framework import viewsets, permissions, status from rest_framework.response import Response from rest_framework.decorators import action from .models import Task, TaskApplication, TaskInvitation, TaskStatus, ApplyStatus, InvitationStatus from .serializers import TaskSerializer, TaskApplicationSerializer, TaskInvitationSerializer class TaskInvitationViewSet(viewsets.ModelViewSet): """ @author: xujl Api说明: 任务主动邀请接口视图。企业用户可主动向OPC专家发送定向任务合作邀请,专家可接受或拒绝该邀请。 """ serializer_class = TaskInvitationSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): user = self.request.user enterprise, _ = _get_user_enterprise(user) if enterprise: # Experts see invitations sent to them # Enterprises see invitations they sent for their tasks return TaskInvitation.objects.filter( models.Q(expert=user) | models.Q(task__enterprise=enterprise) ).distinct().order_by('-created_at') else: return TaskInvitation.objects.filter( models.Q(expert=user) | models.Q(task__publisher=user) ).distinct().order_by('-created_at') def perform_create(self, serializer): task = serializer.validated_data['task'] expert = serializer.validated_data['expert'] if TaskInvitation.objects.filter(task=task, expert=expert).exists(): from rest_framework.exceptions import ValidationError raise ValidationError('已经向该专家发送过邀约') serializer.save() @action(detail=True, methods=['post']) def accept(self, request, pk=None): invitation = self.get_object() if invitation.status == InvitationStatus.ACCEPTED: return Response({'detail': '您已接受过此邀约'}, status=status.HTTP_400_BAD_REQUEST) task = invitation.task invitation.status = InvitationStatus.ACCEPTED invitation.save() from django.utils import timezone history = [] if invitation.message: history.append({ 'sender_id': str(task.publisher.id), 'sender_name': task.publisher.nickname or task.publisher.username, 'sender_avatar': task.publisher.avatar_url, 'sender_role': 'ENTERPRISE', 'content': invitation.message, 'created_at': invitation.created_at.isoformat() }) if isinstance(invitation.messages, list): history.extend(invitation.messages) history.append({ 'sender_id': str(request.user.id), 'sender_name': 'System', 'sender_avatar': '', 'sender_role': 'SYSTEM', 'content': f'【系统记录】专家 {request.user.nickname or request.user.username} 已同意承接并加入了项目', 'created_at': timezone.now().isoformat() }) # Create an application record so the enterprise can track this accepted expert application, created = TaskApplication.objects.get_or_create( task=task, applicant=request.user, defaults={ 'status': ApplyStatus.APPROVED, 'cover_letter': '接受了企业发出的定向邀约', 'expected_price': task.budget_max or 0, 'expected_days': 0, 'negotiation_history': history } ) if not created: if application.status != ApplyStatus.APPROVED: application.status = ApplyStatus.APPROVED # If the application existed but was PENDING or something, we should still update its negotiation history application.negotiation_history = history application.save() if task.status in [TaskStatus.OPEN, TaskStatus.DRAFT]: task.status = TaskStatus.IN_PROGRESS task.save() return Response({'status': '已接受邀约,项目进入进行中状态'}) @action(detail=True, methods=['post']) def reject(self, request, pk=None): invitation = self.get_object() invitation.status = InvitationStatus.REJECTED invitation.save() return Response({'status': '已拒绝邀约'}) @action(detail=True, methods=['post']) def discuss(self, request, pk=None): invitation = self.get_object() user = request.user # Verify user is either the expert or enterprise admin/publisher enterprise, role = _get_user_enterprise(user) is_enterprise = False if user == invitation.expert: sender_role = 'EXPERT' elif user == invitation.task.publisher or (enterprise and invitation.task.enterprise == enterprise): sender_role = 'ENTERPRISE' is_enterprise = True else: from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权参与此洽谈') content = request.data.get('message', '').strip() if not content: from rest_framework.exceptions import ValidationError raise ValidationError('回复内容不能为空') from django.utils import timezone message_obj = { 'sender_id': str(user.id), 'sender_name': user.nickname or user.username, 'sender_avatar': user.avatar_url, 'sender_role': sender_role, 'content': content, 'created_at': timezone.now().isoformat() } # Initialize messages if None somehow if not isinstance(invitation.messages, list): invitation.messages = [] invitation.messages.append(message_obj) # Ensure status transitions to DISCUSSING unless already ACCEPTED or REJECTED if invitation.status == InvitationStatus.PENDING: invitation.status = InvitationStatus.DISCUSSING invitation.save() return Response({'status': '回复已发送', 'message': message_obj}) def _get_user_enterprise(user): from users.models import EnterpriseMember if hasattr(user, 'enterprise') and user.enterprise: return user.enterprise, 'ADMIN' # Owner is effectively ADMIN membership = EnterpriseMember.objects.filter(user=user).first() if membership: return membership.enterprise, membership.role return None, None class TaskViewSet(viewsets.ModelViewSet): """ @author: xujl Api说明: 主线任务广场接口视图。提供企业发布任务、编辑任务信息,以及用户在大厅分页筛选、搜索开放任务的功能。 """ def get_queryset(self): user = self.request.user queryset = Task.objects.filter(is_deleted=False).order_by('-is_recommended', '-recommend_priority', '-created_at') status_param = self.request.query_params.get('status') if status_param: queryset = queryset.filter(status=status_param) enterprise_param = self.request.query_params.get('enterprise') if enterprise_param: queryset = queryset.filter(enterprise_id=enterprise_param) # If global admin, show all tasks if user.is_staff or user.is_superuser or 'ADMIN' in getattr(user, 'roles', []): return queryset # If enterprise user, show their enterprise's tasks enterprise, role = _get_user_enterprise(user) if enterprise and not enterprise_param: return queryset.filter(enterprise=enterprise) # Regular users (OPC experts, etc.) can see all OPEN tasks + tasks they're involved in return queryset serializer_class = TaskSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): user = self.request.user # Admins can publish tasks directly (platform-level tasks) if user.is_staff or user.is_superuser: enterprise, _ = _get_user_enterprise(user) serializer.save(publisher=user, enterprise=enterprise) return enterprise, _ = _get_user_enterprise(user) if not enterprise: from rest_framework.exceptions import PermissionDenied raise PermissionDenied('必须关联企业后才能发布任务') if enterprise.status != 'VERIFIED': from rest_framework.exceptions import PermissionDenied raise PermissionDenied('企业尚未通过认证,暂无权发布任务') serializer.save(publisher=user, enterprise=enterprise) def _check_admin_or_publisher(self, task, user): if user.is_staff or user.is_superuser: return True if 'ADMIN' in getattr(user, 'roles', []): return True if task.publisher == user: return True enterprise, role = _get_user_enterprise(user) if enterprise and task.enterprise == enterprise and role == 'ADMIN': return True return False def perform_update(self, serializer): task = self.get_object() if not self._check_admin_or_publisher(task, self.request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权修改此任务') serializer.save() def perform_destroy(self, instance): if not self._check_admin_or_publisher(instance, self.request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权删除此任务') instance.is_deleted = True instance.save() @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def toggle_recommend(self, request, pk=None): task = self.get_object() task.is_recommended = request.data.get('is_recommended', not task.is_recommended) task.recommend_priority = request.data.get('recommend_priority', task.recommend_priority) task.save(update_fields=['is_recommended', 'recommend_priority']) return Response({'is_recommended': task.is_recommended, 'recommend_priority': task.recommend_priority}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def publish(self, request, pk=None): task = self.get_object() if not self._check_admin_or_publisher(task, request.user): return Response({'detail': '无权操作'}, status=status.HTTP_403_FORBIDDEN) task.status = TaskStatus.OPEN task.save() return Response({'status': '任务已发布'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def cancel_task(self, request, pk=None): task = self.get_object() if not self._check_admin_or_publisher(task, request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权取消此任务') batch_reject = request.data.get('batch_reject', False) # Any application that is PENDING, APPROVED, or DELIVERED should be cancelled. active_apps = task.applications.exclude(status__in=[ApplyStatus.REJECTED, ApplyStatus.COMPLETED, ApplyStatus.WITHDRAWN]) if active_apps.exists(): if not batch_reject: return Response({ 'detail': '该任务有正在进行中的专家或待审核的申请,禁止直接取消。请选择强制批量中止。', 'requires_batch_reject': True }, status=status.HTTP_400_BAD_REQUEST) else: # Batch reject active_apps.update(status=ApplyStatus.REJECTED, reject_reason='任务已被系统或发布方强制中止/取消') task.status = TaskStatus.CANCELLED from django.utils import timezone task.cancelled_at = timezone.now() task.cancel_reason = request.data.get('cancel_reason', '管理员操作下架') task.save() return Response({'status': '任务已下架'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def complete_task(self, request, pk=None): task = self.get_object() if not self._check_admin_or_publisher(task, request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权操作') from .models import ApplyStatus, DeliveryStatus # Check for experts who haven't delivered working_apps = task.applications.filter(status=ApplyStatus.APPROVED) if working_apps.exists(): return Response({'detail': '尚有专家未提交交付物,无法结项。如需结项,请先取消未完成的录用。'}, status=status.HTTP_400_BAD_REQUEST) # Check for experts who delivered but haven't been approved delivered_apps = task.applications.filter(status=ApplyStatus.DELIVERED) for app in delivered_apps: latest_record = app.delivery_records.order_by('-created_at').first() if not latest_record or latest_record.status != DeliveryStatus.APPROVED: return Response({'detail': '尚有专家的交付成果未验收,无法结项。'}, status=status.HTTP_400_BAD_REQUEST) task.status = TaskStatus.COMPLETED from django.utils import timezone task.completed_at = timezone.now() task.save() for app in delivered_apps: app.status = ApplyStatus.COMPLETED app.reviewed_at = timezone.now() app.save() expert = app.applicant if expert: expert.completed_tasks = (expert.completed_tasks or 0) + 1 expert.save() return Response({'status': '任务已成功结项'}) class TaskApplicationViewSet(viewsets.ModelViewSet): """ @author: xujl Api说明: 任务申请投递接口视图。普通用户/专家对感兴趣的任务进行接单投递(包含附件、报价),企业方审批(approve/reject)后即缔结合作。 """ queryset = TaskApplication.objects.all() serializer_class = TaskApplicationSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): user = self.request.user # Admins can see all applications if user.is_staff or user.is_superuser or 'ADMIN' in getattr(user, 'roles', []): qs = TaskApplication.objects.all().order_by('-created_at') else: enterprise, _ = _get_user_enterprise(user) if enterprise: # Publishers/Admins see applications for their enterprise's tasks qs = TaskApplication.objects.filter( models.Q(applicant=user) | models.Q(task__enterprise=enterprise) ).distinct().order_by('-created_at') else: qs = TaskApplication.objects.filter( models.Q(applicant=user) | models.Q(task__publisher=user) ).distinct().order_by('-created_at') task_id = self.request.query_params.get('task') if task_id: qs = qs.filter(task_id=task_id) return qs def perform_create(self, serializer): user = self.request.user if 'OPC_USER' not in getattr(user, 'roles', []): # Check db if property is missing from users.models import UserRole if not UserRole.objects.filter(user=user, role__code='OPC_USER').exists(): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('必须是已认证的 OPC 专家才能承接任务') from opc_cert.models import OpcCertification cert = OpcCertification.objects.filter(user=user, status='APPROVED').exists() if not cert: from rest_framework.exceptions import PermissionDenied raise PermissionDenied('您的 OPC 专家认证未通过审核,无法承接任务') serializer.save(applicant=user) def _check_admin_or_publisher(self, task, user): if user.is_staff or user.is_superuser: return True if 'ADMIN' in getattr(user, 'roles', []): return True if task.publisher == user: return True enterprise, role = _get_user_enterprise(user) if enterprise and task.enterprise == enterprise: return True return False @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def approve(self, request, pk=None): application = self.get_object() if not self._check_admin_or_publisher(application.task, request.user): return Response({'detail': '无权操作'}, status=status.HTTP_403_FORBIDDEN) application.status = ApplyStatus.APPROVED application.reviewed_by = request.user application.save() task = application.task if task.status in [TaskStatus.OPEN, TaskStatus.DRAFT]: task.status = TaskStatus.IN_PROGRESS task.save() return Response({'status': '申请已通过,专家可开始执行'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def reject(self, request, pk=None): application = self.get_object() if not self._check_admin_or_publisher(application.task, request.user): return Response({'detail': '无权操作'}, status=status.HTTP_403_FORBIDDEN) application.status = ApplyStatus.REJECTED application.reject_reason = request.data.get('reason', '') application.reviewed_by = request.user from django.utils import timezone application.reviewed_at = timezone.now() application.save() return Response({'status': '申请已驳回'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def submit_deliverables(self, request, pk=None): application = self.get_object() if application.applicant != request.user: return Response({'detail': '只有承接人自己才能提交成果'}, status=status.HTTP_403_FORBIDDEN) deliverables = request.data.get('deliverables', []) completion_note = request.data.get('completion_note', '') from .models import DeliveryRecord, DeliveryStatus DeliveryRecord.objects.create( application=application, note=completion_note, files=deliverables, status=DeliveryStatus.PENDING ) application.status = ApplyStatus.DELIVERED application.save() return Response({'status': '成果已提交,等待企业验收'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def approve_delivery(self, request, pk=None): application = self.get_object() if not self._check_admin_or_publisher(application.task, request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权验收此成果') record_id = request.data.get('record_id') from .models import DeliveryRecord, DeliveryStatus if record_id: record = application.delivery_records.filter(id=record_id).first() else: record = application.delivery_records.filter(status=DeliveryStatus.PENDING).first() if not record: return Response({'detail': '找不到待验收的交付记录'}, status=status.HTTP_404_NOT_FOUND) record.status = DeliveryStatus.APPROVED record.save() # Note: We do NOT change application.status to COMPLETED here. # It remains DELIVERED until the enterprise explicitly closes the task via complete_task. # This ensures the expert's side doesn't show as fully completed before the project is officially wrapped up. return Response({'status': '该专家的成果已验收通过'}) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def reject_delivery(self, request, pk=None): application = self.get_object() if not self._check_admin_or_publisher(application.task, request.user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied('无权操作') record_id = request.data.get('record_id') from .models import DeliveryRecord, DeliveryStatus if record_id: record = application.delivery_records.filter(id=record_id).first() else: record = application.delivery_records.filter(status=DeliveryStatus.PENDING).first() if not record: return Response({'detail': '找不到待驳回的交付记录'}, status=status.HTTP_404_NOT_FOUND) record.status = DeliveryStatus.REJECTED record.save() # If all records are rejected, application goes back to APPROVED (IN_PROGRESS) if not application.delivery_records.filter(status=DeliveryStatus.PENDING).exists(): application.status = ApplyStatus.APPROVED application.save() return Response({'status': '该次交付已驳回'})