Files
opc-backend/tasks/views.py

488 lines
21 KiB
Python
Raw Normal View History

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': '该次交付已驳回'})