@@ -0,0 +1,186 @@
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 . 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 } )