ExamStudentService.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. using Furion.DatabaseAccessor.Extensions;
  2. using NPOI.SS.UserModel;
  3. using System.Dynamic;
  4. using YBEE.EQM.Core;
  5. namespace YBEE.EQM.Application;
  6. /// <summary>
  7. /// 监测学生管理服务
  8. /// </summary>
  9. public class ExamStudentService : IExamStudentService, ITransient
  10. {
  11. private readonly IRepository<ExamStudent> _rep;
  12. private readonly IExamGradeService _examGradeService;
  13. private readonly ISysDictDataService _sysDictDataService;
  14. private readonly ICourseCombService _courseCombService;
  15. private readonly ISchoolClassService _schoolClassService;
  16. public ExamStudentService(IRepository<ExamStudent> rep, IExamGradeService examGradeService, ISysDictDataService sysDictDataService, ICourseCombService courseCombService, ISchoolClassService schoolClassService)
  17. {
  18. _rep = rep;
  19. _examGradeService = examGradeService;
  20. _sysDictDataService = sysDictDataService;
  21. _courseCombService = courseCombService;
  22. _schoolClassService = schoolClassService;
  23. }
  24. #region 批量导入
  25. /// <summary>
  26. /// 上传批量导入文件
  27. /// </summary>
  28. /// <param name="filePath"></param>
  29. /// <param name="examPlanId"></param>
  30. /// <returns></returns>
  31. public async Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload(string filePath, int examPlanId)
  32. {
  33. UploadExamDataOutput<UploadExamStudentOutput> result = new();
  34. try
  35. {
  36. using FileStream fs = new(filePath, FileMode.Open, FileAccess.Read);
  37. IWorkbook workbook = ExcelUtil.GetWorkbook(filePath, fs);
  38. var sheet = workbook.GetSheetAt(0);
  39. var rows = sheet.GetRowEnumerator();
  40. #region 验证表结构
  41. // 少于2行验证
  42. if (sheet.LastRowNum < 2)
  43. {
  44. result.ErrorMessage.Add("第一行应为填写说明,第二行应为标题行,请勿修改模板结构。");
  45. return result;
  46. }
  47. // 跳过第一行
  48. rows.MoveNext();
  49. // 读取表头
  50. rows.MoveNext();
  51. IRow headerRow = (IRow)rows.Current;
  52. int index = 0;
  53. int CLASS_INDEX = index++;
  54. int NAME_INDEX = index++;
  55. int CERT_TYPE_INDEX = index++;
  56. int ID_NUM_INDEX = index++;
  57. int GENDER_INDEX = index++;
  58. int EXAM_NUM_INDEX = index++;
  59. int COURSE_COMB_INDEX = index++;
  60. int REMARK_INDEX = index++;
  61. int ROOM_INDEX = index++;
  62. int SEAT_INDEX = index;
  63. Dictionary<int, string> headers = new()
  64. {
  65. { CLASS_INDEX, "班级" },
  66. { NAME_INDEX, "姓名" },
  67. { CERT_TYPE_INDEX, "证件类型" },
  68. { ID_NUM_INDEX, "证件号码" },
  69. { GENDER_INDEX, "性别" },
  70. { EXAM_NUM_INDEX, "自编监测号" },
  71. { COURSE_COMB_INDEX, "选科组合" },
  72. { REMARK_INDEX, "备注" },
  73. { ROOM_INDEX, "考场号" },
  74. { SEAT_INDEX, "座位号" },
  75. };
  76. List<string> headerErrors = new();
  77. for (int i = 0; i <= index; i++)
  78. {
  79. if (headerRow.GetCell(i)?.ToString() != headers[i])
  80. {
  81. char letter = (char)('A' + i);
  82. headerErrors.Add(letter.ToString());
  83. }
  84. }
  85. if (headerErrors.Any())
  86. {
  87. string columnErrors = string.Join("、", headerErrors);
  88. result.ErrorMessage.Add($"第2行标题行{columnErrors}列名错误。从A列开始依次应为班级、姓名、证件类型、证件号码、性别、自编监测号、选科组合、备注、考场号和座位号。");
  89. return result;
  90. }
  91. result.StructureCorrect = true;
  92. #endregion
  93. #region 处理数据
  94. // 获取证件类型
  95. var cts = await _sysDictDataService.GetListByDictTypeId(304);
  96. var certificateTypes = cts.ToDictionary(x => x.Name, y => y.Value);
  97. // 获取选科组全
  98. var ccs = await _courseCombService.GetAllList();
  99. var courseCombs = ccs.ToDictionary(x => x.ShortName, y => y.Id);
  100. // 读取数据
  101. List<UploadExamStudentOutput> data = new();
  102. int rn = 0;
  103. while (rows.MoveNext())
  104. {
  105. IRow row = (IRow)rows.Current;
  106. string rv = row.GetCell(0)?.ToString().Trim() ?? "";
  107. rv += row.GetCell(1)?.ToString().Trim() ?? "";
  108. if (rv == "")
  109. {
  110. break;
  111. }
  112. UploadExamStudentOutput item = new() { RowNumber = ++rn };
  113. // 班级
  114. if (short.TryParse(row.GetCell(CLASS_INDEX)?.ToString(), out short classNumber))
  115. {
  116. item.ClassNumber = classNumber;
  117. if (item.ClassNumber < 1 || item.ClassNumber > 35)
  118. {
  119. item.ErrorMessage.Add($"{headers[CLASS_INDEX]}超限");
  120. }
  121. }
  122. else
  123. {
  124. item.ErrorMessage.Add(headers[CLASS_INDEX]);
  125. }
  126. // 姓名
  127. item.Name = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? "");
  128. if (item.Name == "" || item.Name.Length > 100)
  129. {
  130. item.ErrorMessage.Add(headers[NAME_INDEX]);
  131. }
  132. if (item.Name.Length > 100) { item.Name = item.Name[..100]; }
  133. // 证件类型
  134. item.CertificateTypeName = StringUtil.ClearWhite(row.GetCell(CERT_TYPE_INDEX)?.ToString() ?? "");
  135. if (!(item.CertificateTypeName != "" && certificateTypes.ContainsKey(item.CertificateTypeName)))
  136. {
  137. item.ErrorMessage.Add(headers[CERT_TYPE_INDEX]);
  138. }
  139. else
  140. {
  141. item.CertificateType = (CertificateType)certificateTypes[item.CertificateTypeName];
  142. }
  143. // 证件号码
  144. item.IdNumber = StringUtil.ClearWhite(row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper();
  145. if (item.CertificateType == CertificateType.ID_CARD)
  146. {
  147. var idNumberValidate = CertificateNumberValidator.ValidateIdCard(item.IdNumber);
  148. if (!idNumberValidate.Success)
  149. {
  150. item.ErrorMessage.Add($"{item.CertificateTypeName}{idNumberValidate.ErrorMessage}");
  151. }
  152. else
  153. {
  154. item.GenderName = idNumberValidate.Gender == Gender.MALE ? "男" : "女";
  155. item.Gender = idNumberValidate.Gender;
  156. }
  157. }
  158. else
  159. {
  160. // 性别
  161. item.GenderName = StringUtil.ClearWhite(row.GetCell(GENDER_INDEX)?.ToString() ?? "");
  162. if (item.CertificateType != CertificateType.ID_CARD)
  163. {
  164. item.Gender = item.GenderName == "男" ? Gender.MALE : item.GenderName == "女" ? Gender.FEMALE : Gender.UNKNOWN;
  165. }
  166. }
  167. if (item.IdNumber.Length > 50) { item.IdNumber = item.IdNumber[..50]; }
  168. // 自编监测号
  169. item.ExamNumber = StringUtil.ClearWhite(row.GetCell(EXAM_NUM_INDEX)?.ToString() ?? "");
  170. if (item.ExamNumber.Length > 50) { item.ExamNumber = item.ExamNumber[..50]; }
  171. // TODO 根据配置检测是否需要自编监测号
  172. // 选科组合
  173. item.CourseCombName = StringUtil.ClearWhite(row.GetCell(COURSE_COMB_INDEX)?.ToString() ?? "");
  174. // TODO 高中需要处理选科
  175. if (item.CourseCombName != "")
  176. {
  177. if (courseCombs.TryGetValue(item.CourseCombName, out short value))
  178. {
  179. item.CourseCombId = value;
  180. }
  181. else
  182. {
  183. item.ErrorMessage.Add(headers[COURSE_COMB_INDEX]);
  184. }
  185. }
  186. // 备注
  187. item.Remark = StringUtil.ClearWhite(row.GetCell(REMARK_INDEX)?.ToString() ?? "");
  188. if (item.Remark.Length > 200) { item.Remark = item.Remark[..200]; }
  189. // 考场号
  190. item.RoomNumber = StringUtil.ClearWhite(row.GetCell(ROOM_INDEX)?.ToString() ?? "");
  191. if (item.RoomNumber.Length > 20) { item.RoomNumber = item.RoomNumber[..20]; }
  192. // 座位号
  193. item.SeatNumber = StringUtil.ClearWhite(row.GetCell(SEAT_INDEX)?.ToString() ?? "");
  194. if (item.RoomNumber.Length > 20) { item.SeatNumber = item.SeatNumber[..20]; }
  195. // 行是否验证通过
  196. item.IsSuccess = item.ErrorMessage.Count == 0;
  197. data.Add(item);
  198. result.TotalRowCount++;
  199. if (!item.IsSuccess)
  200. {
  201. result.ErrorRowCount++;
  202. }
  203. }
  204. result.Rows = data;
  205. #endregion
  206. workbook.Close();
  207. fs.Close();
  208. }
  209. catch (Exception ex)
  210. {
  211. throw new Exception(ex.Message);
  212. }
  213. finally
  214. {
  215. File.Delete(filePath);
  216. }
  217. return result;
  218. }
  219. /// <summary>
  220. /// 批量导入监测学生
  221. /// </summary>
  222. /// <param name="input"></param>
  223. /// <returns></returns>
  224. public async Task Import(ImportExamStudentInput input)
  225. {
  226. var orgId = input.SysOrgId ?? CurrentSysUserInfo.SysOrgId;
  227. var examPlan = await _rep.Change<ExamPlan>().FirstOrDefaultAsync(t => t.Id == input.ExamPlanId);
  228. var examGrade = await _examGradeService.GetById(input.ExamGradeId);
  229. var classNumbers = input.Items.Select(t => t.ClassNumber).Distinct().ToList();
  230. var classDict = await _schoolClassService.GetImportSchoolClassList(new()
  231. {
  232. SysOrgId = orgId,
  233. SysOrgBranchId = input.SysOrgBranchId,
  234. ExamGrade = examGrade,
  235. ClassNumberList = classNumbers,
  236. });
  237. // 删除年级下所有数据
  238. await _rep.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.GradeId == input.GradeId && t.SysOrgBranchId == input.SysOrgBranchId).ExecuteDeleteAsync();
  239. List<ExamStudent> items = new();
  240. foreach (var ni in input.Items)
  241. {
  242. var item = ni.Adapt<ExamStudent>();
  243. item.ExamPlanId = input.ExamPlanId;
  244. item.SysOrgId = orgId;
  245. item.SysOrgBranchId = input.SysOrgBranchId;
  246. item.SchoolClassId = classDict[ni.ClassNumber];
  247. items.Add(item);
  248. }
  249. await _rep.InsertAsync(items);
  250. }
  251. #endregion
  252. #region 创建编辑
  253. /// <summary>
  254. /// 添加监测学生
  255. /// </summary>
  256. /// <param name="input"></param>
  257. /// <returns></returns>
  258. public async Task Add(AddExamStudentInput input)
  259. {
  260. var orgId = input.SysOrgId ?? CurrentSysUserInfo.SysOrgId;
  261. // 检测同一监测计划中同机构内是否有相同证件号码的学生
  262. var sameItems = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.CertificateType == input.CertificateType && t.IdNumber == input.IdNumber).ProjectToType<ExamStudentOutput>().ToListAsync();
  263. if (sameItems.Any())
  264. {
  265. throw Oops.Oh(ErrorCode.E2003, string.Join("、", sameItems.Select(t => $"{t.ExamGrade.Grade.Name}{t.ClassNumber}班{t.Name}")), "证件号码");
  266. }
  267. var examGrade = await _examGradeService.GetById(input.ExamGradeId);
  268. var schoolClass = await _schoolClassService.GetSchoolClass(orgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
  269. var item = input.Adapt<ExamStudent>();
  270. item.SysOrgId = orgId;
  271. item.SchoolClassId = schoolClass.Id;
  272. await item.InsertAsync();
  273. }
  274. /// <summary>
  275. /// 更新监测学生
  276. /// </summary>
  277. /// <param name="input"></param>
  278. /// <returns></returns>
  279. public async Task Update(UpdateExamStudentInput input)
  280. {
  281. var oitem = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
  282. var examGrade = await _examGradeService.GetById(oitem.ExamGradeId);
  283. var schoolClass = await _schoolClassService.GetSchoolClass(oitem.SysOrgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
  284. var item = input.Adapt<ExamStudent>();
  285. item.SchoolClassId = schoolClass.Id;
  286. await item.UpdateIncludeAsync(new[] {
  287. nameof(item.SchoolClassId),
  288. nameof(item.ClassNumber),
  289. nameof(item.Name),
  290. nameof(item.CertificateType),
  291. nameof(item.IdNumber),
  292. nameof(item.Gender),
  293. nameof(item.BirthDate),
  294. nameof(item.ExamNumber),
  295. nameof(item.StudentNumber),
  296. nameof(item.Remark),
  297. nameof(item.CourseCombId),
  298. nameof(item.SysOrgBranchId),
  299. nameof(item.RoomNumber),
  300. nameof(item.SeatNumber),
  301. });
  302. }
  303. /// <summary>
  304. /// 删除监测学生
  305. /// </summary>
  306. /// <param name="input"></param>
  307. /// <returns></returns>
  308. public async Task Del(BaseId<long> input)
  309. {
  310. var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
  311. await item.DeleteAsync();
  312. }
  313. /// <summary>
  314. /// 清空监测学生
  315. /// </summary>
  316. /// <param name="input"></param>
  317. /// <returns></returns>
  318. public async Task Clear(ClearExamStudentInput input)
  319. {
  320. var orgId = CurrentSysUserInfo.SysOrgId;
  321. await _rep.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId).ExecuteDeleteAsync();
  322. }
  323. #endregion
  324. #region 查询统计
  325. /// <summary>
  326. /// 分页查询监测学生列表
  327. /// </summary>
  328. /// <param name="input"></param>
  329. /// <returns></returns>
  330. public async Task<PageResult<ExamStudentOutput>> QueryPageList(ExamStudentPageInput input)
  331. {
  332. var query = GetQueryBase(input);
  333. query = query.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId);
  334. var ret = await query.OrderBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id).ProjectToType<ExamStudentOutput>().ToADPagedListAsync(input.PageIndex, input.PageSize);
  335. return ret;
  336. }
  337. /// <summary>
  338. /// 获取机构班级上报人数统计列表
  339. /// </summary>
  340. /// <param name="examPlanId"></param>
  341. /// <returns></returns>
  342. public async Task<ExamStudentCountOutput> GetOrgGradeClassStudentCount(int examPlanId)
  343. {
  344. var orgId = CurrentSysUserInfo.SysOrgId;
  345. var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == orgId)
  346. .GroupBy(t => new { t.GradeId, t.ClassNumber })
  347. .Select(t => new
  348. {
  349. t.Key.GradeId,
  350. t.Key.ClassNumber,
  351. t.FirstOrDefault().Grade,
  352. Count = t.Count(),
  353. })
  354. .ToListAsync();
  355. var cols = items.Select(t => t.ClassNumber).Distinct().ToList();
  356. var retItems = items.ToPivotList(c => c.ClassNumber, r => r.GradeId, d => d.Any() ? d.Sum(x => x.Count) : 0);
  357. foreach (var item in retItems)
  358. {
  359. item.Grade = items.FirstOrDefault(t => t.GradeId == item.GradeId).Grade;
  360. item.GradeTotal = items.Where(t => t.GradeId == item.GradeId).Sum(x => x.Count);
  361. }
  362. int total = retItems.Sum(x => x.GradeTotal);
  363. IDictionary<string, object> totalItem = new ExpandoObject();
  364. totalItem.Add("GradeId", 9999);
  365. totalItem.Add("Grade", new { Id = 9999, Name = "合计" });
  366. totalItem.Add("GradeTotal", total);
  367. retItems.Add(totalItem);
  368. return new()
  369. {
  370. ClassNumberList = cols,
  371. Items = retItems,
  372. Total = total,
  373. };
  374. }
  375. #endregion
  376. #region 私有方法
  377. /// <summary>
  378. /// 构建查询
  379. /// </summary>
  380. /// <param name="input"></param>
  381. /// <returns></returns>
  382. private IQueryable<ExamStudent> GetQueryBase(ExamStudentPageInput input)
  383. {
  384. var name = !string.IsNullOrEmpty(input.Name?.Trim());
  385. var idNumber = !string.IsNullOrEmpty(input.IdNumber?.Trim());
  386. var examNumber = !string.IsNullOrEmpty(input.ExamNumber?.Trim());
  387. var studentNumber = !string.IsNullOrEmpty(input.StudentNumber?.Trim());
  388. var roomNumber = !string.IsNullOrEmpty(input.RoomNumber?.Trim());
  389. var seatNumber = !string.IsNullOrEmpty(input.SeatNumber?.Trim());
  390. var query = _rep.DetachedEntities.Where((name, u => EF.Functions.Like(u.Name, $"%{input.Name.Trim()}%")))
  391. .Where((idNumber, u => EF.Functions.Like(u.IdNumber, $"%{input.IdNumber.Trim()}%")))
  392. .Where((examNumber, u => EF.Functions.Like(u.ExamNumber, $"%{input.ExamNumber.Trim()}%")))
  393. .Where((studentNumber, u => EF.Functions.Like(u.StudentNumber, $"%{input.StudentNumber.Trim()}%")))
  394. .Where((roomNumber, u => EF.Functions.Like(u.RoomNumber, $"%{input.RoomNumber.Trim()}%")))
  395. .Where((seatNumber, u => EF.Functions.Like(u.SeatNumber, $"%{input.SeatNumber.Trim()}%")))
  396. .Where(input.CertificateType.HasValue, t => t.CertificateType == input.CertificateType)
  397. .Where(input.Gender.HasValue, t => t.Gender == input.Gender)
  398. .Where(input.GradeId.HasValue, t => t.GradeId == input.GradeId)
  399. .Where(input.CourseCombId.HasValue, t => t.CourseCombId == input.CourseCombId)
  400. .Where(input.ClassNumber.HasValue, t => t.ClassNumber == input.ClassNumber)
  401. .Where(input.SysOrgBranchId.HasValue, t => t.SchoolClass.SysOrgBranchId == input.SysOrgBranchId);
  402. return query;
  403. }
  404. #endregion
  405. }