ExamPaperService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. using Furion.DatabaseAccessor.Extensions;
  2. using YBEE.EQM.Core;
  3. namespace YBEE.EQM.Application;
  4. /// <summary>
  5. /// 试卷管理服务
  6. /// </summary>
  7. public class ExamPaperService : IExamPaperService, ITransient
  8. {
  9. private readonly IRepository<ExamPaper> _repExamPaper;
  10. private readonly ISysRoleService _sysRoleService;
  11. private readonly IExportExcelService _exportExcelService;
  12. public ExamPaperService(IRepository<ExamPaper> repExamPaper, ISysRoleService sysRoleService, IExportExcelService exportExcelService)
  13. {
  14. _repExamPaper = repExamPaper;
  15. _sysRoleService = sysRoleService;
  16. _exportExcelService = exportExcelService;
  17. }
  18. /// <summary>
  19. /// 按监测计划初始化试卷
  20. /// </summary>
  21. /// <param name="input"></param>
  22. /// <returns></returns>
  23. public async Task BatchInit(ExamPaperBatchInitInput input)
  24. {
  25. var examCourses = await _repExamPaper.Change<ExamCourse>().DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).ProjectToType<ExamCourseOutput>().ToListAsync();
  26. if (!examCourses.Any())
  27. {
  28. }
  29. var eitems = await _repExamPaper.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).ToListAsync();
  30. var ecs = from t in examCourses
  31. where !(from p in eitems select new { p.GradeId, p.CourseId }).Contains(new { t.GradeId, t.CourseId })
  32. select t;
  33. if (ecs.Any())
  34. {
  35. foreach (var ec in ecs)
  36. {
  37. var p = new ExamPaper
  38. {
  39. ExamPlanId = input.ExamPlanId,
  40. ExamGradeId = ec.ExamGradeId,
  41. GradeId = ec.GradeId,
  42. ExamCourseId = ec.Id,
  43. CourseId = ec.CourseId,
  44. Score = ec.TotalScore,
  45. Name = $"{ec.ExamGrade.Grade.FullName}{ec.Course.Name}",
  46. };
  47. await _repExamPaper.InsertNowAsync(p);
  48. if (ec.ScoreReportConfig.HeaderColumnNames.Count > ec.ScoreReportConfig.MinorQuestionColumnIndex + 1)
  49. {
  50. List<ExamPaperQuestionMinor> mitems = new();
  51. for (int i = ec.ScoreReportConfig.MinorQuestionColumnIndex; i < ec.ScoreReportConfig.HeaderColumnNames.Count; i++)
  52. {
  53. var columnName = ec.ScoreReportConfig.HeaderColumnNames[i];
  54. var pmi = new ExamPaperQuestionMinor()
  55. {
  56. ExamPaperId = p.Id,
  57. Sequence = i - ec.ScoreReportConfig.MinorQuestionColumnIndex + 1,
  58. ColumnName = columnName,
  59. Name = columnName.Replace("第", "").Replace("题", "").Trim(),
  60. };
  61. mitems.Add(pmi);
  62. }
  63. await _repExamPaper.Change<ExamPaperQuestionMinor>().InsertNowAsync(mitems);
  64. }
  65. }
  66. }
  67. }
  68. /// <summary>
  69. /// 分配双向细目表编制人
  70. /// </summary>
  71. /// <param name="input"></param>
  72. /// <returns></returns>
  73. public async Task AssignTwclWriter(AssignExamPaperWriterInput input)
  74. {
  75. var items = await _repExamPaper.Where(t => input.Ids.Contains(t.Id)).ToListAsync();
  76. foreach (var item in items)
  77. {
  78. item.TwclSysUserId = input.WriterSysUserId;
  79. await item.UpdateIncludeNowAsync(new[] { nameof(item.TwclSysUserId) });
  80. }
  81. }
  82. /// <summary>
  83. /// 分配问题建议撰写人
  84. /// </summary>
  85. /// <param name="input"></param>
  86. /// <returns></returns>
  87. public async Task AssignSuggestionWriter(AssignExamPaperWriterInput input)
  88. {
  89. var items = await _repExamPaper.Where(t => input.Ids.Contains(t.Id)).ToListAsync();
  90. foreach (var item in items)
  91. {
  92. item.SuggestionSysUserId = input.WriterSysUserId;
  93. await item.UpdateIncludeNowAsync(new[] { nameof(item.SuggestionSysUserId) });
  94. }
  95. }
  96. /// <summary>
  97. /// 根据ID获取试卷详情
  98. /// </summary>
  99. /// <param name="id"></param>
  100. /// <returns></returns>
  101. public async Task<ExamPaperOutput> GetById(int id)
  102. {
  103. var item = await _repExamPaper.DetachedEntities.ProjectToType<ExamPaperOutput>().FirstOrDefaultAsync(t => t.Id == id);
  104. return item;
  105. }
  106. /// <summary>
  107. /// 根据监测计划ID获取试卷列表(管理端)
  108. /// </summary>
  109. /// <param name="examPlanId"></param>
  110. /// <returns></returns>
  111. public async Task<List<ExamPaperLiteOutput>> GetListByExamPlanId(int examPlanId)
  112. {
  113. var items = await _repExamPaper.DetachedEntities
  114. .Where(t => t.ExamPlanId == examPlanId)
  115. .OrderBy(t => t.GradeId).ThenBy(t => t.CourseId)
  116. .ProjectToType<ExamPaperLiteOutput>().ToListAsync();
  117. SetSubmitStatus(items);
  118. return items;
  119. }
  120. /// <summary>
  121. /// 获取双向细目表监测计划列表(管理端)
  122. /// </summary>
  123. /// <param name="input"></param>
  124. /// <returns></returns>
  125. public async Task<PageResult<ExamPaperTodoPlanOutput>> QueryExamPlanPageList(ExamPlanPageInput input)
  126. {
  127. List<string> whereCause = new();
  128. if (!string.IsNullOrEmpty(input.Name?.Trim()))
  129. {
  130. whereCause.Add("T3.full_name LIKE '%@name%'");
  131. }
  132. if (input.SemesterId.HasValue)
  133. {
  134. whereCause.Add("T3.semester_id = @semesterId");
  135. }
  136. if (input.Status.HasValue)
  137. {
  138. whereCause.Add("T3.status = @status");
  139. }
  140. string whereSql = $"WHERE (T3.`status` = {(short)ExamStatus.ACTIVE} OR T3.`status` = {(short)ExamStatus.STOPPED}) AND T3.is_deleted = 0";
  141. if (whereCause.Count > 0)
  142. {
  143. whereSql = $"{whereSql} AND {string.Join(" AND ", whereCause)}";
  144. }
  145. var p = new
  146. {
  147. input.PageSize,
  148. PageOffset = (input.PageIndex - 1) * input.PageSize,
  149. input.SemesterId,
  150. input.Status,
  151. input.Name,
  152. };
  153. var totalCount = await _repExamPaper.SqlScalarAsync<int>($@"
  154. SELECT COUNT(1) AS total_count
  155. FROM
  156. (
  157. SELECT T1.exam_plan_id, COUNT(T1.id) AS total_count
  158. FROM exam_paper AS T1
  159. JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
  160. {whereSql}
  161. GROUP BY T1.exam_plan_id
  162. ) AS T
  163. ", p);
  164. string select = $@"
  165. SELECT
  166. T1.exam_plan_id,
  167. T3.full_name AS exam_plan_full_name,
  168. T3.`name` AS exam_plan_name,
  169. T3.semester_id,
  170. T3.`status` AS exam_plan_status,
  171. COUNT(T1.id) AS total_count,
  172. COUNT(CASE WHEN T1.`twcl_status` = {(short)AuditStatus.UNSUBMIT} OR T1.`twcl_status` = {(short)AuditStatus.REJECTED} THEN T1.id ELSE NULL END) AS twcl_unsubmit_count,
  173. COUNT(CASE WHEN T1.`twcl_status` = {(short)AuditStatus.AUDIT} OR T1.`twcl_status` = {(short)AuditStatus.APPROVED} OR T1.`twcl_status` = {(short)AuditStatus.APPROVE_CANCELED} THEN T1.id ELSE NULL END) AS twcl_submitted_count,
  174. COUNT(CASE WHEN T1.`suggestion_status` = {(short)AuditStatus.UNSUBMIT} OR T1.`suggestion_status` = {(short)AuditStatus.REJECTED} THEN T1.id ELSE NULL END) AS suggestion_unsubmit_count,
  175. COUNT(CASE WHEN T1.`suggestion_status` = {(short)AuditStatus.AUDIT} OR T1.`suggestion_status` = {(short)AuditStatus.APPROVED} OR T1.`suggestion_status` = {(short)AuditStatus.APPROVE_CANCELED} THEN T1.id ELSE NULL END) AS suggestion_submitted_count
  176. FROM exam_paper AS T1
  177. JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
  178. {whereSql}
  179. GROUP BY T1.exam_plan_id, T3.full_name, T3.`name`, T3.semester_id, T3.`status`
  180. ORDER BY T3.semester_id DESC, T1.exam_plan_id DESC
  181. LIMIT @pageSize OFFSET @pageOffset
  182. ";
  183. var items = await _repExamPaper.SqlQueriesAsync<ExamPaperTodoPlanOutput>(select, p);
  184. PageResult<ExamPaperTodoPlanOutput> ret = new()
  185. {
  186. PageIndex = input.PageIndex,
  187. PageSize = input.PageSize,
  188. TotalCount = totalCount,
  189. Items = items
  190. };
  191. return ret;
  192. }
  193. /// <summary>
  194. /// 导出TQES导入文件格式文件包
  195. /// </summary>
  196. /// <param name="examPlanId"></param>
  197. /// <returns></returns>
  198. /// <exception cref="Exception"></exception>
  199. public async Task<(string, byte[])> ExportTqesFile(int examPlanId)
  200. {
  201. var examPlan = await _repExamPaper.Change<ExamPlan>().DetachedEntities.ProjectToType<ExamPlanOutput>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001);
  202. // 临时存放目录
  203. string fileRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
  204. Directory.CreateDirectory(fileRoot);
  205. string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-双向细目表");
  206. Directory.CreateDirectory(filePath);
  207. try
  208. {
  209. // 定义EXCEL列
  210. List<ExportExcelColDto<ExamPaperQuestionMinorOutput>> cols = new()
  211. {
  212. new() { Name = "大题号", Width = 10, GetCellValue = (r) => r.ExamPaperQuestionMajor?.Number ?? "" },
  213. new() { Name = "小题号", Width = 18, GetCellValue = (r) => r.ColumnName, Align = ExportExcelCellAlign.LEFT },
  214. new() { Name = "分值", Width = 8, GetCellValue = (r) => r.Score },
  215. new() { Name = "题型", Width = 10, GetCellValue = (r) => r.QuestionCatalog?.GetDescription() ?? "" },
  216. new() { Name = "知识模块", Width = 30, GetCellValue = (r) => r.KnowledgeModule, Align = ExportExcelCellAlign.LEFT },
  217. new() { Name = "知识点", Width = 60, GetCellValue = (r) => r.KnowledgePoint, Align = ExportExcelCellAlign.LEFT },
  218. new() { Name = "认知能力", Width = 10, GetCellValue = (r) => r.CognitiveAbility?.GetDescription() ?? "" },
  219. new() { Name = "预估难度", Width = 10, GetCellValue = (r) => r.EstimatedDifficulty },
  220. new() { Name = "是否必答", Width = 10, GetCellValue = (r) => r.IsChoose ? "否" : "是" },
  221. new() { Name = "备注", Width = 20, GetCellValue = (r) => null, Align = ExportExcelCellAlign.LEFT },
  222. };
  223. var ps = await _repExamPaper.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamPaperLiteOutput>().ToListAsync();
  224. foreach (var p in ps)
  225. {
  226. var items = await _repExamPaper.Change<ExamPaperQuestionMinor>().DetachedEntities.Where(t => t.ExamPaperId == p.Id && t.IsLeaf == true).ProjectToType<ExamPaperQuestionMinorOutput>().ToListAsync();
  227. var bs = _exportExcelService.ExportExcel(new ExportExcelDto<ExamPaperQuestionMinorOutput>()
  228. {
  229. IsXlsx = false,
  230. Title = null,
  231. Columns = cols,
  232. Items = items,
  233. IncludeExportTime = false,
  234. });
  235. await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examPlan.EducationStage.GetDescription()}-{p.Grade.Name}-{p.Course.Name}-双向细目表.xls"), bs);
  236. }
  237. string outFileName = $"{examPlan.Name}-双向细目表.zip";
  238. string outFilePath = Path.Combine(fileRoot, outFileName);
  239. ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
  240. zip.CreateZip(outFilePath, filePath, true, string.Empty);
  241. var retBytes = await File.ReadAllBytesAsync(outFilePath);
  242. return (outFileName, retBytes);
  243. }
  244. catch (Exception ex)
  245. {
  246. throw new Exception("导出错误", ex);
  247. }
  248. finally
  249. {
  250. Directory.Delete(fileRoot, true);
  251. }
  252. }
  253. #region 编撰者
  254. /// <summary>
  255. /// 分页查询编撰人监测计划列表
  256. /// </summary>
  257. /// <param name="input"></param>
  258. /// <returns></returns>
  259. public async Task<PageResult<ExamPaperTodoPlanOutput>> QueryWriterExamPlanPageList(ExamPaperExamPlanPageInput input)
  260. {
  261. List<string> whereCause = new();
  262. if (!string.IsNullOrEmpty(input.Name?.Trim()))
  263. {
  264. whereCause.Add("T3.full_name LIKE '%@name%'");
  265. }
  266. if (input.SemesterId.HasValue)
  267. {
  268. whereCause.Add("T3.semester_id = @semesterId");
  269. }
  270. if (input.Status.HasValue)
  271. {
  272. whereCause.Add("T3.status = @status");
  273. }
  274. string prefix = input.WriterType == ExamPaperWriterType.TWCL ? "twcl" : "suggestion";
  275. string whereSql = $"WHERE T1.{prefix}_sys_user_id = {CurrentSysUserInfo.SysUserId} AND (T3.`status` = {(short)ExamStatus.ACTIVE} OR T3.`status` = {(short)ExamStatus.STOPPED}) AND T3.is_deleted = 0";
  276. if (whereCause.Count > 0)
  277. {
  278. whereSql = $"{whereSql} AND {string.Join(" AND ", whereCause)}";
  279. }
  280. var p = new
  281. {
  282. input.PageSize,
  283. PageOffset = (input.PageIndex - 1) * input.PageSize,
  284. input.SemesterId,
  285. input.Status,
  286. input.Name,
  287. };
  288. var totalCount = await _repExamPaper.SqlScalarAsync<int>($@"
  289. SELECT COUNT(1) AS total_count
  290. FROM
  291. (
  292. SELECT T1.exam_plan_id, COUNT(T1.id) AS total_count
  293. FROM exam_paper AS T1
  294. JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
  295. {whereSql}
  296. GROUP BY T1.exam_plan_id
  297. ) AS T
  298. ", p);
  299. string select = $@"
  300. SELECT
  301. T1.exam_plan_id,
  302. T3.full_name AS exam_plan_full_name,
  303. T3.`name` AS exam_plan_name,
  304. T3.semester_id,
  305. T3.`status` AS exam_plan_status,
  306. COUNT(T1.id) AS total_count,
  307. COUNT(CASE WHEN T1.`{prefix}_status` = {(short)AuditStatus.UNSUBMIT} OR T1.`{prefix}_status` = {(short)AuditStatus.REJECTED} THEN T1.id ELSE NULL END) AS {prefix}_unsubmit_count,
  308. COUNT(CASE WHEN T1.`{prefix}_status` = {(short)AuditStatus.AUDIT} OR T1.`{prefix}_status` = {(short)AuditStatus.APPROVED} OR T1.`{prefix}_status` = {(short)AuditStatus.APPROVE_CANCELED} THEN T1.id ELSE NULL END) AS {prefix}_submitted_count
  309. FROM exam_paper AS T1
  310. JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
  311. {whereSql}
  312. GROUP BY T1.exam_plan_id, T3.full_name, T3.`name`, T3.semester_id, T3.`status`
  313. ORDER BY T3.semester_id DESC, T1.exam_plan_id DESC
  314. LIMIT @pageSize OFFSET @pageOffset
  315. ";
  316. var items = await _repExamPaper.SqlQueriesAsync<ExamPaperTodoPlanOutput>(select, p);
  317. PageResult<ExamPaperTodoPlanOutput> ret = new()
  318. {
  319. PageIndex = input.PageIndex,
  320. PageSize = input.PageSize,
  321. TotalCount = totalCount,
  322. Items = items
  323. };
  324. return ret;
  325. }
  326. /// <summary>
  327. /// 根据监测计划ID获取待处理试卷列表
  328. /// </summary>
  329. /// <param name="examPlanId"></param>
  330. /// <param name="writerType"></param>
  331. /// <returns></returns>
  332. public async Task<List<ExamPaperLiteOutput>> GetWriterListByExamPlanId(int examPlanId, ExamPaperWriterType writerType)
  333. {
  334. var query = _repExamPaper.DetachedEntities.Where(t => t.ExamPlanId == examPlanId);
  335. if (writerType == ExamPaperWriterType.TWCL)
  336. {
  337. query = query.Where(t => t.TwclSysUserId == CurrentSysUserInfo.SysUserId);
  338. }
  339. else
  340. {
  341. query = query.Where(t => t.SuggestionSysUserId == CurrentSysUserInfo.SysUserId);
  342. }
  343. var items = await query.ProjectToType<ExamPaperLiteOutput>().ToListAsync();
  344. SetSubmitStatus(items);
  345. return items;
  346. }
  347. /// <summary>
  348. /// 保存问题建议
  349. /// </summary>
  350. /// <param name="input"></param>
  351. /// <returns></returns>
  352. public async Task SaveSuggestion(SaveExamPaperSuggestion input)
  353. {
  354. var item = await _repExamPaper.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001, "监测科目");
  355. item.Questions = input.Questions;
  356. item.Suggestions = input.Suggestions;
  357. await item.UpdateIncludeAsync(new[] { nameof(item.Questions), nameof(item.Suggestions) });
  358. }
  359. /// <summary>
  360. /// 提交双向细目表
  361. /// </summary>
  362. /// <param name="input"></param>
  363. /// <returns></returns>
  364. public async Task SubmitTwcl(BaseId input)
  365. {
  366. var item = await _repExamPaper.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001, "试卷");
  367. var nc = await _repExamPaper.Change<ExamPaperQuestionMinor>().CountAsync(t => t.ExamPaperId == input.Id && (!t.QuestionCatalog.HasValue || !t.CognitiveAbility.HasValue || t.EstimatedDifficulty <= 0 || t.KnowledgeModule.Trim() == "" || t.KnowledgePoint.Trim() == ""));
  368. if (nc > 0)
  369. {
  370. throw Oops.Oh(ErrorCode.E3002, nc);
  371. }
  372. item.TwclStatus = AuditStatus.APPROVED;
  373. item.TwclAudits = AuditUtil.InsertInto(item.TwclAudits, AuditUtil.CreateNew(AuditActionType.SUBMIT, item.TwclStatus));
  374. await item.UpdateAsync();
  375. }
  376. /// <summary>
  377. /// 提交问题建议
  378. /// </summary>
  379. /// <param name="input"></param>
  380. /// <returns></returns>
  381. public async Task SubmitSuggestion(BaseId input)
  382. {
  383. var item = await _repExamPaper.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001, "试卷");
  384. if (item.Questions.Trim() == "" || item.Suggestions.Trim() == "")
  385. {
  386. throw Oops.Oh(ErrorCode.E3003);
  387. }
  388. item.SuggestionStatus = AuditStatus.APPROVED;
  389. item.SuggestionAudits = AuditUtil.InsertInto(item.SuggestionAudits, AuditUtil.CreateNew(AuditActionType.SUBMIT, item.SuggestionStatus));
  390. await item.UpdateAsync();
  391. }
  392. #endregion
  393. private static void SetSubmitStatus(List<ExamPaperLiteOutput> items)
  394. {
  395. foreach (var item in items)
  396. {
  397. item.TwclSubmitted = item.TwclStatus == AuditStatus.AUDIT || item.TwclStatus == AuditStatus.APPROVED || item.TwclStatus == AuditStatus.APPROVE_CANCELED;
  398. item.SuggestionSubmitted = item.SuggestionStatus == AuditStatus.AUDIT || item.SuggestionStatus == AuditStatus.APPROVED || item.SuggestionStatus == AuditStatus.APPROVE_CANCELED;
  399. }
  400. }
  401. }