Browse Source

1.完善抽样配置;
2.修改导出报表。

beetle 1 year ago
parent
commit
e27bbd1812
100 changed files with 3998 additions and 1275 deletions
  1. 29 10
      YBEE.EQM.Admin/scripts/gapi/index.js
  2. 67 0
      YBEE.EQM.Admin/src/common/helper.ts
  3. 19 18
      YBEE.EQM.Admin/src/components/ChangePassword/index.tsx
  4. 61 0
      YBEE.EQM.Admin/src/components/PasswordComplexityValidation/index.tsx
  5. 1 0
      YBEE.EQM.Admin/src/components/index.ts
  6. 43 16
      YBEE.EQM.Admin/src/pages/auth/Login/index.tsx
  7. 58 31
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamResultList.tsx
  8. 74 40
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleEditModal.tsx
  9. 25 2
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleList.tsx
  10. 14 2
      YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleDetail/index.tsx
  11. 3 2
      YBEE.EQM.Admin/src/pages/exam-center/special-student/special-student-audit/index.tsx
  12. 4 2
      YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentImport/index.tsx
  13. 26 2
      YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentReport/index.tsx
  14. 1 1
      YBEE.EQM.Admin/src/services/apis/ExamAbsentReplaceController.ts
  15. 1 1
      YBEE.EQM.Admin/src/services/apis/ExamOrgResultController.ts
  16. 33 1
      YBEE.EQM.Admin/src/services/apis/ExamPaperController.ts
  17. 1 1
      YBEE.EQM.Admin/src/services/apis/ExamPatriarchQuestionnaireProgressController.ts
  18. 16 0
      YBEE.EQM.Admin/src/services/apis/ExamPlanController.ts
  19. 37 6
      YBEE.EQM.Admin/src/services/apis/ExamReportingAvgRangeController.ts
  20. 45 0
      YBEE.EQM.Admin/src/services/apis/ExamReportingTqesController.ts
  21. 5 5
      YBEE.EQM.Admin/src/services/apis/ExamSampleController.ts
  22. 42 0
      YBEE.EQM.Admin/src/services/apis/ExamScoreExportController.ts
  23. 30 1
      YBEE.EQM.Admin/src/services/apis/ExamScoreImportController.ts
  24. 1 1
      YBEE.EQM.Admin/src/services/apis/ExamSpecialStudentController.ts
  25. 33 1
      YBEE.EQM.Admin/src/services/apis/ExamTeacherCourseController.ts
  26. 1 1
      YBEE.EQM.Admin/src/services/apis/FileController.ts
  27. 5 8
      YBEE.EQM.Admin/src/services/apis/NceeExportController.ts
  28. 2 2
      YBEE.EQM.Admin/src/services/apis/NceeScoreController.ts
  29. 44 0
      YBEE.EQM.Admin/src/services/apis/SysAuthController.ts
  30. 6 0
      YBEE.EQM.Admin/src/services/apis/index.ts
  31. 73 30
      YBEE.EQM.Admin/src/services/typing.d.ts
  32. 10 0
      YBEE.EQM.Application/Base/Grade/Dtos/GradeOutput.cs
  33. 12 20
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceAuditService.cs
  34. 23 23
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceService.cs
  35. 3 13
      YBEE.EQM.Application/Exam/ExamOrgScoreReport/ExamOrgScoreReportAppService.cs
  36. 5 4
      YBEE.EQM.Application/Exam/ExamOrgScoreReport/Services/ExamOrgScoreReportService.cs
  37. 2 8
      YBEE.EQM.Application/Exam/ExamPaper/ExamPaperAppService.cs
  38. 19 15
      YBEE.EQM.Application/Exam/ExamPlan/ExamPlanAppService.cs
  39. 26 19
      YBEE.EQM.Application/Exam/ExamPlan/Services/ExamPlanService.cs
  40. 6 0
      YBEE.EQM.Application/Exam/ExamPlan/Services/IExamPlanService.cs
  41. 10 12
      YBEE.EQM.Application/Exam/ExamQuestionnaire/Services/ExamPatriarchQuestionnaireProgressSync.cs
  42. 113 0
      YBEE.EQM.Application/Exam/ExamReporting/Dtos/ExamReportingTqesAdvWeakDto.cs
  43. 12 0
      YBEE.EQM.Application/Exam/ExamReporting/Dtos/ExamScoreRangeExportDto.cs
  44. 16 10
      YBEE.EQM.Application/Exam/ExamReporting/ExamReportingAvgRangeAppService.cs
  45. 24 0
      YBEE.EQM.Application/Exam/ExamReporting/ExamReportingTqesAppService.cs
  46. 477 129
      YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingAvgRangeService.cs
  47. 114 0
      YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingTqesService.cs
  48. 9 2
      YBEE.EQM.Application/Exam/ExamReporting/Services/IExamReportingAvgRangeService.cs
  49. 14 0
      YBEE.EQM.Application/Exam/ExamReporting/Services/IExamReportingTqesService.cs
  50. 4 11
      YBEE.EQM.Application/Exam/ExamResult/Services/ExamResultService.cs
  51. 4 2
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleInput.cs
  52. 6 0
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleMapper.cs
  53. 9 2
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleOutput.cs
  54. 150 110
      YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleService.cs
  55. 16 0
      YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreExportInput.cs
  56. 80 0
      YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreExportTqesDto.cs
  57. 12 0
      YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreImportDto.cs
  58. 22 0
      YBEE.EQM.Application/Exam/ExamScore/ExamScoreExportAppService.cs
  59. 3 9
      YBEE.EQM.Application/Exam/ExamScore/ExamScoreImportAppService.cs
  60. 218 0
      YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreExportService.cs
  61. 112 60
      YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreImportService.cs
  62. 15 0
      YBEE.EQM.Application/Exam/ExamScore/Services/IExamScoreExportService.cs
  63. 62 18
      YBEE.EQM.Application/Exam/ExamSpecialStudent/Services/ExamSpecialStudentService.cs
  64. 14 14
      YBEE.EQM.Application/Exam/ExamTeacher/Services/ExamTeacherService.cs
  65. 0 1
      YBEE.EQM.Application/Exam/ExamTeacherCourse/ExamTeacherCourseAppService.cs
  66. 14 14
      YBEE.EQM.Application/Exam/ExamTeacherCourse/Services/ExamTeacherCourseService.cs
  67. 15 0
      YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelCellStyle.cs
  68. 4 0
      YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelColDto.cs
  69. 5 0
      YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelDto.cs
  70. 124 9
      YBEE.EQM.Application/ExportExcel/Services/ExportExcelService.cs
  71. 17 0
      YBEE.EQM.Application/ExportExcel/Services/IExportExcelService.cs
  72. 4 10
      YBEE.EQM.Application/File/ResourceFile/ResourceFileAppService.cs
  73. 6 15
      YBEE.EQM.Application/Job/ExamPatriarchQuestionnaireProgressSyncJob.cs
  74. 2 7
      YBEE.EQM.Application/Ncee/NceeCourseComb/NceeCourseCombAppService.cs
  75. 54 0
      YBEE.EQM.Application/Ncee/NceeExport/Dtos/NceeExportDto.cs
  76. 42 0
      YBEE.EQM.Application/Ncee/NceeExport/Dtos/NceeExportInput.cs
  77. 5 13
      YBEE.EQM.Application/Ncee/NceeExport/NceeExportAppService.cs
  78. 2 2
      YBEE.EQM.Application/Ncee/NceeExport/Services/INceeExportService.cs
  79. 385 317
      YBEE.EQM.Application/Ncee/NceeExport/Services/NceeExportService.cs
  80. 3 12
      YBEE.EQM.Application/Ncee/NceeScore/NceeScoreAppService.cs
  81. 1 1
      YBEE.EQM.Application/Ncee/NceeScore/Services/INceeScoreService.cs
  82. 27 29
      YBEE.EQM.Application/Ncee/NceeScore/Services/NceeScoreService.cs
  83. 36 61
      YBEE.EQM.Application/System/Auth/Services/SysAuthService.cs
  84. 14 6
      YBEE.EQM.Application/System/Auth/SysAuthAppService.cs
  85. 3 9
      YBEE.EQM.Application/System/Dict/SysDictDataAppService.cs
  86. 2 8
      YBEE.EQM.Application/System/Dict/SysDictTypeAppService.cs
  87. 20 32
      YBEE.EQM.Application/System/Role/Services/SysRoleService.cs
  88. 5 0
      YBEE.EQM.Application/System/User/Services/ISysUserService.cs
  89. 25 15
      YBEE.EQM.Application/System/User/Services/SysUserService.cs
  90. 12 10
      YBEE.EQM.Application/System/User/SysUserAppService.cs
  91. 4 5
      YBEE.EQM.Application/YBEE.EQM.Application.csproj
  92. 661 28
      YBEE.EQM.Application/YBEE.EQM.Application.xml
  93. 3 2
      YBEE.EQM.Application/applicationsettings.Development.json
  94. 2 1
      YBEE.EQM.Application/applicationsettings.Production.json
  95. 6 0
      YBEE.EQM.Core/Entities/Base/Grade.cs
  96. 9 0
      YBEE.EQM.Core/Entities/Exam/ExamSample.cs
  97. 24 1
      YBEE.EQM.Core/Entities/Exam/ExamScore.cs
  98. 18 2
      YBEE.EQM.Core/Entities/Exam/ExamScoreTotal.cs
  99. 2 0
      YBEE.EQM.Core/Enums/NceeDataScopeType.cs
  100. 20 0
      YBEE.EQM.Core/Enums/NceeDirectionCourse.cs

+ 29 - 10
YBEE.EQM.Admin/scripts/gapi/index.js

@@ -14,7 +14,7 @@ const API_PATH = `${ROOT_PATH}/apis`;
 const JS_KEY_WORDS = [
     'break', 'else', 'new', 'var', 'case', 'finally', 'return', 'void', 'catch', 'for', 'switch',
     'while', 'continue', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in',
-    'try', 'do', 'instranceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
+    'try', 'do', 'instanceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
     'interface', 'static', 'byte', 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized',
     'class', 'float', 'package', 'throws', 'const', 'goto', 'private ', 'transient', 'debugger',
     'implements', 'protected ', 'volatile', 'double', 'import', 'public',
@@ -22,6 +22,7 @@ const JS_KEY_WORDS = [
 
 /** 数值类型映射 */
 const IntegerMapping = {
+    decimal: 'number',
     int16: 'number',
     int32: 'number',
     int64: 'string',
@@ -29,6 +30,7 @@ const IntegerMapping = {
 
 /** 返回结果类型映射 */
 const ResultMapping = {
+    Decimal: 'number',
     Int16: 'number',
     Int32: 'number',
     Int64: 'string',
@@ -36,6 +38,7 @@ const ResultMapping = {
     Object: 'any',
     String: 'string',
     DateTime: 'string',
+    FileContentResult: 'FileContentResult',
 };
 
 /** 统一注释 */
@@ -286,6 +289,15 @@ const getTagDescDict = (tags) => {
     return tagDict;
 };
 
+/** 是否为文件响应类型 */
+const isFileResponse = (actionName, responseType) => {
+    if (responseType == 'FileContentResult') {
+        return true;
+    }
+    const an = actionName.toLowerCase();
+    return an.includes('export') || an.includes('download');
+}
+
 // ----------------------------------------
 
 /** 生成枚举 */
@@ -340,9 +352,6 @@ const generateEntity = (schemas, enums) => {
             continue;
         }
 
-        // const entity = getEntityType(name.split('_')[0], schema);
-        // const ns = name.split('_');
-        // const entity = getEntityType(ns[ns.length - 1], schema);
         const entity = getEntityType(name, schema);
         entities.push(entity);
     }
@@ -417,6 +426,14 @@ const generateEntity = (schemas, enums) => {
                     items?: T[];
                 };
 
+                /** 文件响应类型 */
+                type FileResponse = {
+                    /** 文件名称 */
+                    fileName: string;
+                    /** 文件体 */
+                    data: Blob;
+                };
+                
             ${es}}
         }`,
     );
@@ -450,6 +467,7 @@ const generateController = (paths, tagDict, enums) => {
                         actions: [],
                         actionNames: [],
                         enums: [],
+                        blobActions: [],
                     };
                 }
                 let paramsType = '';
@@ -471,8 +489,9 @@ const generateController = (paths, tagDict, enums) => {
 
                 const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
                 let actionBody = actionFullDesc;
-                // export api
-                if (actionName.toLowerCase().includes('export') || actionName.toLowerCase().includes('download')) {
+                // file response api
+                if (isFileResponse(actionName, responseType)) {
+                    controllers[controllerName].blobActions.push(actionName);
                     actionBody = `${actionFullDesc}export async function ${actionName}(`;
                     if (verb == 'get') {
                         if (paramsType != '') {
@@ -510,7 +529,7 @@ const generateController = (paths, tagDict, enums) => {
                     actionBody = `${actionBody}let fileName = '';\n`;
                     actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
                     actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
-                    actionBody = `${actionBody}return { fileName, data:res.data };\n}\n`;
+                    actionBody = `${actionBody}return { fileName, data:res.data as Blob } as API.FileResponse;\n}\n`;
                 }
                 // other api
                 else {
@@ -567,7 +586,7 @@ const generateController = (paths, tagDict, enums) => {
         csnames.push({ name: ck, description: c.description });
         const controllerFile = `${API_PATH}/${ck}.ts`;
 
-        const isBolb = c.actionNames.findIndex(t => t.includes('export') || t.includes('download')) !== -1;
+        const hasBlob = c.blobActions.length > 0;
 
         fs.writeFileSync(
             controllerFile,
@@ -577,8 +596,8 @@ const generateController = (paths, tagDict, enums) => {
             // ${c.description}
             // --------------------------------------------------------------------------
 
-            import { request${isBolb ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
-            ${isBolb ? 'import contentDisposition from \'content-disposition\';' : ''}
+            import { request${hasBlob ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
+            ${hasBlob ? 'import contentDisposition from \'content-disposition\';' : ''}
 
             ${c.actions.join('\n')}
 

+ 67 - 0
YBEE.EQM.Admin/src/common/helper.ts

@@ -97,6 +97,73 @@ export function encrptRsa(str: string) {
     return encryptStr.encrypt(str); // 进行加密
 }
 
+/** 密码复杂度验证结果类型 */
+export type ValidatePasswordComplexityType = {
+    /** 整体验证是否通过 */
+    isValid?: boolean;
+    /** 长度是否验证通过 */
+    isLenValid?: boolean;
+    /** 数字是否验证通过 */
+    isDigitValid?: boolean;
+    /** 字母是否验证通过 */
+    isLetterValid?: boolean;
+    /** 特殊字符是否验证通过 */
+    isSpecialValid?: boolean;
+    /** 最小长度 */
+    minLen: number;
+    /** 最大长度 */
+    maxLen: number;
+    /** 特殊字符 */
+    special: string;
+};
+export const validatePasswordComplexityInititalValue = {
+    isValid: false,
+    isLenValid: false,
+    isDigitValid: false,
+    isLetterValid: false,
+    isSpecialValid: false,
+    minLen: 6,
+    maxLen: 32,
+    special: '@$!%*#?&+-.,~',
+};
+
+/**
+ * 验证密码复杂度
+ * 长度6~32位,必须1位数字、1位字母、1位特殊字符(@$!%*#?&+-|<>.,~)
+ * @param pwd 原密码串
+ * @param minLen 最小长度,默认6
+ * @param maxLen 最大长度,默认32
+ * @returns
+ */
+export function validatePasswordComplexity(pwd: string, minLen: number = 6, maxLen: number = 32) {
+    const ret: ValidatePasswordComplexityType = {
+        isValid: true,
+        isLenValid: true,
+        isDigitValid: true,
+        isLetterValid: true,
+        isSpecialValid: true,
+        minLen,
+        maxLen,
+        special: validatePasswordComplexityInititalValue.special,
+    };
+
+    if (pwd.length < minLen || pwd.length > maxLen) {
+        ret.isLenValid = false;
+    }
+    if (!/[0-9]/.test(pwd)) {
+        ret.isDigitValid = false;
+    }
+    if (!/[a-zA-Z]/.test(pwd)) {
+        ret.isLetterValid = false;
+    }
+    const r = new RegExp(`[${validatePasswordComplexityInititalValue.special}]`);
+    if (!r.test(pwd)) {
+        ret.isSpecialValid = false;
+    }
+    ret.isValid = ret.isLenValid && ret.isDigitValid && ret.isLetterValid && ret.isSpecialValid;
+    return ret;
+}
+
 /**
  * 生成数字字母随机串
  * @param length 长度

+ 19 - 18
YBEE.EQM.Admin/src/components/ChangePassword/index.tsx

@@ -1,5 +1,5 @@
-import { encrptRsa } from '@/common/helper';
-import { MovableModalForm } from '@/components';
+import { encrptRsa, validatePasswordComplexity, validatePasswordComplexityInititalValue, ValidatePasswordComplexityType } from '@/common/helper';
+import { MovableModalForm, PasswordComplexityValidation } from '@/components';
 import SysUserController from '@/services/apis/SysUserController';
 import type { ProFormInstance } from '@ant-design/pro-components';
 import { App, Form, Input } from 'antd';
@@ -10,12 +10,12 @@ interface ChangePasswordProps {
 }
 const ChangePassword: React.FC<ChangePasswordProps> = ({ onClose }) => {
     const [visible, setVisible] = useState<boolean>(true);
-    const handleClose = () => {
-        setVisible(false);
-        setTimeout(onClose, 0);
-    };
+    const handleClose = () => { setVisible(false); setTimeout(onClose, 300); };
     const formRef = useRef<ProFormInstance>();
 
+    const { minLen, maxLen, special } = validatePasswordComplexityInititalValue;
+    const [passwordValid, setPasswordValid] = useState<ValidatePasswordComplexityType>({ minLen, maxLen, special });
+
     const { message } = App.useApp();
 
     return (
@@ -25,7 +25,7 @@ const ChangePassword: React.FC<ChangePasswordProps> = ({ onClose }) => {
             newPasswordConfirm: string;
         }>
             title="修改密码"
-            width="400px"
+            width={480}
             open={visible}
             formRef={formRef}
             modalProps={{
@@ -54,31 +54,32 @@ const ChangePassword: React.FC<ChangePasswordProps> = ({ onClose }) => {
             }}
         >
             <Form.Item label="原密码" name="oldPassword" rules={[{ required: true }]}>
-                <Input type="password" />
+                <Input.Password showCount minLength={6} maxLength={32} autoComplete="new-password" />
             </Form.Item>
             <Form.Item
-                label="新密码(6至32位)"
+                label="新密码"
                 name="newPassword"
+                required
                 rules={[
-                    { required: true, min: 6, max: 32 },
-                    ({ getFieldValue }) => ({
+                    () => ({
                         validator: (_, value) => {
-                            const np2 = getFieldValue('newPasswordConfirm');
-                            if (!np2 || np2 === '' || np2 === value) {
-                                return Promise.resolve();
+                            const r = validatePasswordComplexity(value);
+                            setPasswordValid(r);
+                            if (!r.isValid) {
+                                return Promise.reject('新密码不符合密码规则要求');
                             }
-                            return Promise.reject('两次输入新密码不一致!');
                         },
                     }),
                 ]}
             >
-                <Input type="password" />
+                <Input.Password showCount minLength={6} maxLength={32} autoComplete="new-password" />
             </Form.Item>
+            <PasswordComplexityValidation result={passwordValid} />
             <Form.Item
                 label="确认新密码"
                 name="newPasswordConfirm"
                 rules={[
-                    { required: true, min: 6, max: 32 },
+                    { required: true },
                     ({ getFieldValue }) => ({
                         validator: (_, value) => {
                             const np = getFieldValue('newPassword');
@@ -90,7 +91,7 @@ const ChangePassword: React.FC<ChangePasswordProps> = ({ onClose }) => {
                     }),
                 ]}
             >
-                <Input type="password" />
+                <Input.Password showCount minLength={6} maxLength={32} autoComplete="new-password" />
             </Form.Item>
         </MovableModalForm>
     );

+ 61 - 0
YBEE.EQM.Admin/src/components/PasswordComplexityValidation/index.tsx

@@ -0,0 +1,61 @@
+import { ValidatePasswordComplexityType } from "@/common/helper";
+import { CheckCircleFilled, CloseCircleFilled, InfoCircleFilled } from "@ant-design/icons";
+import { useEmotionCss } from "@ant-design/use-emotion-css";
+import { Typography, theme } from "antd";
+
+const ValidationIcon: React.FC<{ isValid: boolean | undefined }> = ({ isValid }) => {
+    const { token } = theme.useToken();
+
+    if (isValid === undefined) {
+        return (<InfoCircleFilled style={{ marginRight: token.margin, color: token.colorInfo }} />);
+    }
+    if (isValid) {
+        return (<CheckCircleFilled style={{ marginRight: token.margin, color: token.colorSuccess }} />);
+    }
+    else {
+        return (<CloseCircleFilled style={{ marginRight: token.margin, color: token.colorError }} />);
+    }
+}
+
+const PasswordComplexityValidation: React.FC<{ result?: ValidatePasswordComplexityType }> = ({ result }) => {
+    const pClass = useEmotionCss(({ token }) => {
+        return { marginBottom: `${token.marginXS}px !important` };
+    });
+    const dClass = useEmotionCss(({ token }) => {
+        return {
+            padding: token.padding,
+            borderRadius: token.borderRadius,
+            border: `1px solid ${token.colorBorderSecondary}`,
+            backgroundColor: token.colorBorderSecondary,
+            marginBottom: token.margin,
+        };
+    });
+
+    return (
+        <div className={dClass}>
+            {/* <Typography.Paragraph className={pClass}>
+                密码规则:
+            </Typography.Paragraph> */}
+            <Typography.Paragraph className={pClass}>
+                <ValidationIcon isValid={result?.isDigitValid} />
+                <Typography.Text>必须包含数字</Typography.Text>
+            </Typography.Paragraph>
+            <Typography.Paragraph className={pClass}>
+                <ValidationIcon isValid={result?.isLetterValid} />
+                <Typography.Text>必须包含字母</Typography.Text>
+            </Typography.Paragraph>
+            <Typography.Paragraph className={pClass}>
+                <ValidationIcon isValid={result?.isSpecialValid} />
+                <Typography.Text>必须包含</Typography.Text>
+                <Typography.Text mark>  {result?.special}  </Typography.Text>
+                <Typography.Text>中1位特殊字符</Typography.Text>
+            </Typography.Paragraph>
+            <Typography.Paragraph className={pClass}>
+                <ValidationIcon isValid={result?.isLenValid} />
+                <Typography.Text>长度必须为{result?.minLen}至{result?.maxLen}位</Typography.Text>
+            </Typography.Paragraph>
+        </div>
+    );
+}
+
+export default PasswordComplexityValidation;

+ 1 - 0
YBEE.EQM.Admin/src/components/index.ts

@@ -10,6 +10,7 @@ export { default as JsonEditor } from './JsonEditor';
 export { default as MenuFooter } from './MenuFooter';
 export { default as MovableModal } from './MovableModal';
 export { default as MovableModalForm } from './MovableModalForm';
+export { default as PasswordComplexityValidation } from './PasswordComplexityValidation';
 export { default as StatusIcon } from './StatusIcon';
 export { default as SuperPageContainer } from './SuperPageContainer';
 export { default as SuperTable } from './SuperTable';

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

@@ -1,6 +1,6 @@
 import { ThemeMode } from '@/common/cache';
-import { encrptRsa } from '@/common/helper';
-import { Footer, ThemeSwitch } from '@/components';
+import { encrptRsa, validatePasswordComplexity, validatePasswordComplexityInititalValue, ValidatePasswordComplexityType } from '@/common/helper';
+import { Footer, PasswordComplexityValidation, ThemeSwitch } from '@/components';
 import { isDarkTheme } from '@/layouts/RootLayout';
 import { loginByAccount } from '@/services/account';
 import SysAuthController from '@/services/apis/SysAuthController';
@@ -9,7 +9,7 @@ import type { ProFormInstance } from '@ant-design/pro-components';
 import { ProForm, ProFormCheckbox, ProFormText } from '@ant-design/pro-components';
 import { useEmotionCss } from '@ant-design/use-emotion-css';
 import { useModel } from '@umijs/max';
-import { Alert, App, ConfigProvider, Tooltip, theme } from 'antd';
+import { Alert, App, ConfigProvider, theme, Tooltip, Typography } from 'antd';
 import { useCallback, useEffect, useRef, useState } from 'react';
 import defaultSettings from '../../../../config/defaultSettings';
 import packageInfo from '../../../../package.json';
@@ -218,6 +218,9 @@ const Login: React.FC = () => {
 
     const formRef = useRef<ProFormInstance>();
 
+    const { minLen, maxLen, special } = validatePasswordComplexityInititalValue;
+    const [passwordValid, setPasswordValid] = useState<ValidatePasswordComplexityType>({ minLen, maxLen, special });
+
     // 获取验证码
     const fetchCaptcha = useCallback(async () => {
         setCaptchaLoading(true);
@@ -332,6 +335,21 @@ const Login: React.FC = () => {
                         <div className="avatar">
                             <img src="/images/login-avatar-320.png" alt="login caption" />
                         </div>
+                        {!isActivated && <Alert
+                            // message={`当前用户暂未激活,属于【${orgName}】,暂未激活,若学校有误请勿激活,首次登录请输入新密码更换登录密码!`}
+                            message={
+                                <>
+                                    <Typography.Text>当前用户</Typography.Text>
+                                    <Typography.Text type="danger">未激活</Typography.Text>
+                                    <Typography.Text>,属于</Typography.Text>
+                                    <Typography.Text mark> {orgName} </Typography.Text>
+                                    <Typography.Text>,设置新密码后以激活用户。若用户和学校有误请联系管理员处理,请勿激活!</Typography.Text>
+                                </>
+                            }
+                            type="warning"
+                            showIcon
+                            style={{ marginBottom: 24 }}
+                        />}
                         <div className="contacts">
                             <Tooltip
                                 trigger="click"
@@ -352,12 +370,6 @@ const Login: React.FC = () => {
                     </div>
                     <div className="right">
                         {isActivated && <h1 className="form-title">用户登录</h1>}
-                        {!isActivated && <Alert
-                            message={`当前用户属于【${orgName}】,若有误请勿使用,首登录请输入新密码更换登录密码!`}
-                            type="warning"
-                            showIcon
-                            style={{ marginBottom: 24 }}
-                        />}
                         {status === 'error' && (
                             <Alert
                                 message={errorMessage ?? '错误的用户名和密码!'}
@@ -421,33 +433,48 @@ const Login: React.FC = () => {
                                     name="newPassword"
                                     fieldProps={{
                                         prefix: <LockOutlined />,
+                                        minLength: 6,
+                                        maxLength: 32,
+                                        showCount: true,
                                     }}
-                                    placeholder="新密码(6至32位)"
+                                    placeholder="新密码"
+                                    required
                                     rules={[
-                                        { required: true, min: 6, max: 32 },
+                                        // { required: true, min: 6, max: 32 },
                                         ({ getFieldValue }) => ({
                                             validator: (_, value) => {
                                                 const op = getFieldValue("password");
                                                 if (value === op) {
                                                     return Promise.reject("新密码不能与初始密码一致");
                                                 }
-                                                const np2 = getFieldValue('newPasswordConfirm');
-                                                if (!np2 || np2 === '' || np2 === value) {
-                                                    return Promise.resolve();
+                                                // const np2 = getFieldValue('newPasswordConfirm');
+                                                // if (!np2 || np2 === '' || np2 === value) {
+                                                //     return Promise.resolve();
+                                                // }
+                                                // return Promise.reject('两次输入新密码不一致!');
+
+                                                const r = validatePasswordComplexity(value);
+                                                setPasswordValid(r);
+                                                if (!r.isValid) {
+                                                    return Promise.reject('新密码不符合密码规则要求');
                                                 }
-                                                return Promise.reject('两次输入新密码不一致!');
+                                                return Promise.resolve();
                                             },
                                         }),
                                     ]}
                                 />
+                                <PasswordComplexityValidation result={passwordValid} />
                                 <ProFormText.Password
                                     label="确认新密码"
                                     name="newPasswordConfirm"
                                     fieldProps={{
                                         prefix: <LockOutlined />,
+                                        minLength: 6,
+                                        maxLength: 32,
+                                        showCount: true,
                                     }}
                                     rules={[
-                                        { required: true, min: 6, max: 32 },
+                                        { required: true },
                                         ({ getFieldValue }) => ({
                                             validator: (_, value) => {
                                                 const np = getFieldValue('newPassword');

+ 58 - 31
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamResultList.tsx

@@ -62,38 +62,65 @@ const ExamResultList: React.FC<{ examPlanId: number, semesterId: number }> = ({
         }
     }, [fileList]);
 
-    return (
-        <ProCard
-            style={{ marginTop: token.margin }}
-            title={<CardStepTitle>结果管理</CardStepTitle>}
-        >
-            {semesterId < 20232 &&
-                <>
-                    <Typography.Paragraph>
-                        <Typography.Title level={5}>初始成绩导入</Typography.Title>
-                        <Typography.Text>适用于前未上报学生和抽样的批量学生成绩导入。</Typography.Text>
-                    </Typography.Paragraph>
-                    <Card>
-                        <Space direction="vertical" size="large" style={{ width: '100%' }}>
-                            <Upload {...uploadProps}>
-                                <Button icon={<UploadOutlined />} disabled={uploading}>选择文件...</Button>
-                            </Upload>
-                            <Space>
-                                <Button
-                                    type="primary"
-                                    disabled={fileList.length === 0}
-                                    loading={uploading}
-                                    icon={<SendOutlined />}
-                                    onClick={handleUploadImportWithoutStudentTotalScore}
-                                >{uploading ? '正在导入,请稍候' : '开始导入'}</Button>
-                                {uploadStatus && <Typography.Text type={uploadStatus.success ? 'success' : 'danger'}>{uploadStatus?.message}</Typography.Text>}
-                            </Space>
+    if (semesterId < 20232) {
+        return (
+            <ProCard
+                style={{ marginTop: token.margin }}
+                title={<CardStepTitle>结果管理</CardStepTitle>}
+            >
+                <Typography.Paragraph>
+                    <Typography.Title level={5}>初始成绩导入</Typography.Title>
+                    <Typography.Text>适用于前未上报学生和抽样的批量学生成绩导入。</Typography.Text>
+                </Typography.Paragraph>
+                <Card>
+                    <Space direction="vertical" size="large" style={{ width: '100%' }}>
+                        <Upload {...uploadProps}>
+                            <Button icon={<UploadOutlined />} disabled={uploading}>选择文件...</Button>
+                        </Upload>
+                        <Space>
+                            <Button
+                                type="primary"
+                                disabled={fileList.length === 0}
+                                loading={uploading}
+                                icon={<SendOutlined />}
+                                onClick={handleUploadImportWithoutStudentTotalScore}
+                            >{uploading ? '正在导入,请稍候' : '开始导入'}</Button>
+                            {uploadStatus && <Typography.Text type={uploadStatus.success ? 'success' : 'danger'}>{uploadStatus?.message}</Typography.Text>}
                         </Space>
-                    </Card>
-                </>
-            }
-        </ProCard>
-    );
+                    </Space>
+                </Card>
+            </ProCard>
+        );
+    }
+    return null;
+    // return (
+    //     <ProCard
+    //         style={{ marginTop: token.margin }}
+    //         title={<CardStepTitle>结果管理</CardStepTitle>}
+    //     >
+    //         <Typography.Paragraph>
+    //             <Typography.Title level={5}>成绩导入</Typography.Title>
+    //             <Typography.Text>适用于前未上报学生和抽样的批量学生成绩导入。</Typography.Text>
+    //         </Typography.Paragraph>
+    //         <Card>
+    //             <Space direction="vertical" size="large" style={{ width: '100%' }}>
+    //                 <Upload {...uploadProps}>
+    //                     <Button icon={<UploadOutlined />} disabled={uploading}>选择文件...</Button>
+    //                 </Upload>
+    //                 <Space>
+    //                     <Button
+    //                         type="primary"
+    //                         disabled={fileList.length === 0}
+    //                         loading={uploading}
+    //                         icon={<SendOutlined />}
+    //                         onClick={handleUploadImportWithoutStudentTotalScore}
+    //                     >{uploading ? '正在导入,请稍候' : '开始导入'}</Button>
+    //                     {uploadStatus && <Typography.Text type={uploadStatus.success ? 'success' : 'danger'}>{uploadStatus?.message}</Typography.Text>}
+    //                 </Space>
+    //             </Space>
+    //         </Card>
+    //     </ProCard>
+    // );
 }
 
 export default ExamResultList;

+ 74 - 40
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleEditModal.tsx

@@ -1,6 +1,8 @@
 import { MovableModalForm } from "@/components";
+import ExamPlanController from "@/services/apis/ExamPlanController";
 import ExamSampleController from "@/services/apis/ExamSampleController";
-import { ProFormCheckbox, ProFormDigit, ProFormItem, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
+import { ProFormCheckbox, ProFormDependency, ProFormDigit, ProFormItem, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
+import { useRequest } from "ahooks";
 import { App, Space, Typography } from "antd";
 import { useState } from "react";
 
@@ -22,11 +24,13 @@ const ExamSampleEditModal: React.FC<{
     const [isEnabledGradeNoSampleStudentMin, setIsEnabledGradeNoSampleStudentMin] = useState(data.config?.isEnabledGradeNoSampleStudentMin);
     const [isEnabledClassStudentMin, setIsEnabledClassStudentMin] = useState(data.config?.isEnabledClassStudentMin);
 
+    const { data: refPlanList } = useRequest(async () => { return await ExamPlanController.getSampleRefPlanList({ id: data.examPlanId }) });
+
     const { config, ...restData } = data;
     return (
         <MovableModalForm<API.ExamSampleOutput>
             title="监测抽样方案参数配置"
-            width={720}
+            width={800}
             open={open}
             modalProps={{
                 centered: true,
@@ -48,10 +52,14 @@ const ExamSampleEditModal: React.FC<{
                     let p: API.AddExamSampleInput = {
                         ...restValues,
                         examPlanId: data.examPlanId ?? 0,
-                        config: JSON.stringify({
+                        // config: JSON.stringify({
+                        //     ...data.config,
+                        //     ...config,
+                        // }),
+                        config: {
                             ...data.config,
                             ...config,
-                        }),
+                        },
                     };
                     if ((data.id ?? 0) > 0) {
                         const up = { id: data.id ?? 0, ...p } as API.UpdateExamSampleInput;
@@ -92,13 +100,57 @@ const ExamSampleEditModal: React.FC<{
                     />
                 </>
             }
+            <ProFormSelect
+                label={<strong>抽样参照成绩所在监测计划</strong>}
+                name="examScoreRefExamPlanId"
+                rules={[{ required: true, message: '请选择' }]}
+                fieldProps={{
+                    options: refPlanList?.map(t => ({ label: t.name, value: t.id })) ?? [],
+                }}
+            />
             <ProFormDigit
                 label={<strong>抽样比例</strong>}
                 name={['config', 'percent']}
                 fieldProps={{ addonAfter: '%' }}
-                // width={120}
                 rules={[{ required: true, message: '请输入抽样比例' }]}
             />
+            <ProFormRadio.Group
+                label={<strong>抽样方式</strong>}
+                name={['config', 'isRandomSampling']}
+                fieldProps={{
+                    options: [
+                        { label: '随机抽样', value: true },
+                        { label: '等距抽样', value: false }
+                    ],
+                }}
+                rules={[{ required: true, message: '请选择抽样方式' }]}
+            />
+            <ProFormItem label={<strong>位置间距</strong>} required>
+                <Space size="large">
+                    <div>
+                        <label>开始抽样位置:</label>
+                        <ProFormDigit
+                            name={['config', 'startPosition']}
+                            noStyle
+                            width={96}
+                            fieldProps={{ min: 1, max: 50 }}
+                            // help={false}
+                            rules={[{ required: true, message: '请输入开始抽样位置' }]}
+                        />
+                    </div>
+                    <div>
+                        <label>抽样间距:</label>
+                        <ProFormDigit
+                            name={['config', 'interval']}
+                            noStyle
+                            width={96}
+                            fieldProps={{ min: 1, max: 50 }}
+                            // help={false}
+                            rules={[{ required: true, message: '请输入抽样间距' }]}
+                        />
+                    </div>
+                </Space>
+            </ProFormItem>
             <ProFormItem label={<strong>全抽配置</strong>} tooltip="以下全抽规则序号越小优先级越高">
                 <Space direction="vertical">
                     <div>
@@ -166,46 +218,28 @@ const ExamSampleEditModal: React.FC<{
                     </div>
                 </Space>
             </ProFormItem>
-
-            <ProFormItem label={<strong>位置间距</strong>} required>
-                <Space size="large">
-                    <div>
-                        <label>开始抽样位置:</label>
-                        <ProFormDigit
-                            name={['config', 'startPosition']}
-                            noStyle
-                            width={96}
-                            fieldProps={{ min: 1, max: 50 }}
-                            // help={false}
-                            rules={[{ required: true, message: '请输入开始抽样位置' }]}
-                        />
-                    </div>
-                    <div>
-                        <label>抽样间距:</label>
-                        <ProFormDigit
-                            name={['config', 'interval']}
-                            noStyle
-                            width={96}
-                            fieldProps={{ min: 1, max: 50 }}
-                            // help={false}
-                            rules={[{ required: true, message: '请输入抽样间距' }]}
-                        />
-                    </div>
-                </Space>
-            </ProFormItem>
-
-            <ProFormItem label={<strong>其他设置</strong>}>
+            <ProFormItem label={<strong>特殊学生</strong>}>
                 <Space>
                     <ProFormCheckbox name={['config', 'isExcludeSpecialStudent']} noStyle>
-                        特殊学生不参与抽样
-                    </ProFormCheckbox>
-                    <ProFormCheckbox name={['config', 'isGradeSeatNumberRandom']} noStyle>
-                        监测顺序号在年级内随机打乱
-                        <Typography.Text type="secondary">(默认在班内按前期成绩排序)</Typography.Text>
+                        不参与抽样
                     </ProFormCheckbox>
+                    <ProFormDependency name={[['config', 'isExcludeSpecialStudent'], ['config', 'specialStudentMustApproved']]}>
+                        {({ config }) => (
+                            <ProFormCheckbox
+                                name={['config', 'specialStudentMustApproved']}
+                                noStyle
+                                disabled={!config?.isExcludeSpecialStudent}
+                            >必须审核通过{config?.specialStudentMustApproved ? '(勾选仅审核已通过不参与抽样)' : '(未勾选待审核和审核已通过均不参与抽样)'}</ProFormCheckbox>
+                        )}
+                    </ProFormDependency>
                 </Space>
             </ProFormItem>
-
+            <ProFormItem label={<strong>排序设置</strong>}>
+                <ProFormCheckbox name={['config', 'isGradeSeatNumberRandom']} noStyle>
+                    监测顺序号在年级内随机打乱
+                    <Typography.Text type="secondary">(默认在班内按前期成绩排序)</Typography.Text>
+                </ProFormCheckbox>
+            </ProFormItem>
             <ProFormTextArea
                 label={<strong>备注说明</strong>}
                 name="remark"

+ 25 - 2
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleList.tsx

@@ -5,7 +5,7 @@ import ExamSampleController from "@/services/apis/ExamSampleController";
 import { ExamSampleStatus } from "@/services/enums";
 import { ActionType, ProTable, TableDropdown } from "@ant-design/pro-components";
 import { history, useModel } from "@umijs/max";
-import { App, Button, Space, theme } from "antd";
+import { App, Button, Space, Tooltip, Typography, theme } from "antd";
 import { useCallback, useRef, useState } from "react";
 import ExamSampleEditModal from "./ExamSampleEditModal";
 
@@ -72,7 +72,7 @@ const ExamSampleList: React.FC<{
     // 生成
     const handleGenerate = useCallback((id: number) => {
         modal.confirm({
-            content: '确定立即生成抽样吗?',
+            content: '请仔细确认参数设置是否正确(如成绩引用、特殊学生排除、抽样比例和人数等),确定立即生成抽样吗?',
             okText: '确定',
             cancelText: '取消',
             centered: true,
@@ -157,6 +157,28 @@ const ExamSampleList: React.FC<{
                             return (<a onClick={() => history.push(`/exam-c/plan/sample/detail/${r.id}`)}>{v}</a>);
                         },
                     },
+                    {
+                        title: '成绩引用',
+                        dataIndex: 'examScoreRefExamPlanId',
+                        width: 96,
+                        align: 'center',
+                        render: (_, r) => {
+                            return (
+                                <Tooltip title={r.examScoreRefExamPlan?.name ?? '未设置'}>
+                                    <StatusIcon filled status={(r.examScoreRefExamPlanId ?? 0) > 0 ? 'success' : 'error'} />
+                                </Tooltip>
+                            );
+                        },
+                    },
+                    {
+                        title: '抽样方式',
+                        dataIndex: ['config', 'isRandomSampling'],
+                        width: 80,
+                        align: 'center',
+                        render: (_, r) => {
+                            return r.config.isRandomSampling ? '随机抽样' : '等距抽样';
+                        },
+                    },
                     {
                         title: '抽样比例',
                         dataIndex: ['config', 'percent'],
@@ -336,6 +358,7 @@ const ExamSampleList: React.FC<{
                 ]}
                 rowKey="id"
                 toolbar={{
+                    subTitle: (<Typography.Text type="warning">发布前重点核对<Typography.Text type="danger" strong>成绩引用</Typography.Text>、人数和特殊学生!</Typography.Text>),
                     actions: [
                         <Button
                             key="setting"

+ 14 - 2
YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleDetail/index.tsx

@@ -6,7 +6,7 @@ import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions } from
 import { useEmotionCss } from "@ant-design/use-emotion-css";
 import { history, useModel, useParams } from "@umijs/max";
 import { useRequest } from "ahooks";
-import { App, FloatButton, Tag, theme } from "antd";
+import { App, FloatButton, Tag, theme, Typography } from "antd";
 import lodash from 'lodash';
 import { useCallback, useRef } from "react";
 
@@ -136,7 +136,12 @@ const ExamSampleDetail: React.FC = () => {
                     return null;
                 }
                 if (data?.examSample?.isFixedExamSample) {
-                    return v;
+                    return (
+                        <div className={`${classStuCountClassName}${c?.isSampleAll ? ' selected' : ''}`}>
+                            {v}
+                            {c?.isSampleAll && <CheckCircleFilled className="check-icon" />}
+                        </div>
+                    );
                 }
                 return (
                     <div
@@ -261,6 +266,7 @@ const ExamSampleDetail: React.FC = () => {
                         tooltip="抽样时排除特殊学生"
                     >
                         <EnabledStatus enabled={data?.examSample?.config?.isExcludeSpecialStudent ?? false} />
+                        {data?.examSample?.config?.specialStudentMustApproved ? '(仅审核已通过不参与抽样)' : '(待审核和审核已通过均不参与抽样)'}
                     </ProDescriptions.Item>
                     <ProDescriptions.Item
                         label="随机序号"
@@ -268,6 +274,9 @@ const ExamSampleDetail: React.FC = () => {
                     >
                         <EnabledStatus enabled={data?.examSample?.config?.isGradeSeatNumberRandom ?? false} />
                     </ProDescriptions.Item>
+                    <ProDescriptions.Item label="抽样方式">
+                        {data?.examSample?.config.isRandomSampling ? '随机抽样' : '等距抽样'}
+                    </ProDescriptions.Item>
                     <ProDescriptions.Item label="开始位置">
                         {data?.examSample?.config.startPosition}
                     </ProDescriptions.Item>
@@ -280,6 +289,9 @@ const ExamSampleDetail: React.FC = () => {
                     <ProDescriptions.Item label="方案选定">
                         <EnabledStatus enabled={data?.examSample?.isSelected ?? false} enabledText="已选定" disabledText="未选定" />
                     </ProDescriptions.Item>
+                    <ProDescriptions.Item label="成绩引用">
+                        {data?.examSample?.examScoreRefExamPlan?.name ?? <Typography.Text type="danger">未设置</Typography.Text>}
+                    </ProDescriptions.Item>
                 </ProDescriptions>
             </ProCard>
             <SuperTable<ClassItem>

+ 3 - 2
YBEE.EQM.Admin/src/pages/exam-center/special-student/special-student-audit/index.tsx

@@ -1,6 +1,7 @@
 import { toSelectOptions } from '@/common/converter';
 import { SuperTable } from '@/components';
 import ExamSpecialStudentAuditController from '@/services/apis/ExamSpecialStudentAuditController';
+import { ExamStatus } from '@/services/enums';
 import { RightOutlined } from '@ant-design/icons';
 import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
 import { history, useModel } from '@umijs/max';
@@ -110,12 +111,12 @@ const ExamSpecialStudentAuditPlanList: React.FC = () => {
             title: '操作',
             valueType: 'option',
             width: 80,
-            align: 'center',
+            align: 'right',
             fixed: 'right',
             render: (_, r) => {
                 return (
                     <a onClick={() => history.push(`/exam-c/sp-stu-audit/list/${r.id}`)}>
-                        去处理
+                        {r.status === ExamStatus.ACTIVE ? '去处理' : '去查看'}
                         <RightOutlined style={{ marginLeft: token.marginXXS }} />
                     </a>
                 );

+ 4 - 2
YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentImport/index.tsx

@@ -153,6 +153,7 @@ const OrgExamSpecialStudentImport: React.FC = () => {
         try {
             const importFormValues = await formRef.current?.validateFields();
             if (examBranchPlan?.hasDistrict && !importFormValues) {
+                message.error('校区未选择');
                 return;
             }
 
@@ -187,8 +188,9 @@ const OrgExamSpecialStudentImport: React.FC = () => {
             })
             handleBack();
         }
-        catch {
-            message.error('校区未选择');
+        catch (ex) {
+            console.error(ex);
+            // message.error('校区未选择');
             window.scrollTo({ top: 0, behavior: 'smooth' });
         }
     }

+ 26 - 2
YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentReport/index.tsx

@@ -267,8 +267,31 @@ const OrgExamSpecialStudentReport: React.FC = () => {
             align: 'center',
             valueEnum: getDictValueEnum('audit_status', true),
             render: (_, r) => {
+                // const s = auditStatusDict[r.status];
+                // return <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
+
                 const s = auditStatusDict[r.status];
-                return <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
+                const ta = <Tag color={s.antStatus} style={{ marginRight: 0 }}>{s.name}</Tag>;
+                if (r.isPreIdentified) {
+                    return (
+                        <Space direction="vertical" size="small" style={{ lineHeight: 1 }}>
+                            {ta}
+                            <Typography.Text type="warning" style={{ fontSize: token.fontSizeSM }}>往期已认定</Typography.Text>
+                        </Space>
+                    );
+                }
+                if (r.preTotalScore !== null) {
+                    return (
+                        <Space direction="vertical" size="small" style={{ lineHeight: 1 }}>
+                            {ta}
+                            <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>
+                                前期{r.preTotalCourse}科得分
+                                <Typography.Text type="warning" strong style={{ fontSize: token.fontSizeSM }}>{r.preTotalScore}</Typography.Text>
+                            </Typography.Text>
+                        </Space>
+                    );
+                }
+                return ta;
             },
         },
         // {
@@ -590,7 +613,8 @@ const OrgExamSpecialStudentReport: React.FC = () => {
                     description={
                         <Typography>
                             <ol>
-                                <li>在下方 <Typography.Text strong>特殊学生明细</Typography.Text> 中录入特殊学生信息,<Typography.Text type="danger">并上传佐证材料(必传)</Typography.Text>;</li>
+                                <li><Typography.Text type="danger" strong>只需要上报新增特殊学生,往期已认定的特殊学生不能重复上报;</Typography.Text></li>
+                                <li>在下方 <Typography.Text strong>特殊学生明细</Typography.Text> 中录入特殊学生信息,并上传佐证材料<Typography.Text type="danger">(必传)</Typography.Text>;</li>
                                 <li>特殊学生信息录入完成后,点击 <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>

+ 1 - 1
YBEE.EQM.Admin/src/services/apis/ExamAbsentReplaceController.ts

@@ -114,7 +114,7 @@ export async function exportPrintTable(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 分页查询监测缺测替补列表 POST /api/exam/absent/replace/query-page-list */

+ 1 - 1
YBEE.EQM.Admin/src/services/apis/ExamOrgResultController.ts

@@ -44,7 +44,7 @@ export async function download(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 删除文件 POST /api/exam/org/result/del */

+ 33 - 1
YBEE.EQM.Admin/src/services/apis/ExamPaperController.ts

@@ -7,8 +7,9 @@
 // 试卷管理服务
 // --------------------------------------------------------------------------
 
-import { request } from '@umijs/max';
+import { request, RequestOptions } from '@umijs/max';
 import { ExamPaperWriterType } from '../enums';
+import contentDisposition from 'content-disposition';
 
 /** 按监测计划初始化试卷 POST /api/exam/paper/batch-init */
 export async function batchInit(
@@ -85,6 +86,35 @@ export async function queryExamPlanPageList(
     return res?.data;
 }
 
+/** 导出TQES导入文件格式文件包 POST /api/exam/paper/export-tqes-file */
+export async function exportTqesFile(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/paper/export-tqes-file';
+    const config = {
+        method: 'POST',
+        params,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
 /** 分页查询编撰人监测计划列表 POST /api/exam/paper/query-writer-exam-plan-page-list */
 export async function queryWriterExamPlanPageList(
     data: API.ExamPaperExamPlanPageInput,
@@ -155,6 +185,8 @@ export default {
     getListByExamPlanId,
     /** 获取双向细目表监测计划列表(管理端) POST /api/exam/paper/query-exam-plan-page-list */
     queryExamPlanPageList,
+    /** 导出TQES导入文件格式文件包 POST /api/exam/paper/export-tqes-file */
+    exportTqesFile,
     /** 分页查询编撰人监测计划列表 POST /api/exam/paper/query-writer-exam-plan-page-list */
     queryWriterExamPlanPageList,
     /** 根据监测计划ID获取待处理试卷列表 GET /api/exam/paper/get-writer-list-by-exam-plan-id */

+ 1 - 1
YBEE.EQM.Admin/src/services/apis/ExamPatriarchQuestionnaireProgressController.ts

@@ -46,7 +46,7 @@ export async function exportUncompletedExcel(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 获取各班级问卷填答进度 GET /api/exam/patriarch/questionnaire/progress/get-progress-list */

+ 16 - 0
YBEE.EQM.Admin/src/services/apis/ExamPlanController.ts

@@ -90,6 +90,20 @@ export async function queryStatusCount(
     return res?.data;
 }
 
+/** 获取最近5个抽测参照成绩监测计划 GET /api/exam/plan/get-sample-ref-plan-list */
+export async function getSampleRefPlanList(
+    params: {
+        /**  */
+        id?: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/plan/get-sample-ref-plan-list';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<API.ExamPlanOutput[]>>(url, config);
+    return res?.data;
+}
+
 export default {
     /** 添加监测计划 POST /api/exam/plan/add */
     add,
@@ -109,4 +123,6 @@ export default {
     queryPageList,
     /** 获取我的单据状态数量 POST /api/exam/plan/query-status-count */
     queryStatusCount,
+    /** 获取最近5个抽测参照成绩监测计划 GET /api/exam/plan/get-sample-ref-plan-list */
+    getSampleRefPlanList,
 };

+ 37 - 6
YBEE.EQM.Admin/src/services/apis/ExamReportingAvgRangeController.ts

@@ -10,15 +10,15 @@
 import { request, RequestOptions } from '@umijs/max';
 import contentDisposition from 'content-disposition';
 
-/** 导出表格 POST /api/exam/reporting/avg/range/export */
-export async function exportAction(
+/** 导出全区表格 POST /api/exam/reporting/avg/range/export-total */
+export async function exportTotal(
     params: {
         /** 监测计划ID */
         examplanid: number;
     },
     options?: { [key: string]: any },
 ) {
-    const url = '/api/exam/reporting/avg/range/export';
+    const url = '/api/exam/reporting/avg/range/export-total';
     const config = {
         method: 'POST',
         params,
@@ -36,10 +36,41 @@ export async function exportAction(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
+/** 导出各校分数段统计表 POST /api/exam/reporting/avg/range/export-org */
+export async function exportOrg(
+    params: {
+        /** 监测计划ID */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/reporting/avg/range/export-org';
+    const config = {
+        method: 'POST',
+        params,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 export default {
-    /** 导出表格 POST /api/exam/reporting/avg/range/export */
-    exportAction,
+    /** 导出全区表格 POST /api/exam/reporting/avg/range/export-total */
+    exportTotal,
+    /** 导出各校分数段统计表 POST /api/exam/reporting/avg/range/export-org */
+    exportOrg,
 };

+ 45 - 0
YBEE.EQM.Admin/src/services/apis/ExamReportingTqesController.ts

@@ -0,0 +1,45 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// TQES报表服务
+// --------------------------------------------------------------------------
+
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
+
+/** 根据监测计划ID导出全区优势薄弱 POST /api/exam/reporting/tqes/export-total-adv-weak */
+export async function exportTotalAdvWeak(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/reporting/tqes/export-total-adv-weak';
+    const config = {
+        method: 'POST',
+        params,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
+export default {
+    /** 根据监测计划ID导出全区优势薄弱 POST /api/exam/reporting/tqes/export-total-adv-weak */
+    exportTotalAdvWeak,
+};

+ 5 - 5
YBEE.EQM.Admin/src/services/apis/ExamSampleController.ts

@@ -101,7 +101,7 @@ export async function exportToArchived(data: API.BaseId, options?: { [key: strin
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出给印刷厂和网阅机构文件 POST /api/exam/sample/export-to-printshop */
@@ -124,7 +124,7 @@ export async function exportToPrintshop(data: API.BaseId, options?: { [key: stri
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出给学校 POST /api/exam/sample/export-to-org */
@@ -147,7 +147,7 @@ export async function exportToOrg(data: API.BaseId, options?: { [key: string]: a
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出抽样统计表 POST /api/exam/sample/export-sample-count */
@@ -170,7 +170,7 @@ export async function exportSampleCount(data: API.BaseId, options?: { [key: stri
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出学校抽样统计表 POST /api/exam/sample/export-sample-count-to-org */
@@ -193,7 +193,7 @@ export async function exportSampleCountToOrg(data: API.BaseId, options?: { [key:
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 根据ID获取抽样方案 GET /api/exam/sample/get-by-id */

+ 42 - 0
YBEE.EQM.Admin/src/services/apis/ExamScoreExportController.ts

@@ -0,0 +1,42 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// 导出成绩服务
+// --------------------------------------------------------------------------
+
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
+
+/** 导出TQES输入文件 POST /api/exam-score-export/export-tqes-file */
+export async function exportTqesFile(
+    data: API.ExamScoreExportTqesFileInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam-score-export/export-tqes-file';
+    const config = {
+        method: 'POST',
+        data,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
+export default {
+    /** 导出TQES输入文件 POST /api/exam-score-export/export-tqes-file */
+    exportTqesFile,
+};

+ 30 - 1
YBEE.EQM.Admin/src/services/apis/ExamScoreImportController.ts

@@ -7,7 +7,8 @@
 // 学生成绩导入服务
 // --------------------------------------------------------------------------
 
-import { request } from '@umijs/max';
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
 
 /** 上传文件并完成批量导入前期未上报学生名单的各科成绩 POST /api/exam/score/import/upload-import-without-student-total-score */
 export async function uploadImportWithoutStudentTotalScore(
@@ -31,9 +32,37 @@ export async function uploadImportStudentTotalScore(
     return res?.data;
 }
 
+/** 导入区校合并小题成绩 POST /api/exam/score/import/upload-import-student-minor-score */
+export async function uploadImportStudentMinorScore(
+    data: FormData,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/score/import/upload-import-student-minor-score';
+    const config = {
+        method: 'POST',
+        data,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
 export default {
     /** 上传文件并完成批量导入前期未上报学生名单的各科成绩 POST /api/exam/score/import/upload-import-without-student-total-score */
     uploadImportWithoutStudentTotalScore,
     /** 批量导入学生总成绩 POST /api/exam/score/import/upload-import-student-total-score */
     uploadImportStudentTotalScore,
+    /** 导入区校合并小题成绩 POST /api/exam/score/import/upload-import-student-minor-score */
+    uploadImportStudentMinorScore,
 };

+ 1 - 1
YBEE.EQM.Admin/src/services/apis/ExamSpecialStudentController.ts

@@ -114,7 +114,7 @@ export async function exportPrintTable(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 分页查询监测特殊学生列表 POST /api/exam/special/student/query-page-list */

+ 33 - 1
YBEE.EQM.Admin/src/services/apis/ExamTeacherCourseController.ts

@@ -7,7 +7,8 @@
 // 监测教师任教科目管理服务
 // --------------------------------------------------------------------------
 
-import { request } from '@umijs/max';
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
 
 /** 上传批量导入文件 POST /api/exam/teacher/course/upload */
 export async function upload(data: FormData, options?: { [key: string]: any }) {
@@ -30,6 +31,35 @@ export async function importAction(
     return res?.data;
 }
 
+/** 导出TQES导入文件格式文件包 POST /api/exam/teacher/course/export-tqes-file */
+export async function exportTqesFile(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/teacher/course/export-tqes-file';
+    const config = {
+        method: 'POST',
+        params,
+        ...(options || {}),
+        responseType: 'blob',
+        getResponse: true,
+    } as RequestOptions;
+    const res = await request(url, config);
+    const hcd = res.request.getResponseHeader('Content-Disposition');
+    if (!hcd) {
+        return null;
+    }
+    let fileName = '';
+    const cd = contentDisposition.parse(hcd);
+    if (cd?.parameters?.filename) {
+        fileName = cd?.parameters?.filename;
+    }
+    return { fileName, data: res.data as Blob } as API.FileResponse;
+}
+
 /** 添加监测教师 POST /api/exam/teacher/course/add */
 export async function add(data: API.AddExamTeacherCourseInput, options?: { [key: string]: any }) {
     const url = '/api/exam/teacher/course/add';
@@ -87,6 +117,8 @@ export default {
     upload,
     /** 批量导入监测教师 POST /api/exam/teacher/course/import */
     importAction,
+    /** 导出TQES导入文件格式文件包 POST /api/exam/teacher/course/export-tqes-file */
+    exportTqesFile,
     /** 添加监测教师 POST /api/exam/teacher/course/add */
     add,
     /** 更新监测教师 POST /api/exam/teacher/course/update */

+ 1 - 1
YBEE.EQM.Admin/src/services/apis/FileController.ts

@@ -44,7 +44,7 @@ export async function download(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 通过URL GET /api/file/view */

+ 5 - 8
YBEE.EQM.Admin/src/services/apis/NceeExportController.ts

@@ -36,21 +36,18 @@ export async function exportAllianceDistrict(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出已选科的模拟划线报表 POST /api/ncee/export/export-direction-seleted */
 export async function exportDirectionSeleted(
-    params: {
-        /**  */
-        nceeplanid: number;
-    },
+    data: API.NceeExportInput,
     options?: { [key: string]: any },
 ) {
     const url = '/api/ncee/export/export-direction-seleted';
     const config = {
         method: 'POST',
-        params,
+        data,
         ...(options || {}),
         responseType: 'blob',
         getResponse: true,
@@ -65,7 +62,7 @@ export async function exportDirectionSeleted(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 /** 导出未选科的模拟划线报表 POST /api/ncee/export/export-direction-unseleted */
@@ -94,7 +91,7 @@ export async function exportDirectionUnseleted(
     if (cd?.parameters?.filename) {
         fileName = cd?.parameters?.filename;
     }
-    return { fileName, data: res.data };
+    return { fileName, data: res.data as Blob } as API.FileResponse;
 }
 
 export default {

+ 2 - 2
YBEE.EQM.Admin/src/services/apis/NceeScoreController.ts

@@ -9,7 +9,7 @@
 
 import { request } from '@umijs/max';
 
-/** 上传成绩(仅原始分,适用于五区联考) POST /api/ncee/score/upload-only-raw-score */
+/** 上传成绩(仅原始分,适用于五区联考或本区独立赋分) POST /api/ncee/score/upload-only-raw-score */
 export async function uploadOnlyRawScore(data: FormData, options?: { [key: string]: any }) {
     const url = '/api/ncee/score/upload-only-raw-score';
     const config = { method: 'POST', data, ...(options || {}) };
@@ -48,7 +48,7 @@ export async function execute(
 }
 
 export default {
-    /** 上传成绩(仅原始分,适用于五区联考) POST /api/ncee/score/upload-only-raw-score */
+    /** 上传成绩(仅原始分,适用于五区联考或本区独立赋分) POST /api/ncee/score/upload-only-raw-score */
     uploadOnlyRawScore,
     /** 上传成绩(带转换分和等级,适用于六校联考) POST /api/ncee/score/upload-with-convert-score */
     uploadWithConvertScore,

+ 44 - 0
YBEE.EQM.Admin/src/services/apis/SysAuthController.ts

@@ -52,6 +52,44 @@ export async function verifyCaptcha(
     return res?.data;
 }
 
+/** 获取临时密码 POST /api/sys/auth/get-temp-password */
+export async function getTempPassword(data: API.string[], options?: { [key: string]: any }) {
+    const url = '/api/sys/auth/get-temp-password';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<string[]>>(url, config);
+    return res?.data;
+}
+
+/** getPbkdf2 GET /api/sys/auth/get-pbkdf2 */
+export async function getPbkdf2(
+    params: {
+        /** pwd */
+        pwd?: string;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/sys/auth/get-pbkdf2';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<string>>(url, config);
+    return res?.data;
+}
+
+/** comparePbkdf2 POST /api/sys/auth/compare-pbkdf2 */
+export async function comparePbkdf2(
+    params: {
+        /** pwd */
+        pwd?: string;
+        /** pbpwd */
+        pbpwd?: string;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/sys/auth/compare-pbkdf2';
+    const config = { method: 'POST', params, ...(options || {}) };
+    const res = await request<API.ResponseType<boolean>>(url, config);
+    return res?.data;
+}
+
 export default {
     /** 账户密码登录 POST /api/sys/auth/login-by-account */
     loginByAccount,
@@ -63,4 +101,10 @@ export default {
     getCaptcha,
     /** 校验图形验证码 POST /api/sys/auth/verify-captcha */
     verifyCaptcha,
+    /** 获取临时密码 POST /api/sys/auth/get-temp-password */
+    getTempPassword,
+    /** getPbkdf2 GET /api/sys/auth/get-pbkdf2 */
+    getPbkdf2,
+    /** comparePbkdf2 POST /api/sys/auth/compare-pbkdf2 */
+    comparePbkdf2,
 };

+ 6 - 0
YBEE.EQM.Admin/src/services/apis/index.ts

@@ -24,9 +24,11 @@ import * as ExamPaperQuestionMinorController from './ExamPaperQuestionMinorContr
 import * as ExamPlanController from './ExamPlanController';
 import * as ExamPatriarchQuestionnaireProgressController from './ExamPatriarchQuestionnaireProgressController';
 import * as ExamReportingAvgRangeController from './ExamReportingAvgRangeController';
+import * as ExamReportingTqesController from './ExamReportingTqesController';
 import * as ExamResultController from './ExamResultController';
 import * as ExamSampleStudentController from './ExamSampleStudentController';
 import * as ExamSampleController from './ExamSampleController';
+import * as ExamScoreExportController from './ExamScoreExportController';
 import * as ExamScoreImportController from './ExamScoreImportController';
 import * as ExamSpecialStudentController from './ExamSpecialStudentController';
 import * as ExamSpecialStudentAuditController from './ExamSpecialStudentAuditController';
@@ -93,12 +95,16 @@ export default {
     ExamPatriarchQuestionnaireProgressController,
     /** 统计报表之分数段报表服务 */
     ExamReportingAvgRangeController,
+    /** TQES报表服务 */
+    ExamReportingTqesController,
     /** 反馈结果管理服务 */
     ExamResultController,
     /** 监测抽样学生管理服务 */
     ExamSampleStudentController,
     /** 监测抽样方案管理服务 */
     ExamSampleController,
+    /** 导出成绩服务 */
+    ExamScoreExportController,
     /** 学生成绩导入服务 */
     ExamScoreImportController,
     /** 监测特殊学生上报管理服务 */

+ 73 - 30
YBEE.EQM.Admin/src/services/typing.d.ts

@@ -97,6 +97,14 @@ declare global {
             items?: T[];
         };
 
+        /** 文件响应类型 */
+        type FileResponse = {
+            /** 文件名称 */
+            fileName: string;
+            /** 文件体 */
+            data: Blob;
+        };
+
         /** 添加科目输入参数 */
         type AddCourseInput = {
             /** 名称 */
@@ -290,18 +298,7 @@ declare global {
             examPlanId: number;
             /** 备注 */
             remark?: string;
-            /** 抽样配置
-{
-    percent: 40,
-    onlyOneClassStudentMin: 40,
-    gradeNoSampleStudentMin: 20,
-    classStudentMin: 25,
-    startPosition: 1,
-    interval: 2,
-    isExcludeSpecialStudent: true,
-    isGradeSeatNumberRandom: true,
-} */
-            config: string;
+            config: ExamSampleConfig;
         };
 
         /** 添加监测特殊学生上报输入参数 */
@@ -1507,11 +1504,15 @@ declare global {
             /** 建议 */
             suggestions?: string;
             twclStatus: AuditStatus;
+            /** 双向细目表编制是否已提交 */
+            twclSubmitted?: boolean;
             /** 双向细目表编制审核记录 */
             twclAuditList?: AuditItem[];
             /** 双向细目表编制人用户ID */
             twclSysUserId?: number;
             suggestionStatus: AuditStatus;
+            /** 问题建议是否已提交 */
+            suggestionSubmitted?: boolean;
             /** 问题建议撰写审核记录 */
             suggestionAuditList?: AuditItem[];
             /** 问题建议撰写人用户ID */
@@ -1558,11 +1559,15 @@ declare global {
             /** 建议 */
             suggestions?: string;
             twclStatus: AuditStatus;
+            /** 双向细目表编制是否已提交 */
+            twclSubmitted?: boolean;
             /** 双向细目表编制审核记录 */
             twclAuditList?: AuditItem[];
             /** 双向细目表编制人用户ID */
             twclSysUserId?: number;
             suggestionStatus: AuditStatus;
+            /** 问题建议是否已提交 */
+            suggestionSubmitted?: boolean;
             /** 问题建议撰写审核记录 */
             suggestionAuditList?: AuditItem[];
             /** 问题建议撰写人用户ID */
@@ -1917,16 +1922,18 @@ declare global {
             isExcludeSpecialStudent?: boolean;
             /** 是否年级内随机排序 */
             isGradeSeatNumberRandom?: boolean;
+            /** 排除特殊学生必须是审核通过的,否则只要提交上报的就算 */
+            specialStudentMustApproved?: boolean;
             /** 起始抽样位置 */
             startPosition?: number;
             /** 抽样间距,抽样的两个学生之间隔多少人 */
             interval?: number;
+            /** 是否随机抽样 */
+            isRandomSampling?: boolean;
             /** 全抽班级ID列表 */
             sampleAllSchoolClassIds?: string[];
             /** 排除不抽样学校ID列表 */
             excludeSysOrgIds?: number[];
-            /** 监测抽样引用监测计划ID */
-            examSampleRefExamPlanId?: number;
         };
 
         /** 抽样数量统计输出参数 */
@@ -1987,6 +1994,8 @@ declare global {
             remark?: string;
             status: ExamSampleStatus;
             config: ExamSampleConfig;
+            /** 成绩引用监测计划ID */
+            examScoreRefExamPlanId?: number;
             /** 是否已固定监测抽样方案 */
             isFixedExamSample: boolean;
             educationStage: EducationStage;
@@ -1997,6 +2006,7 @@ declare global {
             /** 选中使用操作用户ID */
             selectedSysUserId?: number;
             selectedSysUser?: SysUserLiteOutput;
+            examScoreRefExamPlan?: ExamPlanLiteOutput;
         };
 
         /** 带监测计划的抽样方案输出参数 */
@@ -2027,6 +2037,8 @@ declare global {
             remark?: string;
             status: ExamSampleStatus;
             config: ExamSampleConfig;
+            /** 成绩引用监测计划ID */
+            examScoreRefExamPlanId?: number;
             /** 是否已固定监测抽样方案 */
             isFixedExamSample: boolean;
             educationStage: EducationStage;
@@ -2037,6 +2049,7 @@ declare global {
             /** 选中使用操作用户ID */
             selectedSysUserId?: number;
             selectedSysUser?: SysUserLiteOutput;
+            examScoreRefExamPlan?: ExamPlanLiteOutput;
             examPlan?: ExamPlanOutput;
         };
 
@@ -2098,6 +2111,14 @@ declare global {
             examSampleType?: ExamSampleType;
         };
 
+        /** 导出TQES输入文件输入参数 */
+        type ExamScoreExportTqesFileInput = {
+            /** 监测计划ID */
+            examPlanId?: number;
+            /** 非特殊学生0分转为缺考 */
+            isZeroToAbsent?: boolean;
+        };
+
         /** 特殊学生审核输入参数 */
         type ExamSepcialStudentAuditInput = {
             /** 监测特殊学生ID列表 */
@@ -2533,6 +2554,7 @@ declare global {
             /** 备注 */
             remark?: string;
             schoolClass?: SchoolClassLiteOutput;
+            sysOrg?: SysOrgLiteOutput;
             sysOrgBranch?: SysOrgLiteOutput;
             examGrade?: ExamGradeOutput;
             course?: CourseOutput;
@@ -2672,6 +2694,8 @@ declare global {
             gradeNumber2: number;
             /** 名称 */
             name: string;
+            /** 名称2 */
+            name2: string;
             /** 全称 */
             fullName: string;
             /** 简称 */
@@ -2692,6 +2716,8 @@ declare global {
             gradeNumber2: number;
             /** 名称 */
             name: string;
+            /** 名称2 */
+            name2: string;
             /** 全称 */
             fullName: string;
             /** 年份(级) */
@@ -3015,6 +3041,26 @@ declare global {
             thirdCourse?: CourseLiteOutput;
         };
 
+        /** 导出新高考报表输入参数 */
+        type NceeExportInput = {
+            /** 计划ID */
+            nceePlanId: number;
+            /** 导出整体分段统计 */
+            isExportScoreRange?: boolean;
+            /** 导出各机构分段统计 */
+            isExportOrgScoreRange?: boolean;
+            /** 导出整体划线统计 */
+            isExportLine?: boolean;
+            /** 导出各机构划线统计 */
+            isExportOrgLine?: boolean;
+            /** 导出整体班级划线统计 */
+            isExportClassLine?: boolean;
+            /** 导出整体转换分 */
+            isExportConvertScore?: boolean;
+            /** 导出各机构划线统计 */
+            isExportOrgConvertScore?: boolean;
+        };
+
         /** 高考模拟划线计划配置 */
         type NceePlanConfig = {
             /** 开启赋分 */
@@ -3025,6 +3071,10 @@ declare global {
             calcCourseLineScoreEnabled?: boolean;
             /** 开启选科组合统计 */
             courseCombStatEnabled?: boolean;
+            /** 开启赋分导出 */
+            exportConvertScoreEnabled?: boolean;
+            /** 开启排名导出 */
+            exportOrderEnabled?: boolean;
         };
 
         /** 高中划线分析计划输出参数 */
@@ -3678,6 +3728,10 @@ declare global {
             code: string;
             /** 带前缀唯一代码 */
             uniqueCode: string;
+            /** TQES学校ID(兼容老系统) */
+            tqesId?: number;
+            /** TQES学校编码(兼容老系统) */
+            tqesCode?: string;
             /** 排序 */
             sort: number;
             status: CommonStatus;
@@ -3707,15 +3761,15 @@ declare global {
             code: string;
             /** 带前缀唯一代码 */
             uniqueCode: string;
+            /** TQES学校ID(兼容老系统) */
+            tqesId?: number;
+            /** TQES学校编码(兼容老系统) */
+            tqesCode?: string;
             /** 排序 */
             sort: number;
             status: CommonStatus;
             /** 父机构ID路径 */
             pids: string;
-            /** TQES学校ID(兼容老系统) */
-            tqesId?: number;
-            /** TQES学校编码(兼容老系统) */
-            tqesCode?: string;
             /** 经度 */
             longitude?: number;
             /** 纬度 */
@@ -4167,18 +4221,7 @@ declare global {
             examPlanId: number;
             /** 备注 */
             remark?: string;
-            /** 抽样配置
-{
-    percent: 40,
-    onlyOneClassStudentMin: 40,
-    gradeNoSampleStudentMin: 20,
-    classStudentMin: 25,
-    startPosition: 1,
-    interval: 2,
-    isExcludeSpecialStudent: true,
-    isGradeSeatNumberRandom: true,
-} */
-            config: string;
+            config: ExamSampleConfig;
             /** 主键 */
             id: number;
             /** 名称 */

+ 10 - 0
YBEE.EQM.Application/Base/Grade/Dtos/GradeOutput.cs

@@ -33,6 +33,11 @@ public class GradeOutput
     [Required]
     public string Name { get; set; }
     /// <summary>
+    /// 名称2
+    /// </summary>
+    [Required]
+    public string Name2 { get; set; }
+    /// <summary>
     /// 全称
     /// </summary>
     [Required]
@@ -84,6 +89,11 @@ public class GradeYearOutput
     [Required]
     public string Name { get; set; }
     /// <summary>
+    /// 名称2
+    /// </summary>
+    [Required]
+    public string Name2 { get; set; }
+    /// <summary>
     /// 全称
     /// </summary>
     [Required]

+ 12 - 20
YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceAuditService.cs

@@ -6,16 +6,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 缺测替补审核服务
 /// </summary>
-public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITransient
+public class ExamAbsentReplaceAuditService(IRepository<ExamAbsentReplace> rep, ISysRoleService sysRoleService) : IExamAbsentReplaceAuditService, ITransient
 {
-    private readonly IRepository<ExamAbsentReplace> _rep;
-    private readonly ISysRoleService _sysRoleService;
-
-    public ExamAbsentReplaceAuditService(IRepository<ExamAbsentReplace> rep, ISysRoleService sysRoleService)
-    {
-        _rep = rep;
-        _sysRoleService = sysRoleService;
-    }
 
     /// <summary>
     /// 提交审核
@@ -24,8 +16,8 @@ public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITr
     /// <returns></returns>
     public async Task Submit(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
-        var dtype = await _rep.Change<ExamDataReport>().Where(t => t.ExamPlanId == item.ExamPlanId && t.Type == DataReportType.ABSENT_REPLACE).FirstOrDefaultAsync();
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var dtype = await rep.Change<ExamDataReport>().Where(t => t.ExamPlanId == item.ExamPlanId && t.Type == DataReportType.ABSENT_REPLACE).FirstOrDefaultAsync();
         if (dtype == null || dtype.Status != ExamStatus.ACTIVE || (item.Status != AuditStatus.UNSUBMIT && item.Status != AuditStatus.REJECTED))
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -43,13 +35,13 @@ public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITr
     public async Task Audit(ExamAbsentReplaceAuditInput input)
     {
         var dt = DateTime.Now;
-        var items = await _rep.Where(t => input.Ids.Contains(t.Id)).ToListAsync();
+        var items = await rep.Where(t => input.Ids.Contains(t.Id)).ToListAsync();
         foreach (var item in items)
         {
             item.Status = input.IsApproved ? AuditStatus.APPROVED : AuditStatus.REJECTED;
             item.Audits = AuditUtil.InsertInto(item.Audits, AuditUtil.CreateNew(input.IsApproved ? AuditActionType.APPROVE : AuditActionType.REJECT, item.Status, dt, input.Remark));
         }
-        await _rep.UpdateAsync(items);
+        await rep.UpdateAsync(items);
     }
 
     /// <summary>
@@ -59,7 +51,7 @@ public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITr
     /// <returns></returns>
     public async Task Reaudit(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         if (item.Status != AuditStatus.APPROVED)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -76,7 +68,7 @@ public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITr
     /// <returns></returns>
     public async Task<PageResult<ExamPlanAuditOutput>> QueryExamPlanPageList(ExamPlanPageInput input)
     {
-        var roleDataScope = await _sysRoleService.GetCurrentUserDataScope();
+        var roleDataScope = await sysRoleService.GetCurrentUserDataScope();
         if (roleDataScope == null || roleDataScope.EducationStages.Count == 0)
         {
             return new();
@@ -122,7 +114,7 @@ public class ExamAbsentReplaceAuditService : IExamAbsentReplaceAuditService, ITr
             input.Name,
         };
 
-        var totalCount = await _rep.SqlScalarAsync<int>($@"
+        var totalCount = await rep.SqlScalarAsync<int>($@"
 SELECT COUNT(1) AS total_count
 FROM
 (
@@ -141,7 +133,7 @@ FROM
 ", p);
 
 
-        var items = await _rep.SqlQueriesAsync<ExamPlanAuditOutput>($@"
+        var items = await rep.SqlQueriesAsync<ExamPlanAuditOutput>($@"
 SELECT
     T1.id, 
     T1.full_name, 
@@ -200,7 +192,7 @@ LIMIT @pageSize OFFSET @pageOffset;
     /// <returns></returns>
     public async Task<PageResult<ExamPlanOrgAuditOutput>> QueryOrgAuditPageList(ExamOrgDataReportAuditPageInput input)
     {
-        var roleDataScope = await _sysRoleService.GetCurrentUserDataScope();
+        var roleDataScope = await sysRoleService.GetCurrentUserDataScope();
         if (roleDataScope == null || roleDataScope.EducationStages.Count == 0)
         {
             return new();
@@ -261,7 +253,7 @@ LEFT JOIN exam_absent_replace AS T4 ON T1.id = T4.exam_plan_id AND T2.sys_org_id
 
         string groupSql = @"GROUP BY T2.exam_plan_id, T1.full_name, T1.`name`, T1.semester_id, T1.education_stage, T1.`status`, T2.sys_org_id, ORG.full_name, ORG.`name`, ORG.`code`, T3.`status`, T3.report_time";
 
-        var totalCount = await _rep.SqlScalarAsync<int>($@"
+        var totalCount = await rep.SqlScalarAsync<int>($@"
 SELECT COUNT(1) AS total_count
 FROM
 (
@@ -272,7 +264,7 @@ FROM
 ) AS T", p);
 
 
-        var items = await _rep.SqlQueriesAsync<ExamPlanOrgAuditOutput>($@"
+        var items = await rep.SqlQueriesAsync<ExamPlanOrgAuditOutput>($@"
 SELECT ROW_NUMBER() OVER (ORDER BY T.exam_plan_id DESC, T.audit_count DESC, T.report_time) AS `row_number`, T.*
 FROM
 (

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

@@ -160,7 +160,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 }
 
                 // 缺测学生姓名
-                item.AbsentName = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? "");
+                item.AbsentName = (row.GetCell(NAME_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.AbsentName == "" || item.AbsentName.Length > 100)
                 {
                     item.ErrorMessage.Add($"{headers[NAME_INDEX]}未填或超出100字");
@@ -168,7 +168,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 if (item.AbsentName.Length > 100) { item.AbsentName = item.AbsentName[..100]; }
 
                 // 缺测学生监测号
-                item.AbsentExamNumber = StringUtil.ClearWhite(row.GetCell(EXAM_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.AbsentExamNumber = (row.GetCell(EXAM_NUM_INDEX)?.ToString() ?? "").ClearWhitespace().ToUpper();
                 if (item.AbsentExamNumber == "" || item.AbsentExamNumber.Length > 20)
                 {
                     item.ErrorMessage.Add($"{headers[EXAM_NUM_INDEX]}格式错误");
@@ -176,7 +176,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 if (item.AbsentExamNumber.Length > 20) { item.AbsentExamNumber = item.AbsentExamNumber[..20]; }
 
                 // 缺测原因
-                item.AbsentReason = StringUtil.ClearWhite(row.GetCell(REASON_INDEX)?.ToString() ?? "");
+                item.AbsentReason = (row.GetCell(REASON_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.AbsentReason == "" || item.AbsentReason.Length > 200)
                 {
                     item.ErrorMessage.Add($"{headers[REASON_INDEX]}未填或超出200字");
@@ -184,7 +184,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 if (item.AbsentReason.Length > 200) { item.AbsentReason = item.AbsentReason[..200]; }
 
                 // 缺测科目
-                item.AbsentCourseText = StringUtil.ClearWhite(row.GetCell(COURSES_INDEX)?.ToString() ?? "");
+                item.AbsentCourseText = (row.GetCell(COURSES_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.AbsentCourseText == "")
                 {
                     item.ErrorMessage.Add($"{headers[COURSES_INDEX]}未填");
@@ -200,7 +200,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 }
 
                 // 缺测学生家长电话
-                item.PatriarchTel = StringUtil.ClearWhite(row.GetCell(TEL_INDEX)?.ToString() ?? "");
+                item.PatriarchTel = (row.GetCell(TEL_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.PatriarchTel == "" || item.PatriarchTel.Length > 50)
                 {
                     item.ErrorMessage.Add($"{headers[TEL_INDEX]}未填或超出50字");
@@ -209,10 +209,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
 
 
                 // 替补学生姓名
-                item.ReplaceName = StringUtil.ClearWhite(row.GetCell(REPLACE_NAME_INDEX)?.ToString() ?? "");
+                item.ReplaceName = (row.GetCell(REPLACE_NAME_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.ReplaceName.Length > 100) { item.ReplaceName = item.ReplaceName[..100]; }
                 // 替补学生监测号
-                item.ReplaceExamNumber = StringUtil.ClearWhite(row.GetCell(REPLACE_EXAM_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.ReplaceExamNumber = (row.GetCell(REPLACE_EXAM_NUM_INDEX)?.ToString() ?? "").ClearWhitespace().ToUpper();
                 if (item.ReplaceExamNumber.Length > 20) { item.ReplaceExamNumber = item.ReplaceExamNumber[..20]; }
                 if (item.ReplaceName.Length == 0 && item.ReplaceExamNumber.Length != 0 || item.ReplaceName.Length != 0 && item.ReplaceExamNumber.Length == 0)
                 {
@@ -221,7 +221,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
 
 
                 // 备注
-                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]; }
 
                 // 行是否验证通过
@@ -287,10 +287,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
                 item.SysOrgId = orgId;
                 item.SysOrgBranchId = input.SysOrgBranchId;
                 item.SchoolClassId = classDict[ni.ClassNumber];
-                item.AbsentName = StringUtil.ClearWhite(item.AbsentName);
-                item.AbsentExamNumber = StringUtil.ClearWhite(item.AbsentExamNumber);
-                item.ReplaceName = StringUtil.ClearWhite(item.ReplaceName);
-                item.ReplaceExamNumber = StringUtil.ClearWhite(item.ReplaceExamNumber);
+                item.AbsentName = item.AbsentName?.ClearWhitespace();
+                item.AbsentExamNumber = item.AbsentExamNumber?.ClearWhitespace();
+                item.ReplaceName = item.ReplaceName?.ClearWhitespace();
+                item.ReplaceExamNumber = item.ReplaceExamNumber?.ClearWhitespace();
                 items.Add(item);
                 c++;
             }
@@ -324,10 +324,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         var item = input.Adapt<ExamAbsentReplace>();
         item.SysOrgId = orgId;
         item.SchoolClassId = schoolClass.Id;
-        item.AbsentName = StringUtil.ClearWhite(item.AbsentName);
-        item.AbsentExamNumber = StringUtil.ClearWhite(item.AbsentExamNumber);
-        item.ReplaceName = StringUtil.ClearWhite(item.ReplaceName);
-        item.ReplaceExamNumber = StringUtil.ClearWhite(item.ReplaceExamNumber);
+        item.AbsentName = item.AbsentName?.ClearWhitespace();
+        item.AbsentExamNumber = item.AbsentExamNumber?.ClearWhitespace();
+        item.ReplaceName = item.ReplaceName?.ClearWhitespace();
+        item.ReplaceExamNumber = item.ReplaceExamNumber?.ClearWhitespace();
 
         await item.InsertAsync();
     }
@@ -345,10 +345,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
 
         var item = input.Adapt<ExamAbsentReplace>();
         item.SchoolClassId = schoolClass.Id;
-        item.AbsentName = StringUtil.ClearWhite(item.AbsentName);
-        item.AbsentExamNumber = StringUtil.ClearWhite(item.AbsentExamNumber);
-        item.ReplaceName = StringUtil.ClearWhite(item.ReplaceName);
-        item.ReplaceExamNumber = StringUtil.ClearWhite(item.ReplaceExamNumber);
+        item.AbsentName = item.AbsentName?.ClearWhitespace();
+        item.AbsentExamNumber = item.AbsentExamNumber?.ClearWhitespace();
+        item.ReplaceName = item.ReplaceName?.ClearWhitespace();
+        item.ReplaceExamNumber = item.ReplaceExamNumber?.ClearWhitespace();
 
         await item.UpdateIncludeAsync(new[] {
             nameof(item.SysOrgBranchId),
@@ -744,10 +744,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     private static (List<string> errorMessage, List<CourseMiniOutput> courses) GetCourses(Dictionary<string, CourseLiteOutput> courseDict, string courses)
     {
-        List<CourseMiniOutput> ret = new();
-        List<string> errs = new();
+        List<CourseMiniOutput> ret = [];
+        List<string> errs = [];
 
-        var cns = StringUtil.ClearWhite(courses).Split(new char[] { '、', ',', ',', ';', ';' });
+        var cns = courses.ClearWhitespace().Split(['、', ',', ',', ';', ';']);
         foreach (var cn in cns)
         {
             if (courseDict.TryGetValue(cn, out CourseLiteOutput c))

+ 3 - 13
YBEE.EQM.Application/Exam/ExamOrgScoreReport/ExamOrgScoreReportAppService.cs

@@ -1,20 +1,13 @@
-using YBEE.EQM.Core;
-
-namespace YBEE.EQM.Application;
+namespace YBEE.EQM.Application;
 
 /// <summary>
 /// 校考成绩上报管理服务
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-org-score-report")]
 [Route("exam/org/score/report")]
-public class ExamOrgScoreReportAppService : IDynamicApiController
+public class ExamOrgScoreReportAppService(IExamOrgScoreReportService examOrgScoreReportService) : IDynamicApiController
 {
-    private readonly IExamOrgScoreReportService _examOrgScoreReportService;
-
-    public ExamOrgScoreReportAppService(IExamOrgScoreReportService examOrgScoreReportService)
-    {
-        _examOrgScoreReportService = examOrgScoreReportService;
-    }
+    private readonly IExamOrgScoreReportService _examOrgScoreReportService = examOrgScoreReportService;
 
     /// <summary>
     /// 上传校考成绩
@@ -56,14 +49,11 @@ public class ExamOrgScoreReportAppService : IDynamicApiController
     //}
 
 
-
-
     /// <summary>
     /// 合并学校上报小题分
     /// </summary>
     /// <param name="examPlanId"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     public async Task<IActionResult> MergeMinorScore(int examPlanId)
     {
         var (fileName, fileBytes) = await _examOrgScoreReportService.MergeMinorScore(examPlanId);

+ 5 - 4
YBEE.EQM.Application/Exam/ExamOrgScoreReport/Services/ExamOrgScoreReportService.cs

@@ -294,7 +294,7 @@ ORDER BY T1.grade_id, T1.course_id
             _exportExcelService.AddCell("班级", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 6);
             _exportExcelService.AddCell("身份证号码", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 20);
             _exportExcelService.AddCell("姓名", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 12);
-            Dictionary<short, int> courseIndexes = new();
+            Dictionary<short, int> courseIndexes = [];
             foreach (var ac in allCourses)
             {
                 courseIndexes.Add(ac.Id, tci);
@@ -333,7 +333,7 @@ JOIN base_grade AS G ON T2.grade_id = G.id
 WHERE T1.exam_sample_id = @esid AND T2.exam_plan_id = @examPlanId AND T2.grade_id = @gradeId;
 ", new { ExamPlanId = examPlanId, fecg.GradeId });
                 // 总分列表
-                List<ExamOrgScoreReportCourseTotalDto> totalItems = new();
+                List<ExamOrgScoreReportCourseTotalDto> totalItems = [];
 
                 // 按科目合并和提取
                 var ecs = ecg.OrderBy(t => t.CourseId).ToList();
@@ -416,7 +416,8 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                                     totalItem.ExamNumber = en;
                                     if (totalItem.ExamNumber.Length != 11)
                                     {
-                                        throw new Exception();
+                                        //throw new Exception();
+                                        throw Oops.Oh(ErrorCode.E2013, $"{fp},考号长度错误,【{en}】");
                                     }
                                     //var orgCode = totalItem.ExamNumber[..3];
                                     //if(orgDict.TryGetValue(orgCode, out SysOrg value))
@@ -530,7 +531,7 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                     var stu = item.First();
                     if (stu.StudentName != stu.ExamStudentName)
                     {
-                        throw Oops.Oh(ErrorCode.E2013, $"{stu.ExamNumber}");
+                        throw Oops.Oh(ErrorCode.E2013, $"{stu.ToJson()}");
                     }
 
                     gci = 0;

+ 2 - 8
YBEE.EQM.Application/Exam/ExamPaper/ExamPaperAppService.cs

@@ -7,14 +7,9 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-paper")]
 [Route("exam/paper")]
-public class ExamPaperAppService : IDynamicApiController
+public class ExamPaperAppService(IExamPaperService examPaperService) : IDynamicApiController
 {
-    private readonly IExamPaperService _examPaperService;
-
-    public ExamPaperAppService(IExamPaperService examPaperService)
-    {
-        _examPaperService = examPaperService;
-    }
+    private readonly IExamPaperService _examPaperService = examPaperService;
 
     /// <summary>
     /// 按监测计划初始化试卷
@@ -77,7 +72,6 @@ public class ExamPaperAppService : IDynamicApiController
     /// <param name="examPlanId"></param>
     /// <returns></returns>
     /// <exception cref="Exception"></exception>
-    [AllowAnonymous]
     public async Task<IActionResult> ExportTqesFile([Required] int examPlanId)
     {
         var (fileName, fileBytes) = await _examPaperService.ExportTqesFile(examPlanId);

+ 19 - 15
YBEE.EQM.Application/Exam/ExamPlan/ExamPlanAppService.cs

@@ -7,13 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-plan")]
 [Route("exam/plan")]
-public class ExamPlanAppService : IDynamicApiController
+public class ExamPlanAppService(IExamPlanService examPlanService) : IDynamicApiController
 {
-    private readonly IExamPlanService _examPlanService;
-    public ExamPlanAppService(IExamPlanService examPlanService)
-    {
-        _examPlanService = examPlanService;
-    }
 
     /// <summary>
     /// 添加监测计划
@@ -22,7 +17,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Add(AddExamPlanInput input)
     {
-        await _examPlanService.Add(input);
+        await examPlanService.Add(input);
     }
     /// <summary>
     /// 更新监测计划
@@ -31,7 +26,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Update(UpdateExamPlanInput input)
     {
-        await _examPlanService.Update(input);
+        await examPlanService.Update(input);
     }
     /// <summary>
     /// 删除监测计划
@@ -40,7 +35,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        await _examPlanService.Del(input);
+        await examPlanService.Del(input);
     }
     /// <summary>
     /// 开始监测
@@ -49,7 +44,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Start(BaseId input)
     {
-        await _examPlanService.Start(input);
+        await examPlanService.Start(input);
     }
     /// <summary>
     /// 结束监测
@@ -58,7 +53,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Stop(BaseId input)
     {
-        await _examPlanService.Stop(input);
+        await examPlanService.Stop(input);
     }
     /// <summary>
     /// 取消监测
@@ -67,7 +62,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Cancel(BaseId input)
     {
-        await _examPlanService.Cancel(input);
+        await examPlanService.Cancel(input);
     }
     /// <summary>
     /// 根据ID获取监测计划
@@ -76,7 +71,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<ExamPlanOutput> GetById([FromQuery][Required] int id)
     {
-        return await _examPlanService.GetById(id);
+        return await examPlanService.GetById(id);
     }
     /// <summary>
     /// 分页查询监测计划列表
@@ -85,7 +80,7 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamPlanOutput>> QueryPageList(ExamPlanPageInput input)
     {
-        return await _examPlanService.QueryPageList(input);
+        return await examPlanService.QueryPageList(input);
     }
     /// <summary>
     /// 获取我的单据状态数量
@@ -93,6 +88,15 @@ public class ExamPlanAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<StatusCount>> QueryStatusCount(ExamPlanPageInput input)
     {
-        return await _examPlanService.QueryStatusCount(input);
+        return await examPlanService.QueryStatusCount(input);
+    }
+    /// <summary>
+    /// 获取最近5个抽测参照成绩监测计划
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    public async Task<List<ExamPlanOutput>> GetSampleRefPlanList(int id)
+    {
+        return await examPlanService.GetSampleRefPlanList(id);
     }
 }

+ 26 - 19
YBEE.EQM.Application/Exam/ExamPlan/Services/ExamPlanService.cs

@@ -6,16 +6,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 监测计划管理服务
 /// </summary>
-public class ExamPlanService : IExamPlanService, ITransient
+public class ExamPlanService(IRepository<ExamPlan> rep, IEducationStageYearsService educationStageYearsService) : IExamPlanService, ITransient
 {
-    private readonly IRepository<ExamPlan> _rep;
-    private readonly IEducationStageYearsService _educationStageYearsService;
-
-    public ExamPlanService(IRepository<ExamPlan> rep, IEducationStageYearsService educationStageYearsService)
-    {
-        _rep = rep;
-        _educationStageYearsService = educationStageYearsService;
-    }
 
     #region 创建更新
     /// <summary>
@@ -25,9 +17,9 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Add(AddExamPlanInput input)
     {
-        var ey = await _educationStageYearsService.GetByEducationStage(input.EducationStage);
+        var ey = await educationStageYearsService.GetByEducationStage(input.EducationStage);
         //var maxExam = await _rep.Where(t => t.EducationStage == input.EducationStage && t.ExamPeriodType == input.ExamPeriodType && t.ExamType == input.ExamType && t.SemesterId == input.SemesterId).OrderByDescending(t => t.Sequence).FirstOrDefaultAsync();
-        var maxExam = await _rep.Where(t => t.EducationStage == input.EducationStage && t.SemesterId == input.SemesterId).OrderByDescending(t => t.Sequence).FirstOrDefaultAsync();
+        var maxExam = await rep.Where(t => t.EducationStage == input.EducationStage && t.SemesterId == input.SemesterId).OrderByDescending(t => t.Sequence).FirstOrDefaultAsync();
         short seq = maxExam?.Sequence ?? 0;
         var item = input.Adapt<ExamPlan>();
         item.Sequence = (short)(seq + 1);
@@ -41,7 +33,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Update(UpdateExamPlanInput input)
     {
-        if (!await _rep.AnyAsync(t => t.Id == input.Id))
+        if (!await rep.AnyAsync(t => t.Id == input.Id))
         {
             throw Oops.Oh(ErrorCode.E2001);
         }
@@ -55,7 +47,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         if (item.Status == ExamStatus.ACTIVE || item.Status == ExamStatus.STOPPED)
         {
             throw Oops.Oh(ErrorCode.E3001, "已使用监测计划");
@@ -72,7 +64,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Start(BaseId input)
     {
-        var item = await _rep.Include(t => t.ExamOrgs).FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.Include(t => t.ExamOrgs).FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         if (item.Status != ExamStatus.READY)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -92,7 +84,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Stop(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         if (item.Status != ExamStatus.ACTIVE)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -108,7 +100,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task Cancel(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         if (item.Status == ExamStatus.ACTIVE)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -127,7 +119,7 @@ public class ExamPlanService : IExamPlanService, ITransient
     /// <returns></returns>
     public async Task<ExamPlanOutput> GetById(int id)
     {
-        var item = await _rep.DetachedEntities.ProjectToType<ExamPlanOutput>()
+        var item = await rep.DetachedEntities.ProjectToType<ExamPlanOutput>()
                                               .FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
         return item;
         //return item.Adapt<ExamPlanOutput>();
@@ -152,12 +144,27 @@ public class ExamPlanService : IExamPlanService, ITransient
         var query = GetQueryBase(input);
         if (query == null)
         {
-            return new List<StatusCount>();
+            return [];
         }
 
         var counts = await query.GroupBy(t => t.Status).Select(t => new StatusCount { Status = (int)t.Key, Count = t.Count() }).ToListAsync();
         return counts;
     }
+    /// <summary>
+    /// 获取最近5个抽测参照成绩监测计划
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    public async Task<List<ExamPlanOutput>> GetSampleRefPlanList(int id)
+    {
+        var examPlan = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var items = await rep.DetachedEntities.Where(t => t.Id < id && t.EducationStage == examPlan.EducationStage)
+                                              .OrderByDescending(t => t.SemesterId).ThenByDescending(t => t.Id)
+                                              .Take(5)
+                                              .ProjectToType<ExamPlanOutput>()
+                                              .ToListAsync();
+        return items;
+    }
     #endregion
 
     #region 私有方法
@@ -171,7 +178,7 @@ public class ExamPlanService : IExamPlanService, ITransient
         var name = !string.IsNullOrEmpty(input.Name?.Trim());
         var searchBeginTime = !string.IsNullOrEmpty(input.SearchBeginTime?.Trim());
         var searchEndTime = !string.IsNullOrEmpty(input.SearchEndTime?.Trim());
-        var query = _rep.DetachedEntities.Where((name, u => EF.Functions.Like(u.Name, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.FullName, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ShortName, $"%{input.Name.Trim()}%")))
+        var query = rep.DetachedEntities.Where((name, u => EF.Functions.Like(u.Name, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.FullName, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ShortName, $"%{input.Name.Trim()}%")))
                                          .Where(input.EducationStage.HasValue, t => t.EducationStage == input.EducationStage)
                                          //.Where(input.ExamPeriodType.HasValue, t => t.ExamPeriodType == input.ExamPeriodType)
                                          //.Where(input.ExamType.HasValue, t => t.ExamType == input.ExamType)

+ 6 - 0
YBEE.EQM.Application/Exam/ExamPlan/Services/IExamPlanService.cs

@@ -51,6 +51,12 @@ public interface IExamPlanService
     /// <returns></returns>
     Task<ExamPlanOutput> GetById(int id);
     /// <summary>
+    /// 获取最近5个抽测参照成绩监测计划
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    Task<List<ExamPlanOutput>> GetSampleRefPlanList(int id);
+    /// <summary>
     /// 分页查询监测计划列表
     /// </summary>
     /// <param name="input"></param>

+ 10 - 12
YBEE.EQM.Application/Exam/ExamQuestionnaire/Services/ExamPatriarchQuestionnaireProgressSync.cs

@@ -1,6 +1,5 @@
 
 using Furion.ClayObject;
-using Furion.JsonSerialization;
 using Furion.RemoteRequest.Extensions;
 using YBEE.EQM.Core;
 
@@ -9,17 +8,16 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 家长问卷填答进度管理服务
 /// </summary>
-public class ExamPatriarchQuestionnaireProgressSync : IExamPatriarchQuestionnaireProgressSync, IScoped
+public class ExamPatriarchQuestionnaireProgressSync(IRepository<ExamPatriarchQuestionnaireProgress> rep) : IExamPatriarchQuestionnaireProgressSync, IScoped
 {
-    private readonly IRepository<ExamPatriarchQuestionnaireProgress> _rep;
-
-    public ExamPatriarchQuestionnaireProgressSync(IRepository<ExamPatriarchQuestionnaireProgress> rep)
-    {
-        _rep = rep;
-    }
-
     public async Task Sync()
     {
+        // 目前已废弃
+        if (DateTime.Now < new DateTime(2023, 1, 1))
+        {
+            return;
+        }
+
         Log.Information("开始同步学生家长问卷进度");
 
         Dictionary<string, string> qs = new()
@@ -30,7 +28,7 @@ public class ExamPatriarchQuestionnaireProgressSync : IExamPatriarchQuestionnair
 
         foreach (var kvp in qs)
         {
-            var maxSubmitTime = await _rep.DetachedEntities.Where(t => t.QuestionnaireCode.ToLower() == kvp.Key.ToLower()).MaxAsync(t => t.SubmitTime);
+            var maxSubmitTime = await rep.DetachedEntities.Where(t => t.QuestionnaireCode.ToLower() == kvp.Key.ToLower()).MaxAsync(t => t.SubmitTime);
             string url = kvp.Value;
             if (maxSubmitTime != null)
             {
@@ -45,7 +43,7 @@ public class ExamPatriarchQuestionnaireProgressSync : IExamPatriarchQuestionnair
             }
             var items = res.data.Where(t => maxSubmitTime == null || t.finishedTime > maxSubmitTime);
             var dt = DateTime.Now;
-            List<ExamPatriarchQuestionnaireProgress> newItems = new();
+            List<ExamPatriarchQuestionnaireProgress> newItems = [];
             foreach (var item in items)
             {
                 newItems.Add(new()
@@ -60,7 +58,7 @@ public class ExamPatriarchQuestionnaireProgressSync : IExamPatriarchQuestionnair
             }
             if (items.Any())
             {
-                await _rep.InsertNowAsync(newItems);
+                await rep.InsertNowAsync(newItems);
             }
         }
     }

+ 113 - 0
YBEE.EQM.Application/Exam/ExamReporting/Dtos/ExamReportingTqesAdvWeakDto.cs

@@ -0,0 +1,113 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 优势薄弱DTO
+/// </summary>
+public class ExamReportingTqesAdvWeakDto
+{
+    /// <summary>
+    /// 行序号
+    /// </summary>
+    public int RowNum { get; set; }
+    /// <summary>
+    /// 城乡类型
+    /// </summary>
+    public UrbanRuralType UrbanRuralType { get; set; }
+    /// <summary>
+    /// 城乡类型名称
+    /// </summary>
+    public string UrbanRuralTypeName { get; set; }
+    /// <summary>
+    /// TQES学校ID
+    /// </summary>
+    public int SchoolId { get; set; }
+    /// <summary>
+    /// 学校名称
+    /// </summary>
+    public string SchoolName { get; set; }
+    /// <summary>
+    /// 学校代码
+    /// </summary>
+    public string SchoolCode { get; set; }
+    /// <summary>
+    /// 优势学科
+    /// </summary>
+    public string AdvCourseName { get; set; }
+    /// <summary>
+    /// 优势学科数量
+    /// </summary>
+    public int AdvCourseCount { get; set; }
+    /// <summary>
+    /// 优势班级
+    /// </summary>
+    public string AdvClassCourseName { get; set; }
+    /// <summary>
+    /// 优势班级数量
+    /// </summary>
+    public int AdvClassCourseCount { get; set; }
+    /// <summary>
+    /// 薄弱学科
+    /// </summary>
+    public string WeakCourseName { get; set; }
+    /// <summary>
+    /// 薄弱学科数量
+    /// </summary>
+    public int WeakCourseCount { get; set; }
+    /// <summary>
+    /// 薄弱班级
+    /// </summary>
+    public string WeakClassCourseName { get; set; }
+    /// <summary>
+    /// 薄弱班级数量
+    /// </summary>
+    public int WeakClassCourseCount { get; set; }
+}
+
+/// <summary>
+/// 优势薄弱最近两期对比DTO
+/// </summary>
+public class ExamReportingTqesAdvWeakCompareDto
+{
+    /// <summary>
+    /// 行序号
+    /// </summary>
+    public int RowNum { get; set; }
+    /// <summary>
+    /// 城乡类型
+    /// </summary>
+    public UrbanRuralType UrbanRuralType { get; set; }
+    /// <summary>
+    /// 城乡类型名称
+    /// </summary>
+    public string UrbanRuralTypeName { get; set; }
+    /// <summary>
+    /// TQES学校ID
+    /// </summary>
+    public int SchoolId { get; set; }
+    /// <summary>
+    /// 学校名称
+    /// </summary>
+    public string SchoolName { get; set; }
+    /// <summary>
+    /// 学校代码
+    /// </summary>
+    public string SchoolCode { get; set; }
+    /// <summary>
+    /// 年级改进
+    /// </summary>
+    public string GradeImpr { get; set; }
+    /// <summary>
+    /// 年级关注
+    /// </summary>
+    public string GradeAttn { get; set; }
+    /// <summary>
+    /// 班级改进
+    /// </summary>
+    public string ClassImpr { get; set; }
+    /// <summary>
+    /// 班级关注
+    /// </summary>
+    public string ClassAttn { get; set; }
+}

+ 12 - 0
YBEE.EQM.Application/Exam/ExamReporting/Dtos/ExamScoreRangeExportDto.cs

@@ -24,6 +24,14 @@ public class ExamScoreRangeExportDto
     /// </summary>
     public int ExamScoreRangeId { get; set; }
     /// <summary>
+    /// 分数段名称
+    /// </summary>
+    public string ExamScoreRangeName { get; set; }
+    /// <summary>
+    /// 分数段别称
+    /// </summary>
+    public string ExamScoreRangeNickName { get; set; }
+    /// <summary>
     /// 总人数
     /// </summary>
     public int TotalCount { get; set; }
@@ -31,4 +39,8 @@ public class ExamScoreRangeExportDto
     /// 分段人数
     /// </summary>
     public int RangeCount { get; set; }
+    /// <summary>
+    /// 科目ID
+    /// </summary>
+    public short CourseId { get; set; }
 }

+ 16 - 10
YBEE.EQM.Application/Exam/ExamReporting/ExamReportingAvgRangeAppService.cs

@@ -5,24 +5,30 @@
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-reporting-avg-range")]
 [Route("exam/reporting/avg/range")]
-public class ExamReportingAvgRangeAppService : IDynamicApiController
+public class ExamReportingAvgRangeAppService(IExamReportingAvgRangeService examReportingAvgRangeService) : IDynamicApiController
 {
-    private readonly IExamReportingAvgRangeService _examReportingAvgRangeService;
-
-    public ExamReportingAvgRangeAppService(IExamReportingAvgRangeService examReportingAvgRangeService)
+    /// <summary>
+    /// 导出全区表格
+    /// </summary>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <returns></returns>
+    public async Task<IActionResult> ExportTotal([FromQuery][Required] int examPlanId)
     {
-        _examReportingAvgRangeService = examReportingAvgRangeService;
+        var (fileName, fileBytes) = await examReportingAvgRangeService.ExportTotal(examPlanId);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
     }
-
     /// <summary>
-    /// 导出表格
+    /// 导出各校分数段统计
     /// </summary>
     /// <param name="examPlanId">监测计划ID</param>
     /// <returns></returns>
-    [AllowAnonymous]
-    public async Task<IActionResult> Export([FromQuery][Required] int examPlanId)
+    /// <exception cref="Exception"></exception>
+    public async Task<IActionResult> ExportOrg([FromQuery][Required] int examPlanId)
     {
-        var (fileName, fileBytes) = await _examReportingAvgRangeService.Export(examPlanId);
+        var (fileName, fileBytes) = await examReportingAvgRangeService.ExportOrg(examPlanId);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,

+ 24 - 0
YBEE.EQM.Application/Exam/ExamReporting/ExamReportingTqesAppService.cs

@@ -0,0 +1,24 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// TQES报表服务
+/// </summary>
+[ApiDescriptionSettings(Name = "exam-reporting-tqes")]
+[Route("exam/reporting/tqes")]
+public class ExamReportingTqesAppService(IExamReportingTqesService examReportingTqesService) : IDynamicApiController
+{
+    /// <summary>
+    /// 根据监测计划ID导出全区优势薄弱
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    public async Task<IActionResult> ExportTotalAdvWeak([FromQuery][Required] int examPlanId)
+    {
+        var (fileName, fileBytes) = await examReportingTqesService.ExportTotalAdvWeak(examPlanId);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
+}

+ 477 - 129
YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingAvgRangeService.cs

@@ -1,4 +1,5 @@
-using NPOI.SS.UserModel;
+using NPOI.SS.Formula.Functions;
+using NPOI.SS.UserModel;
 using NPOI.SS.Util;
 using NPOI.XSSF.UserModel;
 using System.Data;
@@ -9,63 +10,157 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 统计报表之分数段报表服务
 /// </summary>
-public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITransient
+public class ExamReportingAvgRangeService(
+    IRepository rep,
+    ISqlRepository sqlRep,
+    IExamScoreRangeService examScoreRangeService,
+    IExamGradeService examGradeService,
+    IExamOrgService examOrgService,
+    IExamCourseService examCourseService,
+    IExportExcelService exportExcelService) : IExamReportingAvgRangeService, ITransient
 {
-    private readonly IRepository _rep;
-    private readonly ISqlRepository _sqlRep;
-    private readonly IExamScoreRangeService _examScoreRangeService;
-    private readonly IExamGradeService _examGradeService;
-    private readonly IExamCourseService _examCourseService;
-    private readonly IExportExcelService _exportExcelService;
-
-    public ExamReportingAvgRangeService(IRepository rep, ISqlRepository sqlRep, IExamScoreRangeService examScoreRangeService, IExamGradeService examGradeService, IExamCourseService examCourseService, ExportExcelService exportExcelService)
-    {
-        _rep = rep;
-        _sqlRep = sqlRep;
-        _examScoreRangeService = examScoreRangeService;
-        _examGradeService = examGradeService;
-        _examCourseService = examCourseService;
-        _exportExcelService = exportExcelService;
-    }
-
+    #region 导出
     /// <summary>
-    /// 导出分数段统计表
+    /// 导出全区分数段统计表
     /// </summary>
     /// <param name="examPlanId"></param>
     /// <returns></returns>
-    public async Task<(string, byte[])> Export(int examPlanId)
+    public async Task<(string, byte[])> ExportTotal(int examPlanId)
     {
-        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.ProjectToType<ExamPlanOutput>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
-        var scoreRanges = await _examScoreRangeService.GetList();
-        var examGrades = await _examGradeService.GetListByExamPlanId(examPlanId);
-        var examCourses = await _examCourseService.GetListByExamPlanId(examPlanId);
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.ProjectToType<ExamPlanOutput>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var scoreRanges = await examScoreRangeService.GetList();
+        var examGrades = await examGradeService.GetListByExamPlanId(examPlanId);
+        var examCourses = await examCourseService.GetListByExamPlanId(examPlanId);
 
         // 临时存放目录
         string fileRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
         Directory.CreateDirectory(fileRoot);
-        string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-分数段平均分统计");
+        string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-区县报告-平均分及分数段统计");
         Directory.CreateDirectory(filePath);
         try
         {
             foreach (var examGrade in examGrades)
             {
-                string titlePrefix = $"{examPlan.Semester.Name}{examGrade.Grade.Name}期末";
                 for (int i = 0; i < 2; i++)
                 {
-                    IWorkbook wb = new XSSFWorkbook();
-                    var cellStyles = _exportExcelService.GetCellStyle(wb);
+                    string fn = i == 0 ? "区校合并" : "区级监测";
+                    string titlePrefix = $"{examPlan.Name}{fn}{examGrade.Grade.Name}";
+
+                    XSSFWorkbook wb = new();
+                    var cellStyles = exportExcelService.GetCellStyle(wb);
 
                     ExamSampleType? sampleType = i == 1 ? ExamSampleType.DISTRICT : null;
 
-                    await ExportTotalRange(titlePrefix, examPlanId, sampleType, examGrade.GradeId, scoreRanges.Where(t => t.Type == examGrade.ExamScoreRangeType).ToList(), wb, cellStyles);
-                    await ExportCourseRange(titlePrefix, examPlanId, sampleType, examGrade.GradeId, scoreRanges, examCourses.Where(t => t.GradeId == examGrade.GradeId).ToList(), wb, cellStyles);
-                    await ExportCourseAvgScore(titlePrefix, examPlanId, sampleType, examGrade.GradeId, examCourses.Where(t => t.GradeId == examGrade.GradeId).ToList(), wb, cellStyles);
+                    await ExportTotalTotalRange(titlePrefix, examPlanId, sampleType, examGrade.GradeId, scoreRanges.Where(t => t.Type == examGrade.ExamScoreRangeType).ToList(), wb, cellStyles);
+                    await ExportTotalCourseRange(titlePrefix, examPlanId, sampleType, examGrade.GradeId, scoreRanges, examCourses.Where(t => t.GradeId == examGrade.GradeId).ToList(), wb, cellStyles);
+                    await ExportTotalCourseAvgScore(titlePrefix, examPlanId, sampleType, examGrade.GradeId, examCourses.Where(t => t.GradeId == examGrade.GradeId).ToList(), wb, cellStyles);
 
                     MemoryStream ms = new();
                     wb.Write(ms, false);
                     ms.Flush();
-                    string fn = i == 0 ? "全员" : "抽测";
-                    await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examGrade.Grade.Name}-{fn}-分数段统计.xlsx"), ms.ToArray());
+                    
+                    await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examGrade.Grade.Name}-{fn}-平均分及分数段统计.xlsx"), ms.ToArray());
+                    if (!examGrade.IsRequiredSample)
+                    {
+                        break;
+                    }
+                }
+            }
+
+            string outFileName = $"{examPlan.Name}-区县报告-平均分及分数段统计-{DateTime.Now.Ticks}.zip";
+            string outFilePath = Path.Combine(fileRoot, outFileName);
+            ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
+            zip.CreateZip(outFilePath, filePath, true, string.Empty);
+
+            var retBytes = await File.ReadAllBytesAsync(outFilePath);
+            return (outFileName, retBytes);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception("导出错误", ex);
+        }
+        finally
+        {
+            Directory.Delete(fileRoot, true);
+        }
+    }
+
+    /// <summary>
+    /// 导出各校分数段统计表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    public async Task<(string, byte[])> ExportOrg(int examPlanId)
+    {
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.ProjectToType<ExamPlanOutput>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var scoreRanges = await examScoreRangeService.GetList();
+        var examGrades = await examGradeService.GetListByExamPlanId(examPlanId);
+        var examCourses = await examCourseService.GetListByExamPlanId(examPlanId);
+        var examOrgs = (await examOrgService.GetListByExamPlanId(examPlanId)).Where(t => t.IsRequiredExam).ToList();
+
+        // 临时存放目录
+        string fileRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
+        Directory.CreateDirectory(fileRoot);
+        string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-学校报告-平均分及分数段统计");
+        Directory.CreateDirectory(filePath);
+        // 学段目录
+        string eduStageFilePath = Path.Combine(filePath, examPlan.EducationStage.GetDescription());
+        Directory.CreateDirectory(eduStageFilePath);
+        try
+        {
+            // 年级
+            foreach (var examGrade in examGrades)
+            {
+                var ecourses = examCourses.Where(t => t.GradeId == examGrade.GradeId).OrderBy(t => t.CourseId).ToList();
+                for (int i = 0; i < 2; i++)
+                {
+                    string fn = i == 0 ? "区校合并" : "区级监测";
+                    string titlePrefix = $"{fn}{examGrade.Grade.Name}";
+
+                    ExamSampleType? sampleType = i == 1 ? ExamSampleType.DISTRICT : null;
+                    // 平均分
+                    var listAvgScore = await GetAvgScoreList(examPlanId, examGrade.GradeId, sampleType);
+                    // 总分分数段
+                    var listTotalScoreRange = await GetTotalRangeList(examPlanId, examGrade.GradeId, sampleType);
+                    // 学科分数段
+                    var listCourseScoreRange = await GetCourseRangeList(examPlanId, examGrade.GradeId, sampleType);
+
+                    // 学校
+                    foreach (var org in examOrgs)
+                    {
+                        // 学校目录
+                        string orgFilePath = Path.Combine(eduStageFilePath, org.SysOrg.FullName, "学校报告", "1-6 平均分及分数段统计");
+                        if (!Directory.Exists(orgFilePath))
+                        {
+                            Directory.CreateDirectory(orgFilePath);
+                        }
+
+                        XSSFWorkbook wb = new();
+                        var cellStyles = exportExcelService.GetCellStyle(wb);
+
+                        // 平均分
+                        var orgAvgScoreList = listAvgScore.Where(t => t.SysOrgId == org.SysOrgId || t.DataScopeType > 1 || (t.DataScopeType == 1 && t.UrbanRuralType == UrbanRuralType.NONE))
+                                                          .OrderBy(t => t.DataScopeType).ThenBy(t => t.SysOrgId).ThenBy(t => t.UrbanRuralType).ThenBy(t => t.CourseId)
+                                                          .ToList();
+                        ExportOrgCourseAvgScore(titlePrefix, org.SysOrg.Name, orgAvgScoreList, ecourses, wb, cellStyles);
+
+                        // 总分分数段
+                        var orgTotalScoreRangeList = listTotalScoreRange.Where(t => t.SysOrgId == org.SysOrgId).OrderByDescending(t => t.ExamScoreRangeId).ToList();
+                        ExportOrgScoreRange(titlePrefix, org.SysOrg.Name, "总分", orgTotalScoreRangeList, scoreRanges.Where(t => t.Type == examGrade.ExamScoreRangeType).ToList(), wb, cellStyles);
+
+                        // 学科分数段
+                        foreach (var ecourse in ecourses)
+                        {
+                            var orgCourseScoreRangeList = listCourseScoreRange.Where(t => t.SysOrgId == org.SysOrgId && t.CourseId == ecourse.CourseId).ToList();
+                            ExportOrgScoreRange(titlePrefix, org.SysOrg.Name, ecourse.Course.Name, orgCourseScoreRangeList, scoreRanges.Where(t => t.Type == ecourse.ExamScoreRangeType).ToList(), wb, cellStyles);
+                        }
+
+                        MemoryStream ms = new();
+                        wb.Write(ms, false);
+                        ms.Flush();
+                        await File.WriteAllBytesAsync(Path.Combine(orgFilePath, $"{examGrade.Grade.Name}-{fn}-平均分及分数段统计.xlsx"), ms.ToArray());
+                    }
 
                     if (!examGrade.IsRequiredSample)
                     {
@@ -74,7 +169,7 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
                 }
             }
 
-            string outFileName = $"{examPlan.Name}-分数段平均分统计-{DateTime.Now.Ticks}.zip";
+            string outFileName = $"{examPlan.Name}-学校报告-平均分及分数段统计-{DateTime.Now.Ticks}.zip";
             string outFilePath = Path.Combine(fileRoot, outFileName);
             ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
             zip.CreateZip(outFilePath, filePath, true, string.Empty);
@@ -91,7 +186,9 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
             Directory.Delete(fileRoot, true);
         }
     }
+    #endregion
 
+    #region 导出全区表格私有方法
     /// <summary>
     /// 导出总分分数段
     /// </summary>
@@ -103,7 +200,7 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
     /// <param name="wb"></param>
     /// <param name="cellStyles"></param>
     /// <returns></returns>
-    private async Task ExportTotalRange(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamScoreRangeOutput> ranges, IWorkbook wb, ExportExcelCellStyle cellStyles)
+    private async Task ExportTotalTotalRange(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamScoreRangeOutput> ranges, XSSFWorkbook wb, ExportExcelCellStyle cellStyles)
     {
         ISheet sheet = wb.CreateSheet("总分分数段");
         sheet.DisplayGridlines = false;
@@ -115,12 +212,12 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         IRow titleRow = sheet.CreateRow(rowNum++);
         titleRow.HeightInPoints = 40;
         int ci = 0;
-        _exportExcelService.AddCell($"{titlePrefix}总分分数段统计", titleRow, ci++, cellStyles.TitleStyle);
-        _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
-        _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell($"{titlePrefix}总分分数段统计", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
         foreach (var r in ranges)
         {
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
         }
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
         #endregion
@@ -129,12 +226,12 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         IRow headerRow = sheet.CreateRow(rowNum++);
         headerRow.HeightInPoints = 30;
         ci = 0;
-        _exportExcelService.AddCell("学校", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
-        _exportExcelService.AddCell("类别", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
-        _exportExcelService.AddCell("实考\r\n人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
+        exportExcelService.AddCell("学校", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("类别", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
+        exportExcelService.AddCell("实考\r\n人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
         foreach (var r in ranges)
         {
-            _exportExcelService.AddCell(r.NickName, headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
+            exportExcelService.AddCell(r.NickName, headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
         }
         #endregion
 
@@ -150,13 +247,13 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
                 row.HeightInPoints = 20;
                 ci = 0;
                 var forg = orgg.First();
-                _exportExcelService.AddCell(forg.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
-                _exportExcelService.AddCell(forg.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
-                _exportExcelService.AddCell(forg.TotalCount, row, ci++, cellStyles.CenterCellStyle);
+                exportExcelService.AddCell(forg.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
+                exportExcelService.AddCell(forg.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
+                exportExcelService.AddCell(forg.TotalCount, row, ci++, cellStyles.CenterCellStyle);
                 foreach (var r in ranges)
                 {
                     var sr = orgg.FirstOrDefault(t => t.ExamScoreRangeId == r.Id);
-                    _exportExcelService.AddCell(sr?.RangeCount, row, ci++, cellStyles.CenterCellStyle);
+                    exportExcelService.AddCell(sr?.RangeCount, row, ci++, cellStyles.CenterCellStyle);
                 }
             }
         }
@@ -166,14 +263,14 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         IRow totalRow = sheet.CreateRow(rowNum++);
         totalRow.HeightInPoints = 20;
         ci = 0;
-        _exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
-        _exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+        exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+        exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, 1));
-        _exportExcelService.AddCell($"SUM(C3:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+        exportExcelService.AddCell($"SUM(C3:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
         foreach (var r in ranges)
         {
             var cn = ExcelUtil.GetColumnNameByIndex(ci);
-            _exportExcelService.AddCell($"SUM({cn}3:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+            exportExcelService.AddCell($"SUM({cn}3:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
         }
         #endregion
     }
@@ -189,7 +286,7 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
     /// <param name="wb"></param>
     /// <param name="cellStyles"></param>
     /// <returns></returns>
-    private async Task ExportCourseRange(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamScoreRangeOutput> ranges, List<ExamCourseOutput> examCourses, IWorkbook wb, ExportExcelCellStyle cellStyles)
+    private async Task ExportTotalCourseRange(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamScoreRangeOutput> ranges, List<ExamCourseOutput> examCourses, XSSFWorkbook wb, ExportExcelCellStyle cellStyles)
     {
         ISheet sheet = wb.CreateSheet("学科分数段");
         sheet.DisplayGridlines = false;
@@ -199,7 +296,7 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         foreach (var examCourse in examCourses)
         {
             var list = await GetCourseRangeList(examPlanId, gradeId, examCourse.CourseId, sampleType);
-            if (!list.Any())
+            if (list.Count == 0)
             {
                 continue;
             }
@@ -210,12 +307,12 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
             IRow titleRow = sheet.CreateRow(rowNum++);
             titleRow.HeightInPoints = 40;
             int ci = 0;
-            _exportExcelService.AddCell($"{titlePrefix}{examCourse.Course.Name}分数段统计", titleRow, ci++, cellStyles.TitleStyle);
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell($"{titlePrefix}{examCourse.Course.Name}分数段统计", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
             foreach (var r in courseRanges)
             {
-                _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+                exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
             }
             sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
             #endregion
@@ -224,12 +321,12 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
             IRow headerRow = sheet.CreateRow(rowNum++);
             headerRow.HeightInPoints = 20;
             ci = 0;
-            _exportExcelService.AddCell("学校", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
-            _exportExcelService.AddCell("类别", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
-            _exportExcelService.AddCell("实考人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
+            exportExcelService.AddCell("学校", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+            exportExcelService.AddCell("类别", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 6);
+            exportExcelService.AddCell("实考人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
             foreach (var r in courseRanges)
             {
-                _exportExcelService.AddCell(r.NickName, headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
+                exportExcelService.AddCell(r.NickName, headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
             }
             #endregion
 
@@ -246,13 +343,13 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
                     row.HeightInPoints = 20;
                     ci = 0;
                     var forg = orgg.First();
-                    _exportExcelService.AddCell(forg.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
-                    _exportExcelService.AddCell(forg.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
-                    _exportExcelService.AddCell(forg.TotalCount, row, ci++, cellStyles.CenterCellStyle);
+                    exportExcelService.AddCell(forg.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
+                    exportExcelService.AddCell(forg.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
+                    exportExcelService.AddCell(forg.TotalCount, row, ci++, cellStyles.CenterCellStyle);
                     foreach (var r in courseRanges)
                     {
                         var sr = orgg.FirstOrDefault(t => t.ExamScoreRangeId == r.Id);
-                        _exportExcelService.AddCell(sr?.RangeCount, row, ci++, cellStyles.CenterCellStyle);
+                        exportExcelService.AddCell(sr?.RangeCount, row, ci++, cellStyles.CenterCellStyle);
                     }
                 }
             }
@@ -262,14 +359,14 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
             IRow totalRow = sheet.CreateRow(rowNum++);
             totalRow.HeightInPoints = 20;
             ci = 0;
-            _exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
-            _exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+            exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+            exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
             sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, 1));
-            _exportExcelService.AddCell($"SUM(C{rowNum - rc}:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+            exportExcelService.AddCell($"SUM(C{rowNum - rc}:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
             foreach (var r in courseRanges)
             {
                 var cn = ExcelUtil.GetColumnNameByIndex(ci);
-                _exportExcelService.AddCell($"SUM({cn}{rowNum - rc}:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+                exportExcelService.AddCell($"SUM({cn}{rowNum - rc}:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
             }
             #endregion
 
@@ -287,7 +384,7 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
     /// <param name="wb"></param>
     /// <param name="cellStyles"></param>
     /// <returns></returns>
-    private async Task ExportCourseAvgScore(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamCourseOutput> examCourses, IWorkbook wb, ExportExcelCellStyle cellStyles)
+    private async Task ExportTotalCourseAvgScore(string titlePrefix, int examPlanId, ExamSampleType? sampleType, short gradeId, List<ExamCourseOutput> examCourses, XSSFWorkbook wb, ExportExcelCellStyle cellStyles)
     {
         var courses = examCourses.Select(t => t.Course).ToList();
         courses.Insert(0, new CourseLiteOutput() { Id = 0, Name = "总分" });
@@ -312,14 +409,14 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         IRow titleRow = sheet.CreateRow(rowNum++);
         titleRow.HeightInPoints = 40;
         int ci = 0;
-        _exportExcelService.AddCell($"{titlePrefix}各学科均分统计", titleRow, ci++, cellStyles.TitleStyle);
-        _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
-        _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell($"{titlePrefix}各学科均分统计", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
         foreach (var r in courses)
         {
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
-            _exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
         }
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
         #endregion
@@ -328,28 +425,28 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         IRow headerRow1 = sheet.CreateRow(rowNum++);
         headerRow1.HeightInPoints = 20;
         ci = 0;
-        _exportExcelService.AddCell("学校", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
-        _exportExcelService.AddCell("类别", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
-        _exportExcelService.AddCell("实考\r\n人数", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("学校", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("类别", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("实考\r\n人数", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
         foreach (var r in courses)
         {
-            _exportExcelService.AddCell(r.Name, headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
-            _exportExcelService.AddCell("", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
-            _exportExcelService.AddCell("", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
+            exportExcelService.AddCell(r.Name, headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
+            exportExcelService.AddCell("", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
+            exportExcelService.AddCell("", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
             sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, ci - 3, ci - 1));
         }
 
         IRow headerRow2 = sheet.CreateRow(rowNum++);
         headerRow2.HeightInPoints = 30;
         ci = 0;
-        _exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
-        _exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
-        _exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
+        exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
+        exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
+        exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
         foreach (var r in courses)
         {
-            _exportExcelService.AddCell("平均分", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
-            _exportExcelService.AddCell("名次", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 5);
-            _exportExcelService.AddCell("与第一\r\n名分差", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+            exportExcelService.AddCell("平均分", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+            exportExcelService.AddCell("名次", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 5);
+            exportExcelService.AddCell("与最高\r\n分差距", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
         }
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 0, 0));
         sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 1, 1));
@@ -358,12 +455,15 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
 
         #region 数据
         var list = await GetAvgScoreList(examPlanId, gradeId, sampleType);
+        // 过滤掉最高分
+        list = list.Where(t => !(t.DataScopeType == 1 && t.UrbanRuralType == UrbanRuralType.NONE)).ToList();
         // 城乡分组
         var urGroup = list.GroupBy(t => t.UrbanRuralType).OrderBy(t => t.Key == UrbanRuralType.NONE ? 99 : (short)t.Key).ToList();
         foreach (var urg in urGroup)
         {
             // 机构
-            var orgGroup = urg.GroupBy(t => t.SysOrgId).OrderBy(t => t.FirstOrDefault()?.DataScopeType).ThenByDescending(t => t.FirstOrDefault(c => c.CourseId == 0)?.TotalCount).ToList();
+            //var orgGroup = urg.GroupBy(t => t.SysOrgId).OrderBy(t => t.FirstOrDefault()?.DataScopeType).ThenByDescending(t => t.FirstOrDefault(c => c.CourseId == 0)?.TotalCount).ToList();
+            var orgGroup = urg.GroupBy(t => t.SysOrgId).OrderBy(t => t.FirstOrDefault()?.DataScopeType).ThenBy(t => t.FirstOrDefault(c => c.CourseId == 0)?.OrderInTotal).ToList();
             foreach (var orgg in orgGroup)
             {
                 IRow row = sheet.CreateRow(rowNum++);
@@ -377,22 +477,22 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
                     {
                         txt = $"{forg?.UrbanRuralType?.GetDescription()}{txt}";
                     }
-                    _exportExcelService.AddCell(txt, row, ci++, cellStyles.ColumnHeaderStyle);
-                    _exportExcelService.AddCell("", row, ci++, cellStyles.ColumnHeaderStyle);
+                    exportExcelService.AddCell(txt, row, ci++, cellStyles.ColumnHeaderStyle);
+                    exportExcelService.AddCell("", row, ci++, cellStyles.ColumnHeaderStyle);
                     sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, ci - 2, ci - 1));
                 }
                 else
                 {
-                    _exportExcelService.AddCell(forg?.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
-                    _exportExcelService.AddCell(forg?.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
+                    exportExcelService.AddCell(forg?.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
+                    exportExcelService.AddCell(forg?.UrbanRuralType.GetDescription(), row, ci++, cellStyles.CenterCellStyle);
                 }
-                _exportExcelService.AddCell(forg?.TotalCount, row, ci++, forg?.DataScopeType != 1 ? cellStyles.ColumnHeaderStyle : cellStyles.CenterCellStyle);
+                exportExcelService.AddCell(forg?.TotalCount, row, ci++, forg?.DataScopeType != 1 ? cellStyles.ColumnHeaderStyle : cellStyles.CenterCellStyle);
                 foreach (var r in courses)
                 {
                     var sr = orgg.FirstOrDefault(t => t.CourseId == r.Id);
-                    _exportExcelService.AddCell(sr?.AvgScore, row, ci++, sr?.DataScopeType != 1 ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
-                    _exportExcelService.AddCell(sr?.DataScopeType != 1 ? null : sr?.OrderInTotal, row, ci++, cellStyles.CenterCellStyle);
-                    _exportExcelService.AddCell(sr?.AvgScoreDiff, row, ci++, sr?.DataScopeType != 1 ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
+                    exportExcelService.AddCell(sr?.AvgScore, row, ci++, (sr?.DataScopeType != 1 || sr.AvgScoreDiff == 0) ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
+                    exportExcelService.AddCell(sr?.DataScopeType != 1 ? null : sr?.OrderInTotal, row, ci++, cellStyles.CenterCellStyle);
+                    exportExcelService.AddCell(sr?.AvgScoreDiff, row, ci++, sr?.DataScopeType != 1 ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
                 }
             }
         }
@@ -402,19 +502,190 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
         //IRow totalRow = sheet.CreateRow(rowNum++);
         //totalRow.HeightInPoints = 20;
         //ci = 0;
-        //_exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
-        //_exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+        //exportExcelService.AddCell("合计", totalRow, ci++, cellStyles.ColumnHeaderStyle);
+        //exportExcelService.AddCell("", totalRow, ci++, cellStyles.ColumnHeaderStyle);
         //sheet.AddMergedRegion(new NPOI.SS.Util.CellRangeAddress(rowNum - 1, rowNum - 1, 0, 1));
-        //_exportExcelService.AddCell($"SUM(C3:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+        //exportExcelService.AddCell($"SUM(C3:C{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
         //foreach (var r in ranges)
         //{
         //    var cn = ExcelUtil.GetColumnNameByIndex(ci);
-        //    _exportExcelService.AddCell($"SUM({cn}3:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
+        //    exportExcelService.AddCell($"SUM({cn}3:{cn}{rowNum - 1})", totalRow, ci++, cellStyles.ColumnHeaderStyle, cellType: CellType.Formula);
         //}
         //#endregion
     }
+    #endregion
 
-    #region 数据获取
+    #region 导出学校表格私有方法
+    /// <summary>
+    /// 导出学校学科平均分统计表
+    /// </summary>
+    /// <param name="titlePrefix"></param>
+    /// <param name="orgName"></param>
+    /// <param name="list"></param>
+    /// <param name="examCourses"></param>
+    /// <param name="wb"></param>
+    /// <param name="cellStyles"></param>
+    /// <returns></returns>
+    private void ExportOrgCourseAvgScore(string titlePrefix, string orgName, List<ExamScoreAvgExportDto> list, List<ExamCourseOutput> examCourses, XSSFWorkbook wb, ExportExcelCellStyle cellStyles)
+    {
+        var courses = examCourses.Select(t => t.Course).ToList();
+        courses.Insert(0, new CourseLiteOutput() { Id = 0, Name = "总分" });
+
+        ISheet sheet = wb.CreateSheet("学科平均分");
+        sheet.DisplayGridlines = false;
+
+        var cellStyleNumberP2Blod = wb.CreateCellStyle();
+        cellStyleNumberP2Blod.CloneStyleFrom(cellStyles.NumberCellStyleP2);
+        IFont bFont = wb.CreateFont();
+        bFont.IsBold = true;
+        bFont.FontName = cellStyles.TitleFontName;
+        bFont.FontHeightInPoints = 10;
+        cellStyleNumberP2Blod.SetFont(bFont);
+
+        int rowNum = 0;
+
+        #region 标题
+        IRow titleRow = sheet.CreateRow(rowNum++);
+        titleRow.HeightInPoints = 60;
+        int ci = 0;
+        exportExcelService.AddCell($"{orgName}{titlePrefix}各学科均分统计", titleRow, ci++, cellStyles.TitleStyle);
+        exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        foreach (var r in courses)
+        {
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        }
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
+        #endregion
+
+        #region 列头
+        IRow headerRow1 = sheet.CreateRow(rowNum++);
+        headerRow1.HeightInPoints = 20;
+        ci = 0;
+        exportExcelService.AddCell("学校", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("实考\r\n人数", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        foreach (var r in courses)
+        {
+            exportExcelService.AddCell(r.Name, headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
+            exportExcelService.AddCell("", headerRow1, ci++, cellStyles.ColumnFillHeaderStyle);
+            sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, ci - 2, ci - 1));
+        }
+
+        IRow headerRow2 = sheet.CreateRow(rowNum++);
+        headerRow2.HeightInPoints = 30;
+        ci = 0;
+        exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
+        exportExcelService.AddCell("", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle);
+        foreach (var r in courses)
+        {
+            exportExcelService.AddCell("平均分", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+            exportExcelService.AddCell("与最高\r\n分差距", headerRow2, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        }
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 0, 0));
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 2, rowNum - 1, 1, 1));
+        #endregion
+
+        #region 数据
+        // 城乡分组
+        var urGroup = list.GroupBy(t => t.UrbanRuralType).OrderBy(t => t.Key).ToList();
+        foreach (var urg in urGroup)
+        {
+            // 机构
+            var orgGroup = urg.GroupBy(t => t.SysOrgId).OrderBy(t => t.FirstOrDefault()?.DataScopeType).ThenBy(t => t.Key).ThenBy(t => t.FirstOrDefault().UrbanRuralType).ToList();
+            foreach (var orgg in orgGroup)
+            {
+                IRow row = sheet.CreateRow(rowNum++);
+                row.HeightInPoints = 20;
+                ci = 0;
+                var forg = orgg.FirstOrDefault(t => t.CourseId == 0);
+                var isMax = forg.DataScopeType == 1 && forg.UrbanRuralType == UrbanRuralType.NONE;
+                if (forg.DataScopeType > 1)
+                {
+                    var txt = forg?.SysOrgName ?? "";
+                    if (forg?.UrbanRuralType != UrbanRuralType.NONE)
+                    {
+                        txt = $"{forg?.UrbanRuralType?.GetDescription()}学校";
+                    }
+                    exportExcelService.AddCell(txt, row, ci++, cellStyles.LeftCellStyle);
+                }
+                else
+                {
+                    exportExcelService.AddCell(forg?.SysOrgName, row, ci++, isMax ? cellStyles.ColumnHeaderStyle : cellStyles.LeftCellStyle);
+                }
+                exportExcelService.AddCell(isMax ? null : forg?.TotalCount, row, ci++, isMax ? cellStyles.ColumnHeaderStyle : cellStyles.CenterCellStyle);
+                foreach (var r in courses)
+                {
+                    var sr = orgg.FirstOrDefault(t => t.CourseId == r.Id);
+                    exportExcelService.AddCell(sr?.AvgScore, row, ci++, isMax ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
+                    exportExcelService.AddCell((isMax || forg.DataScopeType > 1) ? null : sr?.AvgScoreDiff, row, ci++, isMax ? cellStyleNumberP2Blod : cellStyles.NumberCellStyleP2);
+                }
+            }
+        }
+        #endregion
+    }
+    /// <summary>
+    /// 导出学校学科分数段统计图表
+    /// </summary>
+    /// <param name="titlePrefix"></param>
+    /// <param name="orgName"></param>
+    /// <param name="courseName"></param>
+    /// <param name="list"></param>
+    /// <param name="ranges"></param>
+    /// <param name="wb"></param>
+    /// <param name="cellStyles"></param>
+    private void ExportOrgScoreRange(string titlePrefix, string orgName, string courseName, List<ExamScoreRangeExportDto> list, List<ExamScoreRangeOutput> ranges, XSSFWorkbook wb, ExportExcelCellStyle cellStyles)
+    {
+        ISheet sheet = wb.CreateSheet(courseName);
+        sheet.DisplayGridlines = false;
+
+        int rowNum = 0;
+
+        #region 标题
+        IRow titleRow = sheet.CreateRow(rowNum++);
+        titleRow.HeightInPoints = 60;
+        int ci = 0;
+        exportExcelService.AddCell($"{orgName}{titlePrefix}{courseName}分数段统计", titleRow, ci++, cellStyles.TitleStyle);
+        for (; ci < 15; ci++)
+        {
+            exportExcelService.AddCell("", titleRow, ci++, cellStyles.TitleStyle);
+        }
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, ci - 1));
+        #endregion
+
+        #region 列头
+        IRow headerRow = sheet.CreateRow(rowNum++);
+        ci = 0;
+        exportExcelService.AddCell("分数段", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 14);
+        exportExcelService.AddCell("本段人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("累计人数", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
+        #endregion
+
+        #region 数据
+        decimal scount = 0;
+        foreach (var r in ranges)
+        {
+            var sr = list.FirstOrDefault(t => t.ExamScoreRangeId == r.Id);
+            IRow row = sheet.CreateRow(rowNum++);
+            int rci = 0;
+            exportExcelService.AddCell(r.NickName, row, rci++, cellStyles.CenterCellStyle);
+            scount += sr?.RangeCount ?? 0;
+            exportExcelService.AddCell(sr?.RangeCount ?? 0, row, rci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(scount, row, rci++, cellStyles.CenterCellStyle);
+        }
+        #endregion
+
+        #region 图表
+        const int CHART_ROW_COUNT = 18;
+        XSSFDrawing drawing = (XSSFDrawing)sheet.CreateDrawingPatriarch();
+        int startRow = 1;
+        int endRow = startRow + CHART_ROW_COUNT;
+        XSSFClientAnchor anchor = (XSSFClientAnchor)drawing.CreateAnchor(0, 0, 0, 0, ci + 1, startRow, ci + 12, endRow);
+        exportExcelService.CreateBarChart(sheet, drawing, anchor, 2, ranges.Count + 1, 1, $"{titlePrefix}{courseName}分数段统计", orgName, "人数", "分数段");
+        #endregion
+    }
+    #endregion
+
+    #region 数据获取私有方法
     /// <summary>
     /// 获取总分分数段数据
     /// </summary>
@@ -424,8 +695,8 @@ public class ExamReportingAvgRangeService : IExamReportingAvgRangeService, ITran
     /// <returns></returns>
     private async Task<List<ExamScoreRangeExportDto>> GetTotalRangeList(int examPlanId, short gradeId, ExamSampleType? sampleType)
     {
-        var items = await _sqlRep.SqlQueriesAsync<ExamScoreRangeExportDto>($@"
-SELECT T2.urban_rural_type, T2.`name` AS sys_org_name, T1.*
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreRangeExportDto>($@"
+SELECT T2.urban_rural_type, T2.`name` AS sys_org_name, T3.`name` AS exam_score_range_name, T3.nick_name AS exam_score_range_nick_name, T1.*
 FROM
 (
 	SELECT T1.sys_org_id, T1.exam_score_range_id, COUNT(T1.exam_student_id) AS range_count, MAX(T1.total_count) AS total_count
@@ -433,11 +704,13 @@ FROM
 	(
 		SELECT T1.sys_org_id, T1.exam_score_range_id, T1.exam_student_id, COUNT(T1.exam_student_id) OVER (PARTITION BY T1.sys_org_id) AS total_count
 		FROM exam_score_total AS T1
-		WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
 	) AS T1
 	GROUP BY T1.sys_org_id, T1.exam_score_range_id
 ) AS T1
 JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
+;
 ", new { ExamPlanId = examPlanId, GradeId = gradeId, ExamSampleType = sampleType.HasValue ? (short)sampleType : 0 });
 
         return items;
@@ -452,8 +725,8 @@ JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
     /// <returns></returns>
     private async Task<List<ExamScoreRangeExportDto>> GetCourseRangeList(int examPlanId, short gradeId, short courseId, ExamSampleType? sampleType)
     {
-        var items = await _sqlRep.SqlQueriesAsync<ExamScoreRangeExportDto>($@"
-SELECT T2.urban_rural_type, T2.`name` AS sys_org_name, T1.*
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreRangeExportDto>($@"
+SELECT T2.urban_rural_type, T2.`name` AS sys_org_name, T3.`name` AS exam_score_range_name, T3.nick_name AS exam_score_range_nick_name, T1.*
 FROM
 (
 	SELECT T1.sys_org_id, T1.exam_score_range_id, COUNT(T1.exam_student_id) AS range_count, MAX(T1.total_count) AS total_count
@@ -461,16 +734,47 @@ FROM
 	(
 		SELECT T1.sys_org_id, T1.exam_score_range_id, T1.exam_student_id, COUNT(T1.exam_student_id) OVER (PARTITION BY T1.sys_org_id) AS total_count
 		FROM exam_score AS T1
-		WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @courseId AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @courseId AND T1.is_excluded = 0 AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
 	) AS T1
 	GROUP BY T1.sys_org_id, T1.exam_score_range_id
 ) AS T1
 JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
+;
 ", new { ExamPlanId = examPlanId, GradeId = gradeId, CourseId = courseId, ExamSampleType = sampleType.HasValue ? (short)sampleType : 0 });
 
         return items;
     }
     /// <summary>
+    /// 获取单科分数段数据
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <param name="gradeId"></param>
+    /// <param name="sampleType"></param>
+    /// <returns></returns>
+    private async Task<List<ExamScoreRangeExportDto>> GetCourseRangeList(int examPlanId, short gradeId, ExamSampleType? sampleType)
+    {
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreRangeExportDto>($@"
+SELECT T2.urban_rural_type, T2.`name` AS sys_org_name, T3.`name` AS exam_score_range_name, T3.nick_name AS exam_score_range_nick_name, T1.*
+FROM
+(
+	SELECT T1.sys_org_id, T1.course_id, T1.exam_score_range_id, COUNT(T1.exam_student_id) AS range_count, MAX(T1.total_count) AS total_count
+	FROM
+	(
+		SELECT T1.sys_org_id, T1.course_id, T1.exam_score_range_id, T1.exam_student_id, COUNT(T1.exam_student_id) OVER (PARTITION BY T1.sys_org_id, T1.course_id) AS total_count
+		FROM exam_score AS T1
+		WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+	) AS T1
+	GROUP BY T1.sys_org_id, T1.course_id, T1.exam_score_range_id
+) AS T1
+JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
+;
+", new { ExamPlanId = examPlanId, GradeId = gradeId, ExamSampleType = sampleType.HasValue ? (short)sampleType : 0 });
+
+        return items;
+    }
+    /// <summary>
     /// 获取平均分数据
     /// </summary>
     /// <param name="examPlanId"></param>
@@ -479,7 +783,7 @@ JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
     /// <returns></returns>
     private async Task<List<ExamScoreAvgExportDto>> GetAvgScoreList(int examPlanId, short gradeId, ExamSampleType? sampleType)
     {
-        var items = await _sqlRep.SqlQueriesAsync<ExamScoreAvgExportDto>($@"
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreAvgExportDto>($@"
 SELECT T1.*
 FROM
 (
@@ -487,13 +791,13 @@ FROM
 	SELECT 1 AS data_scope_type, T2.urban_rural_type, T2.`name` AS sys_org_name, T1.*, 
 		RANK() OVER(ORDER BY T1.avg_score DESC) AS order_in_total,
 		RANK() OVER(PARTITION BY T2.urban_rural_type ORDER BY T1.avg_score DESC) AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT sys_org_id, 0 AS course_id, COUNT(exam_student_id) AS total_count, AVG(score) AS avg_score
 		FROM exam_score_total
-		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY sys_org_id
 	) AS T1
 	JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
@@ -504,7 +808,7 @@ FROM
 		(
 			SELECT sys_org_id, AVG(score) AS avg_score
 			FROM exam_score_total
-			WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+			WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 			GROUP BY sys_org_id
 		) AS T
     ) AS T3
@@ -514,13 +818,13 @@ FROM
 	SELECT 1 AS data_scope_type, T2.urban_rural_type, T2.`name` AS sys_org_name, T1.*, 
 		RANK() OVER(PARTITION BY T1.course_id ORDER BY T1.avg_score DESC) AS order_in_total,
 		RANK() OVER(PARTITION BY T1.course_id, T2.urban_rural_type ORDER BY T1.avg_score DESC) AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT sys_org_id, course_id, COUNT(exam_student_id) AS total_count, AVG(score) AS avg_score
 		FROM exam_score
-		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY sys_org_id, course_id
 	) AS T1
 	JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
@@ -531,7 +835,7 @@ FROM
 		(
 			SELECT sys_org_id, course_id, AVG(score) AS avg_score
 			FROM exam_score
-			WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+			WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 			GROUP BY sys_org_id, course_id
 		) AS T
 		GROUP BY T.course_id
@@ -542,19 +846,19 @@ FROM
 	SELECT 2 AS data_scope_type, T1.*, 
 		NULL AS order_in_total,
 		NULL AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT T2.urban_rural_type, '小计' AS sys_org_name, 99999998 AS sys_org_id, 0 AS course_id, COUNT(T1.exam_student_id) AS total_count, AVG(T1.score) AS avg_score
 		FROM exam_score_total AS T1
 		JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
-		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY T2.urban_rural_type
 	) AS T1
 	JOIN 
 	(
-		SELECT MAX(score) AS score_max FROM exam_score_total WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		SELECT MAX(score) AS score_max FROM exam_score_total WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 	) AS T3
 	
 	-- 单科同类小计
@@ -562,20 +866,20 @@ FROM
 	SELECT 2 AS data_scope_type, T1.*, 
 		NULL AS order_in_total,
 		NULL AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT T2.urban_rural_type, '小计' AS sys_org_name, 99999998 AS sys_org_id, T1.course_id, COUNT(T1.exam_student_id) AS total_count, AVG(T1.score) AS avg_score
 		FROM exam_score AS T1
 		JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
-		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY T2.urban_rural_type, T1.course_id
 	) AS T1
 	JOIN (
 		SELECT course_id, MAX(score) AS score_max 
 		FROM exam_score 
-		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY course_id
 	) AS T3 ON T1.course_id = T3.course_id
 	
@@ -584,17 +888,17 @@ FROM
 	SELECT 3 AS data_scope_type, T1.*, 
 		NULL AS order_in_total,
 		NULL AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT 0 AS urban_rural_type, '合计' AS sys_org_name, 99999999 AS sys_org_id, 0 AS course_id, COUNT(T1.exam_student_id) AS total_count, AVG(T1.score) AS avg_score
 		FROM exam_score_total AS T1
-		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 	) AS T1
 	JOIN 
 	(
-		SELECT MAX(score) AS score_max FROM exam_score_total WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		SELECT MAX(score) AS score_max FROM exam_score_total WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 	) AS T3
 	
 	-- 单科全部合计
@@ -602,23 +906,67 @@ FROM
 	SELECT 3 AS data_scope_type, T1.*, 
 		NULL AS order_in_total,
 		NULL AS order_in_same,
-		T3.score_max - T1.avg_score AS avg_score_diff,
+		T1.avg_score - T3.score_max AS avg_score_diff,
 		T3.score_max
 	FROM
 	(
 		SELECT 0 AS urban_rural_type, '合计' AS sys_org_name, 99999999 AS sys_org_id, T1.course_id, COUNT(T1.exam_student_id) AS total_count, AVG(T1.score) AS avg_score
 		FROM exam_score AS T1
-		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.is_excluded = 0 AND T1.score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY T1.course_id
 	) AS T1
 	JOIN (
 		SELECT course_id, MAX(score) AS score_max 
 		FROM exam_score 
-		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
 		GROUP BY course_id
 	) AS T3 ON T1.course_id = T3.course_id
+	
+	-- 总分最高平均分
+	UNION ALL
+	SELECT 1 AS data_scope_type, 
+		0 AS urban_rural_type, 
+		'全区最高分' AS sys_org_name, 
+		99999999 AS sys_org_id, 
+		0 AS course_id, 
+		NULL AS total_count,
+		MAX(T.avg_score) AS avg_score,
+		NULL AS order_in_total,
+		NULL AS order_in_same,
+		NULL AS avg_score_diff,
+		NULL AS score_max 
+	FROM
+	(
+		SELECT sys_org_id, AVG(score) AS avg_score
+		FROM exam_score_total
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		GROUP BY sys_org_id
+	) AS T
+	
+	-- 单科最高平均分
+	UNION ALL
+	SELECT 1 AS data_scope_type, 
+		0 AS urban_rural_type, 
+		'全区最高分' AS sys_org_name, 
+		99999999 AS sys_org_id, 
+		T.course_id, 
+		NULL AS total_count,
+		MAX(T.avg_score) AS avg_score,
+		NULL AS order_in_total,
+		NULL AS order_in_same,
+		NULL AS avg_score_diff,
+		NULL AS score_max
+	FROM
+	(
+		SELECT sys_org_id, course_id, AVG(score) AS avg_score
+		FROM exam_score
+		WHERE exam_plan_id = @examPlanId AND grade_id = @gradeId AND is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+		GROUP BY sys_org_id, course_id
+	) AS T
+	GROUP BY T.course_id
 ) AS T1
-ORDER BY T1.urban_rural_type, T1.data_scope_type, T1.sys_org_id, T1.total_count;
+ORDER BY T1.data_scope_type, T1.urban_rural_type, T1.course_id, T1.order_in_total, T1.sys_org_id, T1.total_count
+;
 ", new { ExamPlanId = examPlanId, GradeId = gradeId, ExamSampleType = sampleType.HasValue ? (short)sampleType : 0 });
         return items;
     }

+ 114 - 0
YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingTqesService.cs

@@ -0,0 +1,114 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// TQES报表服务
+/// </summary>
+/// <param name="tqesSqlRep"></param>
+/// <param name="examPlanRep"></param>
+/// <param name="exportExcelService"></param>
+public class ExamReportingTqesService(ISqlRepository<TqesDbContextLocator> tqesSqlRep, IRepository<ExamPlan> examPlanRep, IExportExcelService exportExcelService) : IExamReportingTqesService, ITransient
+{
+    /// <summary>
+    /// 根据监测计划ID导出全区优势薄弱
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<(string, byte[])> ExportTotalAdvWeak(int examPlanId)
+    {
+        var examPlan = examPlanRep.Include(t => t.Semester).FirstOrDefault(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+
+        // 临时存放目录
+        string fileRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
+        Directory.CreateDirectory(fileRoot);
+        string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-优势薄弱");
+        Directory.CreateDirectory(filePath);
+        try
+        {
+            var semester = examPlan.Semester;
+            // 存储过程参数
+            var p = new
+            {
+                startYear = semester.BeginYear,
+                endYear = semester.EndYear,
+                semesterTypeID = (int)semester.SemesterType,
+                schoolTypeID = (int)examPlan.EducationStage - 1
+            };
+
+            #region 优势薄弱
+            // 优势薄弱
+            var items = await tqesSqlRep.SqlQueriesAsync<ExamReportingTqesAdvWeakDto>("EXECUTE [ir].[USP_RS_PJS_School_AdvWeak] @startYear, @endYear, @semesterTypeID, @schoolTypeID", p);
+            // 定义EXCEL列
+            List<ExportExcelColDto<ExamReportingTqesAdvWeakDto>> cols1 =
+            [
+                new() { Name = "序号", Width = 6, GetCellValue = (r) => r.RowNum },
+                new() { Name = "城乡类别", Width = 10, GetCellValue = (r) => r.UrbanRuralTypeName },
+                new() { Name = "学校名称", Width = 20, GetCellValue = (r) => r.SchoolName, Align = ExportExcelCellAlign.LEFT },
+                new() { Name = "优势学科", Width = 38, GetCellValue = (r) => r.AdvCourseName, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "数量", Width = 6, GetCellValue = (r) => r.AdvCourseCount },
+                new() { Name = "优势班级", Width = 55, GetCellValue = (r) => r.AdvClassCourseName, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "数量", Width = 6, GetCellValue = (r) => r.AdvClassCourseCount },
+                new() { Name = "薄弱学科", Width = 38, GetCellValue = (r) => r.WeakCourseName, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "数量", Width = 6, GetCellValue = (r) => r.WeakCourseCount },
+                new() { Name = "薄弱班级", Width = 55, GetCellValue = (r) => r.WeakClassCourseName, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "数量", Width = 6, GetCellValue = (r) => r.WeakClassCourseCount },
+            ];
+
+            var e1 = exportExcelService.ExportExcel(new ExportExcelDto<ExamReportingTqesAdvWeakDto>()
+            {
+                IsXlsx = true,
+                Title = $"{examPlan.FullName}优势薄弱学科",
+                Columns = cols1,
+                Items = items,
+                IncludeExportTime = false,
+                NotSetRowHeight = true,
+            });
+            await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examPlan.EducationStage.GetDescription()}-优势薄弱学科.xlsx"), e1);
+            #endregion
+
+            #region 优势薄弱(近两期对比)
+            // 优势薄弱(近两期对比)
+            var citems = await tqesSqlRep.SqlQueriesAsync<ExamReportingTqesAdvWeakCompareDto>("EXECUTE [ir].[USP_RS_PJS_School_DE_AdvWeak] @startYear, @endYear, @semesterTypeID, @schoolTypeID", p);
+            // 定义EXCEL列
+            List<ExportExcelColDto<ExamReportingTqesAdvWeakCompareDto>> cols2 =
+            [
+                new() { Name = "序号", Width = 6, GetCellValue = (r) => r.RowNum },
+                new() { Name = "城乡类别", Width = 10, GetCellValue = (r) => r.UrbanRuralTypeName },
+                new() { Name = "学校名称", Width = 20, GetCellValue = (r) => r.SchoolName, Align = ExportExcelCellAlign.LEFT },
+                new() { Name = "年级学科获得改进", Width = 38, GetCellValue = (r) => r.GradeImpr, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "年级学科继续关注", Width = 38, GetCellValue = (r) => r.GradeAttn, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "班级学科获得改进", Width = 55, GetCellValue = (r) => r.ClassImpr, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+                new() { Name = "班级学科继续关注", Width = 55, GetCellValue = (r) => r.ClassAttn, Align = ExportExcelCellAlign.LEFT, WrapText = true },
+            ];
+
+            var e2 = exportExcelService.ExportExcel(new ExportExcelDto<ExamReportingTqesAdvWeakCompareDto>()
+            {
+                IsXlsx = true,
+                Title = $"{examPlan.FullName}优势薄弱学科(最近两期对比)",
+                Columns = cols2,
+                Items = citems,
+                IncludeExportTime = false,
+                NotSetRowHeight = true,
+            });
+            await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examPlan.EducationStage.GetDescription()}-优势薄弱学科(最近两期对比).xlsx"), e2);
+            #endregion
+
+            string outFileName = $"{examPlan.Name}-优势薄弱-{DateTime.Now.Ticks}.zip";
+            string outFilePath = Path.Combine(fileRoot, outFileName);
+            ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
+            zip.CreateZip(outFilePath, filePath, true, string.Empty);
+
+            var retBytes = await File.ReadAllBytesAsync(outFilePath);
+            return (outFileName, retBytes);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception("导出错误", ex);
+        }
+        finally
+        {
+            Directory.Delete(fileRoot, true);
+        }
+    }
+}

+ 9 - 2
YBEE.EQM.Application/Exam/ExamReporting/Services/IExamReportingAvgRangeService.cs

@@ -6,9 +6,16 @@
 public interface IExamReportingAvgRangeService
 {
     /// <summary>
-    /// 导出分数段统计表
+    /// 导出全区分数段统计表
     /// </summary>
     /// <param name="examPlanId"></param>
     /// <returns></returns>
-    Task<(string, byte[])> Export(int examPlanId);
+    Task<(string, byte[])> ExportTotal(int examPlanId);
+    /// <summary>
+    /// 导出各校分数段统计表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    Task<(string, byte[])> ExportOrg(int examPlanId);
 }

+ 14 - 0
YBEE.EQM.Application/Exam/ExamReporting/Services/IExamReportingTqesService.cs

@@ -0,0 +1,14 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// TQES报表服务
+/// </summary>
+public interface IExamReportingTqesService
+{
+    /// <summary>
+    /// 根据监测计划ID导出全区优势薄弱
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<(string, byte[])> ExportTotalAdvWeak(int examPlanId);
+}

+ 4 - 11
YBEE.EQM.Application/Exam/ExamResult/Services/ExamResultService.cs

@@ -1,19 +1,12 @@
-using Furion.DatabaseAccessor.Extensions;
-using YBEE.EQM.Core;
+using YBEE.EQM.Core;
 
 namespace YBEE.EQM.Application;
 
 /// <summary>
 /// 反馈结果管理服务(各机构均适用)
 /// </summary>
-public class ExamResultService : IExamResultService, ITransient
+public class ExamResultService(IRepository<ExamResult> rep) : IExamResultService, ITransient
 {
-    private readonly IRepository<ExamResult> _rep;
-    public ExamResultService(IRepository<ExamResult> rep)
-    {
-        _rep = rep;
-    }
-
     /// <summary>
     /// 添加监测结果文件
     /// </summary>
@@ -22,7 +15,7 @@ public class ExamResultService : IExamResultService, ITransient
     public async Task Add(AddExamResultInput input)
     {
         var item = input.Adapt<ExamResult>();
-        await item.InsertAsync();
+        await rep.InsertNowAsync(item);
     }
 
     /// <summary>
@@ -32,7 +25,7 @@ public class ExamResultService : IExamResultService, ITransient
     /// <returns></returns>
     public async Task<List<ExamResultOutput>> GetListByPublishId(int publishId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamDataPublishId == publishId).ProjectToType<ExamResultOutput>().ToListAsync();
+        var items = await rep.DetachedEntities.Where(t => t.ExamDataPublishId == publishId).ProjectToType<ExamResultOutput>().ToListAsync();
         return items;
     }
 }

+ 4 - 2
YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleInput.cs

@@ -1,4 +1,6 @@
-namespace YBEE.EQM.Application;
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
 
 /// <summary>
 /// 添加抽样方案输入参数
@@ -32,7 +34,7 @@ public class AddExamSampleInput
     /// }
     /// </summary>
     [Required]
-    public string Config { get; set; } = "{}";
+    public ExamSampleConfig Config { get; set; }
 }
 /// <summary>
 /// 更新抽样方案输入参数

+ 6 - 0
YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleMapper.cs

@@ -7,6 +7,12 @@ public class ExamSampleMapper : IRegister
 {
     public void Register(TypeAdapterConfig config)
     {
+        config.ForType<AddExamSampleInput, ExamSample>()
+              .Map(d => d.Config, s => JSON.Serialize(s.Config, null))
+              ;
+        config.ForType<UpdateExamSampleInput, ExamSample>()
+              .Map(d => d.Config, s => JSON.Serialize(s.Config, null))
+              ;
         config.ForType<ExamSample, ExamSampleOutput>()
               .Map(d => d.Config, s => JSON.Deserialize<ExamSampleConfig>(s.Config, null))
               .Map(d => d.IsFixedExamSample, s => s.ExamPlan.IsFixedExamSample)

+ 9 - 2
YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleOutput.cs

@@ -47,6 +47,10 @@ public class ExamSampleOutput : DEntityOutput
     /// </summary>
     [Required]
     public ExamSampleConfig Config { get; set; }
+    /// <summary>
+    /// 成绩引用监测计划ID
+    /// </summary>
+    public int? ExamScoreRefExamPlanId { get; set; }
 
     /// <summary>
     /// 是否已固定监测抽样方案
@@ -73,12 +77,15 @@ public class ExamSampleOutput : DEntityOutput
     /// 选中使用操作用户ID
     /// </summary>
     public int? SelectedSysUserId { get; set; }
-
-
     /// <summary>
     /// 选中使用操作用户
     /// </summary>
     public SysUserLiteOutput SelectedSysUser { get; set; }
+
+    /// <summary>
+    /// 成绩引用监测计划
+    /// </summary>
+    public ExamPlanLiteOutput ExamScoreRefExamPlan { get; set; }
 }
 
 /// <summary>

+ 150 - 110
YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleService.cs

@@ -10,18 +10,10 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 监测抽样方案管理服务
 /// </summary>
-public class ExamSampleService : IExamSampleService, ITransient
+public class ExamSampleService(IRepository<ExamSample> rep,
+    IExportExcelService exportExcelService,
+    ISysDictDataService sysDictDataService) : IExamSampleService, ITransient
 {
-    private readonly IRepository<ExamSample> _rep;
-    private readonly IExportExcelService _exportExcelService;
-    private readonly ISysDictDataService _sysDictDataService;
-
-    public ExamSampleService(IRepository<ExamSample> rep, IExportExcelService exportExcelService, ISysDictDataService sysDictDataService)
-    {
-        _rep = rep;
-        _exportExcelService = exportExcelService;
-        _sysDictDataService = sysDictDataService;
-    }
 
     #region 方案管理
     /// <summary>
@@ -31,8 +23,8 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task Add(AddExamSampleInput input)
     {
-        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
-        var maxSeq = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).MaxAsync(t => (short?)t.Sequence);
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var maxSeq = await rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).MaxAsync(t => (short?)t.Sequence);
 
         var item = input.Adapt<ExamSample>();
         item.Sequence = (short)(maxSeq.HasValue ? (maxSeq + 1) : 1);
@@ -49,7 +41,7 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task Update(UpdateExamSampleInput input)
     {
-        if (!await _rep.AnyAsync(t => t.Id == input.Id))
+        if (!await rep.AnyAsync(t => t.Id == input.Id))
         {
             throw Oops.Oh(ErrorCode.E2001);
         }
@@ -69,9 +61,9 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task Duplicate(BaseId input)
     {
-        var item = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
-        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
-        var maxSeq = await _rep.DetachedEntities.Where(t => t.ExamPlanId == item.ExamPlanId).MaxAsync(t => (short?)t.Sequence);
+        var item = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var maxSeq = await rep.DetachedEntities.Where(t => t.ExamPlanId == item.ExamPlanId).MaxAsync(t => (short?)t.Sequence);
         short sequence = (short)(maxSeq.HasValue ? (maxSeq + 1) : 1);
         string sampleName = $"学生抽样方案{ConvertUtil.ConvertToChinese(sequence)}";
         ExamSample newItem = new()
@@ -94,15 +86,15 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
-        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
         if (examPlan.IsFixedExamSample || item.Status == ExamSampleStatus.RUNNING || item.IsSelected == true)
         {
             throw Oops.Oh(ErrorCode.E3001);
         }
 
         // 批量删除已抽测学生
-        await _rep.Change<ExamSampleStudent>().Where(t => t.ExamSampleId == item.Id).ExecuteDeleteAsync();
+        await rep.Change<ExamSampleStudent>().Where(t => t.ExamSampleId == item.Id).ExecuteDeleteAsync();
         // 删除抽样方案
         await item.DeleteNowAsync();
     }
@@ -113,7 +105,7 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task SaveExamSampleAllClasses(SaveExamSampleAllClasses input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         var config = JSON.Deserialize<ExamSampleConfig>(item.Config);
         config.SampleAllSchoolClassIds = input.ClassIds;
         item.Config = JSON.Serialize(config);
@@ -126,7 +118,7 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task SwitchExamSampleAllClass(SwitchExamSampleAllClassInput input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
         var config = JSON.Deserialize<ExamSampleConfig>(item.Config);
         if (input.IsAdd && !config.SampleAllSchoolClassIds.Any(t => t == input.SchoolClassId))
         {
@@ -146,8 +138,8 @@ public class ExamSampleService : IExamSampleService, ITransient
     /// <returns></returns>
     public async Task SelectSample(BaseId input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
-        var examPlan = await _rep.Change<ExamPlan>().FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examPlan = await rep.Change<ExamPlan>().FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
         item.IsSelected = true;
         item.SelectedTime = DateTime.Now;
         item.SelectedSysUserId = CurrentSysUserInfo.SysUserId;
@@ -164,11 +156,11 @@ public class ExamSampleService : IExamSampleService, ITransient
     public async Task ExecuteSample(BaseId input)
     {
         // 抽样方案
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
         // 监测计划
-        var plan = await _rep.Change<ExamPlan>().DetachedEntities
-                                                .ProjectToType<ExamPlanOutput>()
-                                                .FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var plan = await rep.Change<ExamPlan>().DetachedEntities
+                                               .ProjectToType<ExamPlanOutput>()
+                                               .FirstOrDefaultAsync(t => t.Id == item.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
         // 监测年级字典
         var examGradeDict = plan.ExamGrades.ToDictionary(t => t.GradeId);
         var examSample = item.Adapt<ExamSampleOutput>();
@@ -182,7 +174,7 @@ public class ExamSampleService : IExamSampleService, ITransient
             await item.UpdateIncludeNowAsync(new[] { nameof(item.Status) });
 
             // 获取所有学生信息
-            var stus = await GetStudentScoreList(sampleItem.Config, item.ExamPlanId, plan.ExamGrades.Select(t => t.GradeId).Distinct().ToList());
+            var stus = await GetStudentScoreList(sampleItem.Config, item.ExamScoreRefExamPlanId, item.ExamPlanId, plan.ExamGrades.Select(t => t.GradeId).Distinct().ToList());
             // 获取抽样学生信息
             var sampleStus = GetSampleList(sampleItem.Config, examGradeDict, stus);
             // 获取最终抽样学生列表
@@ -190,12 +182,12 @@ public class ExamSampleService : IExamSampleService, ITransient
 
             // 删除已存在数据
             string deleteSql = $"DELETE FROM exam_sample_student WHERE exam_sample_id = {input.Id}";
-            await _rep.SqlNonQueryAsync(deleteSql);
+            await rep.SqlNonQueryAsync(deleteSql);
 
             #region 批量写入
             int si = 0;
             int stuCount = finalSampleStus.Count;
-            List<string> insertValues = new();
+            List<string> insertValues = [];
             foreach (var stu in finalSampleStus)
             {
                 si++;
@@ -206,7 +198,7 @@ public class ExamSampleService : IExamSampleService, ITransient
                 if (si % 2000 == 0 || si == stuCount)
                 {
                     string insertSql = $"INSERT INTO exam_sample_student(exam_sample_id, exam_student_id, exam_number, sequence, exam_sample_type, is_special_student, pre_total_score) VALUES {string.Join(", ", insertValues)}";
-                    await _rep.SqlNonQueryAsync(insertSql);
+                    await rep.SqlNonQueryAsync(insertSql);
                     insertValues.Clear();
                 }
             }
@@ -236,15 +228,15 @@ public class ExamSampleService : IExamSampleService, ITransient
     public async Task<(string fileName, byte[] fileBytes)> ExportToArchived(int id, bool hideIdNumber = false)
     {
         // 抽样方案
-        var examSample = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
+        var examSample = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
         // 监测计划
-        var plan = await _rep.Change<ExamPlan>().DetachedEntities
+        var plan = await rep.Change<ExamPlan>().DetachedEntities
                                                 .ProjectToType<ExamPlanOutput>()
                                                 .FirstOrDefaultAsync(t => t.Id == examSample.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
         // 监测年级字典
         var examGradeDict = plan.ExamGrades.ToDictionary(t => t.GradeId);
         // 获取证件类型
-        var cts = await _sysDictDataService.GetListByDictTypeId(304);
+        var cts = await sysDictDataService.GetListByDictTypeId(304);
         var certificateTypes = cts.ToDictionary(x => (Core.CertificateType)x.Value, y => y.Name);
 
         // 临时存放目录
@@ -280,7 +272,7 @@ LEFT JOIN sys_org AS T5 ON T2.sys_org_branch_id = T5.id
 WHERE T1.exam_sample_id = {id}
 ";
         // 所有学生
-        var stus = await _rep.SqlQueriesAsync<ExamSampleStudentExportDto>(selectStusSql);
+        var stus = await rep.SqlQueriesAsync<ExamSampleStudentExportDto>(selectStusSql);
 
 
         // 按年级生成考生文件
@@ -336,7 +328,7 @@ WHERE T1.exam_sample_id = {id}
             cols.Add(new() { Name = "名单类型", Width = 10, GetCellValue = (r) => r.ExamSampleType.GetDescription() });
 
             // 导出EXCEL文件
-            var ret = _exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
+            var ret = exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
             {
                 Title = sampleFileName,
                 Columns = cols,
@@ -378,7 +370,7 @@ WHERE T1.exam_sample_id = {id}
             noCols.Add(new() { Name = "名单类型", Width = 10, GetCellValue = (r) => r.ExamSampleType.GetDescription() });
 
             // 导出EXCEL文件
-            var noRet = _exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
+            var noRet = exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
             {
                 Title = noSampleFileName,
                 Columns = noCols,
@@ -410,7 +402,7 @@ WHERE T1.exam_sample_id = {id}
     /// <returns></returns>
     public async Task<(string fileName, byte[] fileBytes)> ExportToPrintshop(int id)
     {
-        var item = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id && t.Status == ExamSampleStatus.SUCCESSFUL && t.IsSelected == true);
+        var item = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id && t.Status == ExamSampleStatus.SUCCESSFUL && t.IsSelected == true);
         return item == null ? throw Oops.Oh(ErrorCode.E2006) : await ExportToArchived(id, true);
     }
     /// <summary>
@@ -421,14 +413,14 @@ WHERE T1.exam_sample_id = {id}
     public async Task<(string fileName, byte[] fileBytes)> ExportToOrg(int id)
     {
         // 抽样方案
-        var examSample = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id && t.ExamPlan.IsFixedExamSample == true && t.Status == ExamSampleStatus.SUCCESSFUL && t.IsSelected == true) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
+        var examSample = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id && t.ExamPlan.IsFixedExamSample == true && t.Status == ExamSampleStatus.SUCCESSFUL && t.IsSelected == true) ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
         // 监测计划
-        var plan = await _rep.Change<ExamPlan>().DetachedEntities
+        var plan = await rep.Change<ExamPlan>().DetachedEntities
                                                 .ProjectToType<ExamPlanOutput>()
                                                 .FirstOrDefaultAsync(t => t.Id == examSample.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
 
         // 必须发布了的才能下载
-        var pb = await _rep.Change<ExamDataPublish>().DetachedEntities.AnyAsync(t => t.ExamPlanId == plan.Id && t.Type == DataPublishType.STUDENT_SAMPLE_LIST && t.Status == PublishStatus.PUBLISHED);
+        var pb = await rep.Change<ExamDataPublish>().DetachedEntities.AnyAsync(t => t.ExamPlanId == plan.Id && t.Type == DataPublishType.STUDENT_SAMPLE_LIST && t.Status == PublishStatus.PUBLISHED);
         if (!pb)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -437,7 +429,7 @@ WHERE T1.exam_sample_id = {id}
         // 监测年级字典
         var examGradeDict = plan.ExamGrades.ToDictionary(t => t.GradeId);
         // 获取证件类型
-        var cts = await _sysDictDataService.GetListByDictTypeId(304);
+        var cts = await sysDictDataService.GetListByDictTypeId(304);
         var certificateTypes = cts.ToDictionary(x => (Core.CertificateType)x.Value, y => y.Name);
 
         // 临时存放目录
@@ -471,7 +463,7 @@ LEFT JOIN sys_org AS T5 ON T2.sys_org_branch_id = T5.id
 WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
 ";
         // 所有学生
-        var stus = await _rep.SqlQueriesAsync<ExamSampleStudentExportDto>(selectStusSql);
+        var stus = await rep.SqlQueriesAsync<ExamSampleStudentExportDto>(selectStusSql);
 
 
         // 按年级生成考生文件
@@ -529,7 +521,7 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
             }
 
             // 导出EXCEL文件
-            var ret = _exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
+            var ret = exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
             {
                 Title = sampleFileName,
                 Columns = cols,
@@ -568,7 +560,7 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
             }
 
             // 导出EXCEL文件
-            var noRet = _exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
+            var noRet = exportExcelService.ExportExcel(new ExportExcelDto<ExamSampleStudentExportDto>()
             {
                 Title = noSampleFileName,
                 Columns = noCols,
@@ -602,16 +594,16 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
     /// <returns></returns>
     public async Task<(string fileName, byte[] fileBytes)> ExportSampleCount(int id)
     {
-        var examSample = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examSample = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
 
         var items = await GetSampleCountListById(id);
 
-        IWorkbook wb = new XSSFWorkbook();
+        XSSFWorkbook wb = new();
         ISheet sheet = wb.CreateSheet();
         sheet.DisplayGridlines = false;
 
         // 获取样式
-        var cellStyle = _exportExcelService.GetCellStyle(wb);
+        var cellStyle = exportExcelService.GetCellStyle(wb);
 
         #region 表头
         int rowNum = 0;
@@ -619,14 +611,14 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
         IRow headerRow = sheet.CreateRow(rowNum++);
         headerRow.Height = ExportExcelCellStyle.DefaultRowHeight;
         int ci = 0;
-        _exportExcelService.AddCell("数据类型", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell("学校代码", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell("学校名称", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
-        _exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell(ExamSampleType.DISTRICT.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell(ExamSampleType.SCHOOL_EXAM.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell("合计", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("数据类型", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("学校代码", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("学校名称", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell(ExamSampleType.DISTRICT.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell(ExamSampleType.SCHOOL_EXAM.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("合计", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
         sheet.CreateFreezePane(0, 1);
         #endregion
 
@@ -665,14 +657,14 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
             }
 
             int rci = 0;
-            _exportExcelService.AddCell(item.TypeName, row, rci++, cstyle);
-            _exportExcelService.AddCell(schoolCode, row, rci++, cstyle);
-            _exportExcelService.AddCell(schoolName, row, rci++, cstyle);
-            _exportExcelService.AddCell(gradeName, row, rci++, cstyle);
-            _exportExcelService.AddCell(classNumber, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.CenterStudentCount, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.SchoolStudentCount, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.TotalStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.TypeName, row, rci++, cstyle);
+            exportExcelService.AddCell(schoolCode, row, rci++, cstyle);
+            exportExcelService.AddCell(schoolName, row, rci++, cstyle);
+            exportExcelService.AddCell(gradeName, row, rci++, cstyle);
+            exportExcelService.AddCell(classNumber, row, rci++, cstyle);
+            exportExcelService.AddCell(item.CenterStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.SchoolStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.TotalStudentCount, row, rci++, cstyle);
 
             rowNum++;
         }
@@ -690,17 +682,17 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
     /// <returns></returns>
     public async Task<(string fileName, byte[] fileBytes)> ExportSampleCountToOrg(int id)
     {
-        var examSample = await _rep.DetachedEntities.Include(t => t.ExamPlan).FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examSample = await rep.DetachedEntities.Include(t => t.ExamPlan).FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
 
         var items = await GetSampleCountListById(id);
         items = items.Where(t => t.TypeId < 4 && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ToList();
 
-        IWorkbook wb = new XSSFWorkbook();
+        XSSFWorkbook wb = new();
         ISheet sheet = wb.CreateSheet();
         sheet.DisplayGridlines = false;
 
         // 获取样式
-        var cellStyle = _exportExcelService.GetCellStyle(wb);
+        var cellStyle = exportExcelService.GetCellStyle(wb);
 
         #region 表头
         int rowNum = 0;
@@ -708,14 +700,14 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
         IRow headerRow = sheet.CreateRow(rowNum++);
         headerRow.Height = ExportExcelCellStyle.DefaultRowHeight;
         int ci = 0;
-        _exportExcelService.AddCell("数据类型", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell("学校代码", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell("学校名称", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
-        _exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
-        _exportExcelService.AddCell(ExamSampleType.DISTRICT.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell(ExamSampleType.SCHOOL_EXAM.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
-        _exportExcelService.AddCell("合计", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("数据类型", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("学校代码", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("学校名称", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell(ExamSampleType.DISTRICT.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell(ExamSampleType.SCHOOL_EXAM.GetDescription(), headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("合计", headerRow, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
         sheet.CreateFreezePane(0, 1);
         #endregion
 
@@ -754,14 +746,14 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
             }
 
             int rci = 0;
-            _exportExcelService.AddCell(item.TypeName, row, rci++, cstyle);
-            _exportExcelService.AddCell(schoolCode, row, rci++, cstyle);
-            _exportExcelService.AddCell(schoolName, row, rci++, cstyle);
-            _exportExcelService.AddCell(gradeName, row, rci++, cstyle);
-            _exportExcelService.AddCell(classNumber, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.CenterStudentCount, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.SchoolStudentCount, row, rci++, cstyle);
-            _exportExcelService.AddCell(item.TotalStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.TypeName, row, rci++, cstyle);
+            exportExcelService.AddCell(schoolCode, row, rci++, cstyle);
+            exportExcelService.AddCell(schoolName, row, rci++, cstyle);
+            exportExcelService.AddCell(gradeName, row, rci++, cstyle);
+            exportExcelService.AddCell(classNumber, row, rci++, cstyle);
+            exportExcelService.AddCell(item.CenterStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.SchoolStudentCount, row, rci++, cstyle);
+            exportExcelService.AddCell(item.TotalStudentCount, row, rci++, cstyle);
 
             rowNum++;
         }
@@ -782,7 +774,8 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
     /// <returns></returns>
     public async Task<ExamSampleOutput> GetById(int id)
     {
-        var item = await _rep.DetachedEntities.ProjectToType<ExamSampleOutput>().FirstOrDefaultAsync(t => t.Id == id);
+        var item = await rep.DetachedEntities.ProjectToType<ExamSampleOutput>().FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        //item.Config.ExamSampleRefExamPlanId;
         return item.Adapt<ExamSampleOutput>();
     }
     /// <summary>
@@ -792,7 +785,7 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
     /// <returns></returns>
     public async Task<List<ExamSampleOutput>> GetListByExamPlanId(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamSampleOutput>().ToListAsync();
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamSampleOutput>().ToListAsync();
         return items;
     }
     /// <summary>
@@ -802,7 +795,7 @@ WHERE T1.exam_sample_id = {id} AND T2.sys_org_id = {CurrentSysUserInfo.SysOrgId}
     /// <returns></returns>
     public async Task<List<ExamSampleCountOutput>> GetSampleCountListById(int id)
     {
-        var examSample = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var examSample = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
 
         string whereSql = $"WHERE T1.exam_plan_id = {examSample.ExamPlanId} AND T2.exam_sample_id = {id}";
         string querySql = @$"
@@ -913,7 +906,7 @@ JOIN
 LEFT JOIN base_grade AS T4 ON T1.grade_id = T4.id
 ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
 ";
-        var items = await _rep.SqlQueriesAsync<ExamSampleCountOutput>(querySql, new
+        var items = await rep.SqlQueriesAsync<ExamSampleCountOutput>(querySql, new
         {
             District = (short)ExamSampleType.DISTRICT,
             SchoolExam = (short)ExamSampleType.SCHOOL_EXAM
@@ -939,12 +932,12 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     /// <returns></returns>
     public async Task<ExamSamplePlanOutput> GetByExamDataPublishId(int examDataPublishId, DataPublishType type)
     {
-        if(type != DataPublishType.STUDENT_SAMPLE_LIST && type != DataPublishType.STUDENT_SAMPLE_COUNT_LIST)
+        if (type != DataPublishType.STUDENT_SAMPLE_LIST && type != DataPublishType.STUDENT_SAMPLE_COUNT_LIST)
         {
             throw Oops.Oh(ErrorCode.E1014, "数据发布");
         }
-        var pub = await _rep.Change<ExamDataPublish>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == examDataPublishId && t.Type == type) ?? throw Oops.Oh(ErrorCode.E2001, "反馈内容");
-        var item = await _rep.DetachedEntities.Include(t => t.ExamPlan)
+        var pub = await rep.Change<ExamDataPublish>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == examDataPublishId && t.Type == type) ?? throw Oops.Oh(ErrorCode.E2001, "反馈内容");
+        var item = await rep.DetachedEntities.Include(t => t.ExamPlan)
                                               .FirstOrDefaultAsync(t => t.ExamPlanId == pub.ExamPlanId && t.ExamPlan.IsFixedExamSample == true && t.Status == ExamSampleStatus.SUCCESSFUL && t.IsSelected == true)
                                               ?? throw Oops.Oh(ErrorCode.E2001, "抽样方案");
 
@@ -1084,10 +1077,8 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     {
         // 抽样比例
         var sampleRate = config.Percent / 100.0;
-
         // 返回结果集
-        List<ExamSampleDto> retItems = new();
-
+        List<ExamSampleDto> retItems = [];
         // 遍历集合
         var ws = stus.Select(t => new { t.SysOrgId, t.SysOrgBranchId, t.GradeId, t.SchoolClassId }).Distinct().ToList();
 
@@ -1198,7 +1189,7 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
                         // 班级学生列表
                         var classStus = stus.Where(t => t.SysOrgId == orgId && t.SysOrgBranchId == branchId && t.GradeId == gradeId && t.SchoolClassId == classId).ToList();
                         // 抽中学生列表
-                        List<ExamSampleDto> sampleStus = new();
+                        List<ExamSampleDto> sampleStus = [];
 
                         // 该班是否有成绩
                         var hasScore = classStus.Any(t => t.TotalScore != null && t.TotalScore > 0);
@@ -1227,7 +1218,7 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
                             else
                             {
                                 // 有成绩的循环抽样
-                                if (hasScore)
+                                if (hasScore && !config.IsRandomSampling)
                                 {
                                     int sq = 1;
                                     CyclicSampling(config, normalStus, sampleStus, maxSampleStuCount, sq);
@@ -1245,7 +1236,7 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
                         else
                         {
                             // 有成绩的循环抽样
-                            if (hasScore)
+                            if (hasScore && !config.IsRandomSampling)
                             {
                                 int sq = 1;
                                 CyclicSampling(config, classStus, sampleStus, maxSampleStuCount, sq);
@@ -1284,7 +1275,9 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     /// <param name="sequence"></param>
     private static void CyclicSampling(ExamSampleConfig config, List<ExamSampleDto> stus, List<ExamSampleDto> sampleStus, int maxStuCount, int sequence)
     {
-        for (int i = config.StartPosition >= stus.Count ? 0 : config.StartPosition - 1; i < stus.Count; i += config.Interval)
+        var interval = config.Interval + 1;
+
+        for (int i = config.StartPosition >= stus.Count ? 0 : config.StartPosition - 1; i < stus.Count; i += interval)
         {
             var stu = stus[i];
             var nstus = stu.Adapt<ExamSampleDto>();
@@ -1313,7 +1306,7 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     /// <returns></returns>
     private static List<ExamSampleDto> RandomSampling(List<ExamSampleDto> stus, int classStuCount, double sampleRate)
     {
-        List<ExamSampleDto> sampleStus = new();
+        List<ExamSampleDto> sampleStus = [];
         var sampleIndexList = GetRandomIndex(classStuCount, sampleRate);
         int sq = 1;
         for (int i = 0; i < stus.Count; i++)
@@ -1337,7 +1330,7 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     private static List<int> GetRandomIndex(int totalCount, double sampleRate)
     {
         Random random = new();
-        List<int> samples = new();
+        List<int> samples = [];
 
         int i = 0;
         var sc = Math.Ceiling(totalCount * sampleRate);
@@ -1358,13 +1351,13 @@ ORDER BY T2.`code`, T1.grade_id, T1.class_number, T1.type_id
     /// 获取学生并带往期成绩
     /// </summary>
     /// <param name="config">抽样配置</param>
+    /// <param name="examScoreRefExamPlanId">成绩引用监测计划ID</param>
     /// <param name="examPlanId">抽样监测计划ID</param>
     /// <param name="grades">年级ID列表</param>
     /// <returns></returns>
-    private async Task<List<ExamSampleDto>> GetStudentScoreList(ExamSampleConfig config, int examPlanId, List<short> grades)
+    private async Task<List<ExamSampleDto>> GetStudentScoreList(ExamSampleConfig config, int? examScoreRefExamPlanId, int examPlanId, List<short> grades)
     {
         string gradeWhere = string.Join(" OR ", grades.Select(t => $"T1.grade_id = {t}"));
-
         string selectSql = $@"
 SELECT 
     T1.id AS exam_student_id,
@@ -1388,10 +1381,9 @@ LEFT JOIN
     SELECT T1.sys_org_id, T1.school_class_id, T2.certificate_type, UPPER(T2.id_number) AS id_number, COUNT(T1.id) AS course_count, SUM(T1.score) AS total_score
     FROM exam_score AS T1
     JOIN exam_student AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.exam_student_id = T2.id
-    WHERE T1.exam_plan_id = @examSampleRefExamPlanId
+    WHERE T1.exam_plan_id = @examScoreRefExamPlanId
     GROUP BY T1.sys_org_id, T1.school_class_id, T2.certificate_type, T2.id_number
 ) AS T2 
--- ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND T1.certificate_type = T2.certificate_type AND UPPER(T1.id_number) = T2.id_number
 ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND UPPER(T1.id_number) = T2.id_number
 JOIN 
 (
@@ -1400,23 +1392,71 @@ JOIN
     WHERE exam_plan_id = @examPlanId AND is_required_exam = 1
 ) AS EO ON T1.exam_plan_id = EO.exam_plan_id AND T1.sys_org_id = EO.sys_org_id
 JOIN sys_org AS ORG ON T1.sys_org_id = ORG.id
--- LEFT JOIN special_student AS T3 ON T1.sys_org_id = T3.sys_org_id AND T1.certificate_type = T3.certificate_type AND UPPER(T1.id_number) = UPPER(T3.id_number)
 LEFT JOIN 
 (
 	SELECT id, exam_plan_id, sys_org_id, certificate_type, id_number 
     FROM exam_special_student
-	WHERE exam_plan_id = @examPlanId AND `status` = @specialStudentStatus
--- ) AS T3 ON T1.exam_plan_id = T3.exam_plan_id AND T1.sys_org_id = T3.sys_org_id AND T1.certificate_type = T3.certificate_type AND UPPER(T1.id_number) = UPPER(T3.id_number)
+	WHERE exam_plan_id = @examPlanId AND `status` = 3
+) AS T3 ON T1.exam_plan_id = T3.exam_plan_id AND T1.sys_org_id = T3.sys_org_id AND UPPER(T1.id_number) = UPPER(T3.id_number)
+WHERE T1.exam_plan_id = @examPlanId AND ({gradeWhere})
+ORDER BY T1.sys_org_id, T1.grade_id, T1.school_class_id, T2.total_score DESC, T1.id
+        ";
+
+        if (!config.SpecialStudentMustApproved)
+        {
+            selectSql = $@"
+SELECT 
+    T1.id AS exam_student_id,
+    T1.`name` AS exam_student_name,
+    T1.certificate_type,
+    UPPER(T1.id_number) AS id_number,
+    T1.sys_org_id,
+    ORG.`code` AS sys_org_code,
+    T1.sys_org_branch_id, 
+    T1.grade_id, 
+    T1.school_class_id, 
+    T1.class_number, 
+    T1.exam_number,
+    T2.course_count,
+    T2.total_score, 
+    CASE WHEN ISNULL(T3.id) THEN 0 ELSE 1 END as is_special_student
+FROM exam_student AS T1
+LEFT JOIN
+(
+    -- 往期总分
+    SELECT T1.sys_org_id, T1.school_class_id, T2.certificate_type, UPPER(T2.id_number) AS id_number, COUNT(T1.id) AS course_count, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    JOIN exam_student AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.exam_student_id = T2.id
+    WHERE T1.exam_plan_id = @examScoreRefExamPlanId
+    GROUP BY T1.sys_org_id, T1.school_class_id, T2.certificate_type, T2.id_number
+) AS T2 
+ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND UPPER(T1.id_number) = T2.id_number
+JOIN 
+(
+    SELECT DISTINCT exam_plan_id, sys_org_id 
+    FROM exam_org 
+    WHERE exam_plan_id = @examPlanId AND is_required_exam = 1
+) AS EO ON T1.exam_plan_id = EO.exam_plan_id AND T1.sys_org_id = EO.sys_org_id
+JOIN sys_org AS ORG ON T1.sys_org_id = ORG.id
+LEFT JOIN 
+(
+	SELECT T1.id, T1.exam_plan_id, T1.sys_org_id, T1.certificate_type, T1.id_number
+	FROM exam_special_student AS T1
+	LEFT JOIN 
+	(
+		SELECT sys_org_id FROM exam_org_data_report WHERE exam_plan_id = @examPlanId AND type = 2 AND `status` = 3
+	) AS T2 ON T1.sys_org_id = T2.sys_org_id
+	WHERE T1.exam_plan_id = @examPlanId AND ((T2.sys_org_id IS NULL AND T1.`status` = 3) OR (T2.sys_org_id IS NOT NULL AND (T1.`status` = 2 OR T1.`status` = 3)))
 ) AS T3 ON T1.exam_plan_id = T3.exam_plan_id AND T1.sys_org_id = T3.sys_org_id AND UPPER(T1.id_number) = UPPER(T3.id_number)
 WHERE T1.exam_plan_id = @examPlanId AND ({gradeWhere})
 ORDER BY T1.sys_org_id, T1.grade_id, T1.school_class_id, T2.total_score DESC, T1.id
 ";
+        }
 
-        var items = await _rep.SqlQueriesAsync<ExamSampleDto>(selectSql, new
+        var items = await rep.SqlQueriesAsync<ExamSampleDto>(selectSql, new
         {
-            ExamSampleRefExamPlanId = config.ExamSampleRefExamPlanId ?? 0,
+            ExamScoreRefExamPlanId = examScoreRefExamPlanId ?? 0,
             ExamPlanId = examPlanId,
-            SpecialStudentStatus = (short)AuditStatus.APPROVED,
         });
         return items;
     }

+ 16 - 0
YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreExportInput.cs

@@ -0,0 +1,16 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出TQES输入文件输入参数
+/// </summary>
+public class ExamScoreExportTqesFileInput
+{
+    /// <summary>
+    /// 监测计划ID
+    /// </summary>
+    public int ExamPlanId { get; set; }
+    /// <summary>
+    /// 非特殊学生0分转为缺考
+    /// </summary>
+    public bool IsZeroToAbsent { get; set; }
+}

+ 80 - 0
YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreExportTqesDto.cs

@@ -0,0 +1,80 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出TQES的学生信息DTO
+/// </summary>
+public class ExamScoreTotalExportTqesDto
+{
+    /// <summary>
+    /// 学校ID
+    /// </summary>
+    public int SysOrgId { get; set; }
+    /// <summary>
+    /// 学校名称
+    /// </summary>
+    public string SysOrgName { get; set; }
+    /// <summary>
+    /// TQES系统中学校ID
+    /// </summary>
+    public int TqesId { get; set; }
+    /// <summary>
+    /// 考号
+    /// </summary>
+    public string ExamNumber { get; set; }
+    /// <summary>
+    /// 年级号
+    /// </summary>
+    public short GradeNumber { get; set; }
+    /// <summary>
+    /// 班级号
+    /// </summary>
+    public short ClassNumber { get; set; }
+    /// <summary>
+    /// 证件号码
+    /// </summary>
+    public string IdNumber { get; set; }
+    /// <summary>
+    /// 姓名
+    /// </summary>
+    public string Name { get; set; }
+    /// <summary>
+    /// 监测学生ID
+    /// </summary>
+    public long ExamStudentId { get; set; }
+    /// <summary>
+    /// 抽样类型
+    /// </summary>
+    public ExamSampleType ExamSampleType { get; set; }
+    /// <summary>
+    /// 是否特殊学生
+    /// </summary>
+    public bool IsSpecial { get; set; }
+}
+/// <summary>
+/// 导出TQES的成绩DTO
+/// </summary>
+public class ExamScoreExportTqesDto
+{
+    /// <summary>
+    /// 监测学生ID
+    /// </summary>
+    public long ExamStudentId { get; set; }
+    /// <summary>
+    /// 科目ID
+    /// </summary>
+    public short CourseId { get; set; }
+    /// <summary>
+    /// 是否缺考
+    /// </summary>
+    public bool IsAbsent { get; set; }
+    /// <summary>
+    /// 是否特殊学生
+    /// </summary>
+    public bool IsSpecial { get; set; }
+    /// <summary>
+    /// 成绩
+    /// </summary>
+    public decimal Score { get; set; }
+}

+ 12 - 0
YBEE.EQM.Application/Exam/ExamScore/Dtos/ExamScoreImportDto.cs

@@ -87,6 +87,18 @@ public class ExamScoreImportDto
     /// 是否排除
     /// </summary>
     public bool IsExcluded { get; set; } = false;
+    /// <summary>
+    /// 是否特殊学生
+    /// </summary>
+    public bool IsSpecial { get; set; } = false;
+    /// <summary>
+    /// 是否缺考
+    /// </summary>
+    public bool IsAbsent { get; set; } = false;
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string Remark { get; set; } = "";
 }
 
 /// <summary>

+ 22 - 0
YBEE.EQM.Application/Exam/ExamScore/ExamScoreExportAppService.cs

@@ -0,0 +1,22 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出成绩服务
+/// </summary>
+/// <param name="examScoreExportService"></param>
+public class ExamScoreExportAppService(IExamScoreExportService examScoreExportService) : IDynamicApiController
+{
+    /// <summary>
+    /// 导出TQES输入文件
+    /// </summary>
+    /// <param name="input">监测计划ID</param>
+    /// <returns></returns>
+    public async Task<IActionResult> ExportTqesFile(ExamScoreExportTqesFileInput input)
+    {
+        var (fileName, fileBytes) = await examScoreExportService.ExportTqesFile(input);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
+}

+ 3 - 9
YBEE.EQM.Application/Exam/ExamScore/ExamScoreImportAppService.cs

@@ -7,13 +7,9 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-score-import")]
 [Route("exam/score/import")]
-public class ExamScoreImportAppService : IDynamicApiController
+public class ExamScoreImportAppService(IExamScoreImportService service) : IDynamicApiController
 {
-    private readonly IExamScoreImportService _examScoreImportService;
-    public ExamScoreImportAppService(IExamScoreImportService service)
-    {
-        _examScoreImportService = service;
-    }
+    private readonly IExamScoreImportService _examScoreImportService = service;
 
     /// <summary>
     /// 上传文件并完成批量导入前期未上报学生名单的各科成绩
@@ -43,7 +39,6 @@ public class ExamScoreImportAppService : IDynamicApiController
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadImportStudentTotalScore([FromForm] UploadExamDataInput input)
@@ -67,10 +62,9 @@ public class ExamScoreImportAppService : IDynamicApiController
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
-    public async Task<IActionResult> UploadImportStudentMinorScore([FromForm] UploadExamDataInput input)
+    public async Task<FileContentResult> UploadImportStudentMinorScore([FromForm] UploadExamDataInput input)
     {
         string fileExt = Path.GetExtension(input.File.FileName).ToLower();
         if (fileExt != ".zip")

+ 218 - 0
YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreExportService.cs

@@ -0,0 +1,218 @@
+using NPOI.HSSF.UserModel;
+using NPOI.SS.UserModel;
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出成绩服务
+/// </summary>
+/// <param name="rep"></param>
+/// <param name="sqlRep"></param>
+/// <param name="examGradeService"></param>
+/// <param name="examCourseService"></param>
+/// <param name="exportExcelService"></param>
+public class ExamScoreExportService(IRepository<ExamScore> rep, ISqlRepository sqlRep, IExamGradeService examGradeService, IExamCourseService examCourseService, IExportExcelService exportExcelService) : IExamScoreExportService, ITransient
+{
+    /// <summary>
+    /// 导出TQES输入文件
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    public async Task<(string, byte[])> ExportTqesFile(ExamScoreExportTqesFileInput input)
+    {
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.ProjectToType<ExamPlanOutput>().FirstOrDefaultAsync(t => t.Id == input.ExamPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var examGrades = await examGradeService.GetListByExamPlanId(input.ExamPlanId);
+        var examCourses = await examCourseService.GetListByExamPlanId(input.ExamPlanId);
+
+        // 临时存放目录
+        string fileRoot = Path.Combine(FileUtil.GetTempFileRoot(), $"{Guid.NewGuid()}");
+        Directory.CreateDirectory(fileRoot);
+        string filePath = Path.Combine(fileRoot, $"{examPlan.Name}-TQES导入数据");
+        Directory.CreateDirectory(filePath);
+        try
+        {
+            foreach (var examGrade in examGrades)
+            {
+                for (int i = 0; i < 2; i++)
+                {
+                    HSSFWorkbook wb = new();
+                    var cellStyles = exportExcelService.GetCellStyle(wb);
+
+                    ExamSampleType? examSampleType = i == 1 ? ExamSampleType.DISTRICT : null;
+
+                    var ecs = examCourses.Where(t => t.ExamGradeId == examGrade.Id).OrderBy(t => t.CourseId).ToList();
+                    await ExportToTqesSheet(input, examGrade.GradeId, examSampleType, ecs, wb, cellStyles);
+
+                    MemoryStream ms = new();
+                    wb.Write(ms, false);
+                    ms.Flush();
+                    string fn = i == 0 ? "全员" : "抽测";
+                    await File.WriteAllBytesAsync(Path.Combine(filePath, $"{examGrade.EducationStage.GetDescription()}-{examGrade.Grade.Name2}-监测成绩-{fn}.xls"), ms.ToArray());
+
+                    if (!examGrade.IsRequiredSample)
+                    {
+                        break;
+                    }
+                }
+            }
+
+            string outFileName = $"{examPlan.Name}-TQES导入数据-{DateTime.Now.Ticks}.zip";
+            string outFilePath = Path.Combine(fileRoot, outFileName);
+            ICSharpCode.SharpZipLib.Zip.FastZip zip = new();
+            zip.CreateZip(outFilePath, filePath, true, string.Empty);
+
+            var retBytes = await File.ReadAllBytesAsync(outFilePath);
+            return (outFileName, retBytes);
+        }
+        catch (Exception ex)
+        {
+            throw new Exception("导出错误", ex);
+        }
+        finally
+        {
+            Directory.Delete(fileRoot, true);
+        }
+    }
+
+    #region 导出方法
+    /// <summary>
+    /// 导出到表格
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="gradeId"></param>
+    /// <param name="examSampleType"></param>
+    /// <param name="examCourses"></param>
+    /// <param name="wb"></param>
+    /// <param name="cellStyles"></param>
+    /// <returns></returns>
+    private async Task ExportToTqesSheet(ExamScoreExportTqesFileInput input, short gradeId, ExamSampleType? examSampleType, List<ExamCourseOutput> examCourses, HSSFWorkbook wb, ExportExcelCellStyle cellStyles)
+    {
+        ISheet sheet = wb.CreateSheet("监测成绩");
+        sheet.DisplayGridlines = false;
+        sheet.CreateFreezePane(0, 1);
+
+        int rowNum = 0;
+
+        #region 列头
+        IRow headerRow = sheet.CreateRow(rowNum++);
+        int ci = 0;
+        exportExcelService.AddCell("学校", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("学校ID", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("考号", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 13);
+        exportExcelService.AddCell("年级", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("班级", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("身份证号码", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("姓名", headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 18);
+        foreach (var c in examCourses)
+        {
+            exportExcelService.AddCell(c.Course.Name, headerRow, ci++, cellStyles.ColumnFillHeaderStyle, sheet, 8);
+        }
+        #endregion
+
+        #region 数据
+        var totalList = await GetTotalList(input.ExamPlanId, gradeId, examSampleType);
+        var scoreList = await GetScoreList(input.ExamPlanId, gradeId, examSampleType);
+        foreach (var stu in totalList)
+        {
+            IRow row = sheet.CreateRow(rowNum++);
+            ci = 0;
+            exportExcelService.AddCell(stu.SysOrgName, row, ci++, cellStyles.LeftCellStyle);
+            exportExcelService.AddCell(stu.TqesId, row, ci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(stu.ExamNumber, row, ci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(stu.GradeNumber, row, ci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(stu.ClassNumber, row, ci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(stu.IdNumber, row, ci++, cellStyles.CenterCellStyle);
+            exportExcelService.AddCell(stu.Name, row, ci++, cellStyles.CenterCellStyle);
+            var scores = scoreList.Where(t => t.ExamStudentId == stu.ExamStudentId).GroupBy(d => d.CourseId).Select(d => new ExamScoreExportTqesDto
+            {
+                CourseId = d.Key,
+                Score = d.Max(s => s.Score),
+                IsSpecial = d.FirstOrDefault()?.IsSpecial ?? false,
+                IsAbsent = d.FirstOrDefault()?.IsAbsent ?? false,
+            }).ToDictionary(d => d.CourseId);
+            foreach (var c in examCourses)
+            {
+                if (stu.IsSpecial)
+                {
+                    exportExcelService.AddCell("特殊", row, ci++, cellStyles.CenterCellStyle);
+                    continue;
+                }
+                if (scores.TryGetValue(c.CourseId, out ExamScoreExportTqesDto score))
+                {
+                    if (score.IsSpecial)
+                    {
+                        exportExcelService.AddCell("特殊", row, ci++, cellStyles.CenterCellStyle);
+                    }
+                    else if ((score.IsAbsent || input.IsZeroToAbsent) && score.Score == 0)
+                    {
+                        exportExcelService.AddCell("缺考", row, ci++, cellStyles.CenterCellStyle);
+                    }
+                    else
+                    {
+                        exportExcelService.AddCell(score.Score, row, ci++, cellStyles.CenterCellStyle);
+                    }
+                }
+                else
+                {
+                    exportExcelService.AddCell(null, row, ci++, cellStyles.CenterCellStyle);
+                }
+            }
+        }
+        #endregion
+    }
+    #endregion
+
+    #region 获取数据
+    /// <summary>
+    /// 获取学生列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <param name="gradeId"></param>
+    /// <param name="examSampleType"></param>
+    /// <returns></returns>
+    private async Task<List<ExamScoreTotalExportTqesDto>> GetTotalList(int examPlanId, short gradeId, ExamSampleType? examSampleType)
+    {
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreTotalExportTqesDto>($@"
+SELECT 
+    T1.sys_org_id, 
+    T2.`name` AS sys_org_name, 
+    T2.tqes_id, 
+    T1.exam_number, 
+    T3.grade_number, 
+    T1.class_number, 
+    CASE WHEN T4.id_number = '' OR T4.id_number IS NULL THEN RIGHT(CONCAT('00000000', T1.exam_plan_id, '-', T1.exam_number), 18) ELSE T4.id_number END AS id_number, 
+    T4.`name`, 
+    T1.exam_student_id, 
+    T1.exam_sample_type, 
+    T1.is_special
+FROM exam_score_total AS T1
+JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
+JOIN base_grade AS T3 ON T1.grade_id = T3.id
+JOIN exam_student AS T4 ON T1.exam_student_id = T4.id
+WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+ORDER BY T1.sys_org_id, T1.grade_id, T1.class_number
+;", new { ExamPlanId = examPlanId, GradeId = gradeId, ExamSampleType = examSampleType.HasValue ? (short)examSampleType : 0 });
+
+        return items;
+    }
+    /// <summary>
+    /// 获取成绩列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <param name="gradeId"></param>
+    /// <param name="examSampleType"></param>
+    /// <returns></returns>
+    private async Task<List<ExamScoreExportTqesDto>> GetScoreList(int examPlanId, short gradeId, ExamSampleType? examSampleType)
+    {
+        var items = await sqlRep.SqlQueriesAsync<ExamScoreExportTqesDto>($@"
+SELECT T1.exam_student_id, T1.course_id, T1.is_absent, T1.is_special, T1.score 
+FROM exam_score AS T1
+WHERE T1.exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND (T1.exam_sample_type = @examSampleType OR @examSampleType = 0)
+;", new { ExamPlanId = examPlanId, GradeId = gradeId, ExamSampleType = examSampleType.HasValue ? (short)examSampleType : 0 });
+
+        return items;
+    }
+    #endregion
+}

+ 112 - 60
YBEE.EQM.Application/Exam/ExamScore/Services/ExamScoreImportService.cs

@@ -1,9 +1,7 @@
 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;
 
@@ -12,20 +10,12 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 学生成绩导入服务
 /// </summary>
-public class ExamScoreImportService : IExamScoreImportService, ITransient
+public class ExamScoreImportService(IRepository<ExamScore> rep, ISchoolClassService schoolClassService, IExamGradeService examGradeService, IExportExcelService exportExcelService) : 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, IExportExcelService exportExcelService)
-    {
-        _rep = rep;
-        _schoolClassService = schoolClassService;
-        _examGradeService = examGradeService;
-        _exportExcelService = exportExcelService;
-    }
+    private readonly IRepository<ExamScore> _rep = rep;
+    private readonly ISchoolClassService _schoolClassService = schoolClassService;
+    private readonly IExamGradeService _examGradeService = examGradeService;
+    private readonly IExportExcelService _exportExcelService = exportExcelService;
 
     /// <summary>
     /// 批量导入前期未上报学生名单的各科成绩(初始化)
@@ -150,9 +140,9 @@ public class ExamScoreImportService : IExamScoreImportService, ITransient
                         SampleType = (ExamSampleType)row.GetCell(SAMPLE_TYPE_INDEX).NumericCellValue,
                         SysOrgId = (short)row.GetCell(SCHOOL_ID_INDEX).NumericCellValue,
                         SysOrgBranchId = sbid > 0 ? (short)sbid : null,
-                        StudentName = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? ""),
+                        StudentName = (row.GetCell(NAME_INDEX)?.ToString() ?? "").ClearWhitespace(),
                         CertificateType = (CertificateType)row.GetCell(CERT_TYPE_INDEX).NumericCellValue,
-                        IdNumber = StringUtil.ClearIdNumber(row.GetCell(ID_NUM_INDEX)?.ToString() ?? ""),
+                        IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ClearWhitespace(),
                         ExamNumber = row.GetCell(EXAM_NUMBER_INDEX)?.ToString() ?? "",
                         GradeNumber = (short)row.GetCell(GRADE_INDEX).NumericCellValue,
                         ClassNumber = (short)row.GetCell(CLASS_INDEX).NumericCellValue,
@@ -344,6 +334,8 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
             int COURSE_COMB_INDEX = index++;
             int GRADE_INDEX = index++;
             int CLASS_INDEX = index++;
+            int SP_INDEX = index++;
+            int ABSENT_REPLACE_INDEX = index++;
             int COURSE_START_INDEX = index;
             Dictionary<int, string> headers = new()
             {
@@ -359,8 +351,10 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                 { COURSE_COMB_INDEX, "选科组合" },
                 { GRADE_INDEX, "年级" },
                 { CLASS_INDEX, "班级" },
+                { SP_INDEX, "特殊学生" },
+                { ABSENT_REPLACE_INDEX, "缺测替补" },
             };
-            List<string> headerErrors = new();
+            List<string> headerErrors = [];
             for (int i = 0; i < COURSE_START_INDEX; i++)
             {
                 if (headerRow.GetCell(i)?.ToString() != headers[i])
@@ -369,7 +363,7 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                     headerErrors.Add(letter.ToString());
                 }
             }
-            if (headerErrors.Any())
+            if (headerErrors.Count != 0)
             {
                 string columnErrors = string.Join("、", headerErrors);
                 //result.ErrorMessage.Add($"第1行标题行{columnErrors}列名错误。从A列开始依次应为抽样类型、学校ID、学校、姓名、证件类型、证件号码、考号、年级、班级。");
@@ -386,7 +380,7 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
             var examCourses = await _rep.Change<ExamCourse>().DetachedEntities.Where(t => t.ExamPlanId == examPlanId).Select(t => new { t.GradeId, t.CourseId }).ToListAsync();
 
             // 获取需要导入的科目列表
-            Dictionary<int, Course> courses = new();
+            Dictionary<int, Course> courses = [];
             for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++)
             {
                 var cn = headerRow.GetCell(gi).ToString() ?? "";
@@ -398,7 +392,7 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
             }
 
             int rn = 1;
-            List<ExamScoreImportDto> sourceItems = new();
+            List<ExamScoreImportDto> sourceItems = [];
             while (rows.MoveNext())
             {
                 rn++;
@@ -409,7 +403,26 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                     break;
                 }
 
+                var sampleType = (ExamSampleType)row.GetCell(SAMPLE_TYPE_INDEX).NumericCellValue;
+                var sysOrgId = (short)row.GetCell(SCHOOL_ID_INDEX).NumericCellValue;
                 var gradeNumber = (short)row.GetCell(GRADE_INDEX).NumericCellValue;
+                var classNumber = (short)row.GetCell(CLASS_INDEX).NumericCellValue;
+                var spStr = row.GetCell(SP_INDEX)?.ToString()?.Trim() ?? "";
+                var isSp = spStr.Contains("特殊");
+                var absentStr = row.GetCell(ABSENT_REPLACE_INDEX)?.ToString()?.Trim() ?? "";
+                string remark = isSp ? spStr : "";
+                if (absentStr.Length > 0)
+                {
+                    if (remark.Length == 0)
+                    {
+                        remark = absentStr;
+                    }
+                    else
+                    {
+                        remark = $"{remark},{absentStr}";
+                    }
+                }
+
                 for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++)
                 {
                     var course = courses[gi];
@@ -426,17 +439,18 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                     ExamScoreImportDto item = new()
                     {
                         RowNumber = rn,
-                        SampleType = (ExamSampleType)row.GetCell(SAMPLE_TYPE_INDEX).NumericCellValue,
-                        SysOrgId = (short)row.GetCell(SCHOOL_ID_INDEX).NumericCellValue,
+                        SampleType = sampleType,
+                        SysOrgId = sysOrgId,
                         SysOrgBranchId = sbid > 0 ? (short)sbid : null,
-                        StudentName = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? ""),
+                        StudentName = (row.GetCell(NAME_INDEX)?.ToString() ?? "").ClearWhitespace(),
                         CertificateType = (CertificateType)row.GetCell(CERT_TYPE_INDEX).NumericCellValue,
-                        IdNumber = StringUtil.ClearIdNumber(row.GetCell(ID_NUM_INDEX)?.ToString() ?? ""),
-                        ExamNumber = row.GetCell(EXAM_NUMBER_INDEX)?.ToString() ?? "",
+                        IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ClearWhitespace(),
+                        ExamNumber = (row.GetCell(EXAM_NUMBER_INDEX)?.ToString() ?? "").ClearWhitespace(),
                         GradeNumber = gradeNumber,
-                        ClassNumber = (short)row.GetCell(CLASS_INDEX).NumericCellValue,
+                        ClassNumber = classNumber,
                         CourseId = course.Id,
                         NceeCourseCombId = ccid > 0 ? (short)ccid : null,
+                        Remark = remark,
                     };
                     string scoreStr = row.GetCell(gi)?.ToString() ?? "";
                     if (decimal.TryParse(scoreStr, out decimal score))
@@ -446,6 +460,12 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                     else if (scoreStr.Contains('特') || scoreStr.Contains('缺'))
                     {
                         item.IsExcluded = true;
+                        item.IsAbsent = scoreStr.Contains('缺');
+                    }
+                    if (isSp)
+                    {
+                        item.IsSpecial = true;
+                        item.IsExcluded = true;
                     }
 
                     if (item.IdNumber.Length == 18)
@@ -490,7 +510,7 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
             string deleteScoreSql = $"DELETE FROM exam_score WHERE exam_plan_id = {examPlanId};";
             await _rep.SqlNonQueryAsync(deleteScoreSql);
 
-            List<string> selects = new();
+            List<string> selects = [];
             var scount = sourceItems.Count;
             for (int i = 0; i < scount; i++)
             {
@@ -507,23 +527,26 @@ ON T1.scid = T2.school_class_id AND T1.en = T2.exam_number
                     ccid = t.NceeCourseCombId.ToString();
                 }
                 var ied = t.IsExcluded ? 1 : 0;
+                var isp = t.IsSpecial ? 1 : 0;
+                var isa = t.IsAbsent ? 1 : 0;
 
                 selects.Add($@"
-SELECT {(short)t.SampleType} est, {t.SysOrgId} soid, {sobid} as sobid, {t.GradeId} gid, {examPlan.SemesterId} smid, {t.SchoolClassId} scid, {t.ClassNumber} cn, '{t.ExamNumber}' en, {ccid} ccid, {t.CourseId} cid, {t.Score} s, {ied} ied ");
+SELECT {(short)t.SampleType} est, {t.SysOrgId} soid, {sobid} as sobid, {t.GradeId} gid, {examPlan.SemesterId} smid, {t.SchoolClassId} scid, {t.ClassNumber} cn, '{t.ExamNumber}' en, {ccid} ccid, {t.CourseId} cid, {t.Score} s, {ied} ied, {isp} isp, {isa} isa, '{t.Remark}' r ");
 
                 if ((i + 1) % 500 == 0 || i == scount - 1)
                 {
                     string insertSql = $@"
 SET @examSampleId = (SELECT MAX(id) FROM exam_sample WHERE exam_plan_id = {examPlanId} AND is_selected = 1);
-INSERT INTO exam_score(exam_plan_id, education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, course_id, score, is_excluded)
-SELECT {examPlanId}, {(short)examPlan.EducationStage}, T1.est, T1.soid, T1.sobid, T1.gid, T1.smid, T1.scid, T1.cn, T2.id, T1.ccid, T1.cid, T1.s, T1.ied
+INSERT INTO exam_score(exam_plan_id, education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id, course_id, score, is_excluded, is_special, is_absent, remark, exam_number)
+SELECT {examPlanId}, {(short)examPlan.EducationStage}, T1.est, T1.soid, T1.sobid, T1.gid, T1.smid, T1.scid, T1.cn, T2.id, T1.ccid, T1.cid, T1.s, T1.ied, T1.isp, T1.isa, T1.r, T1.en
 FROM
 ({string.Join("UNION ALL", selects)}
 ) AS T1
 JOIN (SELECT exam_number, exam_student_id AS id FROM exam_sample_student WHERE exam_sample_id = @examSampleId) AS T2
 ON T1.en = T2.exam_number;
 ";
-                    await _rep.SqlNonQueryAsync(insertSql);
+                    //await _rep.SqlNonQueryAsync(insertSql);
+                    await insertSql.SetCommandTimeout(60000).SqlNonQueryAsync();
                     selects.Clear();
                 }
             }
@@ -535,33 +558,57 @@ ON T1.en = T2.exam_number;
 UPDATE exam_score AS T1 
 JOIN 
 (
-	SELECT T1.id, T1.grade_id, T1.course_id, get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id
-	FROM exam_score AS T1
-	JOIN exam_course AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.grade_id = T2.grade_id AND T1.course_id = T2.course_id
-	WHERE T1.exam_plan_id = {examPlanId}
+    SELECT T1.id, T1.grade_id, T1.course_id, get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id
+    FROM exam_score AS T1
+    JOIN exam_course AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.grade_id = T2.grade_id AND T1.course_id = T2.course_id
+    WHERE T1.exam_plan_id = {examPlanId}
 ) AS T2 ON T1.id = T2.id
 SET T1.exam_score_range_id = T2.exam_score_range_id
 WHERE T1.exam_plan_id = {examPlanId};
 
 -- 更新总成绩和分段
 DELETE FROM exam_score_total WHERE exam_plan_id = {examPlanId};
-INSERT INTO exam_score_total(
-	exam_plan_id,
-	education_stage, 
-	exam_sample_type, 
-	sys_org_id, 
-	sys_org_branch_id, 
-	grade_id, semester_id, 
-	school_class_id, 
-	class_number, 
-	exam_student_id, 
-	ncee_course_comb_id, 
-	course_count, 
-	score,
-	exam_score_range_id,
-	is_excluded
+INSERT INTO exam_score_total
+(
+    exam_plan_id,
+    education_stage, 
+    exam_sample_type, 
+    sys_org_id, 
+    sys_org_branch_id, 
+    grade_id, semester_id, 
+    school_class_id, 
+    class_number, 
+    exam_student_id, 
+    ncee_course_comb_id, 
+    course_count, 
+    score,
+    remark,
+    exam_number,
+    is_special,
+    absent_count,
+    is_excluded,
+    exam_score_range_id
 )
-SELECT {examPlanId}, T1.*, get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id, 0
+SELECT 
+    {examPlanId}, 
+    T1.education_stage, 
+    T1.exam_sample_type, 
+    T1.sys_org_id, 
+    T1.sys_org_branch_id, 
+    T1.grade_id, 
+    T1.semester_id, 
+    T1.school_class_id, 
+    T1.class_number, 
+    T1.exam_student_id, 
+    T1.ncee_course_comb_id, 
+    T1.course_count, 
+    T1.score,
+    T1.remark,
+    T1.exam_number,
+    T1.is_special,
+    T1.absent_count,
+    CASE WHEN T1.is_excluded > 0 THEN 1 ELSE 0 END AS is_excluded,
+    get_exam_score_range_id(T2.exam_score_range_type, T1.score) AS exam_score_range_id
 FROM
 (
 	SELECT 
@@ -575,16 +622,21 @@ FROM
 		exam_student_id, 
 		ncee_course_comb_id, 
 		COUNT(1) AS course_count, 
-		SUM(IFNULL(score, 0)) AS score
+		SUM(IFNULL(score, 0)) AS score,
+        SUM(IFNULL(is_excluded, 0)) AS is_excluded,
+        MAX(remark) AS remark,
+        MAX(exam_number) AS exam_number,
+        MAX(is_special) AS is_special,
+        SUM(is_absent) AS absent_count
 	FROM exam_score
 	WHERE exam_plan_id = {examPlanId}
 	GROUP BY education_stage, exam_sample_type, sys_org_id, sys_org_branch_id, grade_id, semester_id, school_class_id, class_number, exam_student_id, ncee_course_comb_id
 ) AS T1
 JOIN
 (
-	SELECT grade_id, exam_score_range_type FROM exam_grade WHERE exam_plan_id = {examPlanId}
+    SELECT grade_id, exam_score_range_type FROM exam_grade WHERE exam_plan_id = {examPlanId}
 ) AS T2 ON T1.grade_id = T2.grade_id;
-".SetCommandTimeout(6000).SqlNonQueryAsync();
+".SetCommandTimeout(600000).SqlNonQueryAsync();
             #endregion
         }
         catch (Exception ex)
@@ -620,7 +672,7 @@ JOIN
 
         try
         {
-            List<ExamScoreMinorFileInfo> fileInfos = new();
+            List<ExamScoreMinorFileInfo> fileInfos = [];
 
             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);
@@ -680,14 +732,14 @@ JOIN
                 var c_rows = c_sheet.GetRowEnumerator();
 
                 // 输出文件
-                IWorkbook o_workbook = new XSSFWorkbook();
+                XSSFWorkbook o_workbook = new();
                 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();
+                Dictionary<int, ExamPaperQuestionMinor> colMinor = [];
                 c_rows.MoveNext();
                 var c_headerRow = (IRow)c_rows.Current;
                 int colNum = c_headerRow.LastCellNum;
@@ -703,7 +755,7 @@ JOIN
                 }
 
                 // 所有小题成绩列表
-                List<ExamScoreMinorImportDto> items = new();
+                List<ExamScoreMinorImportDto> items = [];
 
                 // 区文件写入
                 while (c_rows.MoveNext())
@@ -858,7 +910,7 @@ JOIN
                 //await _rep.SqlNonQueryAsync(deleteScoreSql);
                 await deleteScoreSql.SetCommandTimeout(60000).SqlNonQueryAsync();
 
-                List<string> values = new();
+                List<string> values = [];
                 var scount = items.Count;
                 for (int i = 0; i < scount; i++)
                 {

+ 15 - 0
YBEE.EQM.Application/Exam/ExamScore/Services/IExamScoreExportService.cs

@@ -0,0 +1,15 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出成绩服务
+/// </summary>
+public interface IExamScoreExportService
+{
+    /// <summary>
+    /// 导出TQES输入文件
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    Task<(string, byte[])> ExportTqesFile(ExamScoreExportTqesFileInput input);
+}

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

@@ -42,6 +42,9 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
         UploadExamDataOutput<UploadExamSpecialStudentOutput> result = new();
         try
         {
+            // 已认证过名单
+            var spStus = await GetList(examPlanId);
+
             using FileStream fs = new(filePath, FileMode.Open, FileAccess.Read);
             IWorkbook workbook = ExcelUtil.GetWorkbook(filePath, fs);
             var sheet = workbook.GetSheetAt(0);
@@ -158,7 +161,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, 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]}未填或超出100字");
@@ -166,7 +169,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, 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]);
@@ -177,7 +180,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
                 }
 
                 // 证件号码
-                item.IdNumber = StringUtil.ClearWhite(row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ClearWhitespace().ToUpper();
                 if (item.CertificateType == CertificateType.ID_CARD)
                 {
                     var idNumberValidate = CertificateNumberValidator.ValidateIdCard(item.IdNumber);
@@ -194,16 +197,20 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, 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;
                     }
                 }
                 if (item.IdNumber.Length > 50) { item.IdNumber = item.IdNumber[..50]; }
+                if (spStus.Any(t => t.IdNumber == item.IdNumber))
+                {
+                    item.ErrorMessage.Add("往期已认定不能重复上报");
+                }
 
                 // 特殊原因
-                item.ApplyReason = StringUtil.ClearWhite(row.GetCell(REASON_INDEX)?.ToString() ?? "");
+                item.ApplyReason = (row.GetCell(REASON_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.ApplyReason == "" || item.ApplyReason.Length > 2000)
                 {
                     item.ErrorMessage.Add($"{headers[REASON_INDEX]}未填或超出2000字");
@@ -211,7 +218,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
                 if (item.ApplyReason.Length > 2000) { item.ApplyReason = item.ApplyReason[..2000]; }
 
                 // 家长电话
-                item.PatriarchTel = StringUtil.ClearWhite(row.GetCell(TEL_INDEX)?.ToString() ?? "");
+                item.PatriarchTel = (row.GetCell(TEL_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.PatriarchTel == "" || item.PatriarchTel.Length > 50)
                 {
                     item.ErrorMessage.Add($"{headers[TEL_INDEX]}未填或超出50字");
@@ -219,7 +226,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
                 if (item.PatriarchTel.Length > 50) { item.PatriarchTel = item.PatriarchTel[..50]; }
 
                 // 备注
-                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]; }
 
                 // 行是否验证通过
@@ -270,6 +277,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
 
         // 往期已认定记录
         var preItems = await _rep.Change<SpecialStudent>().DetachedEntities.Where(t => t.SysOrgId == orgId).ToListAsync();
+        var ePreItems = await GetList(input.ExamPlanId);
 
         int c = 0;
         foreach (var eg in examGrades)
@@ -290,9 +298,13 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, 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.IsPreIdentified = preItems.Any(t => t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber);
+                item.Name = item.Name?.ClearWhitespace();
+                item.IdNumber = item.IdNumber?.ClearWhitespace();
+                item.IsPreIdentified = preItems.Any(t => t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber) || ePreItems.Any(t => t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber);
+                if (item.IsPreIdentified)
+                {
+                    item.Status = AuditStatus.APPROVED;
+                }
                 items.Add(item);
                 c++;
             }
@@ -313,6 +325,12 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
     {
         var orgId = CurrentSysUserInfo.SysOrgId;
 
+        var isPreIdentified = await _rep.AnyAsync(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.IdNumber.ToUpper() == input.IdNumber);
+        if (isPreIdentified)
+        {
+            throw Oops.Oh("往期以认定,不能重复添加");
+        }
+
         // 检测同一监测计划中同机构内是否有相同证件号码的学生
         var sameItems = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.CertificateType == input.CertificateType && t.IdNumber.ToUpper() == input.IdNumber.ToUpper()).ProjectToType<ExamSpecialStudentOutput>().ToListAsync();
         if (sameItems.Any())
@@ -326,9 +344,13 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
         var item = input.Adapt<ExamSpecialStudent>();
         item.SysOrgId = orgId;
         item.SchoolClassId = schoolClass.Id;
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
-        item.IsPreIdentified = await _rep.Change<SpecialStudent>().DetachedEntities.AnyAsync(t => t.SysOrgId == orgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber);
+        item.Name = item.Name?.ClearWhitespace();
+        item.IdNumber = item.IdNumber?.ClearWhitespace();
+        if ((await _rep.Change<SpecialStudent>().DetachedEntities.AnyAsync(t => t.SysOrgId == orgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber)) || isPreIdentified)
+        {
+            item.IsPreIdentified = true;
+            item.Status = AuditStatus.APPROVED;
+        }
 
         ////item.Attachments = new List<AttachmentItem>() { new () { FileId = 1, FileName = "a.png", FileExtName = ".png" } }.ToJson();
         //var k = new List<AttachmentItem>() { new() { FileId = 1, FileName = "a.png", FileExtName = ".png" } };
@@ -350,9 +372,16 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
 
         var item = input.Adapt<ExamSpecialStudent>();
         item.SchoolClassId = schoolClass.Id;
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
-        item.IsPreIdentified = await _rep.Change<SpecialStudent>().DetachedEntities.AnyAsync(t => t.SysOrgId == item.SysOrgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber);
+        item.Name = item.Name?.ClearWhitespace();
+        item.IdNumber = item.IdNumber?.ClearWhitespace();
+        if ((await _rep.Change<SpecialStudent>().DetachedEntities.AnyAsync(t => t.SysOrgId == item.SysOrgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber)) ||
+            (await _rep.AnyAsync(t => t.ExamPlanId == oitem.ExamPlanId && t.SysOrgId == item.SysOrgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber))
+        )
+        {
+            item.IsPreIdentified = true;
+            item.Status = AuditStatus.APPROVED;
+        }
+        //item.IsPreIdentified = await _rep.Change<SpecialStudent>().DetachedEntities.AnyAsync(t => t.SysOrgId == item.SysOrgId && t.CertificateType == item.CertificateType && t.IdNumber.ToUpper() == item.IdNumber);
 
         await item.UpdateIncludeAsync(new[] {
             nameof(item.SchoolClassId),
@@ -415,7 +444,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
     /// <returns></returns>
     public async Task<bool> VerifyAttachment(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ProjectToType<ExamSpecialStudentOutput>().ToListAsync();
+        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId && t.IsPreIdentified == false).ProjectToType<ExamSpecialStudentOutput>().ToListAsync();
         if (items.Any(t => t.AttachmentList.Count == 0))
         {
             return false;
@@ -430,6 +459,10 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
     public async Task Del(BaseId input)
     {
         var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        if (item.IsPreIdentified)
+        {
+            throw Oops.Oh(ErrorCode.E2006);
+        }
         var attachments = JSON.Deserialize<List<AttachmentItem>>(item.Attachments);
         if (attachments != null && attachments.Any())
         {
@@ -452,7 +485,7 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
     public async Task Clear(ClearExamSpecialStudentInput input)
     {
         var orgId = CurrentSysUserInfo.SysOrgId;
-        await _rep.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId).ExecuteDeleteAsync();
+        await _rep.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.IsPreIdentified == false).ExecuteDeleteAsync();
     }
     /// <summary>
     /// 导出监测特殊学生上报打印表格
@@ -754,5 +787,16 @@ public class ExamSpecialStudentService : IExamSpecialStudentService, ITransient
         //r1c1.SetTextPosition(20);//设置高度 
         return pCell;
     }
+
+    /// <summary>
+    /// 获取当前机构指定监测计划中的特殊学生列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    private async Task<List<ExamSpecialStudent>> GetList(int examPlanId)
+    {
+        var items = await _rep.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ToListAsync();
+        return items;
+    }
     #endregion
 }

+ 14 - 14
YBEE.EQM.Application/Exam/ExamTeacher/Services/ExamTeacherService.cs

@@ -111,14 +111,14 @@ public class ExamTeacherService : IExamTeacherService, ITransient
                 UploadExamTeacherOutput item = new() { RowNumber = ++rn };
 
                 // 姓名
-                item.Name = StringUtil.ClearWhite(row.GetCell(NAME_INDEX)?.ToString() ?? "");
+                item.Name = (row.GetCell(NAME_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.Name == "")
                 {
                     item.ErrorMessage.Add(headers[NAME_INDEX]);
                 }
 
                 // 证件类型
-                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]);
@@ -129,7 +129,7 @@ public class ExamTeacherService : IExamTeacherService, ITransient
                 }
 
                 // 证件号码
-                item.IdNumber = StringUtil.ClearWhite(row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ClearWhitespace().ToUpper();
                 if (item.CertificateType == CertificateType.ID_CARD)
                 {
                     var idNumberValidate = CertificateNumberValidator.ValidateIdCard(item.IdNumber);
@@ -146,7 +146,7 @@ public class ExamTeacherService : IExamTeacherService, 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;
@@ -154,7 +154,7 @@ public class ExamTeacherService : IExamTeacherService, ITransient
                 }
 
                 // 校内职务
-                item.SchoolJobTitleName = StringUtil.ClearWhite(row.GetCell(JOB_INDEX)?.ToString() ?? "");
+                item.SchoolJobTitleName = (row.GetCell(JOB_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.SchoolJobTitleName == "")
                 {
                     item.SchoolJobTitle = SchoolJobTitle.TEACHER;
@@ -169,12 +169,12 @@ public class ExamTeacherService : IExamTeacherService, ITransient
                 }
 
                 // 手机号码
-                item.Mobile = StringUtil.ClearWhite(row.GetCell(MOBILE_INDEX)?.ToString() ?? "");
+                item.Mobile = (row.GetCell(MOBILE_INDEX)?.ToString() ?? "").ClearWhitespace();
                 // 电子邮箱
-                item.Email = StringUtil.ClearWhite(row.GetCell(EMAIL_INDEX)?.ToString() ?? "");
+                item.Email = (row.GetCell(EMAIL_INDEX)?.ToString() ?? "").ClearWhitespace();
 
                 // 备注
-                item.Remark = StringUtil.ClearWhite(row.GetCell(REMARK_INDEX)?.ToString() ?? "");
+                item.Remark = (row.GetCell(REMARK_INDEX)?.ToString() ?? "").ClearWhitespace();
                 // 行是否验证通过
                 item.IsSuccess = item.ErrorMessage.Count == 0;
                 data.Add(item);
@@ -215,8 +215,8 @@ public class ExamTeacherService : IExamTeacherService, ITransient
         foreach (var item in input.Items)
         {
             item.SysOrgId ??= CurrentSysUserInfo.SysOrgId;
-            item.Name = StringUtil.ClearWhite(item.Name);
-            item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+            item.Name = item.Name?.ClearWhitespace();
+            item.IdNumber = item.IdNumber?.ClearWhitespace();
         }
         var items = input.Items.Select(t => t.Adapt<ExamTeacher>()).ToList();
         await _rep.InsertAsync(items);
@@ -245,8 +245,8 @@ public class ExamTeacherService : IExamTeacherService, ITransient
         {
             item.SysOrgId = orgId;
         }
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+        item.Name = item.Name?.ClearWhitespace();
+        item.IdNumber = item.IdNumber?.ClearWhitespace();
 
         await item.InsertAsync();
     }
@@ -260,8 +260,8 @@ public class ExamTeacherService : IExamTeacherService, ITransient
         var oitem = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
 
         var item = input.Adapt<ExamTeacher>();
-        item.Name = StringUtil.ClearWhite(item.Name);
-        item.IdNumber = StringUtil.ClearIdNumber(item.IdNumber);
+        item.Name = item.Name?.ClearWhitespace();
+        item.IdNumber = item.IdNumber?.ClearWhitespace();
 
         await item.UpdateIncludeAsync(new[] {
             nameof(item.Name),

+ 0 - 1
YBEE.EQM.Application/Exam/ExamTeacherCourse/ExamTeacherCourseAppService.cs

@@ -58,7 +58,6 @@ public class ExamTeacherCourseAppService : IDynamicApiController
     /// <param name="examPlanId"></param>
     /// <returns></returns>
     /// <exception cref="Exception"></exception>
-    [AllowAnonymous]
     public async Task<IActionResult> ExportTqesFile([Required]int examPlanId)
     {
         var (fileName, fileBytes) = await _examTeacherCourseService.ExportTqesFile(examPlanId);

+ 14 - 14
YBEE.EQM.Application/Exam/ExamTeacherCourse/Services/ExamTeacherCourseService.cs

@@ -154,7 +154,7 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
                 }
 
                 // 科目
-                item.CourseName = StringUtil.ClearWhite(row.GetCell(COURSE_INDEX)?.ToString() ?? "");
+                item.CourseName = (row.GetCell(COURSE_INDEX)?.ToString() ?? "").ClearWhitespace();
                 if (item.CourseName == null)
                 {
                     item.ErrorMessage.Add($"{headers[COURSE_INDEX]}未填");
@@ -177,7 +177,7 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, 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]);
@@ -185,7 +185,7 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, 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]);
@@ -196,7 +196,7 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
                 }
 
                 // 证件号码
-                item.IdNumber = StringUtil.ClearWhite(row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ToUpper();
+                item.IdNumber = (row.GetCell(ID_NUM_INDEX)?.ToString() ?? "").ClearWhitespace().ToUpper();
                 if (item.CertificateType == CertificateType.ID_CARD)
                 {
                     var idNumberValidate = CertificateNumberValidator.ValidateIdCard(item.IdNumber);
@@ -208,7 +208,7 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
                 if (item.IdNumber.Length > 100) { item.IdNumber = item.IdNumber[..100]; }
 
                 // 备注
-                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]; }
 
                 // 行是否验证通过
@@ -271,8 +271,8 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, 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?.ClearWhitespace();
                 items.Add(item);
             }
         }
@@ -300,14 +300,14 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
         try
         {
             // 定义EXCEL列
-            List<ExportExcelColDto<ExamTeacherCourseOutput>> noCols = new()
-            {
+            List<ExportExcelColDto<ExamTeacherCourseOutput>> noCols =
+            [
                 new() { Name = "年级号", Width = 8, GetCellValue = (r) => r.ExamGrade.Grade.GradeNumber2 },
                 new() { Name = "班级号", Width = 8, GetCellValue = (r) => r.ClassNumber },
                 new() { Name = "科目", Width = 8, GetCellValue = (r) => r.Course.Name },
                 new() { Name = "身份证号码", Width = 20, GetCellValue = (r) => r.IdNumber },
                 new() { Name = "姓名", Width = 20, GetCellValue = (r) => r.Name },
-            };
+            ];
 
             var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamTeacherCourseOutput>().ToListAsync();
             var orgs = items.Select(t => new { t.SysOrg.Id, t.SysOrg.Name, t.SysOrg.TqesCode }).Distinct().ToList();
@@ -369,8 +369,8 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
         var item = input.Adapt<ExamTeacherCourse>();
         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?.ClearWhitespace();
 
         await item.InsertAsync();
     }
@@ -399,8 +399,8 @@ public class ExamTeacherCourseService : IExamTeacherCourseService, ITransient
 
         var item = input.Adapt<ExamTeacherCourse>();
         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?.ClearWhitespace();
 
         await item.UpdateIncludeNowAsync(new[] {
             nameof(item.Name),

+ 15 - 0
YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelCellStyle.cs

@@ -47,6 +47,7 @@ public class ExportExcelCellStyle
     /// 带背景列头样式
     /// </summary>
     public ICellStyle ColumnFillHeaderStyle { get; set; }
+    
     /// <summary>
     /// 居中对齐
     /// </summary>
@@ -59,6 +60,20 @@ public class ExportExcelCellStyle
     /// 居右对齐
     /// </summary>
     public ICellStyle RightCellStyle { get; set; }
+
+    /// <summary>
+    /// 居中对齐(自动换行)
+    /// </summary>
+    public ICellStyle CenterWrapCellStyle { get; set; }
+    /// <summary>
+    /// 居左对齐(自动换行)
+    /// </summary>
+    public ICellStyle LeftWrapCellStyle { get; set; }
+    /// <summary>
+    /// 居右对齐(自动换行)
+    /// </summary>
+    public ICellStyle RightWrapCellStyle { get; set; }
+
     /// <summary>
     /// 填充样式
     /// </summary>

+ 4 - 0
YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelColDto.cs

@@ -22,4 +22,8 @@ public class ExportExcelColDto<T>
     /// 获取单元格值
     /// </summary>
     public Func<T, object> GetCellValue { get; set; }
+    /// <summary>
+    /// 是否自动换行
+    /// </summary>
+    public bool WrapText { get; set; } = false;
 }

+ 5 - 0
YBEE.EQM.Application/ExportExcel/Dtos/ExportExcelDto.cs

@@ -73,4 +73,9 @@ public class ExportExcelDto<T>
     /// 表头行高
     /// </summary>
     public short HeaderHeight { get; set; } = ExportExcelCellStyle.DefaultRowHeight;
+
+    /// <summary>
+    /// 不设置行高
+    /// </summary>
+    public bool NotSetRowHeight { get; set; } = false;
 }

+ 124 - 9
YBEE.EQM.Application/ExportExcel/Services/ExportExcelService.cs

@@ -1,5 +1,7 @@
 using NPOI.HSSF.UserModel;
+using NPOI.OpenXmlFormats.Dml.Chart;
 using NPOI.SS.UserModel;
+using NPOI.SS.UserModel.Charts;
 using NPOI.SS.Util;
 using NPOI.XSSF.UserModel;
 
@@ -63,7 +65,10 @@ public class ExportExcelService : IExportExcelService, ISingleton
             if (summary != "")
             {
                 IRow summaryRow = sheet.CreateRow(rowNum++);
-                summaryRow.Height = ExportExcelCellStyle.DefaultRowHeight;
+                if (!input.NotSetRowHeight)
+                {
+                    summaryRow.Height = ExportExcelCellStyle.DefaultRowHeight;
+                }
                 AddCell(summary, summaryRow, 0, cellStyle.SummaryStyle, sheet);
                 sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum - 1, 0, input.Columns.Count - 1));
                 freezeRowCount++;
@@ -79,7 +84,10 @@ public class ExportExcelService : IExportExcelService, ISingleton
 
         #region 列头
         IRow headerRow = sheet.CreateRow(rowNum++);
-        headerRow.Height = input.HeaderHeight;
+        if (!input.NotSetRowHeight)
+        {
+            headerRow.Height = input.HeaderHeight;
+        }
         for (int i = 0; i < input.Columns.Count; i++)
         {
             var col = input.Columns[i];
@@ -92,18 +100,21 @@ public class ExportExcelService : IExportExcelService, ISingleton
         foreach (var item in input.Items)
         {
             IRow row = sheet.CreateRow(rowNum++);
-            row.Height = input.RowHeight ?? ExportExcelCellStyle.DefaultRowHeight;
+            if (!input.NotSetRowHeight)
+            {
+                row.Height = input.RowHeight ?? ExportExcelCellStyle.DefaultRowHeight;
+            }
             for (int i = 0; i < input.Columns.Count; i++)
             {
                 var col = input.Columns[i];
-                ICellStyle cstyle = cellStyle.CenterCellStyle;
+                ICellStyle cstyle = col.WrapText ? cellStyle.CenterWrapCellStyle : cellStyle.CenterCellStyle;
                 switch (col.Align)
                 {
                     case ExportExcelCellAlign.LEFT:
-                        cstyle = cellStyle.LeftCellStyle;
+                        cstyle = col.WrapText ? cellStyle.LeftWrapCellStyle : cellStyle.LeftCellStyle;
                         break;
                     case ExportExcelCellAlign.RIGHT:
-                        cstyle = cellStyle.RightCellStyle;
+                        cstyle = col.WrapText ? cellStyle.RightWrapCellStyle : cellStyle.RightCellStyle;
                         break;
                 }
                 AddCell(col.GetCellValue(item), row, i, cstyle, sheet, col.Width);
@@ -137,9 +148,14 @@ public class ExportExcelService : IExportExcelService, ISingleton
             SummaryStyle = wb.CreateCellStyle(),
             ColumnHeaderStyle = wb.CreateCellStyle(),
             ColumnFillHeaderStyle = wb.CreateCellStyle(),
+
             CenterCellStyle = wb.CreateCellStyle(),
             LeftCellStyle = wb.CreateCellStyle(),
             RightCellStyle = wb.CreateCellStyle(),
+            CenterWrapCellStyle = wb.CreateCellStyle(),
+            LeftWrapCellStyle = wb.CreateCellStyle(),
+            RightWrapCellStyle = wb.CreateCellStyle(),
+
             FillCellStyle = wb.CreateCellStyle(),
             PercentCellStyleP2 = wb.CreateCellStyle(),
             NumberCellStyleP2 = wb.CreateCellStyle(),
@@ -193,15 +209,14 @@ public class ExportExcelService : IExportExcelService, ISingleton
             ((XSSFColor)cellStyle.ColumnFillHeaderStyle.FillForegroundColorColor).SetRgb(new byte[] { 237, 237, 237 });
         }
 
-
-        // 默认居中样式
+        #region 内容样式
+        // 居中样式
         cellStyle.CenterCellStyle.Alignment = HorizontalAlignment.Center;
         cellStyle.CenterCellStyle.VerticalAlignment = VerticalAlignment.Center;
         cellStyle.CenterCellStyle.BorderTop = BorderStyle.Thin;
         cellStyle.CenterCellStyle.BorderLeft = BorderStyle.Thin;
         cellStyle.CenterCellStyle.BorderRight = BorderStyle.Thin;
         cellStyle.CenterCellStyle.BorderBottom = BorderStyle.Thin;
-        cellStyle.CenterCellStyle.WrapText = true;
         IFont cellFont = wb.CreateFont();
         cellFont.FontName = cellStyle.FontName;
         cellFont.FontHeightInPoints = fontSize;
@@ -214,6 +229,19 @@ public class ExportExcelService : IExportExcelService, ISingleton
         // 居右样式
         cellStyle.RightCellStyle.CloneStyleFrom(cellStyle.CenterCellStyle);
         cellStyle.RightCellStyle.Alignment = HorizontalAlignment.Right;
+        #endregion
+
+        #region 自动换行
+        // 居中样式(自动换行)
+        cellStyle.CenterWrapCellStyle.CloneStyleFrom(cellStyle.CenterCellStyle);
+        cellStyle.CenterWrapCellStyle.WrapText = true;
+        // 居左样式(自动换行)
+        cellStyle.LeftWrapCellStyle.CloneStyleFrom(cellStyle.LeftCellStyle);
+        cellStyle.LeftWrapCellStyle.WrapText = true;
+        // 居右样式(自动换行)
+        cellStyle.RightWrapCellStyle.CloneStyleFrom(cellStyle.RightCellStyle);
+        cellStyle.RightWrapCellStyle.WrapText = true;
+        #endregion
 
         // 背景样式
         cellStyle.FillCellStyle.CloneStyleFrom(cellStyle.CenterCellStyle);
@@ -299,4 +327,91 @@ public class ExportExcelService : IExportExcelService, ISingleton
         return cell;
     }
     #endregion
+
+    #region 图表
+    /// <summary>
+    /// 导出柱状图
+    /// </summary>
+    /// <param name="sheet"></param>
+    /// <param name="drawing"></param>
+    /// <param name="anchor"></param>
+    /// <param name="startDataRow"></param>
+    /// <param name="endDataRow"></param>
+    /// <param name="columnIndex"></param>
+    /// <param name="title"></param>
+    /// <param name="serieTitle"></param>
+    /// <param name="catalogTitle"></param>
+    /// <param name="valueTile"></param>
+    public void CreateBarChart(ISheet sheet, IDrawing drawing, IClientAnchor anchor, int startDataRow, int endDataRow, int columnIndex, string title = null, string serieTitle = null, string catalogTitle = null, string valueTile = null)
+    {
+        XSSFChart chart = (XSSFChart)drawing.CreateChart(anchor);
+        if (!string.IsNullOrEmpty(title))
+        {
+            chart.SetTitle(title);
+            chart.GetCTChart().title.tx.rich.p[0].pPr = new NPOI.OpenXmlFormats.Dml.CT_TextParagraphProperties
+            {
+                defRPr = new NPOI.OpenXmlFormats.Dml.CT_TextCharacterProperties() { sz = 1400 }
+            };
+        }
+
+        IBarChartData<string, double> barChartData = chart.ChartDataFactory.CreateBarChartData<string, double>();
+        IChartLegend legend = chart.GetOrCreateLegend();
+        legend.Position = LegendPosition.TopRight;
+        legend.IsOverlay = true;
+
+        IChartAxis bottomAxis = chart.ChartAxisFactory.CreateCategoryAxis(AxisPosition.Bottom);
+        bottomAxis.MajorTickMark = AxisTickMark.None;
+        IValueAxis leftAxis = chart.ChartAxisFactory.CreateValueAxis(AxisPosition.Left);
+        leftAxis.Crosses = AxisCrosses.AutoZero;
+        leftAxis.SetCrossBetween(AxisCrossBetween.Between);
+
+        IChartDataSource<string> categoryAxis = DataSources.FromStringCellRange(sheet, new CellRangeAddress(startDataRow, endDataRow, 0, 0));
+        IChartDataSource<double> valueAxis = DataSources.FromNumericCellRange(sheet, new CellRangeAddress(startDataRow, endDataRow, columnIndex, columnIndex));
+        var serie = barChartData.AddSeries(categoryAxis, valueAxis);
+        if (!string.IsNullOrEmpty(serieTitle))
+        {
+            serie.SetTitle(serieTitle);
+        }
+
+        chart.Plot(barChartData, bottomAxis, leftAxis);
+
+        var plotArea = chart.GetCTChart().plotArea;
+        plotArea.catAx[0].txPr = new CT_TextBody();
+        plotArea.catAx[0].txPr.AddNewP().pPr = new NPOI.OpenXmlFormats.Dml.CT_TextParagraphProperties()
+        {
+            defRPr = new NPOI.OpenXmlFormats.Dml.CT_TextCharacterProperties() { sz = 900 }
+        };
+        plotArea.catAx[0].majorTickMark = new CT_TickMark() { val = ST_TickMark.@out };
+        plotArea.valAx[0].txPr = new CT_TextBody();
+        plotArea.valAx[0].txPr.AddNewP().pPr = new NPOI.OpenXmlFormats.Dml.CT_TextParagraphProperties()
+        {
+            defRPr = new NPOI.OpenXmlFormats.Dml.CT_TextCharacterProperties() { sz = 900 }
+        };
+        plotArea.valAx[0].majorTickMark = new CT_TickMark() { val = ST_TickMark.@out };
+
+        var barChart = plotArea.barChart.First();
+        barChart.barDir = new CT_BarDir { val = ST_BarDir.col };
+
+        if (!string.IsNullOrEmpty(catalogTitle))
+        {
+            var aTitle = new CT_Title
+            {
+                tx = new CT_Tx()
+            };
+            aTitle.tx.rich = new CT_TextBody();
+            aTitle.tx.rich.AddNewP().AddNewR().t = catalogTitle;
+            plotArea.valAx[0].title = aTitle;
+        }
+        if (!string.IsNullOrEmpty(valueTile))
+        {
+            var aTitle = new CT_Title
+            {
+                tx = new CT_Tx()
+            };
+            aTitle.tx.rich = new CT_TextBody();
+            aTitle.tx.rich.AddNewP().AddNewR().t = valueTile;
+            plotArea.catAx[0].title = aTitle;
+        }
+    }
+    #endregion
 }

+ 17 - 0
YBEE.EQM.Application/ExportExcel/Services/IExportExcelService.cs

@@ -36,5 +36,22 @@ public interface IExportExcelService
     /// <param name="zeroToBlank">0转为空白</param>
     /// <param name="cellType"></param>
     ICell AddCell(object value, IRow row, int columnIndex, ICellStyle cellStyle, ISheet sheet = null, int? width = null, bool? zeroToBlank = false, CellType? cellType = null);
+
+    #region 图表
+    /// <summary>
+    /// 导出柱状图
+    /// </summary>
+    /// <param name="sheet"></param>
+    /// <param name="drawing"></param>
+    /// <param name="anchor"></param>
+    /// <param name="startDataRow"></param>
+    /// <param name="endDataRow"></param>
+    /// <param name="columnIndex"></param>
+    /// <param name="title"></param>
+    /// <param name="serieTitle"></param>
+    /// <param name="catalogTitle"></param>
+    /// <param name="valueTile"></param>
+    void CreateBarChart(ISheet sheet, IDrawing drawing, IClientAnchor anchor, int startDataRow, int endDataRow, int columnIndex, string title = null, string serieTitle = null, string catalogTitle = null, string valueTile = null);
+    #endregion
 }
 

+ 4 - 10
YBEE.EQM.Application/File/ResourceFile/ResourceFileAppService.cs

@@ -11,18 +11,12 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "file")]
 [Route("file")]
-public class ResourceFileAppService : IDynamicApiController
+public class ResourceFileAppService(IResourceFileService resourceFileService, IOptions<EqmSiteOptions> options) : IDynamicApiController
 {
-    private readonly IResourceFileService _resourceFileService;
-    private readonly EqmSiteOptions _eqmSiteOptions;
+    private readonly IResourceFileService _resourceFileService = resourceFileService;
+    private readonly EqmSiteOptions _eqmSiteOptions = options.Value;
 
-    private readonly string[] thumbImageTypes = { ".png", ".jpg", "jpeg", ".gif", ".bmp" };
-
-    public ResourceFileAppService(IResourceFileService resourceFileService, IOptions<EqmSiteOptions> options)
-    {
-        _resourceFileService = resourceFileService;
-        _eqmSiteOptions = options.Value;
-    }
+    //private readonly string[] thumbImageTypes = { ".png", ".jpg", "jpeg", ".gif", ".bmp" };
 
     /// <summary>
     /// 上传资源文件

+ 6 - 15
YBEE.EQM.Application/Job/ExamPatriarchQuestionnaireProgressSyncJob.cs

@@ -1,28 +1,19 @@
 using Furion.Schedule;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
+//using Microsoft.Extensions.Logging;
 
 namespace YBEE.EQM.Application;
 
 /// <summary>
 /// 学生家长问卷填答进度同步作业
 /// </summary>
-public class ExamPatriarchQuestionnaireProgressSyncJob : IJob
+//public class ExamPatriarchQuestionnaireProgressSyncJob(ILogger<ExamPatriarchQuestionnaireProgressSyncJob> logger, IServiceScopeFactory scopeFactory) : IJob
+public class ExamPatriarchQuestionnaireProgressSyncJob(IServiceScopeFactory scopeFactory) : IJob
 {
-    private readonly ILogger<ExamPatriarchQuestionnaireProgressSyncJob> _logger;
-    private readonly IServiceScopeFactory _scopeFactory;
-
-    public ExamPatriarchQuestionnaireProgressSyncJob(ILogger<ExamPatriarchQuestionnaireProgressSyncJob> logger, IServiceScopeFactory scopeFactory)
-    {
-        _logger = logger;
-        _scopeFactory = scopeFactory;
-    }
-
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
-        using var serviceScope = _scopeFactory.CreateScope();
-
-        //var examPatriarchQuestionnaireProgressSync = serviceScope.ServiceProvider.GetService<IExamPatriarchQuestionnaireProgressSync>();
-        //await examPatriarchQuestionnaireProgressSync.Sync();
+        using var serviceScope = scopeFactory.CreateScope();
+        var examPatriarchQuestionnaireProgressSync = serviceScope.ServiceProvider.GetService<IExamPatriarchQuestionnaireProgressSync>();
+        await examPatriarchQuestionnaireProgressSync.Sync();
     }
 }

+ 2 - 7
YBEE.EQM.Application/Ncee/NceeCourseComb/NceeCourseCombAppService.cs

@@ -5,13 +5,9 @@
 /// </summary>
 [ApiDescriptionSettings(Name = "ncee-course-comb")]
 [Route("ncee/course/comb")]
-public class NceeCourseCombAppService : IDynamicApiController
+public class NceeCourseCombAppService(INceeCourseCombService courseCombService) : IDynamicApiController
 {
-    private readonly INceeCourseCombService _nceeCourseCombService;
-    public NceeCourseCombAppService(INceeCourseCombService courseCombService)
-    {
-        _nceeCourseCombService = courseCombService;
-    }
+    private readonly INceeCourseCombService _nceeCourseCombService = courseCombService;
 
     /// <summary>
     /// 根据ID获取高中选科组合
@@ -26,7 +22,6 @@ public class NceeCourseCombAppService : IDynamicApiController
     /// 获取所有高中选科组合
     /// </summary>
     /// <returns></returns>
-    [AllowAnonymous]
     public async Task<List<NceeCourseCombOutput>> GetAllList()
     {
         return await _nceeCourseCombService.GetAllList();

+ 54 - 0
YBEE.EQM.Application/Ncee/NceeExport/Dtos/NceeExportDto.cs

@@ -0,0 +1,54 @@
+namespace YBEE.EQM.Application;
+
+public class ExportConvertScoreDto
+{
+    /// <summary>
+    /// 计划ID
+    /// </summary>
+    public int NceePlanId { get; set; }
+    /// <summary>
+    /// 机构列标题
+    /// </summary>
+    public string OrgTitle { get; set; } = "学校";
+    /// <summary>
+    /// 机构信息
+    /// </summary>
+    public SysOrgOutput Org { get; set; }
+    /// <summary>
+    /// 考号列名
+    /// </summary>
+    public string ExamNumberTitle { get; set; } = "考号";
+    /// <summary>
+    /// 是否导出转换区间
+    /// </summary>
+    public bool IsExportConvertRange { get; set; } = false;
+    /// <summary>
+    /// 是否导出排名
+    /// </summary>
+    public bool IsExportOrder { get; set; } = false;
+    /// <summary>
+    /// 是否导出选择方向
+    /// </summary>
+    public bool IsExportDirectionCourse { get; set; } = false;
+    /// <summary>
+    /// 是否导出组合
+    /// </summary>
+    public bool IsExportComb { get; set; } = false;
+    /// <summary>
+    /// 是否导出学生姓名
+    /// </summary>
+    public bool IsExportStudentName { get; set; } = false;
+    /// <summary>
+    /// 是否导出班级
+    /// </summary>
+    public bool IsExportClassNumber { get; set; } = false;
+
+    /// <summary>
+    /// 总序标题(转换分排序)
+    /// </summary>
+    public string OrderTotalTitle { get; set; } = "区序X";
+    /// <summary>
+    /// 机构序标题(转换分排序)
+    /// </summary>
+    public string OrderOrgTitle { get; set; } = "校序X";
+}

+ 42 - 0
YBEE.EQM.Application/Ncee/NceeExport/Dtos/NceeExportInput.cs

@@ -0,0 +1,42 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 导出新高考报表输入参数
+/// </summary>
+public class NceeExportInput
+{
+    /// <summary>
+    /// 计划ID
+    /// </summary>
+    [Required]
+    public int NceePlanId { get; set; }
+
+    /// <summary>
+    /// 导出整体分段统计
+    /// </summary>
+    public bool IsExportScoreRange { get; set; } = true;
+    /// <summary>
+    /// 导出各机构分段统计
+    /// </summary>
+    public bool IsExportOrgScoreRange { get; set; } = true;
+    /// <summary>
+    /// 导出整体划线统计
+    /// </summary>
+    public bool IsExportLine { get; set; } = true;
+    /// <summary>
+    /// 导出各机构划线统计
+    /// </summary>
+    public bool IsExportOrgLine { get; set; } = true;
+    /// <summary>
+    /// 导出整体班级划线统计
+    /// </summary>
+    public bool IsExportClassLine { get; set; } = true;
+    /// <summary>
+    /// 导出整体转换分
+    /// </summary>
+    public bool IsExportConvertScore { get; set; } = true;
+    /// <summary>
+    /// 导出各机构划线统计
+    /// </summary>
+    public bool IsExportOrgConvertScore { get; set; } = true;
+}

+ 5 - 13
YBEE.EQM.Application/Ncee/NceeExport/NceeExportAppService.cs

@@ -5,21 +5,15 @@
 /// </summary>
 [ApiDescriptionSettings(Name = "ncee-export")]
 [Route("ncee/export")]
-public class NceeExportAppService : IDynamicApiController
+public class NceeExportAppService(INceeExportService nceeExportService) : IDynamicApiController
 {
-    private readonly INceeExportService _nceeExportService;
-
-    public NceeExportAppService(INceeExportService nceeExportService)
-    {
-        _nceeExportService = nceeExportService;
-    }
+    private readonly INceeExportService _nceeExportService = nceeExportService;
 
     /// <summary>
     /// 导出联盟区县模拟划线报表
     /// </summary>
     /// <param name="nceePlanId"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     public async Task<IActionResult> ExportAllianceDistrict([FromQuery][Required] int nceePlanId)
     {
         var (fileName, fileBytes) = await _nceeExportService.ExportAllianceDistrict(nceePlanId);
@@ -31,12 +25,11 @@ public class NceeExportAppService : IDynamicApiController
     /// <summary>
     /// 导出已选科的模拟划线报表
     /// </summary>
-    /// <param name="nceePlanId"></param>
+    /// <param name="input">导出参数</param>
     /// <returns></returns>
-    [AllowAnonymous]
-    public async Task<IActionResult> ExportDirectionSeleted([FromQuery][Required] int nceePlanId)
+    public async Task<IActionResult> ExportDirectionSeleted(NceeExportInput input)
     {
-        var (fileName, fileBytes) = await _nceeExportService.ExportDirectionSeleted(nceePlanId);
+        var (fileName, fileBytes) = await _nceeExportService.ExportDirectionSeleted(input);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
@@ -47,7 +40,6 @@ public class NceeExportAppService : IDynamicApiController
     /// </summary>
     /// <param name="nceePlanId"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     public async Task<IActionResult> ExportDirectionUnseleted([FromQuery][Required] int nceePlanId)
     {
         var (fileName, fileBytes) = await _nceeExportService.ExportDirectionUnseleted(nceePlanId);

+ 2 - 2
YBEE.EQM.Application/Ncee/NceeExport/Services/INceeExportService.cs

@@ -16,10 +16,10 @@ public interface INceeExportService
     /// <summary>
     /// 导出已选科的模拟划线报表
     /// </summary>
-    /// <param name="nceePlanId"></param>
+    /// <param name="input">导出参数</param>
     /// <returns></returns>
     /// <exception cref="Exception"></exception>
-    Task<(string, byte[])> ExportDirectionSeleted(int nceePlanId);
+    Task<(string, byte[])> ExportDirectionSeleted(NceeExportInput input);
     /// <summary>
     /// 导出未选科的模拟划线报表
     /// </summary>

File diff suppressed because it is too large
+ 385 - 317
YBEE.EQM.Application/Ncee/NceeExport/Services/NceeExportService.cs


+ 3 - 12
YBEE.EQM.Application/Ncee/NceeScore/NceeScoreAppService.cs

@@ -8,21 +8,15 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "ncee-score")]
 [Route("ncee/score")]
-public class NceeScoreAppService : IDynamicApiController
+public class NceeScoreAppService(INceeScoreService nceeScoreService) : IDynamicApiController
 {
-    private readonly INceeScoreService _nceeScoreService;
-
-    public NceeScoreAppService(INceeScoreService nceeScoreService)
-    {
-        _nceeScoreService = nceeScoreService;
-    }
+    private readonly INceeScoreService _nceeScoreService = nceeScoreService;
 
     /// <summary>
-    /// 上传成绩(仅原始分,适用于五区联考)
+    /// 上传成绩(仅原始分,适用于五区联考或本区独立赋分)
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadOnlyRawScore([FromForm] UploadNceeScoreInput input)
@@ -46,7 +40,6 @@ public class NceeScoreAppService : IDynamicApiController
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadWithConvertScore([FromForm] UploadNceeScoreInput input)
@@ -70,7 +63,6 @@ public class NceeScoreAppService : IDynamicApiController
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     [RequestSizeLimit(long.MaxValue)]
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadNoDirectionCourse([FromForm] UploadNceeScoreInput input)
@@ -94,7 +86,6 @@ public class NceeScoreAppService : IDynamicApiController
     /// </summary>
     /// <param name="nceePlanId"></param>
     /// <returns></returns>
-    [AllowAnonymous]
     public async Task Execute([FromQuery][Required] int nceePlanId)
     {
         await _nceeScoreService.Execute(nceePlanId);

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

@@ -6,7 +6,7 @@
 public interface INceeScoreService
 {
     /// <summary>
-    /// 上传成绩(仅原始分,适用于五区联考)
+    /// 上传成绩(仅原始分,适用于五区联考或本区独立赋分
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>

+ 27 - 29
YBEE.EQM.Application/Ncee/NceeScore/Services/NceeScoreService.cs

@@ -7,18 +7,13 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 高中成绩管理服务
 /// </summary>
-public class NceeScoreService : INceeScoreService, ITransient
+public class NceeScoreService(IRepository<NceeScore> rep) : INceeScoreService, ITransient
 {
-    private readonly IRepository<NceeScore> _rep;
-    private readonly List<short> _chooseCourses = new() { 5, 6, 7, 9 };
-
-    public NceeScoreService(IRepository<NceeScore> rep)
-    {
-        _rep = rep;
-    }
+    private readonly IRepository<NceeScore> _rep = rep;
+    private readonly List<short> _chooseCourses = [5, 6, 7, 9];
 
     /// <summary>
-    /// 上传成绩(仅原始分,适用于五区联考)
+    /// 上传成绩(仅原始分,适用于五区联考或本区独立赋分)
     /// </summary>
     /// <param name="filePath"></param>
     /// <param name="nceePlanId"></param>
@@ -77,7 +72,7 @@ public class NceeScoreService : INceeScoreService, ITransient
                 { COURSE_COMB_INDEX, "选科组合" },
                 { COURSE_COMB_ID_INDEX, "选科组合ID" },
             };
-            List<string> headerErrors = new();
+            List<string> headerErrors = [];
             for (int i = 0; i < COURSE_START_INDEX; i++)
             {
                 if (headerRow.GetCell(i)?.ToString() != headers[i])
@@ -86,7 +81,7 @@ public class NceeScoreService : INceeScoreService, ITransient
                     headerErrors.Add(letter.ToString());
                 }
             }
-            if (headerErrors.Any())
+            if (headerErrors.Count != 0)
             {
                 string columnErrors = string.Join("、", headerErrors);
                 //result.ErrorMessage.Add($"第1行标题行{columnErrors}列名错误。从A列开始依次应为抽样类型、学校ID、学校、姓名、证件类型、证件号码、考号、年级、班级。");
@@ -104,7 +99,7 @@ public class NceeScoreService : INceeScoreService, ITransient
             var courseCombDict = courseCombs.ToDictionary(t => t.Id, t => t);
 
             // 获取需要导入的科目列表
-            Dictionary<int, Course> courses = new();
+            Dictionary<int, Course> courses = [];
             int validCellNum = COURSE_START_INDEX;
             for (int gi = COURSE_START_INDEX; gi < headerRow.LastCellNum; gi++)
             {
@@ -118,8 +113,8 @@ public class NceeScoreService : INceeScoreService, ITransient
             }
 
             int rn = 1;
-            List<NceeStudentImportDto> students = new();
-            List<NceeScoreImportDto> scores = new();
+            List<NceeStudentImportDto> students = [];
+            List<NceeScoreImportDto> scores = [];
             while (rows.MoveNext())
             {
                 rn++;
@@ -183,7 +178,7 @@ public class NceeScoreService : INceeScoreService, ITransient
                         continue;
                     }
 
-                    if (!courses.Keys.Contains(gi)) { continue; }
+                    if (!courses.ContainsKey(gi)) { continue; }
                     var course = courses[gi];
                     NceeScoreImportDto item = new()
                     {
@@ -238,7 +233,7 @@ public class NceeScoreService : INceeScoreService, ITransient
                 uid = 1;
             }
 
-            List<string> insertValues = new();
+            List<string> insertValues = [];
             int scount = students.Count;
             for (int i = 0; i < scount; i++)
             {
@@ -362,7 +357,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, score, scor
                 { COURSE_COMB_INDEX, "选科组合" },
                 { COURSE_COMB_ID_INDEX, "选科组合ID" },
             };
-            List<string> headerErrors = new();
+            List<string> headerErrors = [];
             for (int i = 0; i < COURSE_START_INDEX; i++)
             {
                 if (headerRow.GetCell(i)?.ToString() != headers[i])
@@ -371,7 +366,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, score, scor
                     headerErrors.Add(letter.ToString());
                 }
             }
-            if (headerErrors.Any())
+            if (headerErrors.Count != 0)
             {
                 string columnErrors = string.Join("、", headerErrors);
                 //result.ErrorMessage.Add($"第1行标题行{columnErrors}列名错误。从A列开始依次应为抽样类型、学校ID、学校、姓名、证件类型、证件号码、考号、年级、班级。");
@@ -474,7 +469,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, score, scor
                 //}
 
                 // 取各科成绩
-                List<NceeScoreImportDto> courseScores = new();
+                List<NceeScoreImportDto> courseScores = [];
                 for (int gi = COURSE_START_INDEX; gi < validCellNum; gi++)
                 {
                     var cell = row.GetCell(gi);
@@ -690,7 +685,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, ncee_conver
                 { EXAM_NUMBER_INDEX, "考号" },
                 { CLASS_INDEX, "班级号" },
             };
-            List<string> headerErrors = new();
+            List<string> headerErrors = [];
             for (int i = 0; i < COURSE_START_INDEX; i++)
             {
                 if (headerRow.GetCell(i)?.ToString() != headers[i])
@@ -918,7 +913,7 @@ INSERT INTO ncee_score(id, ncee_plan_id, ncee_student_id, course_id, ncee_conver
     {
         var baseLines = await _rep.Change<NceeBaseLine>().Where(t => t.NceePlanId == nceePlanId).OrderBy(t => t.NceeLineLevel).ToListAsync();
         // 单科有效分
-        List<NceeCourseLineScore> courseLineScores = new();
+        List<NceeCourseLineScore> courseLineScores = [];
 
 
         #region 计算总有效分
@@ -947,16 +942,19 @@ WHERE T1.rn <= (SELECT COUNT(1) FROM ncee_student WHERE ncee_plan_id = @nceePlan
         #region 计算单科有效分
         if (config.CalcCourseLineScoreEnabled)
         {
+            // 1.总分排序,合并单科独立排序
+            // 2.取总分上线分对应行的单科成绩为有效分
+
             foreach (var line in baseLines)
             {
                 // 计算单科有效分
                 var courseLineScoreX = await _rep.SqlQueryAsync<NceeCourseLineScoreCalcDto>($@"
 SELECT 
-    MIN(T1.yuwen_score_x) AS yuwen_score_x, 
-    MIN(T1.shuxue_score_x) AS shuxue_score_x, 
-    MIN(T1.yingyu_score_x) AS yingyu_score_x, 
-    MIN(T1.fangxiang_score_x) AS fangxiang_score_x, 
-    MIN(T1.zonghe_score_x) AS zonghe_score_x
+    MIN(T1.yuwen_score_x) AS yuwen_score_x,             -- 语文
+    MIN(T1.shuxue_score_x) AS shuxue_score_x,           -- 数学
+    MIN(T1.yingyu_score_x) AS yingyu_score_x,           -- 英语
+    MIN(T1.fangxiang_score_x) AS fangxiang_score_x,     -- 方向(物理或历史)
+    MIN(T1.zonghe_score_x) AS zonghe_score_x            -- 综合
 FROM
 (
     SELECT T1.rn, 
@@ -1494,7 +1492,7 @@ FROM
             await _rep.Change<NceeConvertRange>().InsertNowAsync(convertRanges);
 
             // 更新转换分
-            List<string> replaceValues = new();
+            List<string> replaceValues = [];
             int cscount = scores.Count;
             for (int i = 0; i < cscount; i++)
             {
@@ -1593,7 +1591,7 @@ SET T1.order_in_total = T2.order_in_total,
         var cgs = convertGrades.OrderBy(t => t.Id).ToList();
         var scores = nceeScores.OrderByDescending(t => t.Score).ToList();
 
-        List<NceeConvertRange> ncrs = new();
+        List<NceeConvertRange> ncrs = [];
         decimal lastMinScore = decimal.MaxValue;
         foreach (var cg in cgs)
         {
@@ -1604,7 +1602,7 @@ SET T1.order_in_total = T2.order_in_total,
                 CourseId = courseId,
             };
 
-            var i = (int)Math.Min(Math.Ceiling(cg.EndRate * scount), scount - 1);
+            var i = (int)Math.Min(Math.Floor(cg.EndRate * scount), scount - 1);
             if (cg.BeginRate == 0)
             {
                 range.MaxScore = totalMaxScore;

+ 36 - 61
YBEE.EQM.Application/System/Auth/Services/SysAuthService.cs

@@ -9,43 +9,15 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 认证服务
 /// </summary>
-public class SysAuthService : ISysAuthService, ITransient
+public class SysAuthService(IOptions<AuthOptions> options,
+    IHttpContextAccessor httpContextAccessor,
+    IEventPublisher eventPublisher,
+    IRepository<SysUser> userRep,
+    ISysRoleUserService roleUserService,
+    ISysMenuService menuService,
+    IGeneralCaptchaService captchaService) : ISysAuthService, ITransient
 {
-    private readonly AuthOptions _authOptions;
-
-    private readonly IHttpContextAccessor _httpContextAccessor;
-    private readonly IEventPublisher _eventPublisher;
-
-    private readonly IRepository<SysUser> _userRep;
-
-    private readonly ISysRoleService _roleService;
-    private readonly ISysRoleUserService _roleUserService;
-    private readonly ISysMenuService _menuService;
-    private readonly IGeneralCaptchaService _captchaService;
-    private readonly ICacheService _cacheService;
-
-    public SysAuthService(IOptions<AuthOptions> options,
-        IHttpContextAccessor httpContextAccessor,
-        IEventPublisher eventPublisher,
-        IRepository<SysUser> userRep,
-        ISysRoleService roleService,
-        ISysRoleUserService roleUserService,
-        ISysMenuService menuService,
-        IGeneralCaptchaService captchaService,
-        ICacheService cacheService)
-    {
-        _authOptions = options.Value;
-        _httpContextAccessor = httpContextAccessor;
-        _eventPublisher = eventPublisher;
-
-        _userRep = userRep;
-
-        _roleService = roleService;
-        _roleUserService = roleUserService;
-        _menuService = menuService;
-        _captchaService = captchaService;
-        _cacheService = cacheService;
-    }
+    private readonly AuthOptions _authOptions = options.Value;
 
     /// <summary>
     /// 登录处理
@@ -54,7 +26,7 @@ public class SysAuthService : ISysAuthService, ITransient
     /// <returns></returns>
     private async Task<AuthOutput> Login(LoginInput accountLoginInput)
     {
-        var checkCaptcha = _captchaService.CheckCode(accountLoginInput.Captcha);
+        var checkCaptcha = captchaService.CheckCode(accountLoginInput.Captcha);
         if (checkCaptcha.Code != 0)
         {
             throw Oops.Oh(checkCaptcha.Message);
@@ -84,13 +56,14 @@ public class SysAuthService : ISysAuthService, ITransient
         //                          .Include(t => t.SysOrg)
         //                          .Where(u => (u.Account.Equals(accountLoginInput.Account) || u.Mobile.Equals(accountLoginInput.Account) || u.Email.Equals(accountLoginInput.Account)) && u.IsDeleted == false)
         //                          .ToListAsync();
-        var user = await _userRep.Include(t => t.SysOrg)
+        var user = await userRep.Include(t => t.SysOrg)
                                  .FirstOrDefaultAsync(u => u.IsDeleted == false &&
-                                                         (u.Account.Equals(accountLoginInput.Account) || 
-                                                            u.Mobile.Equals(accountLoginInput.Account) || 
+                                                         (u.Account.Equals(accountLoginInput.Account) ||
+                                                            u.Mobile.Equals(accountLoginInput.Account) ||
                                                             u.Email.Equals(accountLoginInput.Account)
                                                          )) ?? throw Oops.Oh(ErrorCode.E1001);
-        if (!AESEncryption.Decrypt(user.Password, _authOptions.AesPassword).Equals(pwd))
+        //if (!AESEncryption.Decrypt(user.Password, _authOptions.AesPassword).Equals(pwd))
+        if (!PBKDF2Encryption.Compare(pwd, user.Password))
         {
             throw Oops.Oh(ErrorCode.E1001);
         }
@@ -98,7 +71,8 @@ public class SysAuthService : ISysAuthService, ITransient
 
         if (!user.IsActivated && newPwd != "")
         {
-            user.Password = AESEncryption.Encrypt(newPwd, _authOptions.AesPassword);
+            //user.Password = AESEncryption.Encrypt(newPwd, _authOptions.AesPassword);
+            user.Password = PBKDF2Encryption.Encrypt(newPwd);
             user.IsActivated = true;
             user.ActivateTime = DateTime.Now;
             await user.UpdateIncludeNowAsync(new[] { nameof(user.Password), nameof(user.IsActivated), nameof(user.ActivateTime) });
@@ -123,7 +97,7 @@ public class SysAuthService : ISysAuthService, ITransient
             throw Oops.Oh(ErrorCode.E1003);
         }
 
-        var isSuperAdmin = await _roleUserService.IsSuperAdmin(user.Id);
+        var isSuperAdmin = await roleUserService.IsSuperAdmin(user.Id);
 
         // 生成Token令牌
         var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
@@ -137,13 +111,13 @@ public class SysAuthService : ISysAuthService, ITransient
         });
 
         // 设置Swagger自动登录
-        _httpContextAccessor.HttpContext.SigninToSwagger(accessToken);
+        httpContextAccessor.HttpContext.SigninToSwagger(accessToken);
 
         // 生成刷新Token令牌
         var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, App.GetOptions<RefreshTokenSettingOptions>().ExpiredTime);
 
         // 设置刷新Token令牌
-        _httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
+        httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
 
         ret.AccessToken = accessToken;
         return ret;
@@ -165,12 +139,12 @@ public class SysAuthService : ISysAuthService, ITransient
     /// <returns></returns>
     public async Task<LoginOutput> GetLoginUser()
     {
-        var user = await _userRep.Include(t => t.SysOrg).FirstOrDefaultAsync(u => u.Id == CurrentSysUserInfo.SysUserId) ?? throw Oops.Oh(ErrorCode.E1002);
+        var user = await userRep.Include(t => t.SysOrg).FirstOrDefaultAsync(u => u.Id == CurrentSysUserInfo.SysUserId) ?? throw Oops.Oh(ErrorCode.E1002);
         var userId = user.Id;
 
         var loginOutput = user.Adapt<LoginOutput>();
 
-        var httpContext = _httpContextAccessor.HttpContext;
+        var httpContext = httpContextAccessor.HttpContext;
 
         loginOutput.LastLoginTime = user.LastLoginTime = DateTime.Now;
         loginOutput.LastLoginIp = user.LastLoginIp = httpContext.GetRequestIPv4();
@@ -180,19 +154,19 @@ public class SysAuthService : ISysAuthService, ITransient
         loginOutput.LastLoginOs = client.OS.Family + client.OS.Major;
 
         // 角色信息
-        loginOutput.SysRoles = await _roleUserService.GetLoginUserRoleList(userId);
+        loginOutput.SysRoles = await roleUserService.GetLoginUserRoleList(userId);
         // 权限信息
-        loginOutput.Permissions = await _menuService.GetLoginPermissionList(userId);
+        loginOutput.Permissions = await menuService.GetLoginPermissionList(userId);
         // 系统所有权限信息
-        loginOutput.AllPermissions = await _menuService.GetAllPermissionList();
+        loginOutput.AllPermissions = await menuService.GetAllPermissionList();
         // 菜单信息
-        loginOutput.Menus = await _menuService.GetLoginAntMenus(userId);
+        loginOutput.Menus = await menuService.GetLoginAntMenus(userId);
 
         // 更新用户最后登录Ip和时间
-        await _userRep.UpdateIncludeAsync(user, new[] { nameof(SysUser.LastLoginIp), nameof(SysUser.LastLoginTime) });
+        await userRep.UpdateIncludeAsync(user, new[] { nameof(SysUser.LastLoginIp), nameof(SysUser.LastLoginTime) });
 
         // 增加登录日志
-        await _eventPublisher.PublishAsync(new ChannelEventSource("Create:VisLog",
+        await eventPublisher.PublishAsync(new ChannelEventSource("Create:VisLog",
             new SysLogVis
             {
                 Name = loginOutput.Name,
@@ -214,12 +188,12 @@ public class SysAuthService : ISysAuthService, ITransient
     /// <returns></returns>
     public async Task Logout()
     {
-        var ip = _httpContextAccessor.HttpContext.GetRequestIPv4();
-        _httpContextAccessor.HttpContext.SignoutToSwagger();
+        var ip = httpContextAccessor.HttpContext.GetRequestIPv4();
+        httpContextAccessor.HttpContext.SignoutToSwagger();
         //_httpContextAccessor.HttpContext.Response.Headers["access-token"] = "invalid token";
 
         // 增加退出日志
-        await _eventPublisher.PublishAsync(new ChannelEventSource("Create:VisLog",
+        await eventPublisher.PublishAsync(new ChannelEventSource("Create:VisLog",
             new SysLogVis
             {
                 Name = CurrentSysUserInfo.Name,
@@ -239,7 +213,7 @@ public class SysAuthService : ISysAuthService, ITransient
     public Task<GeneralCaptchaOutput> GetCaptcha()
     {
         // 图片大小要与前端保持一致(坐标范围)
-        return Task.FromResult(_captchaService.CreateCaptchaImage());
+        return Task.FromResult(captchaService.CreateCaptchaImage());
     }
 
     /// <summary>
@@ -249,7 +223,7 @@ public class SysAuthService : ISysAuthService, ITransient
     /// <returns></returns>
     public Task<GeneralCaptchaOutput> VerifyCaptcha(GeneralCaptchaInput input)
     {
-        return Task.FromResult(_captchaService.CheckCode(input));
+        return Task.FromResult(captchaService.CheckCode(input));
     }
 
     /// <summary>
@@ -259,11 +233,12 @@ public class SysAuthService : ISysAuthService, ITransient
     /// <returns></returns>
     public List<string> GetTempPassword(List<string> input)
     {
-        List<string> ret = new();
+        List<string> ret = [];
         foreach (string pwd in input)
         {
-            var aesPwd = AESEncryption.Encrypt(pwd, _authOptions.AesPassword);
-            ret.Add(aesPwd);
+            //var npwd = AESEncryption.Encrypt(pwd, _authOptions.AesPassword);
+            var npwd = PBKDF2Encryption.Encrypt(pwd);
+            ret.Add(npwd);
         }
         return ret;
     }

+ 14 - 6
YBEE.EQM.Application/System/Auth/SysAuthAppService.cs

@@ -7,13 +7,9 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "sys-auth")]
 [Route("sys/auth")]
-public class SysAuthAppService : IDynamicApiController
+public class SysAuthAppService(ISysAuthService authService) : IDynamicApiController
 {
-    private readonly ISysAuthService _authService;
-    public SysAuthAppService(ISysAuthService authService)
-    {
-        _authService = authService;
-    }
+    private readonly ISysAuthService _authService = authService;
 
     /// <summary>
     /// 账户密码登录
@@ -77,4 +73,16 @@ public class SysAuthAppService : IDynamicApiController
     {
         return _authService.GetTempPassword(input);
     }
+
+    [AllowAnonymous, DisableOpLog]
+    public string GetPbkdf2(string pwd)
+    {
+        return PBKDF2Encryption.Encrypt(pwd);
+    }
+
+    [AllowAnonymous, DisableOpLog]
+    public bool ComparePbkdf2(string pwd, string pbpwd)
+    {
+        return PBKDF2Encryption.Compare(pwd, pbpwd);
+    }
 }

+ 3 - 9
YBEE.EQM.Application/System/Dict/SysDictDataAppService.cs

@@ -7,14 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "sys-dict-data")]
 [Route("sys/dict/data")]
-public class SysDictDataAppService : IDynamicApiController
+public class SysDictDataAppService(ISysDictDataService dictDataService) : IDynamicApiController
 {
-    private readonly ISysDictDataService _dictDataService;
-    public SysDictDataAppService(ISysDictDataService dictDataService)
-    {
-        _dictDataService = dictDataService;
-    }
-
     /// <summary>
     /// 获取所有字典列表
     /// </summary>
@@ -23,7 +17,7 @@ public class SysDictDataAppService : IDynamicApiController
     [DisableOpLog]
     public async Task<List<SysDictDataOutput>> GetAllList()
     {
-        return await _dictDataService.GetAllList();
+        return await dictDataService.GetAllList();
     }
 
     /// <summary>
@@ -35,6 +29,6 @@ public class SysDictDataAppService : IDynamicApiController
     [DisableOpLog]
     public async Task<List<SysDictDataOutput>> GetListByDictTypeId(int dictTypeId)
     {
-        return await _dictDataService.GetListByDictTypeId(dictTypeId);
+        return await dictDataService.GetListByDictTypeId(dictTypeId);
     }
 }

+ 2 - 8
YBEE.EQM.Application/System/Dict/SysDictTypeAppService.cs

@@ -7,14 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "sys-dict-type")]
 [Route("sys/dict/type")]
-public class SysDictTypeAppService : IDynamicApiController
+public class SysDictTypeAppService(ISysDictTypeService dictTypeService) : IDynamicApiController
 {
-    private readonly ISysDictTypeService _dictTypeService;
-    public SysDictTypeAppService(ISysDictTypeService dictTypeService)
-    {
-        _dictTypeService = dictTypeService;
-    }
-
     /// <summary>
     /// 获取所有字典类型列表
     /// </summary>
@@ -23,6 +17,6 @@ public class SysDictTypeAppService : IDynamicApiController
     [DisableOpLog]
     public async Task<List<SysDictTypeOutput>> GetAllList()
     {
-        return await _dictTypeService.GetAllList();
+        return await dictTypeService.GetAllList();
     }
 }

+ 20 - 32
YBEE.EQM.Application/System/Role/Services/SysRoleService.cs

@@ -6,20 +6,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 角色服务
 /// </summary>
-public class SysRoleService : ISysRoleService, ITransient
+public class SysRoleService(IRepository<SysRole> roleRep, IRepository<SysRoleUser> roleUserRep, ISysRoleMenuService roleMenuService, ICacheService cacheService) : ISysRoleService, ITransient
 {
-    private readonly IRepository<SysRole> _roleRep;
-    private readonly IRepository<SysRoleUser> _roleUserRep;
-    private readonly ISysRoleMenuService _roleMenuService;
-    private readonly ICacheService _cacheService;
-
-    public SysRoleService(IRepository<SysRole> roleRep, IRepository<SysRoleUser> roleUserRep, ISysRoleMenuService roleMenuService, ICacheService cacheService)
-    {
-        _roleRep = roleRep;
-        _roleUserRep = roleUserRep;
-        _roleMenuService = roleMenuService;
-        _cacheService = cacheService;
-    }
 
     /// <summary>
     /// 添加角色
@@ -28,7 +16,7 @@ public class SysRoleService : ISysRoleService, ITransient
     /// <returns></returns>
     public async Task Add(AddSysRoleInput input)
     {
-        var isExist = await _roleRep.DetachedEntities.AnyAsync(u => u.Name == input.Name);
+        var isExist = await roleRep.DetachedEntities.AnyAsync(u => u.Name == input.Name);
         if (isExist)
         {
             throw Oops.Oh(ErrorCode.E2004);
@@ -46,7 +34,7 @@ public class SysRoleService : ISysRoleService, ITransient
     /// <returns></returns>
     public async Task Del(DeleteSysRoleInput input)
     {
-        var sysRole = await _roleRep.FirstOrDefaultAsync(u => u.Id == input.Id);
+        var sysRole = await roleRep.FirstOrDefaultAsync(u => u.Id == input.Id);
         if (sysRole.RoleType != RoleType.NORMAL)
         {
             throw Oops.Oh(ErrorCode.E8201);
@@ -55,11 +43,11 @@ public class SysRoleService : ISysRoleService, ITransient
         await sysRole.DeleteNowAsync();
 
         //级联删除该角色对应的用户-角色表关联信息
-        var userRoles = await _roleUserRep.Where(u => u.SysRoleId == sysRole.Id).ToListAsync();
-        await _roleUserRep.DeleteAsync(userRoles);
+        var userRoles = await roleUserRep.Where(u => u.SysRoleId == sysRole.Id).ToListAsync();
+        await roleUserRep.DeleteAsync(userRoles);
 
         //级联删除该角色对应的角色-菜单表关联信息
-        await _roleMenuService.DeleteRoleMenuListByRoleId(sysRole.Id);
+        await roleMenuService.DeleteRoleMenuListByRoleId(sysRole.Id);
     }
     /// <summary>
     /// 更新角色
@@ -68,13 +56,13 @@ public class SysRoleService : ISysRoleService, ITransient
     /// <returns></returns>
     public async Task Update(UpdateSysRoleInput input)
     {
-        var role = await _roleRep.DetachedEntities.FirstOrDefaultAsync(u => u.Id == input.Id);
+        var role = await roleRep.DetachedEntities.FirstOrDefaultAsync(u => u.Id == input.Id);
         if (role.RoleType != RoleType.NORMAL)
         {
             throw Oops.Oh(ErrorCode.E8202);
         }
 
-        var isExist = await _roleRep.DetachedEntities.AnyAsync(u => u.Name == input.Name && u.Id != input.Id);
+        var isExist = await roleRep.DetachedEntities.AnyAsync(u => u.Name == input.Name && u.Id != input.Id);
         if (isExist)
         {
             throw Oops.Oh(ErrorCode.E2004);
@@ -83,22 +71,22 @@ public class SysRoleService : ISysRoleService, ITransient
         var sysRole = input.Adapt<SysRole>();
         await sysRole.UpdateExcludeNowAsync(new[] { nameof(sysRole.RoleDataScope) });
     }
-    /// <summary>
-    /// 设置角色数据权限范围
-    /// </summary>
-    /// <returns></returns>
-    public async Task SetDataScope()
-    {
-        // TODO:
-        // 清空缓存,DelByPatternAsync,CommonConst.CACHE_KEY_PERMISSION
-    }
+    ///// <summary>
+    ///// 设置角色数据权限范围
+    ///// </summary>
+    ///// <returns></returns>
+    //public async Task SetDataScope()
+    //{
+    //    // TODO:
+    //    // 清空缓存,DelByPatternAsync,CommonConst.CACHE_KEY_PERMISSION
+    //}
     /// <summary>
     /// 获取当前用户角色数据权限范围
     /// </summary>
     /// <returns></returns>
     public async Task<RoleDataScope> GetCurrentUserDataScope()
     {
-        var roles = await _cacheService.GetRoleDataScope(CurrentSysUserInfo.SysUserId);
+        var roles = await cacheService.GetRoleDataScope(CurrentSysUserInfo.SysUserId);
         if (roles == null)
         {
             return new();
@@ -121,7 +109,7 @@ public class SysRoleService : ISysRoleService, ITransient
     /// <returns></returns>
     public async Task<List<SysRoleOutput>> GetAllList()
     {
-        return await _roleRep.DetachedEntities.OrderBy(u => u.RoleType).ProjectToType<SysRoleOutput>().ToListAsync();
+        return await roleRep.DetachedEntities.OrderBy(u => u.RoleType).ProjectToType<SysRoleOutput>().ToListAsync();
     }
     /// <summary>
     /// 根据角色ID列表获取角色列表
@@ -130,6 +118,6 @@ public class SysRoleService : ISysRoleService, ITransient
     /// <returns></returns>
     public async Task<List<SysRoleOutput>> GetListByIds(List<int> ids)
     {
-        return await _roleRep.DetachedEntities.Where(t => ids.Contains(t.Id)).ProjectToType<SysRoleOutput>().ToListAsync();
+        return await roleRep.DetachedEntities.Where(t => ids.Contains(t.Id)).ProjectToType<SysRoleOutput>().ToListAsync();
     }
 }

+ 5 - 0
YBEE.EQM.Application/System/User/Services/ISysUserService.cs

@@ -24,5 +24,10 @@ namespace YBEE.EQM.Application
         /// <param name="input"></param>
         /// <returns></returns>
         Task<PageResult<SysUserSimpleOutput>> QueryUserSimplePageList(SysUserPageInput input);
+        ///// <summary>
+        ///// 更换密码加密方式
+        ///// </summary>
+        ///// <returns></returns>
+        //Task UpdatePassword();
     }
 }

+ 25 - 15
YBEE.EQM.Application/System/User/Services/SysUserService.cs

@@ -7,16 +7,9 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 系统用户服务
 /// </summary>
-public class SysUserService : ISysUserService, ITransient
+public class SysUserService(IRepository<SysUser> userRep, IOptions<AuthOptions> options) : ISysUserService, ITransient
 {
-    private readonly IRepository<SysUser> _userRep;
-    private readonly AuthOptions _authOptions;
-
-    public SysUserService(IRepository<SysUser> userRep, IOptions<AuthOptions> options)
-    {
-        _userRep = userRep;
-        _authOptions = options.Value;
-    }
+    private readonly AuthOptions _authOptions = options.Value;
 
     /// <summary>
     /// 获取所有用户列表
@@ -24,7 +17,7 @@ public class SysUserService : ISysUserService, ITransient
     /// <returns></returns>
     public async Task<List<SysUserLiteOutput>> GetAllList()
     {
-        var items = await _userRep.DetachedEntities.Where(u => u.Status == CommonStatus.ENABLE)
+        var items = await userRep.DetachedEntities.Where(u => u.Status == CommonStatus.ENABLE)
                                                     .ProjectToType<SysUserLiteOutput>()
                                                     .ToListAsync();
         return items;
@@ -43,11 +36,13 @@ public class SysUserService : ISysUserService, ITransient
             throw Oops.Oh(ErrorCode.E1006);
         }
 
-        if ((await _userRep.DetachedEntities.AsQueryable().ToListAsync()).Any(t => t.Id == CurrentSysUserInfo.SysUserId && AESEncryption.Decrypt(t.Password, _authOptions.AesPassword).Equals(oldPwd)))
+        //if ((await _userRep.DetachedEntities.AsQueryable().ToListAsync()).Any(t => t.Id == CurrentSysUserInfo.SysUserId && AESEncryption.Decrypt(t.Password, _authOptions.AesPassword).Equals(oldPwd)))
+        if ((await userRep.DetachedEntities.AsQueryable().ToListAsync()).Any(t => t.Id == CurrentSysUserInfo.SysUserId && PBKDF2Encryption.Compare(oldPwd, t.Password)))
         {
-            var user = await _userRep.FirstOrDefaultAsync(t => t.Id == CurrentSysUserInfo.SysUserId);
-            user.Password = AESEncryption.Encrypt(newPwd, _authOptions.AesPassword);
-            await user.UpdateIncludeNowAsync(new[] { "Password" });
+            var user = await userRep.FirstOrDefaultAsync(t => t.Id == CurrentSysUserInfo.SysUserId);
+            //user.Password = AESEncryption.Encrypt(newPwd, _authOptions.AesPassword);
+            user.Password = PBKDF2Encryption.Encrypt(newPwd);
+            await user.UpdateIncludeNowAsync(["Password"]);
         }
         else
         {
@@ -66,6 +61,21 @@ public class SysUserService : ISysUserService, ITransient
         return ret;
     }
 
+    ///// <summary>
+    ///// 更换密码加密方式
+    ///// </summary>
+    ///// <returns></returns>
+    //public async Task UpdatePassword()
+    //{
+    //    var items = await userRep.Where(t => t.Id > 1).ToListAsync();
+    //    foreach (var item in items)
+    //    {
+    //        string pwd = AESEncryption.Decrypt(item.Password, _authOptions.AesPassword);
+    //        item.Password = PBKDF2Encryption.Encrypt(pwd);
+    //        await item.UpdateIncludeAsync(["Password"]);
+    //    }
+    //}
+
     #region 私有方法
     private IQueryable<SysUser> GetQueryBase(SysUserPageInput input)
     {
@@ -75,7 +85,7 @@ public class SysUserService : ISysUserService, ITransient
         var email = !string.IsNullOrEmpty(input.Email?.Trim());
         var searchValue = !string.IsNullOrEmpty(input.SearchValue?.Trim());
 
-        var query = _userRep.DetachedEntities.Where(t => t.IsDeleted == false)
+        var query = userRep.DetachedEntities.Where(t => t.IsDeleted == false)
                                              .Where((searchValue, u => EF.Functions.Like(u.Name, $"%{input.SearchValue.Trim()}%") || EF.Functions.Like(u.Account, $"%{input.SearchValue.Trim()}%")))
                                              .Where(name, u => EF.Functions.Like(u.Name, $"%{input.Name.Trim()}%"))
                                              .Where(mobile, u => EF.Functions.Like(u.Mobile, $"%{input.Mobile.Trim()}%"))

+ 12 - 10
YBEE.EQM.Application/System/User/SysUserAppService.cs

@@ -8,14 +8,8 @@ namespace YBEE.EQM.Application
     [ApiDescriptionSettings(Name = "sys-user")]
     [Route("sys/user")]
     [AppAuthorize]
-    public class SysUserAppService : IDynamicApiController
+    public class SysUserAppService(ISysUserService sysUserService) : IDynamicApiController
     {
-        private readonly ISysUserService _sysUserService;
-        public SysUserAppService(ISysUserService sysUserService)
-        {
-            _sysUserService = sysUserService;
-        }
-
         /// <summary>
         /// 修改密码
         /// </summary>
@@ -23,9 +17,8 @@ namespace YBEE.EQM.Application
         /// <returns></returns>
         public async Task ChangePassword(ChangeSysUserPasswordInput input)
         {
-            await _sysUserService.ChangePassword(input);
+            await sysUserService.ChangePassword(input);
         }
-
         /// <summary>
         /// 查询简要用户列表
         /// </summary>
@@ -33,7 +26,16 @@ namespace YBEE.EQM.Application
         /// <returns></returns>
         public async Task<PageResult<SysUserSimpleOutput>> QueryUserSimplePageList(SysUserPageInput input)
         {
-            return await _sysUserService.QueryUserSimplePageList(input);
+            return await sysUserService.QueryUserSimplePageList(input);
         }
+        ///// <summary>
+        ///// 更换密码加密方式
+        ///// </summary>
+        ///// <returns></returns>
+        //[AllowAnonymous]
+        //public async Task UpdatePassword()
+        //{
+        //    await _sysUserService.UpdatePassword();
+        //}
     }
 }

+ 4 - 5
YBEE.EQM.Application/YBEE.EQM.Application.csproj

@@ -37,10 +37,6 @@
 		</Content>
 	</ItemGroup>
 
-	<ItemGroup>
-	  <ProjectReference Include="..\YBEE.EQM.Core\YBEE.EQM.Core.csproj" />
-	</ItemGroup>
-
 	<ItemGroup>
 	  <Folder Include="Base\Person\Services\" />
 	  <Folder Include="Base\SchoolClassStudent\Services\" />
@@ -52,7 +48,6 @@
 	  <Folder Include="Ncee\NceeConvertRange\Services\" />
 	  <Folder Include="Ncee\NceeCourseLineScore\Dtos\" />
 	  <Folder Include="Ncee\NceeCourseLineScore\Services\" />
-	  <Folder Include="Ncee\NceeExport\Dtos\" />
 	  <Folder Include="Ncee\NceeScoreRange\Dtos\" />
 	  <Folder Include="Ncee\NceeScoreRange\Services\" />
 	  <Folder Include="Ncee\NceeStudent\Services\" />
@@ -60,4 +55,8 @@
 	  <Folder Include="Summary\Services\" />
 	</ItemGroup>
 
+	<ItemGroup>
+	  <ProjectReference Include="..\YBEE.EQM.Core\YBEE.EQM.Core.csproj" />
+	</ItemGroup>
+
 </Project>

File diff suppressed because it is too large
+ 661 - 28
YBEE.EQM.Application/YBEE.EQM.Application.xml


+ 3 - 2
YBEE.EQM.Application/applicationsettings.Development.json

@@ -32,8 +32,9 @@
     ]
   },
   "ConnectionStrings": {
-    //"DbMain": "Data Source=183.230.108.224;Database=ybee_eqm;User ID=root;Password=ir2019@spring;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;"
-    "DbMain": "Data Source=localhost;Database=ybee_eqm;User ID=root;Password=rj123*456;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;"
+    //"DbMain": "Data Source=183.230.108.224;Database=ybee_eqm;User ID=root;Password=ir2019@spring;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;",
+    "DbMain": "Data Source=localhost;Database=ybee_eqm;User ID=root;Password=rj123*456;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;",
+    "TQES": "Data Source=183.230.108.224,5002; Initial Catalog=TQES_NEW; User Id=sa; Password=tqes123*456; TrustServerCertificate=true;"
   },
   "Auth": {
     "AesPassword": "73d42fd3e84462de4218d122b24fdffa",

+ 2 - 1
YBEE.EQM.Application/applicationsettings.Production.json

@@ -32,7 +32,8 @@
     ]
   },
   "ConnectionStrings": {
-    "DbMain": "Data Source=localhost;Database=ybee_eqm;User ID=mc;Password=ir2019@spring;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;"
+    "DbMain": "Data Source=localhost;Database=ybee_eqm;User ID=mc;Password=ir2019@spring;pooling=true;port=3306;sslmode=none;CharSet=utf8;AllowPublicKeyRetrieval=True;",
+    "TQES": "Data Source=localhost; Initial Catalog=TQES_NEW; User Id=sa; Password=tqes123*456; TrustServerCertificate=true;"
   },
   "Auth": {
     "AesPassword": "73d42fd3e84462de4218d122b24fdffa",

+ 6 - 0
YBEE.EQM.Core/Entities/Base/Grade.cs

@@ -43,6 +43,12 @@ public class Grade : IEntity
     [Required, StringLength(50)]
     public string Name { get; set; }
     /// <summary>
+    /// 名称2
+    /// </summary>
+    [Comment("名称2")]
+    [Required, StringLength(50)]
+    public string Name2 { get; set; }
+    /// <summary>
     /// 全称
     /// </summary>
     [Comment("全称")]

+ 9 - 0
YBEE.EQM.Core/Entities/Exam/ExamSample.cs

@@ -73,6 +73,11 @@ public partial class ExamSample : DEntityBase
     [Required, Column(TypeName = "json")]
     public string Config { get; set; } = "{}";
 
+    /// <summary>
+    /// 成绩引用监测计划ID
+    /// </summary>
+    [Comment("成绩引用监测计划ID")]
+    public int? ExamScoreRefExamPlanId { get; set; }
 
     /// <summary>
     /// 是否选中使用的方案
@@ -99,4 +104,8 @@ public partial class ExamSample : DEntityBase
     /// 一对一引用(选中使用操作用户)
     /// </summary>
     public virtual SysUser SelectedSysUser { get; set; }
+    /// <summary>
+    /// 一对一引用(成绩引用监测计划)
+    /// </summary>
+    public virtual ExamPlan ExamScoreRefExamPlan { get; set; }
 }

+ 24 - 1
YBEE.EQM.Core/Entities/Exam/ExamScore.cs

@@ -82,6 +82,12 @@ public class ExamScore : IEntity
     [Comment("选科组合ID")]
     public short? NceeCourseCombId { get; set; }
     /// <summary>
+    /// 监测号
+    /// </summary>
+    [Comment("监测号")]
+    [StringLength(20)]
+    public string ExamNumber { get; set; }
+    /// <summary>
     /// 科目ID
     /// </summary>
     [Comment("科目ID")]
@@ -99,11 +105,28 @@ public class ExamScore : IEntity
     [Comment("是否排除")]
     [Required]
     public bool IsExcluded { get; set; } = false;
-
+    /// <summary>
+    /// 是否特殊学生
+    /// </summary>
+    [Comment("是否特殊学生")]
+    [Required]
+    public bool IsSpecial { get; set; } = false;
+    /// <summary>
+    /// 是否缺考
+    /// </summary>
+    [Comment("是否缺考")]
+    [Required]
+    public bool IsAbsent { get; set; } = false;
     /// <summary>
     /// 分数分段ID
     /// </summary>
     [Comment("分数分段ID")]
     [Required]
     public int ExamScoreRangeId { get; set; } = 0;
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [Comment("备注")]
+    [StringLength(100)]
+    public string Remark { get; set; }
 }

+ 18 - 2
YBEE.EQM.Core/Entities/Exam/ExamScoreTotal.cs

@@ -81,12 +81,17 @@ public class ExamScoreTotal
     [Comment("选科组合ID")]
     public short? NceeCourseCombId { get; set; }
     /// <summary>
+    /// 监测号
+    /// </summary>
+    [Comment("监测号")]
+    [StringLength(20)]
+    public string ExamNumber { get; set; }
+    /// <summary>
     /// 科目数量
     /// </summary>
     [Comment("科目数量")]
     [Required]
     public short CourseCount { get; set; }
-
     /// <summary>
     /// 成绩
     /// </summary>
@@ -99,11 +104,22 @@ public class ExamScoreTotal
     [Comment("是否排除")]
     [Required]
     public bool IsExcluded { get; set; } = false;
-
+    /// <summary>
+    /// 是否特殊学生
+    /// </summary>
+    [Comment("是否特殊学生")]
+    [Required]
+    public bool IsSpecial { get; set; } = false;
     /// <summary>
     /// 分数分段ID
     /// </summary>
     [Comment("分数分段ID")]
     [Required]
     public int ExamScoreRangeId { get; set; } = 0;
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [Comment("备注")]
+    [StringLength(100)]
+    public string Remark { get; set; }
 }

+ 2 - 0
YBEE.EQM.Core/Enums/NceeDataScopeType.cs

@@ -1,5 +1,7 @@
 using System.ComponentModel;
 
+namespace YBEE.EQM.Core;
+
 /// <summary>
 /// 高中模拟划线数据范围类型
 /// </summary>

+ 20 - 0
YBEE.EQM.Core/Enums/NceeDirectionCourse.cs

@@ -0,0 +1,20 @@
+using System.ComponentModel;
+
+namespace YBEE.EQM.Core.Enums;
+
+/// <summary>
+/// 新高考选择方向科目
+/// </summary>
+public enum NceeDirectionCourse
+{
+    /// <summary>
+    /// 物理类
+    /// </summary>
+    [Description("物理类")]
+    PHYSICS = 4,
+    /// <summary>
+    /// 历史类
+    /// </summary>
+    [Description("历史类")]
+    HISTORY = 8
+}

Some files were not shown because too many files changed in this diff