teacher.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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, File, UploadFile
  8. from openpyxl import load_workbook
  9. from sqlalchemy import 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 common.const import SUBJECTS
  15. from core.config import settings
  16. from core.security import hashed_password
  17. from crud.school import crud_class, crud_school, crud_grade
  18. from crud.sysdata.role import crud_role
  19. from crud.user import crud_teacher
  20. from models.school import SchoolClass
  21. from models.user import Admin, Teacher
  22. from schemas.base import ReturnField
  23. from schemas.school.teacher import TeacherList, TeacherDetail, NewTeacher, TeacherInDB, UpdateTeacher
  24. from utils.depends import get_async_db, get_current_user
  25. router = APIRouter()
  26. # 教师列表
  27. @router.get("/teachers",
  28. response_model=TeacherList,
  29. response_model_exclude_none=True,
  30. summary="教师列表")
  31. async def get_teachers(page: Optional[int] = None,
  32. size: Optional[int] = None,
  33. name: str = Query("", description="教师姓名"),
  34. tid: int = Query(0, description="教师ID"),
  35. sid: int = Query(0, description="学校ID"),
  36. gid: int = Query(0, description="年级ID"),
  37. cid: str = Query(0, description="班级ID"),
  38. rid: int = Query(0, description="职务(角色ID)"),
  39. subject: str = Query("", description="任教科目"),
  40. order: str = Query("-id", description="排序字段,用逗号分隔,升降序以-判断,默认-created_at"),
  41. res: ReturnField = Query("", description="返回字段,默认列表展示字段, 自定义id,name,phone"),
  42. db: AsyncSession = Depends(get_async_db),
  43. current_user: Admin = Depends(get_current_user)):
  44. # 过滤条件
  45. _q = []
  46. if name:
  47. _q.append(Teacher.name.like(f"{name}%"))
  48. if tid:
  49. _q.append(Teacher.id == tid)
  50. if sid:
  51. _q.append(Teacher.school_id == sid)
  52. if gid:
  53. _q.append(Teacher.grade_id == gid)
  54. if cid:
  55. _q.append(Teacher.class_id == cid)
  56. if rid:
  57. _q.append(Teacher.role_id == rid)
  58. if subject:
  59. _q.append(Teacher.subject == subject)
  60. if ((page is not None) and page >= 1) and ((size is not None) and size >= 1):
  61. offset = (page - 1) * size
  62. else:
  63. offset = size = None
  64. # 排序字段
  65. order_fields = []
  66. if order:
  67. for x in order.split(","):
  68. field = x.strip()
  69. if field:
  70. if field.startswith("-"):
  71. order_fields.append(desc(getattr(Teacher, field[1:])))
  72. else:
  73. order_fields.append(asc(getattr(Teacher, field)))
  74. total, items = await crud_teacher.find_all(db,
  75. filters=_q,
  76. offset=offset,
  77. limit=size,
  78. order_by=order_fields,
  79. return_fields=res)
  80. for x in items:
  81. if not x.age:
  82. x.age = ""
  83. return {"total": total, "data": items}
  84. # 创建教师
  85. @router.post("/teachers",
  86. response_model=TeacherDetail,
  87. response_model_exclude_none=True,
  88. summary="创建教师")
  89. async def create_teacher(info: NewTeacher,
  90. bgtask: BackgroundTasks,
  91. db: AsyncSession = Depends(get_async_db),
  92. current_user: Admin = Depends(get_current_user)):
  93. # 判断学科是否正确
  94. if info.subject not in SUBJECTS:
  95. return {"errcode": 400, "mess": "教学科目错误!"}
  96. # 判断学校是否存在
  97. db_school = await crud_school.find_one(db,
  98. filters={"id": info.school_id},
  99. return_fields=["name", "category"])
  100. if not db_school:
  101. return {"errcode": 404, "mess": "学校不存在!"}
  102. # 判断年级是否存在
  103. db_grade = await crud_grade.find_one(db, filters={"id": info.grade_id}, return_fields=["name"])
  104. if not db_grade:
  105. return {"errcode": 404, "mess": "年级不存在!"}
  106. # 判断角色是否存在
  107. db_role = await crud_role.find_one(db, filters={"id": info.role_id}, return_fields=["name"])
  108. if not db_role:
  109. return {"errcode": 404, "mess": "角色不存在!"}
  110. # 判断是否存在同年级同名教师
  111. db_teacher = await crud_teacher.count(db, filters={"phone": info.phone})
  112. if db_teacher:
  113. return {"errcode": 400, "mess": "手机号重复!"}
  114. # 判断班级是否存在
  115. class_ids = [x.strip() for x in info.class_id.split(",") if x.strip()]
  116. total, db_class = await crud_class.find_all(db,
  117. filters=[SchoolClass.id.in_(class_ids)],
  118. return_fields=["name"])
  119. if total != len(class_ids):
  120. return {"errcode": 400, "mess": "班级不存在!"}
  121. # 开始创建
  122. obj_in = TeacherInDB(**info.dict(by_alias=True),
  123. period=db_school.category,
  124. school_name=db_school.name,
  125. grade_name=db_grade.name,
  126. class_name=",".join([x.name for x in db_class]),
  127. role_name=db_role.name,
  128. username=info.phone,
  129. password=hashed_password(info.phone[-6:]),
  130. creator_id=current_user.id,
  131. creator_name=current_user.username,
  132. editor_id=current_user.id,
  133. editor_name=current_user.username)
  134. db_obj = await crud_teacher.insert_one(db, obj_in)
  135. # 异步更新班级的教师数量
  136. for cid in class_ids:
  137. bgtask.add_task(bgtask_update_class_teacher_student, cid, "add", 1)
  138. return {"data": db_obj}
  139. @router.get("/teachers/{tid}",
  140. response_model=TeacherDetail,
  141. response_model_exclude_none=True,
  142. summary="教师详情")
  143. async def get_teacher(tid: int = Path(..., description="教师ID"),
  144. db: AsyncSession = Depends(get_async_db),
  145. current_user: Admin = Depends(get_current_user)):
  146. db_obj = await crud_teacher.find_one(db, filters={"id": tid})
  147. return {"data": db_obj}
  148. # 更新教师
  149. @router.put("/teachers/{tid}",
  150. response_model=TeacherDetail,
  151. response_model_exclude_none=True,
  152. summary="更新教师")
  153. async def update_teacher(info: UpdateTeacher,
  154. bgtask: BackgroundTasks,
  155. tid: int = Path(..., description="教师ID"),
  156. db: AsyncSession = Depends(get_async_db),
  157. current_user: Admin = Depends(get_current_user)):
  158. # 判断提交参数是否为空
  159. info_dict = info.dict(exclude_none=True)
  160. if not info_dict:
  161. return {"errcode": 400, "mess": "提交参数为空!"}
  162. # 判断教师是否存在
  163. db_obj = await crud_teacher.find_one(db, filters={"id": tid})
  164. if not db_obj:
  165. return {"errcode": 404, "mess": "教师不存在!"}
  166. # 判断手机号是否重复
  167. if ("phone" in info_dict) and (db_obj.phone != info_dict["phone"]):
  168. db_teacher = await crud_teacher.count(
  169. db, filters=[Teacher.id != tid, Teacher.phone == info.phone])
  170. if db_teacher:
  171. return {"errcode": 400, "mess": "手机号重复!"}
  172. info.username = info.phone[-6:]
  173. # 判断学校是否存在
  174. if ("school_id" in info_dict) and (db_obj.school_id != info_dict["school_id"]):
  175. db_school = await crud_school.find_one(db,
  176. filters={"id": info.school_id},
  177. return_fields=["name", "category"])
  178. if not db_school:
  179. return {"errcode": 404, "mess": "学校不存在!"}
  180. else:
  181. info.school_name = db_school.name
  182. info.period = db_school.category
  183. # 判断年级是否存在
  184. if ("grade_id" in info_dict) and (db_obj.grade_id != info_dict["grade_id"]):
  185. db_grade = await crud_grade.find_one(db,
  186. filters={"id": info.grade_id},
  187. return_fields=["name"])
  188. if not db_grade:
  189. return {"errcode": 404, "mess": "年级不存在!"}
  190. else:
  191. info.grade_name = db_grade.name
  192. # 判断班级是否存在
  193. if ("class_id" in info_dict) and (db_obj.class_id != info_dict["class_id"]):
  194. original_class_set = set(db_obj.class_id.split(","))
  195. new_class_set = set([x.strip() for x in info.class_id.split(",") if x.strip()])
  196. total, db_class = await crud_class.find_all(db,
  197. filters=[SchoolClass.id.in_(new_class_set)],
  198. return_fields=["name"])
  199. if total != len(new_class_set):
  200. return {"errcode": 400, "mess": "班级不存在!"}
  201. else:
  202. info.class_name = ",".join([x.name for x in db_class])
  203. # 待增加教师的班级
  204. add_diff_set = new_class_set - original_class_set
  205. for cid in add_diff_set:
  206. bgtask.add_task(bgtask_update_class_teacher_student, cid, "add", 1, 0)
  207. # 待减少教师的班级
  208. del_diff_set = original_class_set - new_class_set
  209. for cid in del_diff_set:
  210. bgtask.add_task(bgtask_update_class_teacher_student, cid, "del", 1, 0)
  211. # 判断角色是否存在
  212. if ("role_id" in info_dict) and (db_obj.role_id != info_dict["role_id"]):
  213. db_role = await crud_role.find_one(db, filters={"id": info.role_id}, return_fields=["name"])
  214. if not db_role:
  215. return {"errcode": 404, "mess": "角色不存在!"}
  216. else:
  217. info.role_name = db_role.name
  218. # 判断学科是否正确
  219. if ("subject" in info_dict) and (db_obj.subject != info_dict["subject"]):
  220. if info_dict["subject"] not in SUBJECTS:
  221. return {"errcode": 400, "mess": "教学科目错误!"}
  222. # 更新
  223. info.editor_id = current_user.id
  224. info.editor_name = current_user.username
  225. db_obj = await crud_teacher.update(db, db_obj, info)
  226. return {"data": db_obj}
  227. # 删除教师
  228. @router.delete("/teachers/{tid}",
  229. response_model=TeacherDetail,
  230. response_model_exclude_none=True,
  231. summary="删除教师")
  232. async def delete_teacher(bgtask: BackgroundTasks,
  233. tid: Union[int,
  234. str] = Path(...,
  235. description="教师ID,批量删除传用逗号分隔ID(str),单个删除传ID(int)"),
  236. db: AsyncSession = Depends(get_async_db),
  237. current_user: Admin = Depends(get_current_user)):
  238. # 查询所有教师
  239. if isinstance(tid, int):
  240. deleted_count = 1
  241. _q = [Teacher.id == tid]
  242. else:
  243. tids = [int(x.strip()) for x in tid.split(',') if x.strip()]
  244. deleted_count = len(tids)
  245. _q = [Teacher.id.in_(tids)]
  246. total, db_objs = await crud_teacher.find_all(db, filters=_q)
  247. # 删除
  248. for item in db_objs:
  249. await crud_teacher.delete(db, obj_id=item.id)
  250. # 异步更新班级的教师数量
  251. for cid in item.class_id.split(","):
  252. bgtask.add_task(bgtask_update_class_teacher_student, cid, "del", 1)
  253. return {
  254. "data": total,
  255. "mess": "success" if total == deleted_count else f"成功:{total},失败: {deleted_count - total}"
  256. }
  257. # 批量导入教师
  258. @router.post("/teachers/bulk",
  259. response_model=TeacherDetail,
  260. response_model_exclude_none=True,
  261. summary="批量导入教师")
  262. async def import_teacher(bgtask: BackgroundTasks,
  263. datafile: UploadFile = File(..., description="数据文件"),
  264. db: AsyncSession = Depends(get_async_db),
  265. current_user: Admin = Depends(get_current_user)):
  266. # 判断文件格式
  267. if not check_filetype(datafile.filename, ".xlsx"):
  268. return {"errcode": 400, "mess": "文件格式错误!"}
  269. # 把文件写入磁盘,再加载回来
  270. disk_file = os.path.join(settings.UPLOADER_PATH, datafile.filename)
  271. content = await datafile.read()
  272. with open(disk_file, "wb") as f:
  273. f.write(content)
  274. # 返回结果
  275. phones = []
  276. errors = []
  277. success = 0
  278. teachers = []
  279. class_teachers = defaultdict(int)
  280. schools = {}
  281. grades = {}
  282. classes = {}
  283. roles = {}
  284. # 使用openpyxl读取文件
  285. wb = load_workbook(disk_file)
  286. ws = wb.worksheets[0]
  287. counter = 1
  288. for row in ws.iter_rows(min_row=2, max_col=ws.max_column, max_row=ws.max_row, values_only=True):
  289. row = await check_row(row, 10)
  290. if row is None: # 空行
  291. continue
  292. elif not row: # 字段不完整
  293. errors.append(f"第{counter}行: 某些单元格为空!")
  294. continue
  295. # 判断学校是否存在
  296. if row[5] not in schools:
  297. db_school = await crud_school.find_one(db,
  298. filters={"name": row[5]},
  299. return_fields=["id", "category"])
  300. if not db_school:
  301. errors.append(f"第{counter}行: 学校不存在!")
  302. continue
  303. schools[row[5]] = {"id": db_school.id, "category": db_school.category}
  304. # 判断年级是否存在
  305. grade_key = f"{row[5]}-{row[6]}"
  306. if grade_key not in grades:
  307. db_grade = await crud_grade.find_one(db,
  308. filters={
  309. "school_id": schools[row[5]]["id"],
  310. "name": row[6]
  311. },
  312. return_fields=["id"])
  313. if not db_grade:
  314. errors.append(f"第{counter}行: 年级不存在!")
  315. continue
  316. grades[grade_key] = db_grade.id
  317. # 判断角色是否存在
  318. if row[8] not in roles:
  319. db_role = await crud_role.find_one(db, filters={"name": row[8]}, return_fields=["id"])
  320. if not db_role:
  321. errors.append(f"第{counter}行: 角色不存在!")
  322. continue
  323. else:
  324. roles[row[8]] = db_role.id
  325. # 判断手机号是否重复
  326. if row[4] in phones: # 本次导入文件内是否存在重复手机号
  327. errors.append(f"第{counter}行: 手机号重复!")
  328. continue
  329. else:
  330. phones.append(row[4])
  331. # 判断数据库中是否存在重复手机号
  332. existed = await crud_teacher.count(db, filters={"phone": row[4]})
  333. if existed:
  334. errors.append(f"第{counter}行: 手机号重复!")
  335. continue
  336. # 判断学科是否正确
  337. if row[9] not in SUBJECTS:
  338. errors.append(f"第{counter}行: 教学科目错误!")
  339. continue
  340. # 判断班级是否存在
  341. class_names = []
  342. class_ids = []
  343. for x in row[7].split(";"):
  344. name = x.strip()
  345. if name:
  346. class_key = f"{row[5]}-{row[6]}-{name}"
  347. if class_key not in classes:
  348. class_names.append(name)
  349. else:
  350. class_ids.append(classes[class_key])
  351. class_teachers[classes[class_key]] += 1
  352. if class_names:
  353. total, db_classes = await crud_class.find_all(
  354. db,
  355. filters=[
  356. SchoolClass.school_id == schools[row[5]]["id"],
  357. SchoolClass.grade_id == grades[grade_key],
  358. SchoolClass.name.in_(class_names)
  359. ],
  360. return_fields=["id", "name"])
  361. if total != len(class_names):
  362. errors.append(f"第{counter}行: 班级不存在!")
  363. continue
  364. else:
  365. for x in db_classes:
  366. key = f"{row[5]}-{row[6]}-{x.name}"
  367. classes[key] = x.id
  368. class_ids.append(str(x.id))
  369. class_teachers[x.id] += 1
  370. # 创建教师对象
  371. phone = str(row[4])
  372. obj_in = TeacherInDB(username=phone,
  373. password=hashed_password(phone[-6:]),
  374. name=row[1] if row[1] else "",
  375. sex=1 if row[2] == "男" else 0,
  376. age=row[3] or 0,
  377. phone=phone,
  378. sid=schools[row[5]]["id"],
  379. school_name=row[5],
  380. period=schools[row[5]]["category"],
  381. gid=grades[f"{row[5]}-{row[6]}"],
  382. grade_name=row[6],
  383. cid=",".join(class_ids),
  384. class_name=row[7],
  385. rid=roles[row[8]],
  386. role_name=row[8],
  387. subject=row[9],
  388. creator_id=current_user.id,
  389. creator_name=current_user.username,
  390. editor_id=current_user.id,
  391. editor_name=current_user.username)
  392. teachers.append(obj_in)
  393. success += 1
  394. counter += 1
  395. if counter == 50:
  396. try:
  397. await crud_teacher.insert_many(db, copy.deepcopy(teachers))
  398. except Exception as ex:
  399. print(f"[teacher] INSERT-ERROR: {str(ex)}, teachers: {teachers}")
  400. return {"errcode": 1, "mess": "入库失败,请联系技术人员解决!"}
  401. else:
  402. teachers.clear()
  403. counter = 0
  404. if counter:
  405. try:
  406. await crud_teacher.insert_many(db, teachers)
  407. except Exception as ex:
  408. print(f"[teacher] INSERT-ERROR: {str(ex)}, teachers: {teachers}")
  409. return {"errcode": 1, "mess": "入库失败,请联系技术人员解决!"}
  410. # 删除上传文件
  411. os.remove(disk_file)
  412. # 异步更新班级的教师数量
  413. for k, v in class_teachers.items():
  414. bgtask.add_task(bgtask_update_class_teacher_student, int(k), "add", v)
  415. return {"data": {"success": success, "fail": len(errors), "errors": errors}}