tasks.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import json
  4. import os
  5. from typing import List, Dict, Any
  6. from pydantic import Field
  7. from sqlalchemy import update, text, func, select
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from common.const import GRADE_ORDER, OBJECTIVE_QUESTION_TYPES
  10. from crud.base import ModelType
  11. from crud.marktask import crud_task, crud_student_task, crud_student_answer
  12. from crud.problem import crud_class_error_statistic, crud_student_push_record,\
  13. crud_student_error_statistic,crud_class_push_record
  14. from crud.school import crud_grade, crud_system, crud_class
  15. from crud.user import crud_teacher, crud_student
  16. from db.asyncsession import LocalAsyncSession
  17. from models.marktask import StudentMarkTask, StudentAnswer
  18. from models.problem import ClassErrorQuestion, StudentErrorPushRecord, StudentErrorQuestion, ClassErrorPushRecord
  19. from models.school import SchoolGrade, SchoolClass
  20. from models.user import Teacher, Student
  21. from schemas.app.task import BgUpdateTaskReviewInfo, BgUpdateStudentTaskInfo
  22. from schemas.problem import StudentErrorPushRecordInDB, UpdateClassErrorQuestionItem, UpdateStudentErrorQuestionItem
  23. from schemas.school.school import NewGrade
  24. async def bgtask_delete_related_object(sid: int = 0, gid: int = 0, cid: int = 0):
  25. db = LocalAsyncSession()
  26. # 根据学校删除年级、班级、教师、学生等
  27. if sid and (not gid) and (not cid):
  28. # 1、删除年级
  29. await crud_grade.delete(db, where_clauses=[SchoolGrade.school_id == sid])
  30. # 2、删除班级
  31. await crud_class.delete(db, where_clauses=[SchoolClass.school_id == sid])
  32. # TODO: 删除教师、学生、作业、考试、阅卷任务等
  33. await crud_teacher.delete(db, where_clauses=[Teacher.school_id == sid])
  34. await crud_student.delete(db, where_clauses=[Student.school_id == sid])
  35. elif gid and (not cid): # 如果是年级被删除,则删除该年级下的所有班级
  36. await crud_class.delete(db, where_clauses=[SchoolClass.grade_id == gid])
  37. # TODO: 删除学生、作业、考试、阅卷任务等
  38. await crud_student.delete(db, where_clauses=[Student.grade_id == gid])
  39. elif cid: # 如果班级被删除了,则删除该班级下的所有学生
  40. # TODO: 删除学生、作业、考试、阅卷任务等
  41. await crud_student.delete(db, where_clauses=[Student.class_id == cid])
  42. else:
  43. pass
  44. async def bgtask_delete_remote_file(filepath: str):
  45. urls = [x.strip() for x in filepath.split(";")]
  46. try:
  47. for url in urls:
  48. os.remove(url)
  49. except Exception as ex:
  50. print(f"[Error] Delete file: {str(ex)}")
  51. async def bgtask_rank_score(task_id: int = Field(..., description="阅卷任务ID")):
  52. """
  53. 更新学生排名
  54. """
  55. db = LocalAsyncSession()
  56. # 根据 task_id 查询 StudentMarkTask
  57. stmt = f"""SELECT id,
  58. CASE
  59. WHEN @prevRank = score THEN @curRank
  60. WHEN @prevRank := score THEN @curRank := @curRank + 1
  61. END AS rank
  62. FROM { StudentMarkTask.__tablename__ },
  63. (SELECT @curRank :=0, @prevRank := NULL) rb
  64. WHERE task_id={ task_id } AND is_completed=true AND score>0
  65. ORDER BY score desc;
  66. """
  67. current_rank = 1
  68. ranks = await db.execute(stmt)
  69. for item in ranks:
  70. current_rank += 1
  71. stmt = (update(StudentMarkTask).where(StudentMarkTask.id == item[0]).values(rank=item[1]))
  72. await crud_student_task.execute_v2(db, stmt)
  73. # 为0分记录设置排名
  74. stmt = (update(StudentMarkTask).where(StudentMarkTask.task_id == task_id,
  75. StudentMarkTask.score == 0).values(rank=current_rank))
  76. await crud_student_task.execute_v2(db, stmt)
  77. print(f"Calculate Rank OK!!")
  78. async def bgtask_update_marktask(task_id: int, smt_has_marked: bool, smt_passed: bool,
  79. smt_score: float, diff_score: float):
  80. """
  81. 更新阅卷任务
  82. smt_has_marked:是否已被批阅过?
  83. smt_passed:在更新之前是否及格?
  84. smt_score:StudentMarkTask的最终分数
  85. diff_score: 重复批阅时的分数差
  86. """
  87. print("Async Update MarkTask Starting!")
  88. print(
  89. f"the params: task_id={task_id}, smt_has_marked={smt_has_marked},smt_passed={smt_passed}, smt_score={smt_score},diff_score={diff_score} "
  90. )
  91. db = LocalAsyncSession()
  92. # 查询MarkTask是否存在
  93. db_obj = await crud_task.find_one(db, filters={"id": task_id})
  94. # 更新已批阅人数 / 及格人数
  95. # 及格人数更新逻辑
  96. # 1、如果StudentMarkTask本身是及格的,当新分数 <60 时,及格人数 -1;
  97. # 2、如果StudentMarkTask本身是不及格的,当新分数 >=60 时,及格人数 +1;
  98. mt_marked_amount = db_obj.marked_amount
  99. pass_amount = db_obj.pass_amount
  100. if not smt_has_marked:
  101. mt_marked_amount += 1 # 批阅人数
  102. if smt_score >= 60:
  103. pass_amount = db_obj.pass_amount + 1
  104. else:
  105. if smt_passed:
  106. if smt_score < 60:
  107. pass_amount = db_obj.pass_amount - 1
  108. else:
  109. if smt_score >= 60:
  110. pass_amount = db_obj.pass_amount + 1
  111. print("------------------")
  112. print(f"the mt_marked_amount={mt_marked_amount}")
  113. print(f"the pass_amount={pass_amount}")
  114. pass_rate = round(pass_amount / mt_marked_amount, 2)
  115. print(f"the pass_rate={pass_rate}")
  116. # 最高分 / 最低分
  117. stmt = select(func.max(StudentMarkTask.score), func.min(StudentMarkTask.score)).select_from(StudentMarkTask) \
  118. .where(StudentMarkTask.task_id == task_id, StudentMarkTask.is_completed == 1)
  119. high_score, low_score = list(await crud_student_task.execute_v2(db, stmt))[0]
  120. print("the high/low score: ", high_score, low_score)
  121. # 平均分
  122. avg_score = round((db_obj.avg_score * db_obj.marked_amount + diff_score) / mt_marked_amount, 2)
  123. # 批阅状态,0=未开始,1=进行中,2=已完成
  124. status = 2 if mt_marked_amount == db_obj.student_amount else 1
  125. # 更新
  126. mt = BgUpdateTaskReviewInfo(marked_amount=mt_marked_amount,
  127. high_score=high_score,
  128. low_score=low_score,
  129. avg_score=avg_score,
  130. pass_amount=pass_amount,
  131. pass_rate=pass_rate,
  132. status=status)
  133. await crud_task.update(db, db_obj, mt)
  134. # 更新StudentMarkTask排名
  135. # 1、如果StudentMarkTask未被批阅完成:
  136. # 1.1 增加MarkTask的批阅数量;
  137. # 1.2 当新增后的批阅数量等于学生数量时,表示MarkTask被批阅完成,则开始更新排名;
  138. # 2、如果StudentMarkTask批阅已完成,则属于重复批阅,如果两次批阅间存在分数差,则更新排名;
  139. if mt_marked_amount == db_obj.student_amount: # MarkTask批阅完成时,更新排名
  140. await bgtask_rank_score(task_id)
  141. else:
  142. if diff_score and db_obj.status == 2: # 如果重复批阅有分数差,且MarkTask已批阅完成时,更新排名
  143. await bgtask_rank_score(task_id)
  144. print("Async Update MarkTask Successfully!")
  145. async def bgtask_update_student_marktask(task_id: int = Field(..., description="学生阅卷任务ID"),
  146. qtype: int = Field(..., description="试题类型,0:客观题,1: 主观题"),
  147. score: int = Field(..., description="多次批阅之间的得分差"),
  148. has_marked: bool = Field(..., description="是否已被批阅过"),
  149. editor_id: int = Field(..., description="最后编辑人ID"),
  150. editor_name: str = Field(..., description="最后编辑人姓名")):
  151. """
  152. 数据更新流程:
  153. 1、根据task_id更新StudentMarkTask,
  154. 2、如果该学生的所有试题已经被批阅完,则根据StudentMarkTask.task_id 更新 MarkTask 的最高分/最低分/平均分...等信息。
  155. 3、如果MarkTask批阅完成,则对StudentMarkTask进行排名。
  156. """
  157. print("Async Update StudentMarkTask Starting!")
  158. # new db session
  159. db = LocalAsyncSession()
  160. # ------------------ 更新学生阅卷任务-----------------------------------
  161. student_task = await crud_student_task.find_one(db, filters={"id": task_id})
  162. has_completed = student_task.is_completed # 原始批阅状态
  163. smt_is_passed = True if student_task.score >= 60 else False # 是否及格
  164. # 如果试题属于重复批阅,则不增加批阅数量
  165. if not has_marked:
  166. smt_marked_amount = student_task.marked_amount + 1
  167. else:
  168. smt_marked_amount = student_task.marked_amount
  169. # 学生任务更新信息
  170. smt_is_completed = smt_marked_amount == student_task.question_amount
  171. smt = BgUpdateStudentTaskInfo(marked_amount=smt_marked_amount,
  172. is_completed=smt_is_completed,
  173. score=student_task.score + score,
  174. editor_id=editor_id,
  175. editor_name=editor_name)
  176. # 按题型统计得分
  177. if qtype not in OBJECTIVE_QUESTION_TYPES:
  178. smt.subjective_score = student_task.subjective_score + score
  179. else:
  180. smt.objective_score = student_task.objective_score + score
  181. # 开始更新
  182. student_task = await crud_student_task.update(db, student_task, smt)
  183. # ------------------ 更新学生阅卷任务结束 ---------------------------
  184. # 如果单个学生被批阅完成,更新MarkTask信息
  185. if smt_is_completed:
  186. diff_score = student_task.score if not has_completed else score
  187. await bgtask_update_marktask(task_id=student_task.task_id,
  188. smt_has_marked=has_completed,
  189. smt_passed=smt_is_passed,
  190. smt_score=student_task.score,
  191. diff_score=diff_score)
  192. print("Async Update StudentMarkTask Successfully!")
  193. async def bgtask_create_grade(sid: int, category: int):
  194. db = LocalAsyncSession()
  195. # 年级列表
  196. school_system = await crud_system.find_one(db,
  197. filters={"id": category},
  198. return_fields={"grades"})
  199. grade_set = set(school_system.grades.split(","))
  200. # 查询学校是否存在
  201. _, db_grades = await crud_grade.find_all(db, filters={"school_id": sid})
  202. db_grade_set = set([x.name for x in db_grades])
  203. diff_grades = grade_set - db_grade_set
  204. new_grades = [NewGrade(sid=sid, name=x, order=GRADE_ORDER[x]) for x in diff_grades]
  205. try:
  206. await crud_grade.insert_many(db, new_grades)
  207. except Exception as ex:
  208. print(f"AutoCreateGradeError: {str(ex)}")
  209. return
  210. async def bgtask_delete_student_task_question(task_id: int):
  211. db = LocalAsyncSession()
  212. # 查询学生任务ID列表
  213. _, student_tasks = await crud_student_task.find_all(db,
  214. filters={"task_id": task_id},
  215. return_fields=["id"])
  216. student_task_ids = ",".join([str(x.id) for x in student_tasks])
  217. # 删除学生任务和答题
  218. await crud_student_task.delete(db, where_clauses=[StudentMarkTask.task_id == task_id])
  219. await crud_student_answer.delete(db, where_clauses=[StudentAnswer.task_id == task_id])
  220. # 删除学生错题统计
  221. print(8888888888888888)
  222. await crud_student_error_statistic.delete(
  223. db, where_clauses=[StudentErrorQuestion.task_id == task_id])
  224. # 删除班级错题统计
  225. print(9999999999999999)
  226. await crud_class_error_statistic.delete(db,
  227. where_clauses=[ClassErrorQuestion.task_id == task_id])
  228. async def bgtask_update_class_teacher_student(cid: int,
  229. op: str = "add",
  230. teacher_amount: int = 0,
  231. student_amount: int = 0):
  232. """
  233. 更新班级的教师数量和学生数量, op: 操作类型,add=增加, del=减少
  234. """
  235. db = LocalAsyncSession()
  236. if op == "del":
  237. teacher_amount = -teacher_amount
  238. student_amount = -student_amount
  239. stmt = (update(SchoolClass).where(SchoolClass.id == cid).values(
  240. teacher_amount=SchoolClass.teacher_amount + teacher_amount,
  241. student_amount=SchoolClass.student_amount + student_amount))
  242. await crud_class.execute_v2(db, stmt)
  243. print(
  244. f"Async Update Class OK! student_amount={student_amount}, teacher_amount={teacher_amount}")
  245. async def update_student_error_statistic(student_id: int, task_type: str, error_count: int):
  246. """
  247. 更新学生错题统计数据
  248. """
  249. # new db session
  250. db = LocalAsyncSession()
  251. db_error = await crud_student_error_statistic.find_one(db, filters={"student_id": student_id})
  252. update_dict = {"total_errors": db_error.total_errors + error_count}
  253. if task_type == "work":
  254. key = "work_error_count"
  255. else:
  256. key = "exam_error_count"
  257. update_dict[key] = getattr(db_error, key) + error_count
  258. update_dict["error_ratio"] = round(update_dict["total_errors"] / db_error.total_questions * 100,
  259. 2)
  260. # 更新
  261. item = UpdateStudentErrorQuestionItem(**update_dict)
  262. await crud_student_error_statistic.update(db, db_error, item)
  263. # 更新错题统计数据
  264. async def bgtask_update_class_error_statistic(task_id: int, class_id: int, qid: List[int],
  265. count: int, old_score: float, new_score: float,
  266. has_marked: bool):
  267. # new db session
  268. db = LocalAsyncSession()
  269. # update
  270. db_error = await crud_class_error_statistic.find_one(db,
  271. filters={
  272. "task_id": task_id,
  273. "class_id": class_id,
  274. "qid": qid
  275. })
  276. update_dict = {}
  277. print("Async Update Class-Error-Statistic Starting!")
  278. # 如果是错题,则更新错题数量和错题率
  279. if count:
  280. error_count = db_error.error_count + count
  281. error_ratio = round(error_count / db_error.student_count * 100, 2)
  282. update_dict["error_count"] = error_count
  283. update_dict["error_ratio"] = error_ratio
  284. # 更新答案分布
  285. if not db_error.answer_dist:
  286. answer_dist = {}
  287. else:
  288. answer_dist = json.loads(db_error.answer_dist)
  289. # 更新分数的人数
  290. if old_score != 0:
  291. answer_dist[old_score] -= 1
  292. if new_score not in answer_dist:
  293. answer_dist[new_score] = 1
  294. else:
  295. answer_dist[new_score] += 1
  296. if answer_dist:
  297. update_dict["answer_dist"] = json.dumps(answer_dist)
  298. # 更新
  299. if update_dict:
  300. try:
  301. item = UpdateClassErrorQuestionItem(**update_dict)
  302. await crud_class_error_statistic.update(db, db_error, item)
  303. except Exception as ex:
  304. print(f"UpdateClassError: {ex}")
  305. print("Async Update Class-Error-Statistic Successfully!")
  306. # 批量更新错题统计数据
  307. async def bgtask_batch_update_class_error_statistic(task_id: int, class_id: int,
  308. questions: List[Dict[str, Any]]):
  309. """
  310. 批量更新班级错题统计
  311. :param task_id: 阅卷任务ID, int
  312. :param class_id: 班级ID, int
  313. :param questions: 错题统计数据,list,形式:[{"qid": 1, "answer": "A"},...]
  314. """
  315. # new db session
  316. db = LocalAsyncSession()
  317. for q in questions:
  318. # update
  319. db_error = await crud_class_error_statistic.find_one(db,
  320. filters={
  321. "task_id": task_id,
  322. "class_id": class_id,
  323. "qid": q["qid"]
  324. })
  325. update_dict = {}
  326. print("Async Update[batch] Class-Error-Statistic Starting!")
  327. # 如果是错题,则更新错题数量和错题率
  328. error_count = db_error.error_count + 1
  329. error_ratio = round(error_count / db_error.student_count * 100, 2)
  330. update_dict["error_count"] = error_count
  331. update_dict["error_ratio"] = error_ratio
  332. # 更新答案分布
  333. answer_dist = {}
  334. if not db_error.answer_dist:
  335. answer_dist[q["answer"]] = 1
  336. else:
  337. answer_dist = json.loads(db_error.answer_dist)
  338. answer_dist[q["answer"]] = answer_dist.get(q["answer"], 0) + 1
  339. if answer_dist:
  340. update_dict["answer_dist"] = json.dumps(answer_dist)
  341. # 更新
  342. if update_dict:
  343. try:
  344. item = UpdateClassErrorQuestionItem(**update_dict)
  345. await crud_class_error_statistic.update(db, db_error, item)
  346. except Exception as ex:
  347. print(f"UpdateClassError: {ex}")
  348. print("Async Update[batch] Class-Error-Statistic Successfully!")
  349. # 创建学生错题推送记录
  350. async def bgtask_create_student_push_record(task: ModelType, db_class: ModelType,
  351. total_question: int, push_record_id: int,
  352. students: List[ModelType], current_user: Teacher):
  353. db = LocalAsyncSession()
  354. # 学生推送列表
  355. push_records = []
  356. # 查询错题ID
  357. total, db_errors = await crud_class_error_statistic.find_all(
  358. db,
  359. filters=[
  360. ClassErrorQuestion.task_id == task.id,
  361. ClassErrorQuestion.class_id == db_class.id,
  362. ClassErrorQuestion.error_count > 0,
  363. ],
  364. return_fields=["qid"])
  365. push_error_ids = [x.qid for x in db_errors]
  366. # 为每个学生创建推送记录
  367. for stu in students:
  368. obj_in = StudentErrorPushRecordInDB(student_id=stu.id,
  369. student_sno=stu.sno,
  370. student_name=stu.name,
  371. task_id=task.id,
  372. task_name=task.name,
  373. task_type=task.mtype,
  374. question_count=total_question,
  375. push_record_id=push_record_id,
  376. push_error_count=total,
  377. push_error_ids=push_error_ids,
  378. creator_id=current_user.id,
  379. creator_name=current_user.name,
  380. editor_id=current_user.id,
  381. editor_name=current_user.name)
  382. push_records.append(obj_in)
  383. await crud_student_push_record.insert_many(db, push_records)
  384. # 克隆学生错题推送记录
  385. async def bgtask_clone_student_push_record(db: AsyncSession, cls_prd: ModelType,
  386. students: List[ModelType]):
  387. # 从班级推送记录中查出一条记录
  388. db_prd = await crud_student_push_record.find_one(db, filters={"push_record_id": cls_prd.id})
  389. push_records = []
  390. for stu in students:
  391. obj_in = StudentErrorPushRecordInDB(student_id=stu.id,
  392. student_sno=stu.sno,
  393. student_name=stu.name,
  394. task_id=db_prd.task_id,
  395. task_name=db_prd.task_name,
  396. task_type=db_prd.task_type,
  397. question_count=db_prd.total_question,
  398. push_record_id=db_prd.push_record_id,
  399. push_error_count=db_prd.total,
  400. push_error_ids=db_prd.push_error_ids,
  401. creator_id=db_prd.creator_id,
  402. creator_name=db_prd.creator_name,
  403. editor_id=db_prd.editor_id,
  404. editor_name=db_prd.editor_name)
  405. push_records.append(obj_in)
  406. await crud_student_push_record.insert_many(db, push_records)
  407. # 删除学生错题推送记录
  408. async def bgtask_delete_student_push_record(db: AsyncSession,
  409. cls_prid: int,
  410. students: List[int] = None):
  411. _q = [StudentErrorPushRecord.push_record_id == cls_prid]
  412. if students:
  413. _q = [StudentErrorPushRecord.student_id.in_(students)]
  414. try:
  415. await crud_student_push_record.delete(db, where_clauses=_q)
  416. except Exception as ex:
  417. print(f"[ERROR] 删除学生推送列表失败! 错误信息: {str(ex)}")