176 lines
9.2 KiB
Python
176 lines
9.2 KiB
Python
"""CMS 师生关系管理(CMS-REL-1~4)—— 医院管理员(本院)/ 超管(全平台)。"""
|
||
from rest_framework import viewsets, filters, status, serializers
|
||
from rest_framework.decorators import action
|
||
from rest_framework.parsers import MultiPartParser, FormParser
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.response import Response
|
||
from django_filters.rest_framework import DjangoFilterBackend
|
||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||
|
||
from config.exceptions import AppError
|
||
from apps.cms.permissions import IsSuperOrHospitalAdmin, is_super
|
||
from apps.common.excel import xlsx_response, rows_from_xlsx
|
||
from .models import User, TeacherStudentRelation
|
||
|
||
REL_STATUS_LABEL = {0: '已结束', 1: '进行中'}
|
||
REL_IMPORT_HEADERS = ['带教医生手机号', '学生手机号']
|
||
REL_EXPORT_HEADERS = ['ID', '带教医生', '带教医生手机号', '学生', '学生手机号', '状态']
|
||
|
||
|
||
class CmsRelationSerializer(serializers.ModelSerializer):
|
||
"""读取(列表 / 详情)。"""
|
||
teacher_name = serializers.CharField(source='teacher.real_name', read_only=True, default=None)
|
||
teacher_phone = serializers.CharField(source='teacher.phone', read_only=True, default=None)
|
||
student_name = serializers.CharField(source='student.real_name', read_only=True, default=None)
|
||
student_phone = serializers.CharField(source='student.phone', read_only=True, default=None)
|
||
|
||
class Meta:
|
||
model = TeacherStudentRelation
|
||
fields = [
|
||
'id', 'teacher', 'teacher_name', 'teacher_phone',
|
||
'student', 'student_name', 'student_phone',
|
||
'relation_type', 'status', 'created_at',
|
||
]
|
||
|
||
|
||
class CmsRelationWriteSerializer(serializers.ModelSerializer):
|
||
"""新增 / 编辑:teacher 必须是医生、student 必须是学生;医院管理员限本院。"""
|
||
teacher = serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(role_type='doctor'))
|
||
student = serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(role_type='student'))
|
||
|
||
class Meta:
|
||
model = TeacherStudentRelation
|
||
fields = ['teacher', 'student', 'relation_type', 'status']
|
||
|
||
def validate(self, attrs):
|
||
actor = self.context['request'].user
|
||
teacher = attrs.get('teacher') or (self.instance.teacher if self.instance else None)
|
||
student = attrs.get('student') or (self.instance.student if self.instance else None)
|
||
if teacher is None or student is None:
|
||
raise AppError('CMS_VALIDATION_ERROR', '带教医生和学生均必填', status_code=400)
|
||
|
||
if not is_super(actor):
|
||
if not actor.institution_id:
|
||
raise AppError('CMS_NO_INSTITUTION', '当前医院管理员无所属机构', status_code=403)
|
||
if teacher.institution_id != actor.institution_id:
|
||
raise AppError('CMS_REL_SCOPE_FORBIDDEN', '带教医生不属于本院', status_code=403)
|
||
if student.institution_id != actor.institution_id:
|
||
raise AppError('CMS_REL_SCOPE_FORBIDDEN', '学生不属于本院', status_code=403)
|
||
|
||
dup = TeacherStudentRelation.objects.filter(teacher=teacher, student=student)
|
||
if self.instance is not None:
|
||
dup = dup.exclude(pk=self.instance.pk)
|
||
if dup.exists():
|
||
raise AppError('CMS_REL_EXISTS', '该师生关系已存在', status_code=400)
|
||
return attrs
|
||
|
||
|
||
@extend_schema_view(
|
||
list=extend_schema(summary='CMS-REL-1 师生关系列表', tags=['CMS-师生关系']),
|
||
create=extend_schema(summary='CMS-REL-2 新增师生关系', tags=['CMS-师生关系']),
|
||
retrieve=extend_schema(summary='师生关系详情', tags=['CMS-师生关系']),
|
||
partial_update=extend_schema(summary='CMS-REL-2 编辑师生关系', tags=['CMS-师生关系']),
|
||
destroy=extend_schema(summary='CMS-REL-3 停用师生关系(逻辑删除)', tags=['CMS-师生关系']),
|
||
)
|
||
class CmsTeacherStudentRelationViewSet(viewsets.ModelViewSet):
|
||
"""CMS 师生关系管理。超管全平台、医院管理员仅本院。停用为逻辑删除。"""
|
||
permission_classes = [IsAuthenticated, IsSuperOrHospitalAdmin]
|
||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||
filterset_fields = ['teacher', 'student', 'status']
|
||
search_fields = ['teacher__real_name', 'teacher__phone', 'student__real_name', 'student__phone']
|
||
# 仅 GET / POST:查=GET,增删改=POST(编辑→{id}/update/,停用→{id}/disable/)
|
||
http_method_names = ['get', 'post', 'head', 'options']
|
||
|
||
def get_queryset(self):
|
||
qs = TeacherStudentRelation.objects.select_related('teacher', 'student').all().order_by('-created_at')
|
||
user = self.request.user
|
||
if is_super(user):
|
||
return qs
|
||
return qs.filter(teacher__institution_id=user.institution_id)
|
||
|
||
def get_serializer_class(self):
|
||
if self.action in ('create', 'update_relation'):
|
||
return CmsRelationWriteSerializer
|
||
return CmsRelationSerializer
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
write = self.get_serializer(data=request.data)
|
||
write.is_valid(raise_exception=True)
|
||
rel = write.save()
|
||
return Response(CmsRelationSerializer(rel).data, status=status.HTTP_201_CREATED)
|
||
|
||
@extend_schema(summary='CMS-REL-3 编辑师生关系', tags=['CMS-师生关系'])
|
||
@action(detail=True, methods=['post'], url_path='update')
|
||
def update_relation(self, request, pk=None):
|
||
"""编辑师生关系(POST 局部更新,等价旧 PATCH /{id}/)。"""
|
||
instance = self.get_object()
|
||
write = self.get_serializer(instance, data=request.data, partial=True)
|
||
write.is_valid(raise_exception=True)
|
||
rel = write.save()
|
||
return Response(CmsRelationSerializer(rel).data)
|
||
|
||
@extend_schema(summary='CMS-REL-4 停用师生关系(逻辑删除)', tags=['CMS-师生关系'])
|
||
@action(detail=True, methods=['post'], url_path='disable')
|
||
def disable(self, request, pk=None):
|
||
"""停用师生关系(软删除,等价旧 DELETE /{id}/)。"""
|
||
self.get_object().delete()
|
||
return Response({'message': '已停用'})
|
||
|
||
@extend_schema(summary='CMS-REL-4 下载师生关系导入模板', tags=['CMS-师生关系'])
|
||
@action(detail=False, methods=['get'], url_path='import-template')
|
||
def import_template(self, request):
|
||
return xlsx_response('师生关系导入模板.xlsx', REL_IMPORT_HEADERS, [])
|
||
|
||
@extend_schema(summary='CMS-REL-4 导出师生关系', tags=['CMS-师生关系'])
|
||
@action(detail=False, methods=['get'], url_path='export')
|
||
def export(self, request):
|
||
qs = self.filter_queryset(self.get_queryset())
|
||
rows = [
|
||
[r.id,
|
||
r.teacher.real_name if r.teacher_id else '', r.teacher.phone if r.teacher_id else '',
|
||
r.student.real_name if r.student_id else '', r.student.phone if r.student_id else '',
|
||
REL_STATUS_LABEL.get(r.status, r.status)]
|
||
for r in qs
|
||
]
|
||
return xlsx_response('师生关系列表.xlsx', REL_EXPORT_HEADERS, rows)
|
||
|
||
@extend_schema(summary='CMS-REL-4 导入师生关系', tags=['CMS-师生关系'])
|
||
@action(detail=False, methods=['post'], url_path='import',
|
||
parser_classes=[MultiPartParser, FormParser])
|
||
def import_relations(self, request):
|
||
"""Excel 批量导入师生关系。列:带教医生手机号 | 学生手机号。"""
|
||
actor = request.user
|
||
actor_is_super = is_super(actor)
|
||
if not actor_is_super and not actor.institution_id:
|
||
raise AppError('CMS_NO_INSTITUTION', '当前医院管理员无所属机构,无法导入', status_code=403)
|
||
|
||
file = request.FILES.get('file')
|
||
if not file:
|
||
raise AppError('CMS_IMPORT_FILE_REQUIRED', '请上传 .xlsx 文件(字段名 file)', status_code=400)
|
||
try:
|
||
rows = rows_from_xlsx(file)
|
||
except Exception:
|
||
raise AppError('CMS_IMPORT_BAD_FILE', '文件解析失败,请使用导入模板', status_code=400)
|
||
|
||
success, errors = 0, []
|
||
for idx, row in enumerate(rows, start=2):
|
||
t_phone = (row.get('带教医生手机号') or '').strip()
|
||
s_phone = (row.get('学生手机号') or '').strip()
|
||
teacher = User.objects.filter(phone=t_phone, role_type='doctor').first()
|
||
if teacher is None:
|
||
errors.append({'row': idx, 'reason': f'带教医生不存在或非医生:{t_phone}'}); continue
|
||
student = User.objects.filter(phone=s_phone, role_type='student').first()
|
||
if student is None:
|
||
errors.append({'row': idx, 'reason': f'学生不存在或非学生:{s_phone}'}); continue
|
||
if not actor_is_super and (
|
||
teacher.institution_id != actor.institution_id
|
||
or student.institution_id != actor.institution_id
|
||
):
|
||
errors.append({'row': idx, 'reason': '师生不属于本院'}); continue
|
||
if TeacherStudentRelation.objects.filter(teacher=teacher, student=student).exists():
|
||
errors.append({'row': idx, 'reason': '师生关系已存在'}); continue
|
||
TeacherStudentRelation.objects.create(teacher=teacher, student=student, status=1)
|
||
success += 1
|
||
|
||
return Response({'total': len(rows), 'success': success, 'failed': len(errors), 'errors': errors})
|