Files
medical_training/apps/user/cms_relation.py
T

229 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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',
]
def _resolve_rel_user(name, phone, role, label, code):
"""按 姓名 + 手机号 + 角色 解析用户;不匹配抛 400。"""
u = User.objects.filter(phone=phone, real_name=name, role_type=role).first()
if u is None:
raise AppError(code, f'{label}不存在或姓名与手机号不符:{name} / {phone}', status_code=400)
return u
class CmsRelationWriteSerializer(serializers.ModelSerializer):
"""新增 / 编辑:入参用**带教老师姓名+手机号 / 学生姓名+手机号**;按姓名+手机号解析为用户
(带教老师须 doctor、学生须 student);医院管理员限本院。"""
teacher_name = serializers.CharField(write_only=True, required=False, help_text='带教老师姓名(新增必填)')
teacher_phone = serializers.CharField(write_only=True, required=False, help_text='带教老师手机号(新增必填)')
student_name = serializers.CharField(write_only=True, required=False, help_text='学生姓名(新增必填)')
student_phone = serializers.CharField(write_only=True, required=False, help_text='学生手机号(新增必填)')
class Meta:
model = TeacherStudentRelation
fields = ['teacher_name', 'teacher_phone', 'student_name', 'student_phone', 'relation_type', 'status']
def validate(self, attrs):
actor = self.context['request'].user
creating = self.instance is None
t_name = (attrs.pop('teacher_name', None) or '').strip()
t_phone = (attrs.pop('teacher_phone', None) or '').strip()
s_name = (attrs.pop('student_name', None) or '').strip()
s_phone = (attrs.pop('student_phone', None) or '').strip()
if creating and not (t_name and t_phone and s_name and s_phone):
raise AppError('CMS_VALIDATION_ERROR', '带教老师姓名/手机号、学生姓名/手机号均必填', status_code=400)
# 解析(编辑时该方未传任何字段则沿用原值;传了则姓名与手机号需成对)
teacher = self.instance.teacher if self.instance else None
if t_name or t_phone:
if not (t_name and t_phone):
raise AppError('CMS_VALIDATION_ERROR', '带教老师姓名与手机号需同时提供', status_code=400)
teacher = _resolve_rel_user(t_name, t_phone, 'doctor', '带教老师', 'CMS_REL_TEACHER_NOT_FOUND')
student = self.instance.student if self.instance else None
if s_name or s_phone:
if not (s_name and s_phone):
raise AppError('CMS_VALIDATION_ERROR', '学生姓名与手机号需同时提供', status_code=400)
student = _resolve_rel_user(s_name, s_phone, 'student', '学生', 'CMS_REL_STUDENT_NOT_FOUND')
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)
attrs['teacher'] = teacher
attrs['student'] = student
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': '已停用'})
# ── 下拉数据源(医院管理员本院 / 超管全平台;姓名+手机号一并返回)──────────
def _role_users(self, role):
qs = User.objects.filter(role_type=role).order_by('real_name', 'id')
if not is_super(self.request.user):
qs = qs.filter(institution_id=self.request.user.institution_id)
return [{'id': u.id, 'real_name': u.real_name, 'phone': u.phone} for u in qs]
@extend_schema(summary='CMS-REL-6 带教医生下拉(姓名+手机号)', tags=['CMS-师生关系'])
@action(detail=False, methods=['get'], url_path='doctors')
def doctors(self, request):
"""新增/编辑师生关系的「带教医生」下拉数据源,返回 [{id, real_name, phone}]。"""
return Response(self._role_users('doctor'))
@extend_schema(summary='CMS-REL-7 学生下拉(姓名+手机号)', tags=['CMS-师生关系'])
@action(detail=False, methods=['get'], url_path='students')
def students(self, request):
"""新增/编辑师生关系的「学生」下拉数据源,返回 [{id, real_name, phone}]。"""
return Response(self._role_users('student'))
@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_name = (row.get('带教医生姓名') or '').strip()
t_phone = (row.get('带教医生手机号') or '').strip()
s_name = (row.get('学生姓名') or '').strip()
s_phone = (row.get('学生手机号') or '').strip()
teacher = User.objects.filter(phone=t_phone, real_name=t_name, role_type='doctor').first()
if teacher is None:
errors.append({'row': idx, 'reason': f'带教医生不存在或姓名手机号不符:{t_name} / {t_phone}'}); continue
student = User.objects.filter(phone=s_phone, real_name=s_name, role_type='student').first()
if student is None:
errors.append({'row': idx, 'reason': f'学生不存在或姓名手机号不符:{s_name} / {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})