student.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import copy
  4. import os
  5. from collections import defaultdict
  6. from typing import Optional, Union
  7. from fastapi import APIRouter, Query, Depends, Path, UploadFile, File
  8. from openpyxl import load_workbook
  9. from sqlalchemy import or_, and_, desc, asc
  10. from sqlalchemy.ext.asyncio import AsyncSession
  11. from starlette.background import BackgroundTasks
  12. from admin.api.endpoints.school.utils import check_row, check_filetype
  13. from bgtask.tasks import bgtask_update_class_teacher_student
  14. from core.config import settings
  15. from core.security import hashed_password
  16. from crud.school import crud_school, crud_grade, crud_class
  17. from crud.user import crud_student
  18. from models.user import Admin, Student
  19. from schemas.base import ReturnField
  20. from schemas.school.student import StudentList, StudentDetail, NewStudent, StudentInDB, UpdateStudent
  21. from utils.depends import get_async_db, get_current_user
  22. router = APIRouter()
  23. # 学生列表
  24. @router.get("/students",
  25. response_model=StudentList,
  26. response_model_exclude_none=True,
  27. summary="学生列表")
  28. async def get_students(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: str = Query("-id", description="排序字段,用逗号分隔,升降序以-判断"),
  37. res: ReturnField = Query("", description="返回字段,取值: id,name,sno,phone"),
  38. db: AsyncSession = Depends(get_async_db),
  39. current_user: Admin = Depends(get_current_user)):
  40. # 过滤条件
  41. _q = {}
  42. if sno:
  43. _q["sno"] = sno
  44. if name:
  45. _q["name"] = name
  46. if tid:
  47. _q["id"] = tid
  48. if sid:
  49. _q["school_id"] = sid
  50. if gid:
  51. _q["grade_id"] = gid
  52. if cid:
  53. _q["class_id"] = cid
  54. if ((page is not None) and page >= 1) and ((size is not None) and size >= 1):
  55. offset = (page - 1) * size
  56. else:
  57. offset = size = None
  58. # 排序字段
  59. order_fields = []
  60. if order:
  61. for x in order.split(","):
  62. field = x.strip()
  63. if field:
  64. if field.startswith("-"):
  65. order_fields.append(desc(getattr(Student, field[1:])))
  66. else:
  67. order_fields.append(asc(getattr(Student, field)))
  68. total, items = await crud_student.find_all(db,
  69. filters=_q,
  70. offset=offset,
  71. limit=size,
  72. order_by=order_fields,
  73. return_fields=res)
  74. for x in items:
  75. if not x.age:
  76. x.age = ""
  77. return {"total": total, "data": items}
  78. # 创建学生
  79. @router.post("/students",
  80. response_model=StudentDetail,
  81. response_model_exclude_none=True,
  82. summary="创建学生")
  83. async def create_student(info: NewStudent,
  84. bgtask: BackgroundTasks,
  85. db: AsyncSession = Depends(get_async_db),
  86. current_user: Admin = Depends(get_current_user)):
  87. # 判断学校是否存在
  88. db_school = await crud_school.find_one(db,
  89. filters={"id": info.school_id},
  90. return_fields=["name", "category"])
  91. if not db_school:
  92. return {"errcode": 404, "mess": "学校不存在!"}
  93. # 判断年级是否存在
  94. db_grade = await crud_grade.find_one(db, filters={"id": info.grade_id}, return_fields=["name"])
  95. if not db_grade:
  96. return {"errcode": 404, "mess": "年级不存在!"}
  97. # 判断班级是否存在
  98. db_class = await crud_class.find_one(db,
  99. filters={"id": info.class_id},
  100. return_fields=["id", "name"])
  101. if not db_class:
  102. return {"errcode": 404, "mess": "班级不存在!"}
  103. # 判断是否存在同年级同学号
  104. existed = await crud_student.count(db,
  105. filters=[
  106. or_(
  107. and_(Student.school_id == info.school_id,
  108. Student.sno == info.sno),
  109. Student.phone == info.phone)
  110. ])
  111. if existed:
  112. return {"errcode": 400, "mess": "学号或手机号重复!"}
  113. # 开始创建
  114. obj_in = StudentInDB(**info.dict(by_alias=True),
  115. period=db_school.category,
  116. school_name=db_school.name,
  117. grade_name=db_grade.name,
  118. class_name=db_class.name,
  119. username=info.phone,
  120. password=hashed_password(info.phone[-6:]),
  121. creator_id=current_user.id,
  122. creator_name=current_user.username,
  123. editor_id=current_user.id,
  124. editor_name=current_user.username)
  125. db_obj = await crud_student.insert_one(db, obj_in)
  126. # 异步更新班级的学生数量
  127. bgtask.add_task(bgtask_update_class_teacher_student, db_class.id, "add", 0, 1)
  128. return {"data": db_obj}
  129. @router.get("/students/{sid}",
  130. response_model=StudentDetail,
  131. response_model_exclude_none=True,
  132. summary="学生详情")
  133. async def get_student(sid: int = Path(..., description="学生ID"),
  134. db: AsyncSession = Depends(get_async_db),
  135. current_user: Admin = Depends(get_current_user)):
  136. db_obj = await crud_student.find_one(db, filters={"id": sid})
  137. return {"data": db_obj}
  138. # 更新学生
  139. @router.put("/students/{sid}",
  140. response_model=StudentDetail,
  141. response_model_exclude_none=True,
  142. summary="更新学生")
  143. async def update_student(info: UpdateStudent,
  144. bgtask: BackgroundTasks,
  145. sid: int = Path(..., description="学生ID"),
  146. db: AsyncSession = Depends(get_async_db),
  147. current_user: Admin = Depends(get_current_user)):
  148. # 判断提交参数是否为空
  149. info_dict = info.dict(exclude_none=True)
  150. if not info_dict:
  151. return {"errcode": 400, "mess": "提交参数为空!"}
  152. # 判断学生是否存在
  153. db_obj = await crud_student.find_one(db, filters={"id": sid})
  154. if not db_obj:
  155. return {"errcode": 404, "mess": "学生不存在!"}
  156. # 判断学校是否存在
  157. if ("school_id" in info_dict) and (db_obj.school_id != info_dict["school_id"]):
  158. db_school = await crud_school.find_one(db,
  159. filters={"id": info.school_id},
  160. return_fields=["name", "category"])
  161. if not db_school:
  162. return {"errcode": 404, "mess": "学校不存在!"}
  163. else:
  164. info.school_name = db_school.name
  165. info.period = db_school.category
  166. # 判断年级是否存在
  167. if ("grade_id" in info_dict) and (db_obj.grade_id != info_dict["grade_id"]):
  168. db_grade = await crud_grade.find_one(db,
  169. filters={"id": info.grade_id},
  170. return_fields=["name"])
  171. if not db_grade:
  172. return {"errcode": 404, "mess": "年级不存在!"}
  173. else:
  174. info.grade_name = db_grade.name
  175. # 判断班级是否存在
  176. if ("class_id" in info_dict) and (db_obj.class_id != info_dict["class_id"]):
  177. db_class = await crud_class.find_one(db,
  178. filters={"id": info.class_id},
  179. return_fields=["name"])
  180. if not db_class:
  181. return {"errcode": 404, "mess": "班级不存在!"}
  182. else:
  183. info.class_name = db_class.name
  184. bgtask.add_task(bgtask_update_class_teacher_student, db_obj.class_id, "del", 0, 1)
  185. bgtask.add_task(bgtask_update_class_teacher_student, db_class.id, "add", 0, 1)
  186. # 判断手机号是否重复
  187. if ("phone" in info_dict) and (db_obj.phone != info_dict["phone"]):
  188. db_student = await crud_student.find_one(
  189. db, filters=[Student.id != sid, Student.phone == info.phone])
  190. if db_student:
  191. return {"errcode": 400, "mess": "手机号重复!"}
  192. info.username = info.phone[-6:]
  193. # 更新
  194. info.editor_id = current_user.id
  195. info.editor_name = current_user.username
  196. db_obj = await crud_student.update(db, db_obj, info)
  197. return {"data": db_obj}
  198. # 删除学生
  199. @router.delete("/students/{sid}",
  200. response_model=StudentDetail,
  201. response_model_exclude_none=True,
  202. summary="删除学生")
  203. async def delete_student(bgtask: BackgroundTasks,
  204. sid: Union[int,
  205. str] = Path(...,
  206. description="学生ID,批量删除传用逗号分隔ID(str),单个删除传ID(int)"),
  207. db: AsyncSession = Depends(get_async_db),
  208. current_user: Admin = Depends(get_current_user)):
  209. # 查询所有学生
  210. if isinstance(sid, int):
  211. deleted_count = 1
  212. _q = [Student.id == sid]
  213. else:
  214. sids = [int(x.strip()) for x in sid.split(',') if x.strip()]
  215. deleted_count = len(sids)
  216. _q = [Student.id.in_(sids)]
  217. total, db_objs = await crud_student.find_all(db, filters=_q)
  218. # 删除
  219. for item in db_objs:
  220. await crud_student.delete(db, obj_id=item.id)
  221. bgtask.add_task(bgtask_update_class_teacher_student, item.class_id, "del", student_amount=1)
  222. return {
  223. "data": total,
  224. "mess": "success" if total == deleted_count else f"成功:{total},失败: {deleted_count - total}"
  225. }
  226. # 批量导入学生
  227. @router.post("/students/bulk",
  228. response_model=StudentDetail,
  229. response_model_exclude_none=True,
  230. summary="批量导入学生")
  231. async def import_student(bgtask: BackgroundTasks,
  232. datafile: UploadFile = File(..., description="数据文件"),
  233. db: AsyncSession = Depends(get_async_db),
  234. current_user: Admin = Depends(get_current_user)):
  235. # 判断文件格式
  236. if not check_filetype(datafile.filename, ".xlsx"):
  237. return {"errcode": 400, "mess": "文件格式错误!"}
  238. # 把文件写入磁盘,再加载回来
  239. disk_file = os.path.join(settings.UPLOADER_PATH, datafile.filename)
  240. content = await datafile.read()
  241. with open(disk_file, "wb") as f:
  242. f.write(content)
  243. # 返回结果
  244. phones = []
  245. snos = []
  246. errors = []
  247. success = 0
  248. students = []
  249. counter = 0
  250. class_students = defaultdict(int)
  251. schools = {}
  252. grades = {}
  253. classes = {}
  254. # 使用openpyxl读取文件
  255. wb = load_workbook(disk_file)
  256. ws = wb.worksheets[0]
  257. for row in ws.iter_rows(min_row=2, max_col=ws.max_column, max_row=ws.max_row, values_only=True):
  258. row = await check_row(row, 9)
  259. if row is None: # 空行
  260. continue
  261. elif not row: # 字段不完整
  262. errors.append(f"第{counter}行: 某些单元格为空!")
  263. continue
  264. # 判断学校是否存在
  265. if row[6] not in schools:
  266. db_school = await crud_school.find_one(db,
  267. filters={"name": row[6]},
  268. return_fields=["id", "category"])
  269. if not db_school:
  270. errors.append(f"第{counter}行: 学校不存在!")
  271. continue
  272. else:
  273. schools[row[6]] = {"id": db_school.id, "category": db_school.category}
  274. # 判断年级是否存在
  275. grade_key = f"{row[6]}-{row[7]}"
  276. if grade_key not in grades:
  277. db_grade = await crud_grade.find_one(db,
  278. filters={
  279. "school_id": schools[row[6]]["id"],
  280. "name": row[7]
  281. },
  282. return_fields=["id"])
  283. if not db_grade:
  284. errors.append(f"第{counter}行: 年级不存在!")
  285. continue
  286. else:
  287. grades[grade_key] = db_grade.id
  288. # 判断是否存在同名学生
  289. if (row[5] in phones) or (row[1] in snos):
  290. errors.append(f"第{counter}行: 学号或手机号重复!")
  291. continue
  292. else:
  293. phones.append(row[5])
  294. snos.append(row[1])
  295. # 判断数据库是否存在重复的学号或手机号
  296. db_student = await crud_student.count(
  297. db,
  298. filters=[
  299. or_(and_(Student.school_id == schools[row[6]]["id"], Student.sno == row[1]),
  300. Student.phone == row[5])
  301. ])
  302. if db_student:
  303. errors.append(f"第{counter}行: 学号或手机号重复!")
  304. continue
  305. # 判断班级是否存在
  306. class_key = f"{grade_key}-{row[8]}"
  307. if class_key not in classes:
  308. db_class = await crud_class.find_one(db,
  309. filters={
  310. "school_id": schools[row[6]]["id"],
  311. "grade_id": grades[grade_key],
  312. "name": row[8]
  313. },
  314. return_fields=["id"])
  315. if not db_class:
  316. errors.append(f"第{counter}行: 班级不存在!")
  317. continue
  318. else:
  319. classes[class_key] = db_class.id
  320. class_students[classes[class_key]] += 1
  321. # 创建学生
  322. obj_in = StudentInDB(sno=row[1],
  323. username=str(row[5]).strip(),
  324. password=hashed_password(str(row[5]).strip()[-6:]),
  325. name=row[2],
  326. sex=1 if (not row[3] or row[3] == "男") else 0,
  327. age=row[4] if row[4] else 0,
  328. phone=row[5],
  329. sid=schools[row[6]]["id"],
  330. school_name=row[6],
  331. period=schools[row[6]]["category"],
  332. gid=grades[grade_key],
  333. grade_name=row[7],
  334. cid=classes[class_key],
  335. class_name=row[8],
  336. creator_id=current_user.id,
  337. creator_name=current_user.username,
  338. editor_id=current_user.id,
  339. editor_name=current_user.username)
  340. students.append(obj_in)
  341. success += 1
  342. counter += 1
  343. if counter == 50:
  344. try:
  345. await crud_student.insert_many(db, copy.deepcopy(students))
  346. except Exception as ex:
  347. print(f"[student] INSERT-ERROR: {str(ex)}, students: {students}")
  348. return {"errcode": 1, "mess": "入库失败,请联系技术人员解决!"}
  349. else:
  350. students.clear()
  351. counter = 0
  352. if counter:
  353. try:
  354. await crud_student.insert_many(db, students)
  355. except Exception as ex:
  356. print(f"[student] INSERT-ERROR: {str(ex)}, students: {students}")
  357. return {"errcode": 1, "mess": "入库失败,请联系技术人员解决!"}
  358. # 异步更新班级的教师数量
  359. for k, v in class_students.items():
  360. bgtask.add_task(bgtask_update_class_teacher_student, int(k), "add", student_amount=1)
  361. # 删除上传文件
  362. os.remove(disk_file)
  363. return {"data": {"success": success, "fail": len(errors), "errors": errors}}