beetle пре 5 месеци
родитељ
комит
a6870eca25
36 измењених фајлова са 1467 додато и 300 уклоњено
  1. 4 1
      YBEE.EQM.Admin/package.json
  2. 2 4
      YBEE.EQM.Admin/scripts/gapi/index.js
  3. 306 0
      YBEE.EQM.Admin/src/common/exportExcel.ts
  4. 82 0
      YBEE.EQM.Admin/src/common/exportToExcel.ts
  5. 1 1
      YBEE.EQM.Admin/src/pages/auth/Login/index.tsx
  6. 48 6
      YBEE.EQM.Admin/src/pages/exam-org/OrgExamPlanDetail/index.tsx
  7. 3 3
      YBEE.EQM.Admin/src/pages/exam-org/absent-replace/OrgExamAbsentReplaceReport/index.tsx
  8. 184 165
      YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentImport/components/ExamStudentImportEditModal.tsx
  9. 30 2
      YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentImport/index.tsx
  10. 34 5
      YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentReport/components/ExamStudentEditModal.tsx
  11. 23 5
      YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentReport/index.tsx
  12. 2 0
      YBEE.EQM.Admin/src/typings.d.ts
  13. 4 4
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceService.cs
  14. 23 1
      YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreImportDto.cs
  15. 29 0
      YBEE.EQM.Application/Exam/ExamScore/ExamScoreImportAppService.cs
  16. 306 1
      YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreImportService.cs
  17. 7 0
      YBEE.EQM.Application/Exam/ExamScore/Services/IExamScoreImportService.cs
  18. 4 4
      YBEE.EQM.Application/Exam/ExamSpecialStudent/Services/ExamSpecialStudentService.cs
  19. 12 0
      YBEE.EQM.Application/Exam/ExamStudent/Dtos/ExamStudentInput.cs
  20. 2 2
      YBEE.EQM.Application/Exam/ExamStudent/ExamStudentAppService.cs
  21. 25 18
      YBEE.EQM.Application/Exam/ExamStudent/Services/ExamStudentService.cs
  22. 2 1
      YBEE.EQM.Application/Exam/ExamStudent/Services/IExamStudentService.cs
  23. 142 48
      YBEE.EQM.Application/Ncee/NceeExport/Services/NceeExportService.cs
  24. 3 3
      YBEE.EQM.Application/Ncee/NceeScore/Services/INceeScoreService.cs
  25. 55 3
      YBEE.EQM.Application/Ncee/NceeScore/Services/NceeScoreService.cs
  26. 1 1
      YBEE.EQM.Application/YBEE.EQM.Application.csproj
  27. 57 9
      YBEE.EQM.Application/YBEE.EQM.Application.xml
  28. 31 0
      YBEE.EQM.Core/Extensions/StringExtensions.cs
  29. 1 1
      YBEE.EQM.Core/SeedData/SysMenuSeedData.cs
  30. 8 0
      YBEE.EQM.Core/Service/NceePlanConfig.cs
  31. 8 8
      YBEE.EQM.Core/YBEE.EQM.Core.csproj
  32. 24 0
      YBEE.EQM.Core/YBEE.EQM.Core.xml
  33. 1 1
      YBEE.EQM.Database.Migrations/YBEE.EQM.Database.Migrations.csproj
  34. 1 1
      YBEE.EQM.EntityFramework.Core/YBEE.EQM.EntityFramework.Core.csproj
  35. 1 1
      YBEE.EQM.Web.Core/YBEE.EQM.Web.Core.csproj
  36. 1 1
      YBEE.EQM.Web.Entry/YBEE.EQM.Web.Entry.csproj

+ 4 - 1
YBEE.EQM.Admin/package.json

@@ -54,10 +54,12 @@
         "@umijs/route-utils": "^4.0.1",
         "ahooks": "^3.7.8",
         "antd": "^5.14.0",
+        "better-xlsx": "^0.7.6",
         "bignumber.js": "^9.1.1",
         "classnames": "^2.3.2",
         "content-disposition": "^0.5.4",
         "echarts": "^5.4.3",
+        "file-saver": "^2.0.5",
         "jsencrypt": "^3.3.2",
         "lodash": "^4.17.21",
         "moment": "^2.29.4",
@@ -71,7 +73,8 @@
         "react-draggable": "^4.4.5",
         "react-helmet-async": "^1.3.0",
         "react-json-view": "^1.21.3",
-        "vanilla-jsoneditor": "^0.18.11"
+        "vanilla-jsoneditor": "^0.18.11",
+        "xlsx": "^0.18.5"
     },
     "devDependencies": {
         "@ant-design/pro-cli": "^3.1.0",

+ 2 - 4
YBEE.EQM.Admin/scripts/gapi/index.js

@@ -219,13 +219,11 @@ const getXnRestfulResultType = (propRef) => {
         return getResultType(ps);
     }
     if (ps[0] == 'List') {
-        return `${getResultType(ps[1])}[]`;
+        return `${getResultType(rtype.replaceAll('List_', ''))}[]`;
     }
     if (ps[0] == 'PageResult') {
-        return `API.PageResponse<${getResultType(ps[1])}>`;
+        return `API.PageResponse<${getResultType(rtype.replaceAll('PageResult_', ''))}>`;
     }
-    // return getResultType(ps[1]);
-    // return getResultType(ps[0]);
     return getResultType(rtype);
 };
 

+ 306 - 0
YBEE.EQM.Admin/src/common/exportExcel.ts

@@ -0,0 +1,306 @@
+// import { ColumnType } from "antd/es/table";
+// import BetterXlsxFile from 'better-xlsx';
+// import { saveAs } from 'file-saver';
+// import lodash from 'lodash';
+
+
+// /** 导出excel列类型 */
+// // export type ExportExcelColumnsType<RecordType = any> = (ProColumns<RecordType> | ColumnType<RecordType>) & { children?: ExportExcelColumnsType<RecordType>[] };
+// export type ExportExcelColumnsType<RecordType = any> = ColumnType<RecordType> & { children?: ExportExcelColumnsType<RecordType>[] };
+// /** 导出excel属性 */
+// export type ExportExcelProps<RecordType> = {
+//     /** 标题 */
+//     title?: string;
+//     /** 副标题 */
+//     subTitle?: string;
+//     /** 摘要 */
+//     summary?: string;
+//     /** 导出文件名 */
+//     fileName?: string;
+//     /** 列定义 */
+//     columns: ExportExcelColumnsType<RecordType>[];
+//     /** 数据 */
+//     dataSource: RecordType[];
+// }
+
+
+// /** 获取表头行数 */
+// const getColumnDepth = <RecordType>(cols: ExportExcelColumnsType<RecordType>[]) => {
+//     const eleDepths: number[] = [];
+//     cols.forEach(ele => {
+//         let depth = 0;
+//         if (Array.isArray(ele.children)) {
+//             depth = getColumnDepth(ele.children);
+//         }
+//         eleDepths.push(depth);
+//     });
+//     return 1 + (lodash.max(eleDepths) ?? 0);
+// }
+// /* 计算表头列数 */
+// const getColumnCount = <RecordType>(cols: ExportExcelColumnsType<RecordType>[]) => {
+//     let colCount = 0;
+//     cols.forEach(ele => {
+//         if (ele.children) {
+//             getColumnCount(ele.children);
+//         }
+//         else {
+//             colCount++;
+//         }
+//     });
+//     return colCount;
+// }
+// /* 按顺序展平column */
+// const columnLine = <RecordType>(cols: ExportExcelColumnsType<RecordType>[]) => {
+//     let columnLineArr: ExportExcelColumnsType<RecordType>[] = [];
+//     cols.forEach(ele => {
+//         if (ele.children === undefined || ele.children.length === 0) {
+//             columnLineArr.push(ele);
+//         } else {
+//             columnLineArr = columnLineArr.concat(columnLine(ele.children));
+//         }
+//     });
+//     return columnLineArr;
+// }
+// /** 设置单元格边框 */
+// const setBorder = (cell: any) => {
+//     const borderColor = 'ff000000';
+//     cell.style.border.top = 'thin';
+//     cell.style.border.topColor = borderColor;
+//     cell.style.border.left = 'thin';
+//     cell.style.border.leftColor = borderColor;
+//     cell.style.border.bottom = 'thin';
+//     cell.style.border.bottomColor = borderColor;
+//     cell.style.border.right = 'thin';
+//     cell.style.border.rightColor = borderColor;
+// }
+// /* 初始化表头 */
+// const initColumn = <RecordType>(sheet: any, depth: number, cols: ExportExcelColumnsType<RecordType>[], rowIndex: number, columnIndex: number, level: number = 0) => {
+//     let colIndex = columnIndex;
+//     cols.forEach((item) => {
+//         let hCell = sheet.cell(rowIndex, colIndex);
+//         setBorder(hCell);
+//         hCell.style.font.name = '黑体';
+//         hCell.style.font.bold = true;
+//         hCell.style.font.size = 10;
+//         hCell.style.fill.patternType = 'solid';
+//         hCell.style.fill.fgColor = 'fffafafa';
+//         hCell.style.fill.bgColor = 'ffffffff';
+
+//         // 如果没有子元素, 撑满列
+//         if (item.title === '操作') {
+//             hCell.value = '';
+//             return;
+//         }
+
+//         if (!item.children || item.children.length === 0) {
+//             // 第一行加一个单元格
+//             hCell.value = item.title;
+//             hCell.vMerge = depth - level - 1;
+//             hCell.style.align.h = 'center';
+//             hCell.style.align.v = 'center';
+//             colIndex++;
+//         }
+//         else {
+//             let childrenNum = 0;
+//             const getColumns = (arr: ExportExcelColumnsType<RecordType>[]) => {
+//                 arr.forEach(ele => {
+//                     if (ele.children) {
+//                         getColumns(ele.children);
+//                     }
+//                     else {
+//                         childrenNum++;
+//                     }
+//                 });
+//             }
+//             getColumns(item.children);
+
+//             hCell.hMerge = childrenNum - 1;
+//             hCell.value = item.title;
+//             hCell.style.align.h = 'center';
+//             hCell.style.align.v = 'center';
+
+//             let rowCopy = rowIndex;
+//             rowCopy++;
+
+//             initColumn(sheet, depth, item.children, rowCopy, colIndex, level + 1);
+
+//             // 下次单元格起点
+//             colIndex = colIndex + childrenNum;
+//         }
+//     });
+// }
+// // 通过数组方式读取多级属性的函数
+// const getNestedProperty = (obj: any, keys: string[]) => {
+//     return keys.reduce((accumulator, key) => {
+//         return (accumulator && accumulator[key] !== undefined) ? accumulator[key] : undefined;
+//     }, obj);
+// }
+
+// /** 导入excel */
+// export default <RecordType>(props: ExportExcelProps<RecordType>) => {
+//     console.log(props);
+//     const { title, subTitle, summary, fileName, columns, dataSource } = props;
+
+//     return new Promise((resolve, reject) => {
+//         try {
+//             // 新建工作薄
+//             const file = new BetterXlsxFile.File();
+//             // 新建工作表
+//             let sheet = file.addSheet('sheet1');
+//             // 行数
+//             let rowCount = 0;
+//             // 获取表头行数
+//             let depth = getColumnDepth(columns);
+//             // 获取表头的列数
+//             let columnNum = getColumnCount(columns);
+//             // 按顺序展平column
+//             let columnLineArr = columnLine(columns);
+
+//             const cc = columnLineArr.length - 1;
+
+//             // ---------------------------------------------
+//             // 表头
+//             // ---------------------------------------------
+//             rowCount++;
+
+//             let rowTitle = sheet.addRow();
+//             rowTitle.setHeightCM(2);
+
+//             let cellTitle = rowTitle.addCell();
+//             cellTitle.value = title;
+//             cellTitle.style.font.name = '黑体';
+//             cellTitle.style.font.bold = true;
+//             cellTitle.style.font.size = 16;
+//             cellTitle.style.align.v = 'center';
+//             cellTitle.style.align.h = 'center';
+//             cellTitle.hMerge = cc;
+
+//             if (subTitle) {
+//                 rowCount++;
+
+//                 let rowSubTitle = sheet.addRow();
+//                 rowSubTitle.setHeightCM(1);
+
+//                 let cellSubTitle = rowSubTitle.addCell();
+//                 cellSubTitle.value = subTitle;
+//                 cellSubTitle.style.font.name = '黑体';
+//                 cellSubTitle.style.font.size = 12;
+//                 cellSubTitle.style.align.v = 'center';
+//                 cellSubTitle.style.align.h = 'center';
+//                 cellSubTitle.hMerge = cc;
+//             }
+//             if (summary) {
+//                 rowCount++;
+
+//                 let rowSummary = sheet.addRow();
+//                 rowSummary.setHeightCM(1);
+
+//                 let cellSummary = rowSummary.addCell();
+//                 cellSummary.value = summary;
+//                 cellSummary.style.font.name = '黑体';
+//                 cellSummary.style.font.size = 10;
+//                 cellSummary.style.align.v = 'center';
+//                 cellSummary.style.align.h = 'left';
+//                 cellSummary.hMerge = cc;
+//             }
+//             // ---------------------------------------------
+
+//             sheet.addRow();
+//             rowCount++;
+
+//             // 新建表头行数
+//             let rowArr = [];
+//             for (let k = 0; k < depth; k++) {
+//                 rowArr.push(sheet.addRow());
+//             }
+
+//             // 根据列数填充单元格
+//             rowArr.forEach(ele => {
+//                 for (let j = 0; j < columnNum; j++) {
+//                     let cell = ele.addCell();
+//                     cell.value = j;
+//                     setBorder(cell);
+//                 }
+//             });
+
+//             // 初始化表头
+//             initColumn(sheet, depth, columns, rowCount, 0, 0);
+//             rowCount += depth;
+
+//             // 根据column,将dataSource里面的数据排序,并且转化为二维数组
+//             let dataSourceArr: any[] = [];
+//             dataSource.forEach((ele, index) => {
+//                 let dataTemp: any[] = [];
+//                 columnLineArr.forEach((item, i) => {
+//                     let v = '';
+//                     if (item.dataIndex && (ele as any).hasOwnProperty(item.dataIndex)) {
+//                         if (Array.isArray(item.dataIndex)) {
+//                             v = getNestedProperty(ele, item.dataIndex as string[]);
+//                         }
+//                         v = ele[item.dataIndex];
+//                     }
+//                     if (typeof item.render === 'function') {
+//                         const rv = item.render(v, ele, index);
+//                         if (rv && rv.hasOwnProperty('children')) {
+//                             v = (rv as any).children ?? '';
+//                         } else {
+//                             v = `${rv ?? ''}`;
+//                         }
+//                     }
+
+//                     let mc = {};
+//                     const mergeCell = ele.mergeCell || new Map();
+//                     if (mergeCell.has(i)) {
+//                         mc = mergeCell.get(i);
+//                     }
+
+//                     dataTemp.push({
+//                         value: v,
+//                         props: mc,
+//                     });
+//                 });
+//                 dataSourceArr.push(dataTemp);
+//             });
+
+//             // 绘画表格数据
+//             dataSourceArr.forEach((item, index) => {
+//                 //根据数据,创建对应个数的行
+//                 let row = sheet.addRow();
+//                 // row.setHeightCM(0.8);
+//                 //创建对应个数的单元格
+//                 item.forEach((ele: any) => {
+//                     let cell = row.addCell();
+
+//                     cell.value = ele.value;
+//                     if (ele.props) {
+//                         cell.vMerge = (ele.props.rowSpan || 1) - 1;
+//                         if (ele.props.colSpan) {
+//                             cell.hMerge = ele.props.colSpan - 1;
+//                         }
+//                     }
+
+//                     setBorder(cell);
+//                     cell.style.font.size = 10;
+//                     cell.style.align.v = 'center';
+//                     cell.style.align.h = 'center';
+//                 });
+//             });
+
+//             rowCount += dataSourceArr.length;
+
+//             // //设置每列的宽度
+//             // for (var i = 0; i < columnNum; i++) {
+//             //     sheet.col(i).width = 5;
+//             // }
+
+
+//             file.saveAs('blob').then((content: any) => {
+//                 saveAs(content, `${fileName || title}.xlsx`);
+//                 resolve(true);
+//             });
+//         }
+//         catch (err) {
+//             reject(err);
+//         }
+//     });
+// }

+ 82 - 0
YBEE.EQM.Admin/src/common/exportToExcel.ts

@@ -0,0 +1,82 @@
+// import { saveAs } from 'file-saver';
+// import * as XLSX from 'xlsx';
+
+// interface Column {
+//     title?: string;
+//     dataIndex?: string;
+//     children?: Column[];
+// }
+
+// interface Data {
+//     [key: string]: any;
+// }
+
+// interface Props {
+//     columns: Column[];
+//     data: Data[];
+// }
+
+// const getHeaderRows = (columns: Column[], depth: number = 0): any => {
+//     const headers: any[] = [];
+//     const merges: any[] = [];
+//     const traverse = (cols: Column[], depth: number) => {
+//         headers[depth] = headers[depth] || [];
+//         cols.forEach((col) => {
+//             headers[depth].push(col.title);
+//             if (col.children && col.children.length > 0) {
+//                 merges.push({
+//                     s: { r: depth, c: headers[depth].length - 1 },
+//                     e: { r: depth, c: headers[depth].length - 1 + col.children.length - 1 },
+//                 });
+//                 traverse(col.children, depth + 1);
+//             } else {
+//                 for (let i = depth + 1; i < headers.length; i++) {
+//                     headers[i].push('');
+//                 }
+//             }
+//         });
+//     };
+//     traverse(columns, depth);
+//     return { headers, merges };
+// };
+
+// const exportToExcel = (props: Props) => {
+//     const { columns, data } = props;
+
+//     const { headers, merges } = getHeaderRows(columns);
+
+//     // Flatten data based on columns
+//     const flattenData = (data: Data[], columns: Column[]): any[] => {
+//         return data.map((row) => {
+//             const flattenedRow: any[] = [];
+//             const flatten = (cols: Column[], row: Data) => {
+//                 cols.forEach((col) => {
+//                     if (col.children && col.children.length > 0) {
+//                         flatten(col.children, row);
+//                     } else {
+//                         flattenedRow.push(row[col.dataIndex!]);
+//                     }
+//                 });
+//             };
+//             flatten(columns, row);
+//             return flattenedRow;
+//         });
+//     };
+
+//     const rows = flattenData(data, columns);
+
+//     const wsData = [...headers, ...rows];
+
+//     // Create worksheet
+//     const worksheet = XLSX.utils.aoa_to_sheet(wsData);
+//     worksheet['!merges'] = merges;
+
+//     // Create workbook and export
+//     const workbook = XLSX.utils.book_new();
+//     XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+//     const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
+
+//     saveAs(new Blob([wbout], { type: 'application/octet-stream' }), 'data.xlsx');
+// };
+
+// export default exportToExcel;

+ 1 - 1
YBEE.EQM.Admin/src/pages/auth/Login/index.tsx

@@ -270,7 +270,7 @@ const Login: React.FC = () => {
 
             if (auth.isActivated) {
                 if (rememberMe) {
-                    localStorage.setItem(REMEMBER_ME, JSON.stringify(values));
+                    localStorage.setItem(REMEMBER_ME, JSON.stringify({ account, password, rememberMe }));
                 } else {
                     localStorage.removeItem(REMEMBER_ME);
                 }

+ 48 - 6
YBEE.EQM.Admin/src/pages/exam-org/OrgExamPlanDetail/index.tsx

@@ -1,4 +1,5 @@
-import { CardStepTitle } from "@/components";
+import { TrueOrFalseValueEnum } from "@/common/valueEnum";
+import { CardStepTitle, TagStatus } from "@/components";
 import ExamPlanController from "@/services/apis/ExamPlanController";
 import { ReloadOutlined } from "@ant-design/icons";
 import { PageContainer, ProCard, ProDescriptions, ProTable } from "@ant-design/pro-components";
@@ -66,16 +67,57 @@ const OrgExamPlanDetail: React.FC = () => {
                             return index + 1;
                         },
                     },
-                    { title: '名称', dataIndex: ['grade', 'name'], width: 112, align: 'center' },
-                    { title: '全称', dataIndex: ['grade', 'fullName'], width: 160, align: 'center' },
-                    { title: '级(入学年)', dataIndex: 'gradeBeginName', width: 128, align: 'center' },
-                    { title: '届(毕业年)', dataIndex: 'gradeEndName', width: 128, align: 'center' },
+                    {
+                        title: '名称',
+                        dataIndex: ['grade', 'name'],
+                        width: 112,
+                        align: 'center',
+                    },
+                    {
+                        title: '全称',
+                        dataIndex: ['grade', 'fullName'],
+                        width: 160,
+                        align: 'center',
+                    },
+                    {
+                        title: '级(入学年)',
+                        dataIndex: 'gradeBeginName',
+                        width: 128,
+                        align: 'center',
+                    },
+                    {
+                        title: '届(毕业年)',
+                        dataIndex: 'gradeEndName',
+                        width: 128,
+                        align: 'center',
+                    },
+                    {
+                        title: '需要自编监测号',
+                        dataIndex: 'isRequiredSelfExamNumber',
+                        width: 120,
+                        align: 'center',
+                        render: (v, r) => {
+                            const s = TrueOrFalseValueEnum[`${r.isRequiredSelfExamNumber}`];
+                            return (<TagStatus status={s.status}>{s.text}</TagStatus>);
+                        },
+                    },
+                    {
+                        title: '自编监测号长度',
+                        dataIndex: 'selfExamNumberLength',
+                        width: 120,
+                        align: 'center',
+                        render: (v, r) => {
+                            if (r.isRequiredSelfExamNumber) {
+                                return v;
+                            }
+                            return '/';
+                        },
+                    },
                     {},
                 ]}
             />
             <OrgExamDataReportList examPlanId={reqParams.id} />
             <OrgExamDataPublishList examPlanId={reqParams.id} />
-
             <FloatButton.BackTop visibilityHeight={100} />
         </PageContainer>
     );

+ 3 - 3
YBEE.EQM.Admin/src/pages/exam-org/absent-replace/OrgExamAbsentReplaceReport/index.tsx

@@ -128,7 +128,7 @@ const OrgExamAbsentReplaceReport: React.FC = () => {
     const handleSubmit = useCallback(() => {
         return new Promise<void>((resolve, reject) => {
             if ((studentCountData?.total ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
-                message.error('未上传《缺测替补学生明细表》和《会议记录》打印盖章的扫描电子文件');
+                message.error('未上传《缺测替补学生明细表》、《缺测学生书面说明》和《会议记录》打印盖章的扫描电子文件');
                 reject();
                 return;
             }
@@ -600,7 +600,7 @@ const OrgExamAbsentReplaceReport: React.FC = () => {
                         <Typography>
                             <ol>
                                 <li>在下方 <Typography.Text strong>缺测替补学生明细</Typography.Text> 中录入缺测替补学生信息,<Typography.Text type="danger">并上传佐证材料(必传)</Typography.Text>;</li>
-                                <li>缺测替补学生信息录入完成后,点击 <Typography.Text strong>下载打印表格文件</Typography.Text> 下载文件打印签字盖章;</li>
+                                <li><Typography.Text strong type="danger">所有科目监测结束后</Typography.Text>,点击 <Typography.Text strong>下载打印表格文件</Typography.Text> 下载文件打印签字盖章;</li>
                                 <li>扫描已签字盖章的文件为电子文档(PDF或图片);</li>
                                 <li>在 <Typography.Text strong> 缺测替补学生上报</Typography.Text> 的 <Typography.Text strong>上传《缺测替补学生明细表》打印、签字、盖章的扫描电子文件</Typography.Text> 中上传电子文档<Typography.Text type="danger">(必传)</Typography.Text>;</li>
                                 <li>确认无误后点击 <Typography.Text strong> 缺测替补学生上报</Typography.Text> 右侧的 <Typography.Text strong>立即上报</Typography.Text> 按钮完成上传。</li>
@@ -609,7 +609,7 @@ const OrgExamAbsentReplaceReport: React.FC = () => {
                     }
                 />
 
-                <Typography.Title level={5} style={{ marginTop: token.marginSM }} type="danger">上传《缺测替补学生明细表》打印、签字、盖章的扫描电子文件(最多上传6个文件):</Typography.Title>
+                <Typography.Title level={5} style={{ marginTop: token.marginSM }} type="danger">上传《缺测替补学生明细表》、《缺测学生书面说明》和《会议记录》打印、签字、盖章的扫描电子文件(最多上传6个文件):</Typography.Title>
                 <Card bordered ref={tourUploadRef}>
                     <Space direction="vertical" style={{ width: '100%' }}>
                         {reportData?.examOrgDataReport?.attachmentList?.map((t, i) => {

+ 184 - 165
YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentImport/components/ExamStudentImportEditModal.tsx

@@ -4,15 +4,17 @@ import { CertificateType } from '@/services/enums';
 import { FormInstance, ProFormDigit, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
 import { useModel } from '@umijs/max';
 import { Col, Row } from 'antd';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 
 /** 修改监测学生信息 */
 const ExamStudentImportEditModal: React.FC<{
+    isRequiredSelfExamNumber?: boolean;
+    selfExamNumberLength?: number;
     data: Partial<API.UploadExamStudentOutput>;
     hasNceeCourseComb?: boolean;
     onFinish: (values: API.UploadExamStudentOutput) => void;
     onClose?: () => void;
-}> = ({ data, hasNceeCourseComb, onFinish, onClose }) => {
+}> = ({ data, isRequiredSelfExamNumber, selfExamNumberLength, hasNceeCourseComb, onFinish, onClose }) => {
     const [open, setOpen] = useState<boolean>(true);
     const handleClose = () => { setOpen(false); setTimeout(() => onClose?.(), 300); };
 
@@ -23,177 +25,194 @@ const ExamStudentImportEditModal: React.FC<{
 
     const [certificateType, setCertificateType] = useState(data.certificateType);
 
+    useEffect(() => { formRef.current?.validateFields(); }, []);
+
     return (
-        <>
-            <MovableModalForm<API.UploadExamStudentOutput>
-                title="修改导入学生信息"
-                width={800}
-                open={open}
-                formRef={formRef}
-                initialValues={{
-                    ...data,
-                    certificateType: data.certificateType !== undefined ? `${data.certificateType}` : undefined,
-                    gender: data.gender !== undefined ? `${data.gender}` : undefined,
-                    nceeCourseCombId: data.nceeCourseCombId !== undefined ? data.nceeCourseCombId : undefined,
-                }}
-                modalProps={{
-                    centered: true,
-                    maskClosable: false,
-                    onCancel: () => {
-                        formRef?.current?.resetFields();
-                        handleClose();
-                    },
-                }}
-                onFinish={async (values) => {
-                    const { name, idNumber, examNumber, roomNumber, seatNumber, remark, nceeCourseCombId, ...restValues } = values;
-                    const cmb = baseData?.nceeCourseCombs?.find(t => t.id === nceeCourseCombId);
-                    onFinish({
-                        ...data,
-                        ...restValues,
-                        name: name.replaceAll(/\s/g, ''),
-                        idNumber: idNumber?.replaceAll(/\s/g, ''),
-                        examNumber: examNumber?.replaceAll(/\s/g, ''),
-                        roomNumber: roomNumber?.replaceAll(/\s/g, ''),
-                        seatNumber: seatNumber?.replaceAll(/\s/g, ''),
-                        remark: remark?.replaceAll(/\s/g, ''),
-                        nceeCourseCombId,
-                        nceeCourseCombName: cmb?.shortName,
-                        isSuccess: true,
-                        errorMessage: [],
-                    });
+        <MovableModalForm<API.UploadExamStudentOutput>
+            title="修改导入学生信息"
+            width={800}
+            open={open}
+            formRef={formRef}
+            initialValues={{
+                ...data,
+                certificateType: data.certificateType !== undefined ? `${data.certificateType}` : undefined,
+                gender: data.gender !== undefined ? `${data.gender}` : undefined,
+                nceeCourseCombId: data.nceeCourseCombId !== undefined ? data.nceeCourseCombId : undefined,
+            }}
+            modalProps={{
+                centered: true,
+                maskClosable: false,
+                onCancel: () => {
+                    formRef?.current?.resetFields();
                     handleClose();
-                }}
-            >
-                <Row gutter={[24, 0]}>
-                    <Col span={12}>
-                        <ProFormDigit
-                            label="班级"
-                            tooltip="班级最小1,最大35"
-                            name="classNumber"
-                            min={1}
-                            max={35}
-                            required
-                            rules={[{ required: true }]}
-                        />
-                    </Col>
-                    <Col span={12}>
-                        <ProFormText
-                            label="姓名"
-                            name="name"
-                            required
-                            rules={[{ required: true, min: 2, max: 100 }]}
-                            fieldProps={{
-                                minLength: 2,
-                                maxLength: 100,
-                                showCount: true,
-                            }}
-                        />
-                    </Col>
-                    <Col span={12}>
-                        <ProFormSelect
-                            label="证件类型"
-                            name="certificateType"
-                            rules={[{ required: true }]}
-                            valueEnum={getDictValueEnum('certificate_type')}
-                            onChange={(v) => setCertificateType(JSON.parse(v as string))}
-                        />
-                    </Col>
-                    <Col span={12}>
-                        <ProFormText
-                            label="证件号码"
-                            name="idNumber"
-                            dependencies={['certificateType']}
-                            fieldProps={{
-                                maxLength: 18,
-                                showCount: true,
-                            }}
-                            required={certificateType !== CertificateType.NONE}
-                            rules={[
-                                ({ getFieldValue, setFieldsValue }) => ({
-                                    validator: (_, value) => {
-                                        const ct = JSON.parse(getFieldValue("certificateType"));
-                                        if (ct === CertificateType.NONE) {
+                },
+            }}
+            onFinish={async (values) => {
+                const { name, idNumber, examNumber, roomNumber, seatNumber, remark, nceeCourseCombId, ...restValues } = values;
+                const cmb = baseData?.nceeCourseCombs?.find(t => t.id === nceeCourseCombId);
+                onFinish({
+                    ...data,
+                    ...restValues,
+                    name: name.replaceAll(/\s/g, ''),
+                    idNumber: idNumber?.replaceAll(/\s/g, ''),
+                    examNumber: examNumber?.replaceAll(/\s/g, ''),
+                    roomNumber: roomNumber?.replaceAll(/\s/g, ''),
+                    seatNumber: seatNumber?.replaceAll(/\s/g, ''),
+                    remark: remark?.replaceAll(/\s/g, ''),
+                    nceeCourseCombId,
+                    nceeCourseCombName: cmb?.shortName,
+                    isSuccess: true,
+                    errorMessage: [],
+                });
+                handleClose();
+            }}
+        >
+            <Row gutter={[24, 0]}>
+                <Col span={12}>
+                    <ProFormDigit
+                        label="班级"
+                        tooltip="班级最小1,最大35"
+                        name="classNumber"
+                        min={1}
+                        max={35}
+                        required
+                        rules={[{ required: true }]}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormText
+                        label="姓名"
+                        name="name"
+                        required
+                        rules={[{ required: true, min: 2, max: 100 }]}
+                        fieldProps={{
+                            minLength: 2,
+                            maxLength: 100,
+                            showCount: true,
+                        }}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormSelect
+                        label="证件类型"
+                        name="certificateType"
+                        rules={[{ required: true }]}
+                        valueEnum={getDictValueEnum('certificate_type')}
+                        onChange={(v) => setCertificateType(JSON.parse(v as string))}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormText
+                        label="证件号码"
+                        name="idNumber"
+                        dependencies={['certificateType']}
+                        fieldProps={{
+                            maxLength: 18,
+                            showCount: true,
+                        }}
+                        required={certificateType !== CertificateType.NONE}
+                        rules={[
+                            ({ getFieldValue, setFieldsValue }) => ({
+                                validator: (_, value) => {
+                                    const ct = JSON.parse(getFieldValue("certificateType"));
+                                    if (ct === CertificateType.NONE) {
+                                        return Promise.resolve();
+                                    }
+                                    if (ct === CertificateType.ID_CARD) {
+                                        const idc = validateIdCard(value);
+                                        if (idc.success) {
+                                            setFieldsValue({ gender: `${idc.gender}` });
                                             return Promise.resolve();
                                         }
-                                        if (ct === CertificateType.ID_CARD) {
-                                            const idc = validateIdCard(value);
-                                            if (idc.success) {
-                                                setFieldsValue({ gender: `${idc.gender}` });
-                                                return Promise.resolve();
-                                            }
-                                            else {
-                                                return Promise.reject(`身份证号${idc.errorMessage}有误`);
-                                            }
-                                        }
-                                        if (value.length > 0) {
-                                            return Promise.resolve();
+                                        else {
+                                            return Promise.reject(`身份证号${idc.errorMessage}有误`);
                                         }
-                                        return Promise.reject('证件号码必须输入');
-                                    },
-                                }),
-                            ]}
-                        />
-                    </Col>
-                    <Col span={12}>
-                        <ProFormRadio.Group
-                            label="性别"
-                            name="gender"
-                            valueEnum={getDictValueEnum('gender')}
-                            required
-                            rules={[{ required: true }]}
-                        />
-                    </Col>
+                                    }
+                                    if (value.length > 0) {
+                                        return Promise.resolve();
+                                    }
+                                    return Promise.reject('证件号码必须输入');
+                                },
+                            }),
+                        ]}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormRadio.Group
+                        label="性别"
+                        name="gender"
+                        valueEnum={getDictValueEnum('gender')}
+                        required
+                        rules={[{ required: true }]}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormText
+                        label={`自编监测号${isRequiredSelfExamNumber ? `(当前年级必录,长度为${selfExamNumberLength}位)` : ''}`}
+                        // label={`自编监测号`}
+                        // tooltip={`(当前年级必录,长度为${selfExamNumberLength}位)`}
+                        name="examNumber"
+                        fieldProps={{
+                            maxLength: isRequiredSelfExamNumber ? selfExamNumberLength : 20,
+                            showCount: true
+                        }}
+                        required={isRequiredSelfExamNumber}
+                        rules={[
+                            // { required: isRequiredSelfExamNumber, message: '当前年级必录' },
+                            () => ({
+                                validator: (_, value) => {
+                                    if (!isRequiredSelfExamNumber) {
+                                        return Promise.resolve();
+                                    }
+                                    if ((value ?? '').length !== selfExamNumberLength) {
+                                        return Promise.reject(`当前年级必录,长度为${selfExamNumberLength}位`);
+                                    }
+                                    return Promise.resolve();
+                                },
+                            }),
+                        ]}
+                    />
+                </Col>
+                {hasNceeCourseComb &&
                     <Col span={12}>
-                        <ProFormText
-                            label="自编监测号"
-                            name="examNumber"
-                            fieldProps={{
-                                maxLength: 20,
-                                showCount: true
-                            }}
-                        />
-                    </Col>
-                    {hasNceeCourseComb &&
-                        <Col span={12}>
-                            <ProFormSelect
-                                label="选科组合"
-                                name="nceeCourseCombId"
-                                options={baseData?.nceeCourseCombs?.map(t => ({ label: t.shortName, value: t.id }))}
-                            />
-                        </Col>
-                    }
-                    <Col span={12}>
-                        <ProFormText
-                            label="考场号"
-                            name="roomNumber"
-                            fieldProps={{
-                                maxLength: 20,
-                                showCount: true
-                            }}
-                        />
-                    </Col>
-                    <Col span={12}>
-                        <ProFormText
-                            label="座位号"
-                            name="seatNumber"
-                            fieldProps={{
-                                maxLength: 20,
-                                showCount: true
-                            }}
+                        <ProFormSelect
+                            label="选科组合"
+                            name="nceeCourseCombId"
+                            options={baseData?.nceeCourseCombs?.map(t => ({ label: t.shortName, value: t.id }))}
                         />
                     </Col>
-                </Row>
-                <ProFormTextArea
-                    label="备注"
-                    name="remark"
-                    fieldProps={{
-                        maxLength: 200,
-                        rows: 4,
-                        showCount: true,
-                    }}
-                />
-            </MovableModalForm >
-        </>
+                }
+                <Col span={12}>
+                    <ProFormText
+                        label="考场号"
+                        name="roomNumber"
+                        fieldProps={{
+                            maxLength: 20,
+                            showCount: true
+                        }}
+                    />
+                </Col>
+                <Col span={12}>
+                    <ProFormText
+                        label="座位号"
+                        name="seatNumber"
+                        fieldProps={{
+                            maxLength: 20,
+                            showCount: true
+                        }}
+                    />
+                </Col>
+            </Row>
+            <ProFormTextArea
+                label="备注"
+                name="remark"
+                fieldProps={{
+                    maxLength: 200,
+                    rows: 4,
+                    showCount: true,
+                }}
+            />
+        </MovableModalForm >
     );
 };
 

+ 30 - 2
YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentImport/index.tsx

@@ -37,6 +37,8 @@ const OrgExamStudentImport: React.FC = () => {
     const currentRef = useRef<API.UploadExamStudentOutput>();
     const [editOpen, setEditOpen] = useState(false);
 
+    const [grade, setGrade] = useState<API.ExamGradeOutput>();
+
     const { token } = theme.useToken();
     const { message, modal } = App.useApp();
 
@@ -103,8 +105,14 @@ const OrgExamStudentImport: React.FC = () => {
 
     // 上传
     const handleUpload = async () => {
+        if (!grade) {
+            message.error('请先选择年级');
+            return;
+        }
+
         const formData = new FormData();
         formData.append('examPlanId', `${reqParams.examPlanId}`);
+        formData.append('examGradeId', `${grade?.id}`);
         fileList.forEach((file) => {
             formData.append('file', file as RcFile);
         });
@@ -389,7 +397,21 @@ const OrgExamStudentImport: React.FC = () => {
                         style={{ width: 240 }}
                         required
                         rules={[{ required: true, message: '年级必须选择' }]}
+                        onChange={(e: number) => {
+                            const g = baseData?.examGrades.find(t => t.id === e);
+                            setGrade(g);
+                        }}
                     />
+                    {grade && grade.isRequiredSelfExamNumber &&
+                        <Alert
+                            type="warning"
+                            message={`该年级必须自编监测号(长度为${grade.selfExamNumberLength}位)`}
+                            showIcon
+                            style={{
+                                marginRight: token.marginLG,
+                            }}
+                        />
+                    }
                     {baseData?.branches && baseData.branches.length > 0 &&
                         <ProFormSelect
                             label="校区"
@@ -471,12 +493,16 @@ const OrgExamStudentImport: React.FC = () => {
             </ProCard>
 
             <ProCard
-                title={<CardStepTitle>第三步:上传文件</CardStepTitle>}
+                title={
+                    <CardStepTitle>
+                        第三步:上传文件
+                        {!grade && <Typography.Text type="danger">(请先选择年级!)</Typography.Text>}
+                    </CardStepTitle>}
                 style={{ marginTop: token.margin }}
                 ref={tourChooseFileRef}
             >
                 <Space direction="vertical" style={{ width: '100%' }}>
-                    <Upload.Dragger {...uploadProps}>
+                    <Upload.Dragger {...uploadProps} disabled={!grade}>
                         <p className="ant-upload-drag-icon"><InboxOutlined /></p>
                         <p className="ant-upload-text">点击或拖入文件到此处</p>
                     </Upload.Dragger>
@@ -644,6 +670,8 @@ const OrgExamStudentImport: React.FC = () => {
 
             {editOpen && currentRef.current &&
                 <ExamStudentImportEditModal
+                    isRequiredSelfExamNumber={grade?.isRequiredSelfExamNumber}
+                    selfExamNumberLength={grade?.selfExamNumberLength}
                     data={currentRef.current}
                     hasNceeCourseComb={baseData?.hasNceeCourseComb}
                     onFinish={handleEdit}

+ 34 - 5
YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentReport/components/ExamStudentEditModal.tsx

@@ -6,7 +6,7 @@ import { CertificateType } from '@/services/enums';
 import { FormInstance, ProFormDigit, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
 import { useModel } from '@umijs/max';
 import { App, Col, Row } from 'antd';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 
 export type GradeBranchData = {
     examGrades: API.ExamGradeOutput[];
@@ -32,12 +32,21 @@ const ExamStudentEditModal: React.FC<{
     const { baseData } = useModel('useBaseData');
 
     const [certType, setCertType] = useState(data.certificateType);
+    const [grade, setGrade] = useState<API.ExamGradeOutput>();
+
+    useEffect(() => {
+        if (!data.examGradeId) {
+            return;
+        }
+        const g = gradeBranch?.examGrades.find(t => t.id === data.examGradeId);
+        setGrade(g);
+    }, []);
 
     return (
         <>
             <MovableModalForm<API.ExamStudentOutput>
                 title={`${data.id === '0' ? '添加' : '修改'}监测学生信息`}
-                width={1080}
+                width={800}
                 open={open}
                 formRef={formRef}
                 initialValues={{
@@ -93,6 +102,11 @@ const ExamStudentEditModal: React.FC<{
                             required
                             rules={[{ required: true }]}
                             disabled={data.id !== '0'}
+                            onChange={(e: number) => {
+                                const g = gradeBranch?.examGrades.find(t => t.id === e);
+                                setGrade(g);
+                                formRef.current?.validateFields(['examNumber']);
+                            }}
                         />
                     </Col>
                     {gradeBranch.branches && gradeBranch.branches.length > 0 &&
@@ -187,13 +201,28 @@ const ExamStudentEditModal: React.FC<{
                     </Col>
                     <Col span={12}>
                         <ProFormText
-                            label="自编监测号"
+                            // label="自编监测号"
+                            label={`自编监测号${grade?.isRequiredSelfExamNumber ? `(当前年级必录,长度为${grade?.selfExamNumberLength}位)` : ''}`}
                             name="examNumber"
-                            tooltip="仅有明确要求时填写"
+                            // tooltip="仅有明确要求时填写"
                             fieldProps={{
-                                maxLength: 20,
+                                maxLength: grade?.isRequiredSelfExamNumber ? grade?.selfExamNumberLength : 20,
                                 showCount: true
                             }}
+                            required={grade?.isRequiredSelfExamNumber}
+                            rules={[
+                                () => ({
+                                    validator: (_, value) => {
+                                        if (!grade?.isRequiredSelfExamNumber) {
+                                            return Promise.resolve();
+                                        }
+                                        if ((value ?? '').length !== grade?.selfExamNumberLength) {
+                                            return Promise.reject(`当前年级必录,长度为${grade?.selfExamNumberLength}位`);
+                                        }
+                                        return Promise.resolve();
+                                    },
+                                }),
+                            ]}
                         />
                     </Col>
                     {gradeBranch.hasNceeCourseComb &&

+ 23 - 5
YBEE.EQM.Admin/src/pages/exam-org/student/OrgExamStudentReport/index.tsx

@@ -1,5 +1,5 @@
 import { toValueEnum } from "@/common/converter";
-import { CardStepTitle, SuperTable } from "@/components";
+import { CardStepTitle, FileLink, SuperTable } from "@/components";
 import ReportButton from "@/components/ReportButton";
 import ExamGradeController from "@/services/apis/ExamGradeController";
 import ExamOrgDataReportController from "@/services/apis/ExamOrgDataReportController";
@@ -11,7 +11,7 @@ import { BulbOutlined, PlusOutlined, ReloadOutlined, UploadOutlined } from "@ant
 import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions, ProTable } from "@ant-design/pro-components";
 import { history, useModel, useParams } from "@umijs/max";
 import { useRequest } from "ahooks";
-import { Alert, App, Badge, Button, Drawer, Tag, Tour, Typography, theme } from "antd";
+import { Alert, App, Badge, Button, Drawer, Space, Tag, Tour, Typography, theme } from "antd";
 import { useCallback, useRef, useState } from "react";
 import ExamStudentEditModal from "./components/ExamStudentEditModal";
 
@@ -345,16 +345,34 @@ const OrgExamStudentReport: React.FC = () => {
                     <ProDescriptions.Item label="上报人员">{reportData?.examOrgDataReport?.reportSysUser?.name}</ProDescriptions.Item>
                     <ProDescriptions.Item label="上报时间">{reportData?.examOrgDataReport?.reportTime}</ProDescriptions.Item>
                     <ProDescriptions.Item label="备注说明">{reportData?.examOrgDataReport?.remark}</ProDescriptions.Item>
-                    <ProDescriptions.Item label="上报说明" span={3}>
+
+                </ProDescriptions>
+                <ProDescriptions layout="vertical" column={1}>
+                    <ProDescriptions.Item label="上报说明">
                         <Typography.Text ellipsis>{reportData?.examDataReport?.remark || '无'}</Typography.Text>
                         {reportData?.examDataReport?.remark && <Button type="link" size="small" onClick={() => setRemarkOpen(true)}>详情</Button>}
                     </ProDescriptions.Item>
+                    {reportData?.examDataReport?.attachmentList &&
+                        <ProDescriptions.Item label="附件文件">
+                            <Space direction="vertical" style={{ width: '100%' }}>
+                                {reportData?.examDataReport?.attachmentList?.map((t, i) => (
+                                    <FileLink key={i} {...t} card />
+                                ))}
+                            </Space>
+                        </ProDescriptions.Item>
+                    }
                 </ProDescriptions>
                 <Drawer open={remarkOpen} title="上报说明" width={720} maskClosable onClose={() => setRemarkOpen(false)}>
                     <pre>{reportData?.examDataReport?.remark || '无'}</pre>
                 </Drawer>
             </ProCard>
-            <Alert type="warning" showIcon message="确认各年级各班级填报无误后一次性上报,上报后不能再修改和上报!" closable style={{ marginTop: token.margin, color: token.colorError }} />
+            <Alert
+                type="warning"
+                showIcon
+                closable
+                style={{ marginTop: token.margin, color: token.colorError }}
+                message="确认各年级各班级填报无误后一次性上报,上报后不能再修改和上报!"
+            />
             <ProTable<any>
                 style={{ marginTop: token.margin }}
                 search={false}
@@ -369,7 +387,7 @@ const OrgExamStudentReport: React.FC = () => {
                 columnEmptyText=""
                 dataSource={studentCountData?.items ?? []}
                 toolbar={{
-                    title: <CardStepTitle>数量统计</CardStepTitle>,
+                    title: (<CardStepTitle>数量统计</CardStepTitle>),
                     actions: reportable ? [
                         <span key="tooltip">离上报结束还有</span>,
                         <ReportButton

+ 2 - 0
YBEE.EQM.Admin/src/typings.d.ts

@@ -49,3 +49,5 @@ type AppConfigType = {
 /** 统一配置 */
 declare let AppConfig: AppConfigType;
 
+declare module 'better-xlsx';
+declare module 'file-saver';

+ 4 - 4
YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceService.cs

@@ -488,7 +488,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
             XWPFParagraph pHeader = doc.CreateParagraph();
             pHeader.Alignment = ParagraphAlignment.CENTER;
             pHeader.SpacingLineRule = LineSpacingRule.AUTO;
-            pHeader.setSpacingBetween(1.5, LineSpacingRule.AUTO);
+            pHeader.SetSpacingBetween(1.5, LineSpacingRule.AUTO);
             var pHeaderRun = pHeader.CreateRun();
             pHeaderRun.SetText($"{gradeFirst.SysOrg.FullName.Replace("重庆市", "").Replace("重庆", "")}缺测替补明细表({gradeFirst.Grade.Name})");
             pHeaderRun.FontSize = 18;
@@ -500,7 +500,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
             // 签字行
             XWPFParagraph pSign = doc.CreateParagraph();
             pSign.SpacingLineRule = LineSpacingRule.AUTO;
-            pSign.setSpacingBetween(2, LineSpacingRule.AUTO);
+            pSign.SetSpacingBetween(2, LineSpacingRule.AUTO);
             var pSignRun = pSign.CreateRun();
             pSignRun.SetText("学校盖章:                                       纪检负责人:                                       校长签字:");
             pSignRun.FontSize = 12;
@@ -508,7 +508,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
             // 签字行
             XWPFParagraph pCount = doc.CreateParagraph();
             pCount.SpacingLineRule = LineSpacingRule.AUTO;
-            pCount.setSpacingBetween(2, LineSpacingRule.AUTO);
+            pCount.SetSpacingBetween(2, LineSpacingRule.AUTO);
             var dt = DateTime.Now;
             var pCountRun = pCount.CreateRun();
             pCountRun.SetText($"填表人:                                           联系电话:                                            督考签字:                                        缺测替补计({gitem.Count()})人          {dt.Year}年{dt.Month}月{dt.Day}日");
@@ -609,7 +609,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         {
             var p = doc.CreateParagraph();
             p.SpacingLineRule = LineSpacingRule.AUTO;
-            p.setSpacingBetween(lineSpacing, LineSpacingRule.AUTO);
+            p.SetSpacingBetween(lineSpacing, LineSpacingRule.AUTO);
             p.CreateRun().FontSize = 12;
         }
     }

+ 23 - 1
YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreImportDto.cs

@@ -87,4 +87,26 @@ public class ExamScoreImportDto
     /// 是否排除
     /// </summary>
     public bool IsExcluded { get; set; } = false;
-}
+}
+
+/// <summary>
+/// 小题成绩导入文件信息
+/// </summary>
+public class ExamScoreMinorFileInfo
+{
+    public ExamSampleType ExamSampleType { get; set; }
+    public short GradeId { get; set; }
+    public Grade Grade { get; set; }
+    public short CourseId { get; set; }
+    public Course Course { get; set; }
+    public string FilePath { get; set; }
+}
+
+public class ExamScoreMinorImportDto
+{
+    public short SysOrgId { get; set; } = 0;
+    public long ExamStudentId { get; set; } = 0;
+    public int ExamPaperQuestionMinorId { get; set; } = 0;
+    public decimal Score { get; set; } = 0;
+    public decimal StarScore { get; set; } = 0;
+}

+ 29 - 0
YBEE.EQM.Application/Exam/ExamScore/ExamScoreImportAppService.cs

@@ -61,4 +61,33 @@ public class ExamScoreImportAppService : IDynamicApiController
         fs.Close();
         await _examScoreImportService.UploadImportStudentTotalScore(filePath, input.ExamPlanId);
     }
+
+    /// <summary>
+    /// 导入区校合并小题成绩
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [RequestSizeLimit(long.MaxValue)]
+    [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
+    public async Task<IActionResult> UploadImportStudentMinorScore([FromForm] UploadExamDataInput input)
+    {
+        string fileExt = Path.GetExtension(input.File.FileName).ToLower();
+        if (fileExt != ".zip")
+        {
+            throw Oops.Oh(ErrorCode.E1010);
+        }
+
+        string filePath = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}{fileExt}");
+        using FileStream fs = File.Create(filePath);
+        await input.File.CopyToAsync(fs);
+        await fs.FlushAsync();
+        fs.Close();
+
+        var (fileName, fileBytes) = await _examScoreImportService.UploadImportStudentMinorScore(filePath, input.ExamPlanId);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
 }

+ 306 - 1
YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreImportService.cs

@@ -1,6 +1,10 @@
 using Furion.ClayObject.Extensions;
 using Furion.DatabaseAccessor.Extensions;
+using NPOI.OpenXmlFormats.Spreadsheet;
 using NPOI.SS.UserModel;
+using NPOI.XSSF.UserModel;
+using System;
+using System.IO.Compression;
 using YBEE.EQM.Core;
 
 namespace YBEE.EQM.Application;
@@ -13,12 +17,14 @@ public class ExamScoreImportService : IExamScoreImportService, ITransient
     private readonly IRepository<ExamScore> _rep;
     private readonly ISchoolClassService _schoolClassService;
     private readonly IExamGradeService _examGradeService;
+    private readonly IExportExcelService _exportExcelService;
 
-    public ExamScoreImportService(IRepository<ExamScore> rep, ISchoolClassService schoolClassService, IExamGradeService examGradeService)
+    public ExamScoreImportService(IRepository<ExamScore> rep, ISchoolClassService schoolClassService, IExamGradeService examGradeService, IExportExcelService exportExcelService)
     {
         _rep = rep;
         _schoolClassService = schoolClassService;
         _examGradeService = examGradeService;
+        _exportExcelService = exportExcelService;
     }
 
     /// <summary>
@@ -590,4 +596,303 @@ JOIN
             File.Delete(filePath);
         }
     }
+
+    /// <summary>
+    /// 上传并合并小题成绩导出
+    /// </summary>
+    /// <param name="filePath"></param>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    public async Task<(string, byte[])> UploadImportStudentMinorScore(string filePath, int examPlanId)
+    {
+        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+
+        // 解压输出目录
+        string unzipPath = Path.Combine(FileUtil.GetTempFileRoot(), Path.GetFileNameWithoutExtension(filePath));
+        Directory.CreateDirectory(unzipPath);
+
+        // 临时存放目录
+        string outputRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
+        Directory.CreateDirectory(outputRoot);
+        string outputFilePath = Path.Combine(outputRoot, $"{examPlan.Name}-区校合并-小题成绩");
+        Directory.CreateDirectory(outputFilePath);
+
+        try
+        {
+            List<ExamScoreMinorFileInfo> fileInfos = new();
+
+            var gradeDict = (await _rep.Change<Grade>().DetachedEntities.Where(t => t.EducationStage == examPlan.EducationStage).ToListAsync()).ToDictionary(t => t.Name);
+            var courseDict = (await _rep.Change<Course>().DetachedEntities.ToListAsync()).ToDictionary(t => t.Name);
+
+            #region 1.读取文件信息
+            ZipFile.ExtractToDirectory(filePath, unzipPath, System.Text.Encoding.GetEncoding("GBK"), true);
+            var sampleTypePaths = Directory.GetDirectories(unzipPath);
+            foreach (var stpath in sampleTypePaths)
+            {
+                var tn = Path.GetFileNameWithoutExtension(stpath).Split("-");
+                ExamSampleType est;
+                if (int.TryParse(tn[0], out int estInt))
+                {
+                    est = (ExamSampleType)estInt;
+                }
+                else
+                {
+                    throw new ArgumentException("监测抽样类型错误");
+                }
+
+                var scoreFiles = Directory.GetFiles(stpath).Where(t => t.EndsWith(".xlsx") || t.EndsWith(".xls")).Where(t => !t.StartsWith("~"));
+                foreach (var scoreFile in scoreFiles)
+                {
+                    var fn = Path.GetFileNameWithoutExtension(scoreFile).Split("-");
+                    var gradeName = fn[1];
+                    var grade = gradeDict[gradeName];
+                    var courseName = fn[2];
+                    var course = courseDict[courseName];
+                    fileInfos.Add(new()
+                    {
+                        ExamSampleType = est,
+                        FilePath = scoreFile,
+                        GradeId = grade.Id,
+                        Grade = grade,
+                        CourseId = course.Id,
+                        Course = course,
+                    });
+                }
+            }
+            #endregion
+
+            #region 2.合并文件
+            var cfiles = fileInfos.Where(t => t.ExamSampleType == ExamSampleType.DISTRICT);
+            foreach (var cfile in cfiles)
+            {
+                var qminors = await _rep.Change<ExamPaperQuestionMinor>().DetachedEntities.Where(t => t.ExamPaper.ExamPlanId == examPlanId && t.ExamPaper.CourseId == cfile.CourseId && t.ExamPaper.GradeId == cfile.GradeId && t.IsLeaf == true).ToListAsync();
+                var examSampleStudents = await _rep.Change<ExamSampleStudent>().DetachedEntities
+                                                                               .Where(t => t.ExamSample.ExamPlanId == examPlanId && t.ExamSample.IsSelected == true && t.ExamStudent.GradeId == cfile.GradeId)
+                                                                               .Select(t => new { t.ExamNumber, t.ExamStudentId, t.ExamStudent.SysOrgId })
+                                                                               .ToListAsync();
+                var sfile = fileInfos.FirstOrDefault(t => t.ExamSampleType == ExamSampleType.SCHOOL_EXAM && t.GradeId == cfile.GradeId && t.CourseId == cfile.CourseId) ?? throw new ArgumentNullException($"{cfile}对应校考小题成绩未找到");
+
+                // 区文件
+                using FileStream c_fs = new(cfile.FilePath, FileMode.Open, FileAccess.Read);
+                IWorkbook c_workbook = ExcelUtil.GetWorkbook(filePath, c_fs);
+                var c_sheet = c_workbook.GetSheetAt(0);
+                var c_rows = c_sheet.GetRowEnumerator();
+
+                // 输出文件
+                IWorkbook o_workbook = new XSSFWorkbook();
+                ISheet o_sheet = o_workbook.CreateSheet();
+                var o_cellStyles = _exportExcelService.GetCellStyle(o_workbook);
+                int o_rowNum = 0;
+                IRow o_headerRow = o_sheet.CreateRow(o_rowNum++);
+
+                // 标题行
+                Dictionary<int, ExamPaperQuestionMinor> colMinor = new();
+                c_rows.MoveNext();
+                var c_headerRow = (IRow)c_rows.Current;
+                int colNum = c_headerRow.LastCellNum;
+                for (int ci = 0; ci < colNum; ci++)
+                {
+                    string colName = c_headerRow.GetCell(ci)?.ToString() ?? "";
+                    _exportExcelService.AddCell(c_headerRow.GetCell(ci)?.ToString(), o_headerRow, ci, o_cellStyles.CenterCellStyle, o_sheet, width: ci == 1 ? 12 : ci == 2 ? 30 : null);
+                    var qm = qminors.FirstOrDefault(t => t.ColumnName == colName);
+                    if (qm != null)
+                    {
+                        colMinor.Add(ci, qm);
+                    }
+                }
+
+                // 所有小题成绩列表
+                List<ExamScoreMinorImportDto> items = new();
+
+                // 区文件写入
+                while (c_rows.MoveNext())
+                {
+                    var row = (IRow)c_rows.Current;
+                    var c0 = row.GetCell(0);
+                    if (c0 == null || c0.ToString() == "")
+                    {
+                        break;
+                    }
+
+                    ExamScoreMinorImportDto item = new();
+                    IRow o_row = o_sheet.CreateRow(o_rowNum++);
+                    for (int tci = 0; tci < colNum; tci++)
+                    {
+                        var cell = row.GetCell(tci);
+                        if (cell == null)
+                        {
+                            continue;
+                        }
+                        if (cell.CellType == CellType.Numeric)
+                        {
+                            _exportExcelService.AddCell(cell.NumericCellValue, o_row, tci, o_cellStyles.CenterCellStyle, o_sheet);
+                        }
+                        else
+                        {
+                            _exportExcelService.AddCell(cell.ToString(), o_row, tci, o_cellStyles.CenterCellStyle, o_sheet);
+                        }
+
+                        if (tci == 1)
+                        {
+                            var examNumber = cell.ToString();
+                            var stu = examSampleStudents.FirstOrDefault(t => t.ExamNumber == examNumber);
+                            if (stu != null)
+                            {
+                                item.ExamStudentId = stu.ExamStudentId;
+                                item.SysOrgId = stu.SysOrgId;
+                            }
+                        }
+                        else if (tci >= 7 && colMinor.TryGetValue(tci, out ExamPaperQuestionMinor cm))
+                        {
+                            if (cell.CellType == CellType.Numeric)
+                            {
+                                item.Score = (decimal)cell.NumericCellValue;
+                            }
+                            else
+                            {
+                                if (decimal.TryParse(cell.ToString(), out decimal s))
+                                {
+                                    item.Score = s;
+                                }
+                            }
+                            item.StarScore = cm.Score == 0 ? 0 : item.Score / cm.Score;
+                            if (item.ExamStudentId != 0)
+                            {
+                                items.Add(new()
+                                {
+                                    ExamStudentId = item.ExamStudentId,
+                                    SysOrgId = item.SysOrgId,
+                                    ExamPaperQuestionMinorId = cm.Id,
+                                    Score = item.Score,
+                                    StarScore = item.StarScore
+                                });
+                            }
+                        }
+                    }
+                }
+
+                // 校文件
+                using FileStream s_fs = new(sfile.FilePath, FileMode.Open, FileAccess.Read);
+                IWorkbook s_workbook = ExcelUtil.GetWorkbook(sfile.FilePath, s_fs);
+                var s_sheet = s_workbook.GetSheetAt(0);
+                var s_rows = s_sheet.GetRowEnumerator();
+
+                s_rows.MoveNext();
+                // 校文件写入
+                while (s_rows.MoveNext())
+                {
+                    var row = (IRow)s_rows.Current;
+                    var c0 = row.GetCell(0);
+                    if (c0 == null || c0.ToString() == "")
+                    {
+                        break;
+                    }
+
+                    ExamScoreMinorImportDto item = new();
+                    IRow o_row = o_sheet.CreateRow(o_rowNum++);
+                    for (int tci = 0; tci < colNum; tci++)
+                    {
+                        var cell = row.GetCell(tci);
+                        if (cell == null)
+                        {
+                            continue;
+                        }
+                        if (cell.CellType == CellType.Numeric)
+                        {
+                            _exportExcelService.AddCell(cell.NumericCellValue, o_row, tci, o_cellStyles.CenterCellStyle, o_sheet);
+                        }
+                        else
+                        {
+                            _exportExcelService.AddCell(cell.ToString(), o_row, tci, o_cellStyles.CenterCellStyle, o_sheet);
+                        }
+
+                        if (tci == 1)
+                        {
+                            var examNumber = cell.ToString();
+                            var stu = examSampleStudents.FirstOrDefault(t => t.ExamNumber == examNumber);
+                            if (stu != null)
+                            {
+                                item.ExamStudentId = stu.ExamStudentId;
+                                item.SysOrgId = stu.SysOrgId;
+                            }
+                        }
+                        else if (tci >= 7 && colMinor.TryGetValue(tci, out ExamPaperQuestionMinor cm))
+                        {
+                            if (cell.CellType == CellType.Numeric)
+                            {
+                                item.Score = (decimal)cell.NumericCellValue;
+                            }
+                            else
+                            {
+                                if (decimal.TryParse(cell.ToString(), out decimal s))
+                                {
+                                    item.Score = s;
+                                }
+                            }
+                            item.StarScore = cm.Score == 0 ? 0 : item.Score / cm.Score;
+                            if (item.ExamStudentId != 0)
+                            {
+                                items.Add(new()
+                                {
+                                    ExamStudentId = item.ExamStudentId,
+                                    SysOrgId = item.SysOrgId,
+                                    ExamPaperQuestionMinorId = cm.Id,
+                                    Score = item.Score,
+                                    StarScore = item.StarScore
+                                });
+                            }
+                        }
+                    }
+                }
+
+                o_sheet.CreateFreezePane(0, 1);
+                MemoryStream o_ms = new();
+                o_workbook.Write(o_ms, false);
+                o_ms.Flush();
+                await File.WriteAllBytesAsync(Path.Combine(outputFilePath, $"{examPlan.EducationStage.GetDescription()}-{cfile.Grade.Name}-{cfile.Course.Name}-小题成绩.xlsx"), o_ms.ToArray());
+
+                #region 批量导入
+                // 清理成绩
+                string deleteScoreSql = $"DELETE FROM exam_score_minor WHERE exam_plan_id = {examPlanId} AND grade_id = {cfile.GradeId} AND course_id = {cfile.CourseId};";
+                //await _rep.SqlNonQueryAsync(deleteScoreSql);
+                await deleteScoreSql.SetCommandTimeout(60000).SqlNonQueryAsync();
+
+                List<string> values = new();
+                var scount = items.Count;
+                for (int i = 0; i < scount; i++)
+                {
+                    var item = items[i];
+                    values.Add($"({examPlanId},{item.SysOrgId},{item.ExamStudentId},{cfile.GradeId},{cfile.CourseId},{item.ExamPaperQuestionMinorId},{item.Score},{item.StarScore})");
+                    if ((i + 1) % 2000 == 0 || i == scount - 1)
+                    {
+                        string insertSql = $"INSERT INTO exam_score_minor(exam_plan_id,sys_org_id,exam_student_id,grade_id,course_id,exam_paper_question_minor_id,score,star_score) VALUES {string.Join(",", values)}";
+                        //await _rep.SqlNonQueryAsync(insertSql);
+                        await insertSql.SetCommandTimeout(60000).SqlNonQueryAsync();
+                        values.Clear();
+                    }
+                }
+                #endregion
+            }
+            #endregion
+
+            string outFileName = $"{examPlan.Name}-区校合并-小题成绩.zip";
+            string oFilePath = Path.Combine(outputRoot, outFileName);
+            ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
+            zip.CreateZip(oFilePath, outputFilePath, true, string.Empty);
+
+            var retBytes = await File.ReadAllBytesAsync(oFilePath);
+            return (outFileName, retBytes);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception(ex.Message);
+        }
+        finally
+        {
+            File.Delete(filePath);
+            Directory.Delete(unzipPath, true);
+            Directory.Delete(outputRoot, true);
+        }
+    }
 }

+ 7 - 0
YBEE.EQM.Application/Exam/ExamScore/Services/IExamScoreImportService.cs

@@ -19,4 +19,11 @@ public interface IExamScoreImportService
     /// <param name="examPlanId"></param>
     /// <returns></returns>
     Task UploadImportStudentTotalScore(string filePath, int examPlanId);
+    /// <summary>
+    /// 导入区校合并小题成绩
+    /// </summary>
+    /// <param name="filePath"></param>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<(string, byte[])> UploadImportStudentMinorScore(string filePath, int examPlanId);
 }

+ 4 - 4
YBEE.EQM.Application/Exam/ExamSpecialStudent/Services/ExamSpecialStudentService.cs

@@ -496,7 +496,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
             XWPFParagraph pHeader = doc.CreateParagraph();
             pHeader.Alignment = ParagraphAlignment.CENTER;
             pHeader.SpacingLineRule = LineSpacingRule.AUTO;
-            pHeader.setSpacingBetween(1.5, LineSpacingRule.AUTO);
+            pHeader.SetSpacingBetween(1.5, LineSpacingRule.AUTO);
             var pHeaderRun = pHeader.CreateRun();
             pHeaderRun.SetText($"{gradeFirst.SysOrg.FullName.Replace("重庆市", "")}特殊学生明细表({gradeFirst.Grade.Name})");
             pHeaderRun.FontSize = 18;
@@ -508,7 +508,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
             // 签字行
             XWPFParagraph pSign = doc.CreateParagraph();
             pSign.SpacingLineRule = LineSpacingRule.AUTO;
-            pSign.setSpacingBetween(2, LineSpacingRule.AUTO);
+            pSign.SetSpacingBetween(2, LineSpacingRule.AUTO);
             var pSignRun = pSign.CreateRun();
             pSignRun.SetText("学校盖章:                        纪检负责人:                        校长签字:");
             pSignRun.FontSize = 12;
@@ -516,7 +516,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
             // 签字行
             XWPFParagraph pCount = doc.CreateParagraph();
             pCount.SpacingLineRule = LineSpacingRule.AUTO;
-            pCount.setSpacingBetween(2, LineSpacingRule.AUTO);
+            pCount.SetSpacingBetween(2, LineSpacingRule.AUTO);
             var dt = DateTime.Now;
             var pCountRun = pCount.CreateRun();
             pCountRun.SetText($"填表人:                            联系电话:                             特殊学生计({gitem.Count()})人      {dt.Year}年{dt.Month}月{dt.Day}日");
@@ -589,7 +589,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
         {
             var p = doc.CreateParagraph();
             p.SpacingLineRule = LineSpacingRule.AUTO;
-            p.setSpacingBetween(lineSpacing, LineSpacingRule.AUTO);
+            p.SetSpacingBetween(lineSpacing, LineSpacingRule.AUTO);
             p.CreateRun().FontSize = 12;
         }
     }

+ 12 - 0
YBEE.EQM.Application/Exam/ExamStudent/Dtos/ExamStudentInput.cs

@@ -2,6 +2,18 @@
 
 namespace YBEE.EQM.Application;
 
+/// <summary>
+/// 上传学生信息输入参数
+/// </summary>
+public class UploadExamStudentInput: UploadExamDataInput
+{
+    /// <summary>
+    /// 监测年级ID
+    /// </summary>
+    [Required]
+    public int ExamGradeId { get; set; }
+}
+
 /// <summary>
 /// 导入监测学生输入参数
 /// </summary>

+ 2 - 2
YBEE.EQM.Application/Exam/ExamStudent/ExamStudentAppService.cs

@@ -23,7 +23,7 @@ public class ExamStudentAppService : IDynamicApiController
     /// <returns></returns>
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
-    public async Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload([FromForm] UploadExamDataInput input)
+    public async Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload([FromForm] UploadExamStudentInput input)
     {
         string fileExt = Path.GetExtension(input.File.FileName).ToLower();
         if (fileExt != ".xls" && fileExt != ".xlsx")
@@ -37,7 +37,7 @@ public class ExamStudentAppService : IDynamicApiController
         await fs.FlushAsync();
         fs.Close();
 
-        var ret = await _examStudentService.Upload(filePath, input.ExamPlanId);
+        var ret = await _examStudentService.Upload(filePath, input.ExamPlanId, input.ExamGradeId);
         return ret;
     }
     /// <summary>

+ 25 - 18
YBEE.EQM.Application/Exam/ExamStudent/Services/ExamStudentService.cs

@@ -31,9 +31,12 @@ public class ExamStudentService : IExamStudentService, ITransient
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="examPlanId"></param>
+    /// <param name="examGradeId"></param>
     /// <returns></returns>
-    public async Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload(string filePath, int examPlanId)
+    public async Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload(string filePath, int examPlanId, int examGradeId)
     {
+        var examGrade = await _examGradeService.GetById(examGradeId);
+
         UploadExamDataOutput<UploadExamStudentOutput> result = new();
         try
         {
@@ -89,7 +92,7 @@ public class ExamStudentService : IExamStudentService, ITransient
                     headerErrors.Add(letter.ToString());
                 }
             }
-            if (headerErrors.Any())
+            if (headerErrors.Count != 0)
             {
                 string columnErrors = string.Join("、", headerErrors);
                 result.ErrorMessage.Add($"第2行标题行{columnErrors}列名错误。从A列开始依次应为班级、姓名、证件类型、证件号码、性别、自编监测号、选科组合、备注、考场号和座位号。");
@@ -136,7 +139,7 @@ public class ExamStudentService : IExamStudentService, ITransient
                 }
 
                 // 姓名
-                item.Name = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? "");
+                item.Name = (row.GetCell(NAME_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.Name == "" || item.Name.Length > 100)
                 {
                     item.ErrorMessage.Add(headers[NAME_INDEX]);
@@ -144,7 +147,7 @@ public class ExamStudentService : IExamStudentService, ITransient
                 if (item.Name.Length > 100) { item.Name = item.Name[..100]; }
 
                 // 证件类型
-                item.CertificateTypeName = StringUtil.ClearWhite(row.GetCell(CERT_TYPE_INDEX)?.ToString() ?? "");
+                item.CertificateTypeName = (row.GetCell(CERT_TYPE_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (!(item.CertificateTypeName != "" && certificateTypes.ContainsKey(item.CertificateTypeName)))
                 {
                     item.ErrorMessage.Add(headers[CERT_TYPE_INDEX]);
@@ -155,7 +158,7 @@ public class ExamStudentService : IExamStudentService, ITransient
                 }
 
                 // 证件号码
-                item.IdNumber = StringUtil.ClearWhite(row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper().ClearWhitespace();
                 if (item.CertificateType == CertificateType.ID_CARD)
                 {
                     var idNumberValidate = CertificateNumberValidator.ValidateIdCard(item.IdNumber);
@@ -172,7 +175,7 @@ public class ExamStudentService : IExamStudentService, ITransient
                 else
                 {
                     // 性别
-                    item.GenderName = StringUtil.ClearWhite(row.GetCell(GENDER_INDEX)?.ToString() ?? "");
+                    item.GenderName = (row.GetCell(GENDER_INDEX)?.ToString() ?? "").ClearWhitespace();
                     if (item.CertificateType != CertificateType.ID_CARD)
                     {
                         item.Gender = item.GenderName == "男" ? Gender.MALE : item.GenderName == "女" ? Gender.FEMALE : Gender.UNKNOWN;
@@ -181,12 +184,16 @@ public class ExamStudentService : IExamStudentService, ITransient
                 if (item.IdNumber.Length > 50) { item.IdNumber = item.IdNumber[..50]; }
 
                 // 自编监测号
-                item.ExamNumber = StringUtil.ClearWhite(row.GetCell(EXAM_NUM_INDEX)?.ToString() ?? "");
+                item.ExamNumber = (row.GetCell(EXAM_NUM_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.ExamNumber.Length > 50) { item.ExamNumber = item.ExamNumber[..50]; }
-                // TODO 根据配置检测是否需要自编监测号
+                // 根据配置验证自编号
+                if (examGrade.IsRequiredSelfExamNumber && item.ExamNumber.Length != examGrade.SelfExamNumberLength)
+                {
+                    item.ErrorMessage.Add($"{headers[EXAM_NUM_INDEX]}(长度{examGrade.SelfExamNumberLength}位)");
+                }
 
                 // 选科组合
-                item.NceeCourseCombName = StringUtil.ClearWhite(row.GetCell(COURSE_COMB_INDEX)?.ToString() ?? "");
+                item.NceeCourseCombName = (row.GetCell(COURSE_COMB_INDEX)?.ToString() ?? "").ClearWhitespace();
                 // TODO 高中需要处理选科
                 if (item.NceeCourseCombName != "")
                 {
@@ -201,15 +208,15 @@ public class ExamStudentService : IExamStudentService, ITransient
                 }
 
                 // 备注
-                item.Remark = StringUtil.ClearWhite(row.GetCell(REMARK_INDEX)?.ToString() ?? "");
+                item.Remark = (row.GetCell(REMARK_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.Remark.Length > 200) { item.Remark = item.Remark[..200]; }
 
                 // 考场号
-                item.RoomNumber = StringUtil.ClearWhite(row.GetCell(ROOM_INDEX)?.ToString() ?? "");
+                item.RoomNumber = (row.GetCell(ROOM_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.RoomNumber.Length > 20) { item.RoomNumber = item.RoomNumber[..20]; }
 
                 // 座位号
-                item.SeatNumber = StringUtil.ClearWhite(row.GetCell(SEAT_INDEX)?.ToString() ?? "");
+                item.SeatNumber = (row.GetCell(SEAT_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.RoomNumber.Length > 20) { item.SeatNumber = item.SeatNumber[..20]; }
 
                 // 行是否验证通过
@@ -269,8 +276,8 @@ public class ExamStudentService : IExamStudentService, ITransient
             item.SysOrgId = orgId;
             item.SysOrgBranchId = input.SysOrgBranchId;
             item.SchoolClassId = classDict[ni.ClassNumber];
-            item.Name = StringUtil.ClearWhite(item.Name);
-            item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+            item.Name = item.Name.ClearWhitespace();
+            item.IdNumber = item.IdNumber.IdNumberStdProcessing();
             items.Add(item);
         }
         await _rep.InsertAsync(items);
@@ -300,8 +307,8 @@ public class ExamStudentService : IExamStudentService, ITransient
         var item = input.Adapt<ExamStudent>();
         item.SysOrgId = orgId;
         item.SchoolClassId = schoolClass.Id;
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+        item.Name = item.Name.ClearWhitespace();
+        item.IdNumber = item.IdNumber.IdNumberStdProcessing();
 
         await item.InsertAsync();
     }
@@ -319,8 +326,8 @@ public class ExamStudentService : IExamStudentService, ITransient
 
         var item = input.Adapt<ExamStudent>();
         item.SchoolClassId = schoolClass.Id;
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+        item.Name = item.Name.ClearWhitespace();
+        item.IdNumber = item.IdNumber.IdNumberStdProcessing();
 
         await item.UpdateIncludeAsync(new[] {
             nameof(item.SchoolClassId),

+ 2 - 1
YBEE.EQM.Application/Exam/ExamStudent/Services/IExamStudentService.cs

@@ -13,8 +13,9 @@ public interface IExamStudentService
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="examPlanId"></param>
+    /// <param name="examGradeId"></param>
     /// <returns></returns>
-    Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload(string filePath, int examPlanId);
+    Task<UploadExamDataOutput<UploadExamStudentOutput>> Upload(string filePath, int examPlanId, int examGradeId);
     /// <summary>
     /// 批量导入监测学生
     /// </summary>

+ 142 - 48
YBEE.EQM.Application/Ncee/NceeExport/Services/NceeExportService.cs

@@ -60,21 +60,24 @@ public class NceeExportService : INceeExportService, ITransient
             var lbs = await ExportLine(nceePlanId, courses, lineLevelTable, "区县");
             await File.WriteAllBytesAsync(lbPath, lbs);
 
-            // 导出机构转换成绩
-            var orgConvertScores = await ExportOrgConvertScore(nceePlanId, orgList, "区县", isExportConvertRange: nceePlan.Config.ConvertEnabled);
-            foreach (var convertScore in orgConvertScores)
+            if (nceePlan.Config.ExportConvertScoreEnabled)
             {
-                string orgPath = Path.Combine(filePath, convertScore.Key);
-                Directory.CreateDirectory(orgPath);
-                await File.WriteAllBytesAsync(Path.Combine(orgPath, $"{nceePlan.Name}-转换成绩-{convertScore.Key}.xlsx"), convertScore.Value);
-                File.Copy(rangePath4, Path.Combine(orgPath, Path.GetFileName(rangePath4)));
-                File.Copy(rangePath8, Path.Combine(orgPath, Path.GetFileName(rangePath8)));
-                File.Copy(lbPath, Path.Combine(orgPath, Path.GetFileName(lbPath)));
-            }
+                // 导出机构转换成绩
+                var orgConvertScores = await ExportOrgConvertScore(nceePlanId, orgList, "区县", isExportConvertRange: nceePlan.Config.ConvertEnabled, isExportOrder: nceePlan.Config.ExportOrderEnabled);
+                foreach (var convertScore in orgConvertScores)
+                {
+                    string orgPath = Path.Combine(filePath, convertScore.Key);
+                    Directory.CreateDirectory(orgPath);
+                    await File.WriteAllBytesAsync(Path.Combine(orgPath, $"{nceePlan.Name}-转换成绩-{convertScore.Key.Split("-")[1]}.xlsx"), convertScore.Value);
+                    File.Copy(rangePath4, Path.Combine(orgPath, Path.GetFileName(rangePath4)));
+                    File.Copy(rangePath8, Path.Combine(orgPath, Path.GetFileName(rangePath8)));
+                    File.Copy(lbPath, Path.Combine(orgPath, Path.GetFileName(lbPath)));
+                }
 
-            // 导出全部转换成绩
-            var convertScoreBytes = await ExportConvertScore(nceePlanId, "区县", isExportConvertRange: nceePlan.Config.ConvertEnabled);
-            await File.WriteAllBytesAsync(Path.Combine(filePath, $"{nceePlan.Name}-转换成绩-总表.xlsx"), convertScoreBytes);
+                // 导出全部转换成绩
+                var convertScoreBytes = await ExportConvertScore(nceePlanId, "区县", isExportConvertRange: nceePlan.Config.ConvertEnabled, isExportOrder: nceePlan.Config.ExportOrderEnabled);
+                await File.WriteAllBytesAsync(Path.Combine(filePath, $"{nceePlan.Name}-转换成绩-总表.xlsx"), convertScoreBytes);
+            }
 
             string outFileName = $"{nceePlan.Name}-统计结果.zip";
             string outFilePath = Path.Combine(fileRoot, outFileName);
@@ -160,18 +163,21 @@ public class NceeExportService : INceeExportService, ITransient
                 await File.WriteAllBytesAsync(Path.Combine(orgPath, $"{nceePlan.Name}-{range.Key}-分段统计-历史类.xlsx"), range.Value);
             }
 
-            // 导出各机构转换成绩
-            var orgConvertScores = await ExportOrgConvertScore(nceePlanId, orgList, "学校", isExportConvertRange: nceePlan.Config.ConvertEnabled);
-            foreach (var convertScore in orgConvertScores)
+            if (nceePlan.Config.ExportConvertScoreEnabled)
             {
-                string orgPath = Path.Combine(filePath, convertScore.Key);
-                Directory.CreateDirectory(orgPath);
-                await File.WriteAllBytesAsync(Path.Combine(orgPath, $"{nceePlan.Name}-转换成绩-{convertScore.Key}.xlsx"), convertScore.Value);
-            }
+                // 导出各机构转换成绩
+                var orgConvertScores = await ExportOrgConvertScore(nceePlanId, orgList, "学校", isExportConvertRange: nceePlan.Config.ConvertEnabled);
+                foreach (var convertScore in orgConvertScores)
+                {
+                    string orgPath = Path.Combine(filePath, convertScore.Key);
+                    Directory.CreateDirectory(orgPath);
+                    await File.WriteAllBytesAsync(Path.Combine(orgPath, $"{nceePlan.Name}-转换成绩-{convertScore.Key}.xlsx"), convertScore.Value);
+                }
 
-            // 导出全部转换成绩
-            var convertScoreBytes = await ExportConvertScore(nceePlanId, "学校", isExportConvertRange: nceePlan.Config.ConvertEnabled);
-            await File.WriteAllBytesAsync(Path.Combine(filePath, $"{nceePlan.Name}-转换成绩-总表.xlsx"), convertScoreBytes);
+                // 导出全部转换成绩
+                var convertScoreBytes = await ExportConvertScore(nceePlanId, "学校", isExportConvertRange: nceePlan.Config.ConvertEnabled);
+                await File.WriteAllBytesAsync(Path.Combine(filePath, $"{nceePlan.Name}-转换成绩-总表.xlsx"), convertScoreBytes);
+            }
 
             string outFileName = $"{nceePlan.Name}-统计结果.zip";
             string outFilePath = Path.Combine(fileRoot, outFileName);
@@ -866,22 +872,50 @@ public class NceeExportService : INceeExportService, ITransient
         int ci = 0;
         int scoreWidth = 10;
         _exportExcelService.AddCell("上线合并汇总", titleRow, ci++, cellStyle.TitleStyle, sheet, scoreWidth);
-        for (; ci < 8; ci++)
+        for (; ci < 16; ci++)
         {
             _exportExcelService.AddCell("", titleRow, ci, cellStyle.TitleStyle, sheet, scoreWidth);
         }
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
 
+        IRow headerRow0 = sheet.CreateRow(rowNum++);
+        headerRow0.HeightInPoints = 15;
+        ci = 0;
+        _exportExcelService.AddCell(orgTitle, headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("两类合并", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("物理类", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("历史类", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow0, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+
         IRow headerRow1 = sheet.CreateRow(rowNum++);
         headerRow1.HeightInPoints = 15;
         ci = 0;
-        _exportExcelService.AddCell(orgTitle, headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("参考人数", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("特控", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("本科", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("参考人数", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("特控", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("本科", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
-        _exportExcelService.AddCell("高职", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("参考人数", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("特控", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("本科", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("", headerRow1, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
 
         IRow headerRow2 = sheet.CreateRow(rowNum++);
@@ -893,14 +927,33 @@ public class NceeExportService : INceeExportService, ITransient
         _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("人数", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("人数", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("人数", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
         _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("人数", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("人数", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+        _exportExcelService.AddCell("比例(%)", headerRow2, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 3, rowNum - 1, 0, 0));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 3, rowNum - 3, 1, 5));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 3, rowNum - 3, 6, 10));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 3, rowNum - 3, 11, 15));
 
-        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 0, 0));
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 1, 1));
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 2, 3));
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 4, 5));
-        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 6, 7));
+
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 6, 6));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 7, 8));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 9, 10));
+
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 11, 11));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 12, 13));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 2, 14, 15));
         #endregion
 
         #region 数据
@@ -915,8 +968,16 @@ public class NceeExportService : INceeExportService, ITransient
             _exportExcelService.AddCell(dr["line_rate_1"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
             _exportExcelService.AddCell(dr["line_count_2"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
             _exportExcelService.AddCell(dr["line_rate_2"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
-            _exportExcelService.AddCell(dr["line_count_3"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
-            _exportExcelService.AddCell(dr["line_rate_3"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_4_total_count"], row, rci++, cellStyle.CenterCellStyle);
+            _exportExcelService.AddCell(dr["d_4_line_count_1"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_4_line_rate_1"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_4_line_count_2"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_4_line_rate_2"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_8_total_count"], row, rci++, cellStyle.CenterCellStyle);
+            _exportExcelService.AddCell(dr["d_8_line_count_1"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_8_line_rate_1"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_8_line_count_2"], row, rci++, cellStyle.CenterCellStyle, zeroToBlank: true);
+            _exportExcelService.AddCell(dr["d_8_line_rate_2"], row, rci++, cellStyle.PercentCellStyleP2, zeroToBlank: true);
         }
         #endregion
     }
@@ -1639,8 +1700,8 @@ public class NceeExportService : INceeExportService, ITransient
             _exportExcelService.AddCell("总分X", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
             if (isExportOrder)
             {
-                _exportExcelService.AddCell("联考", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
-                _exportExcelService.AddCell("区", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+                _exportExcelService.AddCell("联考序X", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
+                _exportExcelService.AddCell("区序X", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
             }
             _exportExcelService.AddCell("语文", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
             _exportExcelService.AddCell("数学", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, scoreWidth);
@@ -2772,30 +2833,63 @@ public class NceeExportService : INceeExportService, ITransient
     /// <returns></returns>
     private async Task<DataTable> GetLineCountTable(int nceePlanId)
     {
-        var dt = await _rep.SqlQueryAsync(@"
-SELECT IFNULL(T2.short_name, '合计') AS sys_org_name, T1.*, 
-	T1.line_count_1 / T1.total_count AS line_rate_1,
-	T1.line_count_2 / T1.total_count AS line_rate_2,
-	T1.line_count_3 / T1.total_count AS line_rate_3
+        var dt = await _rep.SqlQueryAsync($@"
+SELECT T1.*,
+	T2.d_4_total_count, T2.d_4_line_count_1, T2.d_4_line_count_2, T2.d_4_line_count_3,
+	T2.d_4_line_count_1 / T2.d_4_total_count AS d_4_line_rate_1,
+	T2.d_4_line_count_2 / T2.d_4_total_count AS d_4_line_rate_2,
+	T2.d_4_line_count_3 / T2.d_4_total_count AS d_4_line_rate_3,
+	T2.d_8_total_count, T2.d_8_line_count_1, T2.d_8_line_count_2, T2.d_8_line_count_3,
+	T2.d_8_line_count_1 / T2.d_8_total_count AS d_8_line_rate_1,
+	T2.d_8_line_count_2 / T2.d_8_total_count AS d_8_line_rate_2,
+	T2.d_8_line_count_3 / T2.d_8_total_count AS d_8_line_rate_3
 FROM
+(
+	SELECT IFNULL(T2.short_name, '合计') AS sys_org_name, T1.*, 
+		T1.line_count_1 / T1.total_count AS line_rate_1,
+		T1.line_count_2 / T1.total_count AS line_rate_2,
+		T1.line_count_3 / T1.total_count AS line_rate_3
+	FROM
+	(
+		SELECT T1.type, T1.sys_org_id,
+			MAX(T1.total_count) AS total_count,
+			SUM(CASE WHEN T1.ncee_line_level = 1 THEN T1.line_count ELSE 0 END) AS line_count_1,
+			SUM(CASE WHEN T1.ncee_line_level = 2 THEN T1.line_count ELSE 0 END) AS line_count_2,
+			SUM(CASE WHEN T1.ncee_line_level = 3 THEN T1.line_count ELSE 0 END) AS line_count_3
+		FROM 
+		(
+			SELECT T1.type, T1.sys_org_id, T1.ncee_line_level, SUM(T1.line_count) AS line_count, SUM(T1.total_count) AS total_count
+			FROM ncee_line_total AS T1
+			WHERE T1.ncee_plan_id = {nceePlanId} AND (T1.type = 1 OR T1.type = 4) AND T1.ncee_course_comb_id IS NULL
+			GROUP BY T1.type, T1.sys_org_id, T1.ncee_line_level
+		) AS T1
+		GROUP BY T1.type, T1.sys_org_id
+	) AS T1
+	LEFT JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+) AS T1
+LEFT JOIN
 (
 	SELECT T1.type, T1.sys_org_id,
-		MAX(T1.total_count) AS total_count,
-		SUM(CASE WHEN T1.ncee_line_level = 1 THEN T1.line_count ELSE 0 END) AS line_count_1,
-		SUM(CASE WHEN T1.ncee_line_level = 2 THEN T1.line_count ELSE 0 END) AS line_count_2,
-		SUM(CASE WHEN T1.ncee_line_level = 3 THEN T1.line_count ELSE 0 END) AS line_count_3
-	FROM 
+		MAX(CASE WHEN T1.direction_course_id = 4 THEN T1.total_count ELSE 0 END) AS d_4_total_count,
+		SUM(CASE WHEN T1.direction_course_id = 4 AND T1.ncee_line_level = 1 THEN T1.line_count ELSE 0 END) AS d_4_line_count_1,
+		SUM(CASE WHEN T1.direction_course_id = 4 AND T1.ncee_line_level = 2 THEN T1.line_count ELSE 0 END) AS d_4_line_count_2,
+		SUM(CASE WHEN T1.direction_course_id = 4 AND T1.ncee_line_level = 3 THEN T1.line_count ELSE 0 END) AS d_4_line_count_3,
+		MAX(CASE WHEN T1.direction_course_id = 8 THEN T1.total_count ELSE 0 END) AS d_8_total_count,
+		SUM(CASE WHEN T1.direction_course_id = 8 AND T1.ncee_line_level = 1 THEN T1.line_count ELSE 0 END) AS d_8_line_count_1,
+		SUM(CASE WHEN T1.direction_course_id = 8 AND T1.ncee_line_level = 2 THEN T1.line_count ELSE 0 END) AS d_8_line_count_2,
+		SUM(CASE WHEN T1.direction_course_id = 8 AND T1.ncee_line_level = 3 THEN T1.line_count ELSE 0 END) AS d_8_line_count_3
+	FROM
 	(
-		SELECT T1.type, T1.sys_org_id, T1.ncee_line_level, SUM(T1.line_count) AS line_count, SUM(T1.total_count) AS total_count
+		SELECT T1.direction_course_id, T1.type, T1.sys_org_id, T1.ncee_line_level, SUM(T1.line_count) AS line_count, SUM(T1.total_count) AS total_count
 		FROM ncee_line_total AS T1
-		WHERE T1.ncee_plan_id = @nceePlanId AND (T1.type = 1 OR T1.type = 4) AND T1.ncee_course_comb_id IS NULL
-		GROUP BY T1.type, T1.sys_org_id, T1.ncee_line_level
+		WHERE T1.ncee_plan_id = {nceePlanId} AND (T1.type = 1 OR T1.type = 4) AND T1.ncee_course_comb_id IS NULL
+		GROUP BY T1.direction_course_id, T1.type, T1.sys_org_id, T1.ncee_line_level
 	) AS T1
 	GROUP BY T1.type, T1.sys_org_id
-) AS T1
-LEFT JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+) AS T2 ON T1.type = T2.type AND IFNULL(T1.sys_org_id, 0) = IFNULL(T2.sys_org_id, 0)
 ORDER BY T1.type, T1.sys_org_id
-", new { NceePlanId = nceePlanId });
+
+");
 
         return dt;
     }

+ 3 - 3
YBEE.EQM.Application/Ncee/NceeScore/Services/INceeScoreService.cs

@@ -6,14 +6,14 @@
 public interface INceeScoreService
 {
     /// <summary>
-    /// 上传成绩(仅原始分)
+    /// 上传成绩(仅原始分,适用于五区联考
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
     /// <returns></returns>
     Task UploadOnlyRawScore(string filePath, int nceePlanId);
     /// <summary>
-    /// 带转换分和等级
+    /// 上传成绩(带转换分和等级,适用于六校联考)
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
@@ -21,7 +21,7 @@ public interface INceeScoreService
     /// <exception cref="Exception"></exception>
     Task UploadWithConvertScore(string filePath, int nceePlanId);
     /// <summary>
-    /// 上传未选科原始成绩
+    /// 上传未选科原始成绩(适用于高一未选科)
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>

+ 55 - 3
YBEE.EQM.Application/Ncee/NceeScore/Services/NceeScoreService.cs

@@ -18,7 +18,7 @@ public class NceeScoreService : INceeScoreService, ITransient
     }
 
     /// <summary>
-    /// 上传成绩(仅原始分)
+    /// 上传成绩(仅原始分,适用于五区联考
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
@@ -302,7 +302,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, score, scor
         }
     }
     /// <summary>
-    /// 带转换分和等级
+    /// 上传成绩(带转换分和等级,适用于六校联考)
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
@@ -638,7 +638,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, ncee_conver
         }
     }
     /// <summary>
-    /// 上传未选科原始成绩
+    /// 上传未选科原始成绩(适用于高一未选科)
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
@@ -899,6 +899,10 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, ncee_conver
         {
             await ExecuteScoreConvert(nceePlanId);
         }
+        else
+        {
+            await ExecuteUpdateOrder(nceePlanId);
+        }
         // 执行划线
         await ExecuteLine(nceePlanId, nceePlanConfig);
     }
@@ -1303,6 +1307,7 @@ FROM
 ) AS T1
 ", new { NceePlanId = nceePlanId, line.NceeLineLevel, line.DirectionCourseId, line.LineScoreX, courseLine.CourseId, CourseLineScoreX = courseLine.LineScoreX });
 
+
                 // 单上线:机构 - 方向 - 组合
                 await _rep.SqlNonQueryAsync($@"
 INSERT INTO ncee_line_course(type, ncee_plan_id, ncee_line_level, direction_course_id, course_id, ncee_course_comb_id, sys_org_id, grade_id, line_count, total_count, line_rate, is_double_line)
@@ -1379,6 +1384,7 @@ FROM
 ) AS T1
 ", new { NceePlanId = nceePlanId, line.NceeLineLevel, line.DirectionCourseId, line.LineScoreX, courseLine.CourseId, CourseLineScoreX = courseLine.LineScoreX });
 
+
                 // 机构 - 班级 - 方向 - 组合
                 await _rep.SqlNonQueryAsync($@"
 INSERT INTO ncee_line_course(type, ncee_plan_id, ncee_line_level, direction_course_id, course_id, ncee_course_comb_id, sys_org_id, grade_id, class_number, line_count, total_count, line_rate, is_double_line)
@@ -1512,6 +1518,7 @@ REPLACE INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, score, sco
 
         // 更新总分
         await _rep.SqlNonQueryAsync($@"
+-- 更新总分
 UPDATE ncee_student AS T1
 JOIN
 (
@@ -1522,6 +1529,51 @@ JOIN
 ) AS T2 ON T1.id = T2.ncee_student_id
 SET T1.score = T2.score, T1.score_x = T2.score_x
 WHERE T1.ncee_plan_id = {nceePlanId}
+;
+-- 更新排名
+UPDATE ncee_student AS T1
+JOIN
+(
+	SELECT id, 
+		RANK() OVER(PARTITION BY direction_course_id ORDER BY score DESC) AS order_in_total,
+		RANK() OVER(PARTITION BY direction_course_id ORDER BY score_x DESC) AS order_in_total_x,
+		RANK() OVER(PARTITION BY direction_course_id, sys_org_id ORDER BY score DESC) AS order_in_org,
+		RANK() OVER(PARTITION BY direction_course_id, sys_org_id ORDER BY score_x DESC) AS order_in_org_x
+	FROM ncee_student 
+	WHERE ncee_plan_id = {nceePlanId}
+) AS T2 ON T1.id = T2.id
+SET T1.order_in_total = T2.order_in_total, 
+	T1.order_in_total_x = T2.order_in_total_x,
+	T1.order_in_org = T2.order_in_org,
+	T1.order_in_org_x = T2.order_in_org_x
+;
+");
+    }
+    /// <summary>
+    /// 更新排名
+    /// </summary>
+    /// <param name="nceePlanId"></param>
+    /// <returns></returns>
+    private async Task ExecuteUpdateOrder(int nceePlanId)
+    {
+        // 更新总分
+        await _rep.SqlNonQueryAsync($@"
+-- 更新排名
+UPDATE ncee_student AS T1
+JOIN
+(
+	SELECT id, 
+		RANK() OVER(PARTITION BY direction_course_id ORDER BY score DESC) AS order_in_total,
+		RANK() OVER(PARTITION BY direction_course_id ORDER BY score_x DESC) AS order_in_total_x,
+		RANK() OVER(PARTITION BY direction_course_id, sys_org_id ORDER BY score DESC) AS order_in_org,
+		RANK() OVER(PARTITION BY direction_course_id, sys_org_id ORDER BY score_x DESC) AS order_in_org_x
+	FROM ncee_student 
+	WHERE ncee_plan_id = {nceePlanId}
+) AS T2 ON T1.id = T2.id
+SET T1.order_in_total = T2.order_in_total, 
+	T1.order_in_total_x = T2.order_in_total_x,
+	T1.order_in_org = T2.order_in_org,
+	T1.order_in_org_x = T2.order_in_org_x
 ");
     }
     /// <summary>

+ 1 - 1
YBEE.EQM.Application/YBEE.EQM.Application.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 		<NoWarn>1701;1702;1591</NoWarn>
 		<DocumentationFile>YBEE.EQM.Application.xml</DocumentationFile>
 		<ImplicitUsings>enable</ImplicitUsings>

+ 57 - 9
YBEE.EQM.Application/YBEE.EQM.Application.xml

@@ -8846,6 +8846,11 @@
             是否排除
             </summary>
         </member>
+        <member name="T:YBEE.EQM.Application.ExamScoreMinorFileInfo">
+            <summary>
+            小题成绩导入文件信息
+            </summary>
+        </member>
         <member name="P:YBEE.EQM.Application.AddExamScoreInput.ExamPlanId">
             <summary>
             监测计划ID
@@ -8930,6 +8935,13 @@
             <param name="input"></param>
             <returns></returns>
         </member>
+        <member name="M:YBEE.EQM.Application.ExamScoreImportAppService.UploadImportStudentMinorScore(YBEE.EQM.Application.UploadExamDataInput)">
+            <summary>
+            导入区校合并小题成绩
+            </summary>
+            <param name="input"></param>
+            <returns></returns>
+        </member>
         <member name="T:YBEE.EQM.Application.ExamScoreImportService">
             <summary>
             学生成绩导入服务
@@ -8951,6 +8963,15 @@
             <param name="examPlanId"></param>
             <returns></returns>
         </member>
+        <member name="M:YBEE.EQM.Application.ExamScoreImportService.UploadImportStudentMinorScore(System.String,System.Int32)">
+            <summary>
+            上传并合并小题成绩导出
+            </summary>
+            <param name="filePath"></param>
+            <param name="examPlanId"></param>
+            <returns></returns>
+            <exception cref="T:System.Exception"></exception>
+        </member>
         <member name="T:YBEE.EQM.Application.IExamScoreImportService">
             <summary>
             学生成绩导入服务
@@ -8972,6 +8993,14 @@
             <param name="examPlanId"></param>
             <returns></returns>
         </member>
+        <member name="M:YBEE.EQM.Application.IExamScoreImportService.UploadImportStudentMinorScore(System.String,System.Int32)">
+            <summary>
+            导入区校合并小题成绩
+            </summary>
+            <param name="filePath"></param>
+            <param name="examPlanId"></param>
+            <returns></returns>
+        </member>
         <member name="T:YBEE.EQM.Application.ImportExamSpecialStudentItem">
             <summary>
             导入监测特殊学生输入参数
@@ -9989,6 +10018,16 @@
             </summary>
             <returns></returns>
         </member>
+        <member name="T:YBEE.EQM.Application.UploadExamStudentInput">
+            <summary>
+            上传学生信息输入参数
+            </summary>
+        </member>
+        <member name="P:YBEE.EQM.Application.UploadExamStudentInput.ExamGradeId">
+            <summary>
+            监测年级ID
+            </summary>
+        </member>
         <member name="T:YBEE.EQM.Application.ImportExamStudentItem">
             <summary>
             导入监测学生输入参数
@@ -10729,7 +10768,7 @@
             监测学生管理服务
             </summary>
         </member>
-        <member name="M:YBEE.EQM.Application.ExamStudentAppService.Upload(YBEE.EQM.Application.UploadExamDataInput)">
+        <member name="M:YBEE.EQM.Application.ExamStudentAppService.Upload(YBEE.EQM.Application.UploadExamStudentInput)">
             <summary>
             上传批量导入文件
             </summary>
@@ -10797,12 +10836,13 @@
             监测学生管理服务
             </summary>
         </member>
-        <member name="M:YBEE.EQM.Application.ExamStudentService.Upload(System.String,System.Int32)">
+        <member name="M:YBEE.EQM.Application.ExamStudentService.Upload(System.String,System.Int32,System.Int32)">
             <summary>
             上传批量导入文件
             </summary>
             <param name="filePath"></param>
             <param name="examPlanId"></param>
+            <param name="examGradeId"></param>
             <returns></returns>
         </member>
         <member name="M:YBEE.EQM.Application.ExamStudentService.Import(YBEE.EQM.Application.ImportExamStudentInput)">
@@ -10873,12 +10913,13 @@
             监测学生管理服务
             </summary>
         </member>
-        <member name="M:YBEE.EQM.Application.IExamStudentService.Upload(System.String,System.Int32)">
+        <member name="M:YBEE.EQM.Application.IExamStudentService.Upload(System.String,System.Int32,System.Int32)">
             <summary>
             上传批量文件
             </summary>
             <param name="filePath"></param>
             <param name="examPlanId"></param>
+            <param name="examGradeId"></param>
             <returns></returns>
         </member>
         <member name="M:YBEE.EQM.Application.IExamStudentService.Import(YBEE.EQM.Application.ImportExamStudentInput)">
@@ -13712,7 +13753,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.INceeScoreService.UploadOnlyRawScore(System.String,System.Int32)">
             <summary>
-            上传成绩(仅原始分)
+            上传成绩(仅原始分,适用于五区联考
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13720,7 +13761,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.INceeScoreService.UploadWithConvertScore(System.String,System.Int32)">
             <summary>
-            带转换分和等级
+            上传成绩(带转换分和等级,适用于六校联考)
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13729,7 +13770,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.INceeScoreService.UploadNoDirectionCourse(System.String,System.Int32)">
             <summary>
-            上传未选科原始成绩
+            上传未选科原始成绩(适用于高一未选科)
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13750,7 +13791,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.NceeScoreService.UploadOnlyRawScore(System.String,System.Int32)">
             <summary>
-            上传成绩(仅原始分)
+            上传成绩(仅原始分,适用于五区联考
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13758,7 +13799,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.NceeScoreService.UploadWithConvertScore(System.String,System.Int32)">
             <summary>
-            带转换分和等级
+            上传成绩(带转换分和等级,适用于六校联考)
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13767,7 +13808,7 @@
         </member>
         <member name="M:YBEE.EQM.Application.NceeScoreService.UploadNoDirectionCourse(System.String,System.Int32)">
             <summary>
-            上传未选科原始成绩
+            上传未选科原始成绩(适用于高一未选科)
             </summary>
             <param name="filePath"></param>
             <param name="nceePlanId"></param>
@@ -13796,6 +13837,13 @@
             <param name="nceePlanId"></param>
             <returns></returns>
         </member>
+        <member name="M:YBEE.EQM.Application.NceeScoreService.ExecuteUpdateOrder(System.Int32)">
+            <summary>
+            更新排名
+            </summary>
+            <param name="nceePlanId"></param>
+            <returns></returns>
+        </member>
         <member name="M:YBEE.EQM.Application.NceeScoreService.GetConvertRange(System.Int32,System.Int16,System.Collections.Generic.List{YBEE.EQM.Core.NceeConvertGrade},System.Collections.Generic.List{YBEE.EQM.Core.NceeScore})">
             <summary>
             获取转换区间

+ 31 - 0
YBEE.EQM.Core/Extensions/StringExtensions.cs

@@ -0,0 +1,31 @@
+using System.Linq;
+
+namespace YBEE.EQM.Core;
+
+public static class StringExtensions
+{
+    /// <summary>
+    /// 清除空白字符
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    public static string ClearWhitespace(this string str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        return string.Concat(str.Where(c => !char.IsWhiteSpace(c)));
+    }
+    /// <summary>
+    /// 证件号码规范化处理
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    public static string IdNumberStdProcessing(this string str)
+    {
+        var nstr = ClearWhitespace(str) ?? "";
+        nstr = nstr.Replace("(", ")").Replace(")", ")").Replace("-", "-").Replace("——", "-");
+        return nstr.ToUpper();
+    }
+}

+ 1 - 1
YBEE.EQM.Core/SeedData/SysMenuSeedData.cs

@@ -175,7 +175,7 @@ public class SysMenuSeedData : IEntitySeedData<SysMenu>
             new SysMenu { Id = 18006, Pid = 18005, Pids = "[0],[18000],[18005],", Name = "添加", Code = "add", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course.add", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },
             new SysMenu { Id = 18007, Pid = 18005, Pids = "[0],[18000],[18005],", Name = "修改", Code = "edit", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course.edit", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },
             new SysMenu { Id = 18008, Pid = 18005, Pids = "[0],[18000],[18005],", Name = "删除", Code = "del", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course.del", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },
-            new SysMenu { Id = 18009, Pid = 18000, Pids = "[0],[18000],", Name = "高中选科组合", Code = "course-comb", MenuType = MenuType.MENU, Icon = "", Router = "/bd/course-comb", Component = "", Permission = "", OpenType = MenuOpenType.COMPONENT, Visible = true, Sort = 30, Status = CommonStatus.ENABLE },
+            new SysMenu { Id = 18009, Pid = 18000, Pids = "[0],[18000],", Name = "高中选科组合", Code = "ncee-course-comb", MenuType = MenuType.MENU, Icon = "", Router = "/bd/ncee-course-comb", Component = "", Permission = "", OpenType = MenuOpenType.COMPONENT, Visible = true, Sort = 30, Status = CommonStatus.ENABLE },
             new SysMenu { Id = 18010, Pid = 18009, Pids = "[0],[18000],[18009],", Name = "添加", Code = "add", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course-comb.add", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },
             new SysMenu { Id = 18011, Pid = 18009, Pids = "[0],[18000],[18009],", Name = "修改", Code = "edit", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course-comb.edit", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },
             new SysMenu { Id = 18012, Pid = 18009, Pids = "[0],[18000],[18009],", Name = "删除", Code = "del", MenuType = MenuType.BUTTON, Icon = "", Router = "", Component = "", Permission = "bd.course-comb.del", OpenType = MenuOpenType.NONE, Visible = true, Sort = 0, Status = CommonStatus.ENABLE },

+ 8 - 0
YBEE.EQM.Core/Service/NceePlanConfig.cs

@@ -21,4 +21,12 @@ public class NceePlanConfig
     /// 开启选科组合统计
     /// </summary>
     public bool CourseCombStatEnabled { get; set; } = true;
+    /// <summary>
+    /// 开启赋分导出
+    /// </summary>
+    public bool ExportConvertScoreEnabled { get; set; } = false;
+    /// <summary>
+    /// 开启排名导出
+    /// </summary>
+    public bool ExportOrderEnabled { get; set; } = false;
 }

+ 8 - 8
YBEE.EQM.Core/YBEE.EQM.Core.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 		<NoWarn>1701;1702;1591</NoWarn>
 		<DocumentationFile>YBEE.EQM.Core.xml</DocumentationFile>
 	</PropertyGroup>
@@ -26,13 +26,13 @@
 	</ItemGroup>
 
 	<ItemGroup>
-		<PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
-		<PackageReference Include="Furion" Version="4.8.8.42" />
-		<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.8.8.42" />
-		<PackageReference Include="Furion.Extras.Logging.Serilog" Version="4.8.8.42" />
-		<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.8.8.42" />
-		<PackageReference Include="NPOI" Version="2.6.1" />
-		<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
+		<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
+		<PackageReference Include="Furion" Version="4.9.2.25" />
+		<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.2.25" />
+		<PackageReference Include="Furion.Extras.Logging.Serilog" Version="4.9.2.25" />
+		<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.2.25" />
+		<PackageReference Include="NPOI" Version="2.7.0" />
+		<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
 		<PackageReference Include="SharpZipLib" Version="1.4.2" />
 		<PackageReference Include="UAParser" Version="3.1.47" />
 		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />

+ 24 - 0
YBEE.EQM.Core/YBEE.EQM.Core.xml

@@ -5221,6 +5221,20 @@
             分组名称
             </summary>
         </member>
+        <member name="M:YBEE.EQM.Core.StringExtensions.ClearWhitespace(System.String)">
+            <summary>
+            清除空白字符
+            </summary>
+            <param name="str"></param>
+            <returns></returns>
+        </member>
+        <member name="M:YBEE.EQM.Core.StringExtensions.IdNumberStdProcessing(System.String)">
+            <summary>
+            证件号码规范化处理
+            </summary>
+            <param name="str"></param>
+            <returns></returns>
+        </member>
         <member name="T:YBEE.EQM.Core.DisableOpLogAttribute">
             <summary>
             禁用操作日志
@@ -5624,6 +5638,16 @@
             开启选科组合统计
             </summary>
         </member>
+        <member name="P:YBEE.EQM.Core.NceePlanConfig.ExportConvertScoreEnabled">
+            <summary>
+            开启赋分导出
+            </summary>
+        </member>
+        <member name="P:YBEE.EQM.Core.NceePlanConfig.ExportOrderEnabled">
+            <summary>
+            开启排名导出
+            </summary>
+        </member>
         <member name="T:YBEE.EQM.Core.RoleDataScope">
             <summary>
             角色数据权限范围

+ 1 - 1
YBEE.EQM.Database.Migrations/YBEE.EQM.Database.Migrations.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 	</PropertyGroup>
 
 

+ 1 - 1
YBEE.EQM.EntityFramework.Core/YBEE.EQM.EntityFramework.Core.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 	</PropertyGroup>
 
 

+ 1 - 1
YBEE.EQM.Web.Core/YBEE.EQM.Web.Core.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 		<NoWarn>1701;1702;1591</NoWarn>
 		<DocumentationFile>YBEE.EQM.Web.Core.xml</DocumentationFile>
 	</PropertyGroup>

+ 1 - 1
YBEE.EQM.Web.Entry/YBEE.EQM.Web.Entry.csproj

@@ -3,7 +3,7 @@
 
 
 	<PropertyGroup>
-		<TargetFramework>net7.0</TargetFramework>
+		<TargetFramework>net8.0</TargetFramework>
 		<ImplicitUsings>enable</ImplicitUsings>
 		<SatelliteResourceLanguages>en-US</SatelliteResourceLanguages>
 		<PublishReadyToRunComposite>true</PublishReadyToRunComposite>