student.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import copy
  4. import os
  5. from typing import Optional
  6. from fastapi import APIRouter, Query, Depends, Path, UploadFile, File
  7. from openpyxl import load_workbook
  8. from sqlalchemy import or_, text
  9. from sqlalchemy.ext.asyncio import AsyncSession
  10. from starlette.background import BackgroundTasks
  11. from admin.api.endpoints.school.utils import check_row
  12. from core.config import settings
  13. from core.security import hashed_password
  14. from crud.school import crud_school, crud_grade, crud_class
  15. from crud.user import crud_student
  16. from models.user import SysUser
  17. from schemas.base import OrderByField, ReturnField
  18. from schemas.school.student import (StudentList, StudentDetail, NewStudent,
  19. StudentInDB, UpdateStudent)
  20. from utils.depends import get_async_db, get_current_user
  21. router = APIRouter()
  22. # 学生列表
  23. @router.get("/students",
  24. response_model=StudentList,
  25. response_model_exclude_none=True,
  26. summary="学生列表")
  27. async def get_students(
  28. page: Optional[int] = None,
  29. size: Optional[int] = None,
  30. sno: str = Query("", description="学号"),
  31. name: str = Query("", description="学生姓名"),
  32. tid: int = Query(0, description="学生ID"),
  33. sid: int = Query(0, description="学校ID"),
  34. gid: int = Query(0, description="年级ID"),
  35. cid: int = Query(0, description="班级ID"),
  36. order: OrderByField = Query("-created_at",
  37. description="排序字段,用逗号分隔,升降序以-判断,默认-created_at"),
  38. res: ReturnField = Query("", description="返回字段,取值: id,name,sno,phone"),
  39. db: AsyncSession = Depends(get_async_db),
  40. current_user: SysUser = Depends(get_current_user)):
  41. # 过滤条件
  42. filters = {}
  43. if sno:
  44. filters["sno"] = sno
  45. if name:
  46. filters["name"] = name
  47. if tid:
  48. filters["id"] = tid
  49. if sid:
  50. filters["school_id"] = sid
  51. if gid:
  52. filters["grade_id"] = gid
  53. if cid:
  54. filters["class_id"] = cid
  55. if ((page is not None) and page >= 1) and ((size is not None)
  56. and size >= 1):
  57. offset = (page - 1) * size
  58. else:
  59. offset = size = None
  60. if isinstance(order, str):
  61. order = [text(order)]
  62. total, items = await crud_student.find_all(db,
  63. filters=filters,
  64. offset=offset,
  65. limit=size,
  66. order_by=order,
  67. return_fields=res)
  68. return {"total": total, "data": items}
  69. # 创建学生
  70. @router.post("/students",
  71. response_model=StudentDetail,
  72. response_model_exclude_none=True,
  73. summary="创建学生")
  74. async def create_student(info: NewStudent,
  75. db: AsyncSession = Depends(get_async_db),
  76. current_user: SysUser = Depends(get_current_user)):
  77. # 判断学校是否存在
  78. db_school = await crud_school.find_one(db, filters={"id": info.school_id})
  79. if not db_school:
  80. return {"errcode": 404, "mess": "学校不存在!"}
  81. # 判断年级是否存在
  82. db_grade = await crud_grade.find_one(db, filters={"id": info.grade_id})
  83. if not db_grade:
  84. return {"errcode": 404, "mess": "年级不存在!"}
  85. # 判断班级是否存在
  86. db_class = await crud_class.find_one(db, filters={"id": info.class_id})
  87. if not db_class:
  88. return {"errcode": 404, "mess": "班级不存在!"}
  89. # 判断是否存在同年级同学号
  90. existed = await crud_student.count(
  91. db,
  92. filters=[
  93. or_(text(f"school_id = {info.school_id} AND sno = '{info.sno}'"),
  94. text(f"phone = '{info.phone}'"))
  95. ])
  96. if existed:
  97. return {"errcode": 400, "mess": "学号或手机号重复!"}
  98. # 开始创建
  99. obj_in = StudentInDB(**info.dict(by_alias=True),
  100. school_name=db_school.name,
  101. grade_name=db_grade.name,
  102. class_name=db_class.name,
  103. username=info.phone,
  104. password=hashed_password(info.phone[-6:]),
  105. creator_id=current_user.id,
  106. creator_name=current_user.username,
  107. editor_id=current_user.id,
  108. editor_name=current_user.username)
  109. db_obj = await crud_student.insert_one(db, obj_in)
  110. return {"data": db_obj}
  111. @router.get("/students/{sid}",
  112. response_model=StudentDetail,
  113. response_model_exclude_none=True,
  114. summary="学生详情")
  115. async def get_student(sid: int = Path(..., description="学生ID"),
  116. db: AsyncSession = Depends(get_async_db),
  117. current_user: SysUser = Depends(get_current_user)):
  118. db_obj = await crud_student.find_one(db, filters={"id": sid})
  119. return {"data": db_obj}
  120. # 更新学生
  121. @router.put("/students/{sid}",
  122. response_model=StudentDetail,
  123. response_model_exclude_none=True,
  124. summary="更新学生")
  125. async def update_student(info: UpdateStudent,
  126. sid: int = Path(..., description="学生ID"),
  127. db: AsyncSession = Depends(get_async_db),
  128. current_user: SysUser = Depends(get_current_user)):
  129. # 判断提交参数是否为空
  130. info_dict = info.dict(exclude_none=True)
  131. if not info_dict:
  132. return {"errcode": 400, "mess": "提交参数为空!"}
  133. # 判断学生是否存在
  134. db_obj = await crud_student.find_one(db, filters={"id": sid})
  135. if not db_obj:
  136. return {"errcode": 404, "mess": "学生不存在!"}
  137. # 判断学校是否存在
  138. if ("school_id"
  139. in info_dict) and (db_obj.school_id != info_dict["school_id"]):
  140. db_school = await crud_school.find_one(db,
  141. filters={"id": info.school_id})
  142. if not db_school:
  143. return {"errcode": 404, "mess": "学校不存在!"}
  144. else:
  145. info.school_name = db_school.name
  146. # 判断年级是否存在
  147. if ("grade_id" in info_dict) and (db_obj.grade_id != info_dict["grade_id"]):
  148. db_grade = await crud_grade.find_one(db, filters={"id": info.grade_id})
  149. if not db_grade:
  150. return {"errcode": 404, "mess": "年级不存在!"}
  151. else:
  152. info.grade_name = db_grade.name
  153. # 判断班级是否存在
  154. if ("class_id" in info_dict) and (db_obj.class_id != info_dict["class_id"]):
  155. db_class = await crud_class.find_one(db, filters={"id": info.class_id})
  156. if not db_class:
  157. return {"errcode": 404, "mess": "班级不存在!"}
  158. else:
  159. info.class_name = db_class.name
  160. # 判断手机号是否重复
  161. if ("phone" in info_dict) and (db_obj.phone != info_dict["phone"]):
  162. db_student = await crud_student.find_one(
  163. db, filters=[text(f"id != {sid} AND phone = '{info.phone}'")])
  164. if db_student:
  165. return {"errcode": 400, "mess": "手机号重复!"}
  166. info.username = info.phone[-6:]
  167. # 更新
  168. info.editor_id = current_user.id
  169. info.editor_name = current_user.username
  170. db_obj = await crud_student.update(db, db_obj, info)
  171. return {"data": db_obj}
  172. # 删除学生
  173. @router.delete("/students/{sid}",
  174. response_model=StudentDetail,
  175. response_model_exclude_none=True,
  176. summary="删除学生")
  177. async def delete_student(bg_task: BackgroundTasks,
  178. sid: int = Path(..., description="学生ID"),
  179. db: AsyncSession = Depends(get_async_db),
  180. current_user: SysUser = Depends(get_current_user)):
  181. existed = await crud_student.count(db, {"id": sid})
  182. if not existed:
  183. return {"errcode": 404, "mess": "学生不存在!"}
  184. else:
  185. await crud_student.delete(db, obj_id=sid)
  186. # TODO: 删除关联数据
  187. # bg_task.add_task(delete_related_object, db, cid=id)
  188. return {"data": None}
  189. # 批量导入学生
  190. @router.post("/students/bulk",
  191. response_model=StudentDetail,
  192. response_model_exclude_none=True,
  193. summary="批量导入学生")
  194. async def import_student(datafile: UploadFile = File(..., description="数据文件"),
  195. db: AsyncSession = Depends(get_async_db),
  196. current_user: SysUser = Depends(get_current_user)):
  197. # 判断文件格式
  198. if not datafile.filename.endswith(".xlsx"):
  199. return {"errcode": 400, "mess": "文件格式错误!"}
  200. # 把文件写入磁盘,再加载回来
  201. disk_file = os.path.join(settings.UPLOADER_PATH, datafile.filename)
  202. content = await datafile.read()
  203. with open(disk_file, "wb") as f:
  204. f.write(content)
  205. # 返回结果
  206. errors = []
  207. success = 0
  208. students = []
  209. counter = 0
  210. # 使用openpyxl读取文件
  211. wb = load_workbook(disk_file)
  212. ws = wb.worksheets[0]
  213. for row in ws.iter_rows(min_row=2,
  214. max_col=ws.max_column,
  215. max_row=ws.max_row,
  216. values_only=True):
  217. row = await check_row(row, 9)
  218. if row is None: # 空行
  219. continue
  220. elif not row: # 字段不完整
  221. errors.append(f"第{row[0]}行: 某些单元格为空!")
  222. continue
  223. # 判断学校是否存在
  224. db_school = await crud_school.find_one(db, filters={"name": row[6]})
  225. if not db_school:
  226. errors.append(f"第{row[0]}行: 学校不存在!")
  227. continue
  228. # 判断年级是否存在
  229. db_grade = await crud_grade.find_one(db,
  230. filters={
  231. "school_id": db_school.id,
  232. "name": row[7]
  233. })
  234. if not db_grade:
  235. errors.append(f"第{row[0]}行: 年级不存在!")
  236. continue
  237. # 判断班级是否存在
  238. db_class = await crud_class.find_one(db,
  239. filters={
  240. "school_id": db_school.id,
  241. "grade_id": db_grade.id,
  242. "name": row[8]
  243. })
  244. if not db_class:
  245. errors.append(f"第{row[0]}行: 班级不存在!")
  246. continue
  247. # 判断是否存在同名学生
  248. db_student = await crud_student.find_one(
  249. db,
  250. filters=[
  251. or_(text(f"school_id = {db_school.id} AND sno = '{row[1]}'"),
  252. text(f"phone = '{row[5]}'"))
  253. ])
  254. if db_student:
  255. errors.append(f"第{row[0]}行: 学号或手机号重复!")
  256. continue
  257. # 创建教师对象
  258. obj_in = StudentInDB(sno=row[1],
  259. username=row[5],
  260. password=hashed_password(row[5][-6:]),
  261. name=row[2],
  262. sex=1 if row[3] == "男" else 0,
  263. age=row[4],
  264. phone=row[5],
  265. sid=db_school.id,
  266. school_name=row[6],
  267. gid=db_grade.id,
  268. grade_name=row[7],
  269. cid=db_class.id,
  270. class_name=row[8],
  271. creator_id=current_user.id,
  272. creator_name=current_user.username,
  273. editor_id=current_user.id,
  274. editor_name=current_user.username)
  275. students.append(obj_in)
  276. success += 1
  277. counter += 1
  278. if counter == 50:
  279. await crud_student.insert_many(db, copy.deepcopy(students))
  280. students.clear()
  281. counter = 0
  282. if counter:
  283. await crud_student.insert_many(db, students)
  284. # 删除上传文件
  285. os.remove(disk_file)
  286. return {"data": {"success": success, "fail": len(errors), "errors": errors}}