using Furion.ClayObject.Extensions; using Furion.DatabaseAccessor.Extensions; using NPOI.SS.UserModel; using YBEE.EQM.Core; namespace YBEE.EQM.Application; /// /// 学生成绩导入服务 /// public class ExamScoreImportService : IExamScoreImportService, ITransient { private readonly IRepository _rep; private readonly ISchoolClassService _schoolClassService; private readonly IExamGradeService _examGradeService; public ExamScoreImportService(IRepository rep, ISchoolClassService schoolClassService, IExamGradeService examGradeService) { _rep = rep; _schoolClassService = schoolClassService; _examGradeService = examGradeService; } /// /// 批量导入前期未上报学生名单的各科成绩(初始化) /// /// /// /// public async Task UploadImportWithoutStudentTotalScore(string filePath, int examPlanId) { /* ---------------------------------------------------------- * 处理步骤: * 1.验证表头 * 2.读取数据 * 3.处理班级 * 4.处理学生 * 5.批量插入 ---------------------------------------------------------- */ try { #region 1.验证表结构 using FileStream fs = new(filePath, FileMode.Open, FileAccess.Read); IWorkbook workbook = ExcelUtil.GetWorkbook(filePath, fs); var sheet = workbook.GetSheetAt(0); var rows = sheet.GetRowEnumerator(); // 少于2行验证 if (sheet.LastRowNum < 2) { //"第一行应为填写说明,第二行应为标题行,请勿修改模板结构。" return; } // 读取表头 rows.MoveNext(); IRow headerRow = (IRow)rows.Current; int index = 0; int SAMPLE_TYPE_INDEX = index++; int SCHOOL_ID_INDEX = index++; int SCHOOL_NAME_INDEX = index++; int SCHOOL_BID_INDEX = index++; int SCHOOL_BNAME_INDEX = index++; int NAME_INDEX = index++; int CERT_TYPE_INDEX = index++; int ID_NUM_INDEX = index++; int EXAM_NUMBER_INDEX = index++; int COURSE_COMB_INDEX = index++; int GRADE_INDEX = index++; int CLASS_INDEX = index++; int COURSE_START_INDEX = index; Dictionary headers = new() { { SAMPLE_TYPE_INDEX, "抽样类型" }, { SCHOOL_ID_INDEX, "学校ID" }, { SCHOOL_NAME_INDEX, "学校" }, { SCHOOL_BID_INDEX, "校区ID" }, { SCHOOL_BNAME_INDEX, "校区" }, { NAME_INDEX, "姓名" }, { CERT_TYPE_INDEX, "证件类型" }, { ID_NUM_INDEX, "证件号码" }, { EXAM_NUMBER_INDEX, "考号" }, { COURSE_COMB_INDEX, "选科组合" }, { GRADE_INDEX, "年级" }, { CLASS_INDEX, "班级" }, }; List headerErrors = new(); for (int i = 0; i < COURSE_START_INDEX; i++) { if (headerRow.GetCell(i)?.ToString() != headers[i]) { char letter = (char)('A' + i); headerErrors.Add(letter.ToString()); } } if (headerErrors.Any()) { string columnErrors = string.Join("、", headerErrors); //result.ErrorMessage.Add($"第1行标题行{columnErrors}列名错误。从A列开始依次应为抽样类型、学校ID、学校、姓名、证件类型、证件号码、考号、年级、班级。"); //return result; return; } //result.StructureCorrect = true; #endregion #region 2.读取数据 var examPlan = await _rep.Change().DetachedEntities.FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划"); var examGradeDict = (await _examGradeService.GetListByExamPlanId(examPlanId)).ToDictionary(t => t.Grade.GradeNumber); var courseDict = (await _rep.Change().DetachedEntities.ToListAsync()).ToDictionary(t => t.Name); // 获取需要导入的科目列表 Dictionary courses = new(); for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++) { var cn = headerRow.GetCell(gi).ToString() ?? ""; if (string.IsNullOrEmpty(cn)) { continue; } courses.Add(gi, courseDict[cn]); } int rn = 1; List sourceItems = new(); while (rows.MoveNext()) { rn++; IRow row = (IRow)rows.Current; string rv = row.GetCell(0)?.ToString().Trim() ?? ""; if (rv == "") { break; } for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++) { var course = courses[gi]; var sbid = row.GetCell(SCHOOL_BID_INDEX)?.NumericCellValue; var ccid = row.GetCell(COURSE_COMB_INDEX)?.NumericCellValue; ExamScoreImportDto item = new() { RowNumber = rn, SampleType = (ExamSampleType)row.GetCell(SAMPLE_TYPE_INDEX).NumericCellValue, SysOrgId = (short)row.GetCell(SCHOOL_ID_INDEX).NumericCellValue, SysOrgBranchId = sbid > 0 ? (short)sbid : null, StudentName = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? ""), CertificateType = (CertificateType)row.GetCell(CERT_TYPE_INDEX).NumericCellValue, IdNumber = StringUtil.ClearIdNumber(row.GetCell(ID_NUM_INDEX)?.ToString() ?? ""), ExamNumber = row.GetCell(EXAM_NUMBER_INDEX)?.ToString() ?? "", GradeNumber = (short)row.GetCell(GRADE_INDEX).NumericCellValue, ClassNumber = (short)row.GetCell(CLASS_INDEX).NumericCellValue, CourseId = course.Id, NceeCourseCombId = ccid > 0 ? (short)ccid : null, }; string scoreStr = row.GetCell(gi)?.ToString() ?? ""; if (decimal.TryParse(scoreStr, out decimal score)) { item.Score = score; } else if (scoreStr.Contains('特') || scoreStr.Contains('缺')) { item.IsExcluded = true; } if (item.IdNumber.Length == 18) { if (int.TryParse(item.IdNumber.AsSpan(16, 1), out int gn)) { item.Gender = (short)(gn % 2 == 0 ? 2 : 1); } } var eg = examGradeDict[item.GradeNumber]; item.ExamGradeId = eg.Id; item.GradeId = eg.GradeId; item.GradeBeginYear = eg.GradeBeginYear; item.GradeEndYear = eg.GradeEndYear; item.SchoolClassId = _schoolClassService.GetSchoolClassId(examPlan.EducationStage, item.SysOrgId, eg.GradeBeginYear, item.ClassNumber); sourceItems.Add(item); } } workbook.Close(); fs.Close(); #endregion #region 3.处理班级 var scs = sourceItems.GroupBy(t => t.SchoolClassId).Select(t => new BatchInsertSchoolClassInput { Id = t.Key, SysOrgId = t.FirstOrDefault().SysOrgId, SysOrgBranchId = t.FirstOrDefault().SysOrgBranchId, EducationStage = examPlan.EducationStage, EducationYears = examPlan.EducationYears, ClassNumber = t.FirstOrDefault().ClassNumber, GradeBeginYear = t.FirstOrDefault().GradeBeginYear, GradeEndYear = t.FirstOrDefault().GradeEndYear, }).Distinct().ToList(); // 插入没有的班级 await _schoolClassService.BatchInsert(scs); #endregion #region 4.处理学生 // 清理学生 string deleteStudentSql = $"DELETE FROM exam_student WHERE exam_plan_id = {examPlanId};"; await _rep.SqlNonQueryAsync(deleteStudentSql); var uid = CurrentSysUserInfo.SysUserId; if (uid == 0) { uid = 1; } var stus = sourceItems.GroupBy(t => t.RowNumber).Select(t => t.FirstOrDefault()).ToList(); List insertValues = new(); int scount = stus.Count; for (int i = 0; i < scount; i++) { var t = stus[i]; string sobid = "NULL"; if (t.SysOrgBranchId.HasValue) { sobid = t.SysOrgBranchId.ToString(); } insertValues.Add($"({examPlanId}, {t.SysOrgId}, {sobid}, {t.ExamGradeId}, {t.GradeId}, {t.SchoolClassId}, {t.ClassNumber}, '{t.ExamNumber}', '{t.StudentName}', {(short)t.CertificateType}, '{t.IdNumber}', {t.Gender}, {uid}, CURRENT_TIMESTAMP, 0)"); if ((i + 1) % 1000 == 0 || i == scount - 1) { string insertSql = $@" INSERT INTO exam_student(exam_plan_id, sys_org_id, sys_org_branch_id, exam_grade_id, grade_id, school_class_id, class_number, exam_number, `name`, certificate_type, id_number, gender, create_sys_user_id, create_time, is_deleted) VALUES {string.Join(",", insertValues)} "; await _rep.SqlNonQueryAsync(insertSql); insertValues.Clear(); } } #endregion #region 5.批量导入 // 清理成绩 string deleteScoreSql = $"DELETE FROM exam_score WHERE exam_plan_id = {examPlanId};"; await _rep.SqlNonQueryAsync(deleteScoreSql); List selects = new(); scount = sourceItems.Count; for (int i = 0; i < scount; i++) { var t = sourceItems[i]; string sobid = "NULL"; if (t.SysOrgBranchId.HasValue) { sobid = t.SysOrgBranchId.ToString(); } string ccid = "NULL"; if (t.NceeCourseCombId.HasValue) { ccid = t.NceeCourseCombId.ToString(); } var ied = t.IsExcluded ? 1 : 0; selects.Add($@" SELECT {(short)t.SampleType} est, {t.SysOrgId} soid, {sobid} as sobid, {t.GradeId} gid, {examPlan.SemesterId} smid, {t.SchoolClassId} scid, {t.ClassNumber} cn, '{t.ExamNumber}' en, {ccid} ccid, {t.CourseId} cid, {t.Score} s, {ied} ied "); if ((i + 1) % 500 == 0 || i == scount - 1) { string insertSql = $@" INSERT INTO exam_score(exam_plan_id, education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, course_id, score, is_excluded) SELECT {examPlanId}, {(short)examPlan.EducationStage}, T1.est, T1.soid, T1.sobid, T1.gid, T1.smid, T1.scid, T1.cn, T2.id, T1.ccid, T1.cid, T1.s, T1.ied FROM ({string.Join("UNION ALL", selects)} ) AS T1 JOIN (SELECT id, school_class_id, exam_number FROM exam_student WHERE exam_plan_id = {examPlanId}) AS T2 ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number "; await _rep.SqlNonQueryAsync(insertSql); selects.Clear(); } } #endregion } catch (Exception ex) { throw new Exception(ex.Message); } finally { File.Delete(filePath); } } /// /// 批量导入学生总成绩 /// /// /// /// public async Task UploadImportStudentTotalScore(string filePath, int examPlanId) { /* ---------------------------------------------------------- * 处理步骤: * 1.验证表头 * 2.读取数据 * 3.处理班级 * 4.批量插入 ---------------------------------------------------------- */ try { #region 1.验证表结构 using FileStream fs = new(filePath, FileMode.Open, FileAccess.Read); IWorkbook workbook = ExcelUtil.GetWorkbook(filePath, fs); var sheet = workbook.GetSheetAt(0); var rows = sheet.GetRowEnumerator(); // 少于2行验证 if (sheet.LastRowNum < 2) { //"第一行应为填写说明,第二行应为标题行,请勿修改模板结构。" return; } // 读取表头 rows.MoveNext(); IRow headerRow = (IRow)rows.Current; int index = 0; int SAMPLE_TYPE_INDEX = index++; int SCHOOL_ID_INDEX = index++; int SCHOOL_NAME_INDEX = index++; int SCHOOL_BID_INDEX = index++; int SCHOOL_BNAME_INDEX = index++; int NAME_INDEX = index++; int CERT_TYPE_INDEX = index++; int ID_NUM_INDEX = index++; int EXAM_NUMBER_INDEX = index++; int COURSE_COMB_INDEX = index++; int GRADE_INDEX = index++; int CLASS_INDEX = index++; int COURSE_START_INDEX = index; Dictionary headers = new() { { SAMPLE_TYPE_INDEX, "抽样类型" }, { SCHOOL_ID_INDEX, "学校ID" }, { SCHOOL_NAME_INDEX, "学校" }, { SCHOOL_BID_INDEX, "校区ID" }, { SCHOOL_BNAME_INDEX, "校区" }, { NAME_INDEX, "姓名" }, { CERT_TYPE_INDEX, "证件类型" }, { ID_NUM_INDEX, "证件号码" }, { EXAM_NUMBER_INDEX, "考号" }, { COURSE_COMB_INDEX, "选科组合" }, { GRADE_INDEX, "年级" }, { CLASS_INDEX, "班级" }, }; List headerErrors = new(); for (int i = 0; i < COURSE_START_INDEX; i++) { if (headerRow.GetCell(i)?.ToString() != headers[i]) { char letter = (char)('A' + i); headerErrors.Add(letter.ToString()); } } if (headerErrors.Any()) { string columnErrors = string.Join("、", headerErrors); //result.ErrorMessage.Add($"第1行标题行{columnErrors}列名错误。从A列开始依次应为抽样类型、学校ID、学校、姓名、证件类型、证件号码、考号、年级、班级。"); //return result; return; } //result.StructureCorrect = true; #endregion #region 2.读取数据 var examPlan = await _rep.Change().DetachedEntities.FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划"); var examGradeDict = (await _examGradeService.GetListByExamPlanId(examPlanId)).ToDictionary(t => t.Grade.GradeNumber); var courseDict = (await _rep.Change().DetachedEntities.ToListAsync()).ToDictionary(t => t.Name); var examCourses = await _rep.Change().DetachedEntities.Where(t => t.ExamPlanId == examPlanId).Select(t => new { t.GradeId, t.CourseId }).ToListAsync(); // 获取需要导入的科目列表 Dictionary courses = new(); for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++) { var cn = headerRow.GetCell(gi).ToString() ?? ""; if (string.IsNullOrEmpty(cn)) { continue; } courses.Add(gi, courseDict[cn]); } int rn = 1; List sourceItems = new(); while (rows.MoveNext()) { rn++; IRow row = (IRow)rows.Current; string rv = row.GetCell(0)?.ToString().Trim() ?? ""; if (rv == "") { break; } var gradeNumber = (short)row.GetCell(GRADE_INDEX).NumericCellValue; for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++) { var course = courses[gi]; var eg = examGradeDict[gradeNumber]; // 跳过未监测科目 if (!examCourses.Any(t => t.GradeId == eg.GradeId && t.CourseId == course.Id)) { continue; } var sbid = row.GetCell(SCHOOL_BID_INDEX)?.NumericCellValue; var ccid = row.GetCell(COURSE_COMB_INDEX)?.NumericCellValue; ExamScoreImportDto item = new() { RowNumber = rn, SampleType = (ExamSampleType)row.GetCell(SAMPLE_TYPE_INDEX).NumericCellValue, SysOrgId = (short)row.GetCell(SCHOOL_ID_INDEX).NumericCellValue, SysOrgBranchId = sbid > 0 ? (short)sbid : null, StudentName = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? ""), CertificateType = (CertificateType)row.GetCell(CERT_TYPE_INDEX).NumericCellValue, IdNumber = StringUtil.ClearIdNumber(row.GetCell(ID_NUM_INDEX)?.ToString() ?? ""), ExamNumber = row.GetCell(EXAM_NUMBER_INDEX)?.ToString() ?? "", GradeNumber = gradeNumber, ClassNumber = (short)row.GetCell(CLASS_INDEX).NumericCellValue, CourseId = course.Id, NceeCourseCombId = ccid > 0 ? (short)ccid : null, }; string scoreStr = row.GetCell(gi)?.ToString() ?? ""; if (decimal.TryParse(scoreStr, out decimal score)) { item.Score = score; } else if (scoreStr.Contains('特') || scoreStr.Contains('缺')) { item.IsExcluded = true; } if (item.IdNumber.Length == 18) { if (int.TryParse(item.IdNumber.AsSpan(16, 1), out int gn)) { item.Gender = (short)(gn % 2 == 0 ? 2 : 1); } } item.ExamGradeId = eg.Id; item.GradeId = eg.GradeId; item.GradeBeginYear = eg.GradeBeginYear; item.GradeEndYear = eg.GradeEndYear; item.SchoolClassId = _schoolClassService.GetSchoolClassId(examPlan.EducationStage, item.SysOrgId, eg.GradeBeginYear, item.ClassNumber); sourceItems.Add(item); } } workbook.Close(); fs.Close(); #endregion #region 3.处理班级 var scs = sourceItems.GroupBy(t => t.SchoolClassId).Select(t => new BatchInsertSchoolClassInput { Id = t.Key, SysOrgId = t.FirstOrDefault().SysOrgId, SysOrgBranchId = t.FirstOrDefault().SysOrgBranchId, EducationStage = examPlan.EducationStage, EducationYears = examPlan.EducationYears, ClassNumber = t.FirstOrDefault().ClassNumber, GradeBeginYear = t.FirstOrDefault().GradeBeginYear, GradeEndYear = t.FirstOrDefault().GradeEndYear, }).Distinct().ToList(); // 插入没有的班级 await _schoolClassService.BatchInsert(scs); #endregion #region 4.批量导入 // 清理成绩 string deleteScoreSql = $"DELETE FROM exam_score WHERE exam_plan_id = {examPlanId};"; await _rep.SqlNonQueryAsync(deleteScoreSql); List selects = new(); var scount = sourceItems.Count; for (int i = 0; i < scount; i++) { var t = sourceItems[i]; string sobid = "NULL"; if (t.SysOrgBranchId.HasValue) { sobid = t.SysOrgBranchId.ToString(); } string ccid = "NULL"; if (t.NceeCourseCombId.HasValue) { ccid = t.NceeCourseCombId.ToString(); } var ied = t.IsExcluded ? 1 : 0; selects.Add($@" SELECT {(short)t.SampleType} est, {t.SysOrgId} soid, {sobid} as sobid, {t.GradeId} gid, {examPlan.SemesterId} smid, {t.SchoolClassId} scid, {t.ClassNumber} cn, '{t.ExamNumber}' en, {ccid} ccid, {t.CourseId} cid, {t.Score} s, {ied} ied "); if ((i + 1) % 500 == 0 || i == scount - 1) { string insertSql = $@" SET @examSampleId = (SELECT MAX(id) FROM exam_sample WHERE exam_plan_id = {examPlanId} AND is_selected = 1); INSERT INTO exam_score(exam_plan_id, education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, course_id, score, is_excluded) SELECT {examPlanId}, {(short)examPlan.EducationStage}, T1.est, T1.soid, T1.sobid, T1.gid, T1.smid, T1.scid, T1.cn, T2.id, T1.ccid, T1.cid, T1.s, T1.ied FROM ({string.Join("UNION ALL", selects)} ) AS T1 JOIN (SELECT exam_number, exam_student_id AS id FROM exam_sample_student WHERE exam_sample_id = @examSampleId) AS T2 ON T1.en = T2.exam_number; "; await _rep.SqlNonQueryAsync(insertSql); selects.Clear(); } } #endregion #region 5.计算总分和分段 await $@" -- 更新各科成绩分段 UPDATE exam_score AS T1 JOIN ( SELECT T1.id, T1.grade_id, T1.course_id, get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id FROM exam_score AS T1 JOIN exam_course AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.grade_id = T2.grade_id AND T1.course_id = T2.course_id WHERE T1.exam_plan_id = {examPlanId} ) AS T2 ON T1.id = T2.id SET T1.exam_score_range_id = T2.exam_score_range_id WHERE T1.exam_plan_id = {examPlanId}; -- 更新总成绩和分段 DELETE FROM exam_score_total WHERE exam_plan_id = {examPlanId}; INSERT INTO exam_score_total( exam_plan_id, education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, course_count, score, exam_score_range_id, is_excluded ) SELECT {examPlanId}, T1.*, get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id, 0 FROM ( SELECT education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, COUNT(1) AS course_count, SUM(IFNULL(score, 0)) AS score FROM exam_score WHERE exam_plan_id = {examPlanId} GROUP BY education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id ) AS T1 JOIN ( SELECT grade_id, exam_score_range_type FROM exam_grade WHERE exam_plan_id = {examPlanId} ) AS T2 ON T1.grade_id = T2.grade_id; ".SetCommandTimeout(6000).SqlNonQueryAsync(); #endregion } catch (Exception ex) { throw new Exception(ex.Message); } finally { File.Delete(filePath); } } }