Files
medical_training/apps/organization/views.py
T
2026-06-11 13:57:46 +08:00

188 lines
9.6 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.
import os
from django.conf import settings
from rest_framework import viewsets, filters
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.user.models import Institution, Department
from apps.cms.permissions import IsSuperAdmin
from apps.common.excel import xlsx_response, rows_from_xlsx
from .serializers import CmsInstitutionSerializer, CmsDepartmentSerializer
ALLOWED_BANNER_EXT = ('.png', '.jpg', '.jpeg', '.webp')
MAX_BANNER_BYTES = 5 * 1024 * 1024 # 5MB
INST_IMPORT_HEADERS = ['机构编码', '名称', '类型', '等级', '', '']
INST_EXPORT_HEADERS = ['ID', '机构编码', '名称', '类型', '等级', '', '']
DEPT_IMPORT_HEADERS = ['科室名称', '分类']
DEPT_EXPORT_HEADERS = ['ID', '科室名称', '分类']
@extend_schema_view(
list=extend_schema(summary='CMS-INST-1 机构列表(分页+搜索)', tags=['CMS-机构']),
create=extend_schema(summary='CMS-INST-2 新建机构', tags=['CMS-机构']),
retrieve=extend_schema(summary='CMS-INST-3 机构详情', tags=['CMS-机构']),
partial_update=extend_schema(summary='CMS-INST-4 编辑机构', tags=['CMS-机构']),
destroy=extend_schema(summary='CMS-INST-5 停用机构(逻辑删除)', tags=['CMS-机构']),
)
class CmsInstitutionViewSet(viewsets.ModelViewSet):
"""CMS 机构(医院)管理 —— 仅超级管理员(CMS-INST-1~6)。
list: 机构列表(分页、搜索 name/code、过滤 type/province/city
create: 新建机构(code 唯一)
retrieve: 机构详情
partial_update: 编辑机构(PATCH
destroy: 停用机构(逻辑删除;Institution 继承 SoftDeleteModel
banner: 上传机构 Banner 图(CMS-INST-6
"""
queryset = Institution.objects.all().order_by('-created_at')
serializer_class = CmsInstitutionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_fields = ['type', 'province', 'city']
search_fields = ['name', 'code']
# 仅允许 GET/POST/PATCH/DELETE,编辑统一走 PATCH(屏蔽 PUT)
http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
# destroy 走 ModelViewSet 默认实现:instance.delete() 经 SoftDeleteModel 改为逻辑删除(停用)
@extend_schema(summary='CMS-INST-6 上传机构 Banner 图', tags=['CMS-机构'])
@action(detail=True, methods=['post'], url_path='banner',
parser_classes=[MultiPartParser, FormParser])
def banner(self, request, pk=None):
"""上传机构 Banner 图(multipart/form-data,字段名 file)。"""
inst = self.get_object()
file = request.FILES.get('file')
if not file:
raise AppError('CMS_BANNER_FILE_REQUIRED', '请上传图片文件(字段名 file', status_code=400)
if file.size > MAX_BANNER_BYTES:
raise AppError('CMS_BANNER_TOO_LARGE', '图片不能超过 5MB', status_code=400)
ext = os.path.splitext(file.name)[1].lower()
if ext not in ALLOWED_BANNER_EXT:
raise AppError('CMS_BANNER_BAD_TYPE', '仅支持 png/jpg/jpeg/webp 图片', status_code=400)
rel_dir = 'institutions'
base_static = settings.STATICFILES_DIRS[0]
target_dir = os.path.join(base_static, rel_dir)
os.makedirs(target_dir, exist_ok=True)
filename = f'inst_{inst.id}_banner{ext}'
with open(os.path.join(target_dir, filename), 'wb') as fh:
for chunk in file.chunks():
fh.write(chunk)
inst.banner_url = f'{rel_dir}/{filename}'
inst.save(update_fields=['banner_url', 'updated_at'])
serializer = self.get_serializer(inst)
return Response({'message': '上传成功', 'banner_url': serializer.data['banner_url']})
@extend_schema(summary='CMS-INST-6b 下载机构导入模板', tags=['CMS-机构'])
@action(detail=False, methods=['get'], url_path='import-template')
def import_template(self, request):
return xlsx_response('机构导入模板.xlsx', INST_IMPORT_HEADERS, [])
@extend_schema(summary='CMS-INST-7 导出机构', tags=['CMS-机构'])
@action(detail=False, methods=['get'], url_path='export')
def export(self, request):
qs = self.filter_queryset(self.get_queryset())
rows = [[i.id, i.code, i.name, i.type, i.level, i.province, i.city] for i in qs]
return xlsx_response('机构列表.xlsx', INST_EXPORT_HEADERS, rows)
@extend_schema(summary='CMS-INST-5 导入机构', tags=['CMS-机构'])
@action(detail=False, methods=['post'], url_path='import',
parser_classes=[MultiPartParser, FormParser])
def import_institutions(self, request):
"""Excel 批量导入机构。列:机构编码 | 名称 | 类型 | 等级 | 省 | 市。"""
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)
existing_codes = set(Institution.all_objects.values_list('code', flat=True))
success, errors = 0, []
for idx, row in enumerate(rows, start=2):
code = (row.get('机构编码') or '').strip()
name = (row.get('名称') or '').strip()
if not code:
errors.append({'row': idx, 'reason': '机构编码为空'}); continue
if not name:
errors.append({'row': idx, 'reason': '名称为空'}); continue
if code in existing_codes:
errors.append({'row': idx, 'reason': f'机构编码已存在:{code}'}); continue
Institution.objects.create(
code=code, name=name,
type=(row.get('类型') or 'hospital').strip() or 'hospital',
level=(row.get('等级') or '').strip(),
province=(row.get('') or '').strip(),
city=(row.get('') or '').strip(),
)
existing_codes.add(code)
success += 1
return Response({'total': len(rows), 'success': success, 'failed': len(errors), 'errors': errors})
@extend_schema_view(
list=extend_schema(summary='CMS-DEPT-1 科室列表(全局)', tags=['CMS-科室']),
create=extend_schema(summary='CMS-DEPT-2 新增科室', tags=['CMS-科室']),
retrieve=extend_schema(summary='科室详情', tags=['CMS-科室']),
partial_update=extend_schema(summary='CMS-DEPT-3 编辑科室', tags=['CMS-科室']),
destroy=extend_schema(summary='CMS-DEPT-4 停用科室(逻辑删除)', tags=['CMS-科室']),
)
class CmsDepartmentViewSet(viewsets.ModelViewSet):
"""CMS 科室管理(全局科室)—— 仅超级管理员。停用为逻辑删除。"""
queryset = Department.objects.all().order_by('name')
serializer_class = CmsDepartmentSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_fields = ['category']
search_fields = ['name']
http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
@extend_schema(summary='CMS-DEPT-6b 下载科室导入模板', tags=['CMS-科室'])
@action(detail=False, methods=['get'], url_path='import-template')
def import_template(self, request):
return xlsx_response('科室导入模板.xlsx', DEPT_IMPORT_HEADERS, [])
@extend_schema(summary='CMS-DEPT-6 导出科室', tags=['CMS-科室'])
@action(detail=False, methods=['get'], url_path='export')
def export(self, request):
qs = self.filter_queryset(self.get_queryset())
rows = [[d.id, d.name, d.category] for d in qs]
return xlsx_response('科室列表.xlsx', DEPT_EXPORT_HEADERS, rows)
@extend_schema(summary='CMS-DEPT-5 导入科室', tags=['CMS-科室'])
@action(detail=False, methods=['post'], url_path='import',
parser_classes=[MultiPartParser, FormParser])
def import_departments(self, request):
"""Excel 批量导入科室。列:科室名称 | 分类(可选)。"""
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)
# 含已停用科室:避免导入与软删科室同名而产生重复行
existing = set(Department.all_objects.values_list('name', flat=True))
success, errors = 0, []
for idx, row in enumerate(rows, start=2):
name = (row.get('科室名称') or '').strip()
if not name:
errors.append({'row': idx, 'reason': '科室名称为空'}); continue
if name in existing:
errors.append({'row': idx, 'reason': f'科室已存在(含已停用):{name}'}); continue
Department.objects.create(name=name, category=(row.get('分类') or '').strip())
existing.add(name)
success += 1
return Response({'total': len(rows), 'success': success, 'failed': len(errors), 'errors': errors})