Bläddra i källkod

1.增加小学、初中的有效分分析;
2.增加用户密码重置;
3.增加特殊和缺测查询和导出。

beetle 1 år sedan
förälder
incheckning
fdc1305119
100 ändrade filer med 7668 tillägg och 1242 borttagningar
  1. 5 0
      .gitignore
  2. 59 0
      YBEE.EQM.Admin/config/routes.ts
  3. 2 1
      YBEE.EQM.Admin/package.json
  4. BIN
      YBEE.EQM.Admin/public/handbook/缺测替补学生抽取操作说明.pdf
  5. 29 0
      YBEE.EQM.Admin/src/common/helper.ts
  6. 6 4
      YBEE.EQM.Admin/src/components/FileUpload/index.tsx
  7. 46 20
      YBEE.EQM.Admin/src/components/SuperTable/index.tsx
  8. 6 0
      YBEE.EQM.Admin/src/global.less
  9. 7 0
      YBEE.EQM.Admin/src/pages/auth/Login/index.tsx
  10. 27 15
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamOrgList.tsx
  11. 10 13
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleEditModal.tsx
  12. 330 218
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleList.tsx
  13. 36 11
      YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/index.tsx
  14. 305 0
      YBEE.EQM.Admin/src/pages/exam-center/absent-replace/AbsentReplaceList/index.tsx
  15. 96 0
      YBEE.EQM.Admin/src/pages/exam-center/absent-replace/index.tsx
  16. 153 0
      YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleCount/index.tsx
  17. 44 86
      YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleDetail/index.tsx
  18. 300 0
      YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleStudent/index.tsx
  19. 108 0
      YBEE.EQM.Admin/src/pages/exam-center/sample/components/ExamSampleBaseInfo.tsx
  20. 316 0
      YBEE.EQM.Admin/src/pages/exam-center/special-student/SpecialStudentList/index.tsx
  21. 96 0
      YBEE.EQM.Admin/src/pages/exam-center/special-student/index.tsx
  22. 3 0
      YBEE.EQM.Admin/src/pages/exam-org/OrgExamPlan/index.tsx
  23. 7 2
      YBEE.EQM.Admin/src/pages/exam-org/absent-replace/OrgExamAbsentReplaceReport/components/ExamAbsentReplaceDetailDrawer.tsx
  24. 174 0
      YBEE.EQM.Admin/src/pages/exam-org/sample-replace/OrgExamSampleReplaceList/components/OrgExamSampleReplaceSampleDrawer.tsx
  25. 303 0
      YBEE.EQM.Admin/src/pages/exam-org/sample-replace/OrgExamSampleReplaceList/index.tsx
  26. 105 0
      YBEE.EQM.Admin/src/pages/exam-org/sample-replace/index.tsx
  27. 1 1
      YBEE.EQM.Admin/src/pages/exam-org/sample/OrgExamSampleList/index.tsx
  28. 6 2
      YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentReport/components/ExamSpecialStudentEditModal.tsx
  29. 27 14
      YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentReport/index.tsx
  30. 179 0
      YBEE.EQM.Admin/src/pages/ncee/NceePlan/components/NceePlanEditModal.tsx
  31. 234 0
      YBEE.EQM.Admin/src/pages/ncee/NceePlan/index.tsx
  32. 81 0
      YBEE.EQM.Admin/src/pages/ncee/NceePlanDetail/index.tsx
  33. 2 2
      YBEE.EQM.Admin/src/pages/system/Role/components/RoleUserAddModal.tsx
  34. 5 0
      YBEE.EQM.Admin/src/pages/system/Role/components/UserList.tsx
  35. 124 36
      YBEE.EQM.Admin/src/pages/system/User/index.tsx
  36. 29 0
      YBEE.EQM.Admin/src/services/apis/EsaProcessingController.ts
  37. 99 0
      YBEE.EQM.Admin/src/services/apis/ExamAbsentReplaceCenterController.ts
  38. 22 3
      YBEE.EQM.Admin/src/services/apis/ExamDataPublishController.ts
  39. 16 0
      YBEE.EQM.Admin/src/services/apis/ExamDataReportController.ts
  40. 16 0
      YBEE.EQM.Admin/src/services/apis/ExamOrgController.ts
  41. 26 0
      YBEE.EQM.Admin/src/services/apis/ExamPlanController.ts
  42. 45 3
      YBEE.EQM.Admin/src/services/apis/ExamSampleController.ts
  43. 100 0
      YBEE.EQM.Admin/src/services/apis/ExamSampleReplaceController.ts
  44. 16 0
      YBEE.EQM.Admin/src/services/apis/ExamSampleStudentController.ts
  45. 99 0
      YBEE.EQM.Admin/src/services/apis/ExamSpecialStudentCenterController.ts
  46. 13 0
      YBEE.EQM.Admin/src/services/apis/NceePlanController.ts
  47. 29 0
      YBEE.EQM.Admin/src/services/apis/SysUserController.ts
  48. 12 0
      YBEE.EQM.Admin/src/services/apis/index.ts
  49. 519 3
      YBEE.EQM.Admin/src/services/typing.d.ts
  50. 1 1
      YBEE.EQM.Application/Base/Semester/Services/SemesterService.cs
  51. 21 0
      YBEE.EQM.Application/Esa/Dtos/EsaProcessingDto.cs
  52. 23 0
      YBEE.EQM.Application/Esa/EsaProcessingAppService.cs
  53. 719 0
      YBEE.EQM.Application/Esa/Services/EsaProcessingService.cs
  54. 14 0
      YBEE.EQM.Application/Esa/Services/IEsaProcessingService.cs
  55. 1 0
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Dtos/ExamAbsentReplaceMapper.cs
  56. 13 2
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Dtos/ExamAbsentReplaceOutput.cs
  57. 14 23
      YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceAppService.cs
  58. 6 13
      YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceAuditAppService.cs
  59. 55 0
      YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceCenterAppService.cs
  60. 27 30
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceAuditService.cs
  61. 225 0
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceCenterService.cs
  62. 68 93
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceService.cs
  63. 33 0
      YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/IExamAbsentReplaceCenterService.cs
  64. 19 0
      YBEE.EQM.Application/Exam/ExamDataPublish/Dtos/ExamDataPublishInput.cs
  65. 77 0
      YBEE.EQM.Application/Exam/ExamDataPublish/Dtos/ExamDataPublishOutput.cs
  66. 21 18
      YBEE.EQM.Application/Exam/ExamDataPublish/ExamDataPublishAppService.cs
  67. 96 18
      YBEE.EQM.Application/Exam/ExamDataPublish/Services/ExamDataPublishService.cs
  68. 9 2
      YBEE.EQM.Application/Exam/ExamDataPublish/Services/IExamDataPublishService.cs
  69. 11 0
      YBEE.EQM.Application/Exam/ExamDataReport/Dtos/ExamDataReportInput.cs
  70. 11 0
      YBEE.EQM.Application/Exam/ExamDataReport/Dtos/ExamDataReportOutput.cs
  71. 21 21
      YBEE.EQM.Application/Exam/ExamDataReport/ExamDataReportAppService.cs
  72. 35 24
      YBEE.EQM.Application/Exam/ExamDataReport/Services/ExamDataReportService.cs
  73. 7 2
      YBEE.EQM.Application/Exam/ExamDataReport/Services/IExamDataReportService.cs
  74. 20 10
      YBEE.EQM.Application/Exam/ExamOrg/Dtos/ExamOrgOutput.cs
  75. 18 16
      YBEE.EQM.Application/Exam/ExamOrg/ExamOrgAppService.cs
  76. 37 29
      YBEE.EQM.Application/Exam/ExamOrg/Services/ExamOrgService.cs
  77. 6 0
      YBEE.EQM.Application/Exam/ExamOrg/Services/IExamOrgService.cs
  78. 4 4
      YBEE.EQM.Application/Exam/ExamOrgDataReport/Dtos/ExamOrgDataReportOutput.cs
  79. 21 19
      YBEE.EQM.Application/Exam/ExamOrgDataReport/Services/ExamOrgDataReportService.cs
  80. 68 77
      YBEE.EQM.Application/Exam/ExamOrgScoreReport/Services/ExamOrgScoreReportService.cs
  81. 25 3
      YBEE.EQM.Application/Exam/ExamPlan/Dtos/ExamPlanOutput.cs
  82. 24 2
      YBEE.EQM.Application/Exam/ExamPlan/ExamPlanAppService.cs
  83. 11 2
      YBEE.EQM.Application/Exam/ExamPlan/Services/ExamPlanService.cs
  84. 6 0
      YBEE.EQM.Application/Exam/ExamPlan/Services/IExamPlanService.cs
  85. 195 196
      YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingAvgRangeService.cs
  86. 4 0
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleDto.cs
  87. 4 0
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleInput.cs
  88. 86 2
      YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleOutput.cs
  89. 48 31
      YBEE.EQM.Application/Exam/ExamSample/ExamSampleAppService.cs
  90. 545 167
      YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleService.cs
  91. 114 0
      YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleWorker.cs
  92. 35 3
      YBEE.EQM.Application/Exam/ExamSample/Services/IExamSampleService.cs
  93. 69 0
      YBEE.EQM.Application/Exam/ExamSampleReplace/Dtos/ExamSampleReplaceInput.cs
  94. 101 0
      YBEE.EQM.Application/Exam/ExamSampleReplace/Dtos/ExamSampleReplaceOutput.cs
  95. 63 0
      YBEE.EQM.Application/Exam/ExamSampleReplace/ExamSampleReplaceAppService.cs
  96. 280 0
      YBEE.EQM.Application/Exam/ExamSampleReplace/Services/ExamSampleReplaceService.cs
  97. 40 0
      YBEE.EQM.Application/Exam/ExamSampleReplace/Services/IExamSampleReplaceService.cs
  98. 9 0
      YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentInput.cs
  99. 14 0
      YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentMapper.cs
  100. 21 0
      YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentOutput.cs

+ 5 - 0
.gitignore

@@ -23,3 +23,8 @@ temp/
 # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
 # should NOT be excluded as they contain compiler settings and other important
 # information for Eclipse / Flash Builder.
+
+
+**/applicationsettings.*.json
+**/applicationsettings.Development.json
+

+ 59 - 0
YBEE.EQM.Admin/config/routes.ts

@@ -80,6 +80,16 @@ export default [
                 path: '/exam-c/plan/sample/detail/:id',
                 component: './exam-center/sample/ExamSampleDetail',
             },
+            /** 监测抽样名单 */
+            {
+                path: '/exam-c/plan/sample/stu-list/:id',
+                component: './exam-center/sample/ExamSampleStudent',
+            },
+            /** 监测抽样统计 */
+            {
+                path: '/exam-c/plan/sample/count/:id',
+                component: './exam-center/sample/ExamSampleCount',
+            },
             /** 监测科目管理*/
             {
                 path: '/exam-c/plan/course/:examPlanId',
@@ -87,6 +97,16 @@ export default [
             },
 
             // ------------------------------------------------
+            /** 监测特殊学生管理计划列表 */
+            {
+                path: '/exam-c/sp-stu',
+                component: './exam-center/special-student',
+            },
+            /** 监测特殊学生列表 */
+            {
+                path: '/exam-c/sp-stu/list/:examPlanId',
+                component: './exam-center/special-student/SpecialStudentList',
+            },
             /** 监测特殊学生审核计划列表 */
             {
                 path: '/exam-c/sp-stu-audit',
@@ -104,6 +124,16 @@ export default [
             },
 
             // ------------------------------------------------
+            /** 监测缺测替补学生管理计划列表 */
+            {
+                path: '/exam-c/absent',
+                component: './exam-center/absent-replace',
+            },
+            /** 监测缺测替补学生列表 */
+            {
+                path: '/exam-c/absent/list/:examPlanId',
+                component: './exam-center/absent-replace/AbsentReplaceList',
+            },
             /** 监测缺测替补学生审核计划列表 */
             {
                 path: '/exam-c/absent-audit',
@@ -169,6 +199,23 @@ export default [
         ],
     },
 
+    /** 高中分析 */
+    {
+        path: '/ncee',
+        routes: [
+            // ------------------------------------------------
+            /** 分析计划管理 */
+            {
+                path: '/ncee/plan',
+                component: './ncee/NceePlan',
+            },
+            /** 分析计划详情 */
+            {
+                path: '/ncee/plan/detail/:id',
+                component: './ncee/NceePlanDetail',
+            },
+        ],
+    },
 
     // -------------------------------------------------------------------
     // 学校端
@@ -320,6 +367,18 @@ export default [
                 path: '/exam-s/questionnaire/patriarch/progress/:examPlanId',
                 component: './exam-org/questionnaire/patriarch/OrgExamPatriarchProgress',
             },
+
+
+            // ------------------------------------------------
+            /** 缺测替补抽取列表 */
+            {
+                path: '/exam-s/replace',
+                component: './exam-org/sample-replace',
+            },
+            {
+                path: '/exam-s/replace/list/:examPlanId/:examDataPublishId',
+                component: './exam-org/sample-replace/OrgExamSampleReplaceList',
+            },
         ],
     },
 

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

@@ -1,6 +1,6 @@
 {
     "name": "ybee.eqm",
-    "version": "1.0.1",
+    "version": "1.1.0",
     "private": true,
     "description": "",
     "scripts": {
@@ -66,6 +66,7 @@
         "omit.js": "^2.0.2",
         "query-string": "^8.1.0",
         "rc-menu": "^9.8.2",
+        "rc-resize-observer": "^1.4.3",
         "rc-util": "^5.27.2",
         "react": "^18.2.0",
         "react-dev-inspector": "^2.0.0",

BIN
YBEE.EQM.Admin/public/handbook/缺测替补学生抽取操作说明.pdf


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

@@ -1,3 +1,4 @@
+import { SortOrder } from "antd/lib/table/interface";
 import JSEncrypt from "jsencrypt";
 
 /** 数组项交换位置 */
@@ -233,6 +234,32 @@ export function calcElementTop(ele?: HTMLElement | null) {
     return t;
 }
 
+/**
+ * 构造排序列表
+ * @param sorts 
+ * @param onlyLeaf 仅保留叶子节点
+ * @returns 
+ */
+export function buildSortOrders(sorts?: { [key: string]: SortOrder | undefined }, onlyLeaf?: boolean) {
+    let sortFields: string[] = [];
+    if (!sorts) {
+        return sortFields;
+    }
+    for (let key in sorts) {
+        if (sorts.hasOwnProperty(key)) {
+            let a = sorts[key] === 'ascend' ? 'asc' : 'desc';
+            if (onlyLeaf) {
+                const ks = key.split(',');
+                sortFields.push(`${ks[ks.length - 1]} ${a}`);
+            }
+            else {
+                sortFields.push(`${key.replaceAll(',', '.')} ${a}`);
+            }
+        }
+    }
+    return sortFields;
+}
+
 export default {
     /** 数组项交换位置 */
     swapArrayItem,
@@ -252,4 +279,6 @@ export default {
     decryptJWT,
     /** 获取元素屏幕 Offset Top */
     calcElementTop,
+    /** 构造排序列表 */
+    buildSortOrders,
 };

+ 6 - 4
YBEE.EQM.Admin/src/components/FileUpload/index.tsx

@@ -4,8 +4,7 @@ import { App, Button, Progress, Tooltip, theme } from "antd";
 import { useRef, useState } from "react";
 import TextEllipsisMiddle from "../TextEllipsisMiddle";
 
-/** 列表式文件上传 */
-const FileUpload: React.FC<{
+export type FileUploadProps = {
     /** 添加文件按钮文本 */
     addText?: string;
     /** 上传错误重选文件按钮文本 */
@@ -20,7 +19,10 @@ const FileUpload: React.FC<{
     limitSize?: number;
     /** 上传处理回调 */
     onUpload: (file: File, onUploadProgress: (progress: number) => void) => Promise<{ success: boolean; errorType?: 'fileTypeError'; errorMessage?: string; }>;
-}> = ({ addText, reselectText, retryText, accept, tipText, limitSize, onUpload }) => {
+};
+
+/** 列表式文件上传 */
+const FileUpload: React.FC<FileUploadProps> = ({ addText, reselectText, retryText, accept, tipText, limitSize, onUpload }) => {
     const [uploading, setUploading] = useState(false);
     const [file, setFile] = useState<File>();
     const [uploadStatus, setUploadStatus] = useState<'active' | 'exception' | 'success' | 'normal'>('normal');
@@ -150,7 +152,7 @@ const FileUpload: React.FC<{
                 />
                 {!file &&
                     <div className="add">
-                        <Button type="link" icon={<PlusOutlined />} disabled={uploading} onClick={handleChoiceFile}>
+                        <Button type="link" size="small" icon={<PlusOutlined />} disabled={uploading} onClick={handleChoiceFile}>
                             {addText ?? '上传文件'}
                         </Button>
                         {(tipText || limitSize) && <span className="tip">{tipText}{limitSize && `(最大${limitSize}MB)`}</span>}

+ 46 - 20
YBEE.EQM.Admin/src/components/SuperTable/index.tsx

@@ -2,10 +2,11 @@ import { DefaultPagination } from '@/common/constant';
 import type { ParamsType, ProTableProps, RequestData } from '@ant-design/pro-components';
 import { ProTable } from '@ant-design/pro-components';
 import type { SearchConfig } from '@ant-design/pro-table/lib/components/Form/FormRender';
+import { TablePaginationConfig } from 'antd';
 import type { SortOrder } from 'antd/lib/table/interface';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 
-export type MesTableProps<T, U, ValueType> = ProTableProps<T, U, ValueType>;
+export type MesTableProps<T, U, ValueType> = ProTableProps<T, U, ValueType> & { initialPageSize?: number };
 
 /**
  * ProTable封装
@@ -19,14 +20,35 @@ const SuperTable = <
 >(
     props: MesTableProps<T, U, ValueType>,
 ) => {
-    const { search, pagination, options, ...restProps } = props;
+    const { search, pagination, options, initialPageSize, ...restProps } = props;
     const { filterType, ...restSearch } = search || {};
 
-    const [pageSize, setPageSize] = useState(DefaultPagination.PAGE_SIZE);
+    // const [pageSize, setPageSize] = useState(DefaultPagination.PAGE_SIZE);
+    const [superPagination, setSuperPagination] = useState<false | TablePaginationConfig | undefined>(pagination === false ? false : {
+        pageSizeOptions: [5, 10, 15, 20, 25, 30, 50, 100, 200, 500],
+        showSizeChanger: true,
+        pageSize: initialPageSize ?? DefaultPagination.PAGE_SIZE,
+        size: 'default',
+        onChange(current: number, pageSize: number) {
+            // setPageSize(pageSize);
+            setSuperPagination((v: any) => ({ ...v, current, pageSize }));
+        },
+
+        ...(pagination ?? {}),
+    });
+
+    useEffect(() => {
+        if (pagination === false) {
+            setSuperPagination(false);
+        }
+        else {
+            setSuperPagination(v => ({ ...v, ...(pagination || {}) }));
+        }
+    }, [pagination]);
 
     const ww = window.document.body.clientWidth;
 
-    let newSearch: SearchConfig = {
+    const newSearch: SearchConfig = {
         span: {
             xs: 24,
             sm: 12,
@@ -40,31 +62,35 @@ const SuperTable = <
     };
 
     if (!filterType || filterType !== 'light') {
-        // newSearch.showHiddenNum = true;
+        newSearch.showHiddenNum = true;
     }
-    newSearch = { ...newSearch, ...restSearch };
+    // newSearch = { ...newSearch, ...restSearch };
+
+    const psearch: any = search === false ? false : { ...newSearch, ...restSearch };
 
-    const { onChange, ...restPagination } = pagination || {};
+    // const { onChange, ...restPagination } = pagination || {};
 
     return (
         <ProTable<T, U, ValueType>
             rowKey="id"
             bordered
             size="small"
-            search={search === false ? false : newSearch}
+            // search={search === false ? false : newSearch}
+            search={psearch}
             sticky={{ offsetHeader: 56 }}
             pagination={
-                pagination !== false && {
-                    pageSizeOptions: [10, 15, 20, 25, 30, 50, 100, 200, 500, 1000],
-                    showSizeChanger: true,
-                    pageSize: pageSize,
-                    size: 'default',
-                    onChange(page, pageSize) {
-                        setPageSize(pageSize);
-                        onChange?.(page, pageSize);
-                    },
-                    ...restPagination,
-                }
+                // pagination !== false && {
+                //     pageSizeOptions: [10, 15, 20, 25, 30, 50, 100, 200, 500, 1000],
+                //     showSizeChanger: true,
+                //     pageSize: pageSize,
+                //     size: 'default',
+                //     onChange(page, pageSize) {
+                //         setPageSize(pageSize);
+                //         onChange?.(page, pageSize);
+                //     },
+                //     ...restPagination,
+                // }
+                superPagination
             }
             options={{
                 density: false,

+ 6 - 0
YBEE.EQM.Admin/src/global.less

@@ -123,4 +123,10 @@ ol {
 
 .yb-row-editable {
   background-color: #00000011;
+}
+
+.yb-row-summary {
+  background-color: #fafafa;
+  font-weight: bold;
+  font-style: italic;
 }

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

@@ -221,14 +221,21 @@ const Login: React.FC = () => {
     const { minLen, maxLen, special } = validatePasswordComplexityInititalValue;
     const [passwordValid, setPasswordValid] = useState<ValidatePasswordComplexityType>({ minLen, maxLen, special });
 
+    const timerRef = useRef<any>();
+
     // 获取验证码
     const fetchCaptcha = useCallback(async () => {
+        if (timerRef.current) {
+            clearInterval(timerRef.current);
+        }
         setCaptchaLoading(true);
         const captcha = await SysAuthController.getCaptcha();
         setCaptchaLoading(false);
         if (captcha) {
             setCaptcha(captcha);
         }
+        // 2分钟自动刷新验证码
+        timerRef.current = setInterval(fetchCaptcha, 120000);
     }, []);
     useEffect(() => {
         fetchCaptcha();

+ 27 - 15
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamOrgList.tsx

@@ -1,9 +1,9 @@
 import { TrueOrFalseValueEnum } from "@/common/valueEnum";
-import { CardStepTitle, StatusIcon, TagStatus } from "@/components";
+import { CardStepTitle, StatusIcon, SuperTable, TagStatus } from "@/components";
 import ExamOrgController from "@/services/apis/ExamOrgController";
 import ExamOrgDataReportController from "@/services/apis/ExamOrgDataReportController";
 import { DataReportStatus } from "@/services/enums";
-import { ActionType, ProColumns, ProTable } from "@ant-design/pro-components";
+import { ActionType, ProColumns } from "@ant-design/pro-components";
 import { useModel } from "@umijs/max";
 import { App, Button, Space, theme } from "antd";
 import { useCallback, useRef, useState } from "react";
@@ -156,7 +156,7 @@ const ExamOrgList: React.FC<{ examPlanId: number, examDataReports: API.ExamDataR
 
     return (
         <>
-            <ProTable<API.ExamOrgOutput>
+            <SuperTable<API.ExamOrgOutput>
                 headerTitle={<CardStepTitle>监测机构</CardStepTitle>}
                 style={{ marginTop: token.margin }}
                 search={false}
@@ -183,18 +183,30 @@ const ExamOrgList: React.FC<{ examPlanId: number, examDataReports: API.ExamDataR
                         orgActionRef.current?.reset?.();
                     },
                 }}
-                request={async (params) => {
-                    const res = await ExamOrgController.queryPageList({
-                        pageIndex: params.current ?? 1,
-                        pageSize: params.pageSize ?? 20,
-                        examPlanId,
-                        orgName: searchKeyWordsRef.current
-                    });
-                    return {
-                        data: res?.items,
-                        success: true,
-                        total: res?.totalCount,
-                    };
+                // request={async (params) => {
+                request={async (params, sort) => {
+                    // const res = await ExamOrgController.queryPageList({
+                    //     pageIndex: params.current ?? 1,
+                    //     pageSize: params.pageSize ?? 20,
+                    //     examPlanId,
+                    //     orgName: searchKeyWordsRef.current
+                    // });
+                    // return {
+                    //     data: res?.items,
+                    //     success: true,
+                    //     total: res?.totalCount,
+                    // };
+                    try {
+                        return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                            const res = await ExamOrgController.queryPageList({
+                                ...p,
+                                examPlanId,
+                                orgName: searchKeyWordsRef.current,
+                            });
+                            return res;
+                        });
+                    }
+                    catch (ex) { return {}; }
                 }}
                 rowSelection={{
                     selectedRowKeys,

+ 10 - 13
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleEditModal.tsx

@@ -1,7 +1,6 @@
-import { MovableModalForm } from "@/components";
 import ExamPlanController from "@/services/apis/ExamPlanController";
 import ExamSampleController from "@/services/apis/ExamSampleController";
-import { ProFormCheckbox, ProFormDependency, ProFormDigit, ProFormItem, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
+import { DrawerForm, 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";
@@ -28,20 +27,22 @@ const ExamSampleEditModal: React.FC<{
 
     const { config, ...restData } = data;
     return (
-        <MovableModalForm<API.ExamSampleOutput>
+        <DrawerForm<API.ExamSampleOutput>
             title="监测抽样方案参数配置"
             width={800}
             open={open}
-            modalProps={{
-                centered: true,
+            drawerProps={{
                 maskClosable: false,
-                onCancel: handleClose,
+                onClose: handleClose,
             }}
             initialValues={{
                 config: {
                     percent: 40,
                     startPosition: 1,
                     interval: 2,
+                    isRandomSampling: false,
+                    isExcludeSpecialStudent: true,
+                    specialStudentMustApproved: true,
                     ...config,
                 },
                 ...restData
@@ -52,10 +53,6 @@ const ExamSampleEditModal: React.FC<{
                     let p: API.AddExamSampleInput = {
                         ...restValues,
                         examPlanId: data.examPlanId ?? 0,
-                        // config: JSON.stringify({
-                        //     ...data.config,
-                        //     ...config,
-                        // }),
                         config: {
                             ...data.config,
                             ...config,
@@ -120,7 +117,7 @@ const ExamSampleEditModal: React.FC<{
                 fieldProps={{
                     options: [
                         { label: '随机抽样', value: true },
-                        { label: '等距抽样', value: false }
+                        { label: <Typography.Text>等距抽样<Typography.Text type="secondary">(班级无成绩时将采用随机抽样)</Typography.Text></Typography.Text>, value: false }
                     ],
                 }}
                 rules={[{ required: true, message: '请选择抽样方式' }]}
@@ -213,7 +210,7 @@ const ExamSampleEditModal: React.FC<{
                                 help={false}
                                 rules={[{ required: isEnabledClassStudentMin }]}
                             />
-                            <label>人,则该学生全抽</label>
+                            <label>人,则该学生全抽</label>
                         </Space>
                     </div>
                 </Space>
@@ -244,7 +241,7 @@ const ExamSampleEditModal: React.FC<{
                 label={<strong>备注说明</strong>}
                 name="remark"
             />
-        </MovableModalForm>
+        </DrawerForm>
     );
 }
 

+ 330 - 218
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/components/ExamSampleList.tsx

@@ -1,19 +1,26 @@
 import { downloadFileByBlob } from "@/common/net/download";
 import { TrueOrFalseValueEnum } from "@/common/valueEnum";
 import { CardStepTitle, StatusIcon, TagStatus } from "@/components";
+import ExamPlanController from "@/services/apis/ExamPlanController";
 import ExamSampleController from "@/services/apis/ExamSampleController";
 import { ExamSampleStatus } from "@/services/enums";
-import { ActionType, ProTable, TableDropdown } from "@ant-design/pro-components";
+import { DownOutlined, ThunderboltOutlined } from "@ant-design/icons";
+import { ActionType, ProColumns, ProTable, TableDropdown } from "@ant-design/pro-components";
 import { history, useModel } from "@umijs/max";
+import { useRequest } from "ahooks";
 import { App, Button, Space, Tooltip, Typography, theme } from "antd";
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
 import ExamSampleEditModal from "./ExamSampleEditModal";
 
 /** 监测抽样管理 */
 const ExamSampleList: React.FC<{
     /** 监测计划ID */
     examPlanId: number;
-}> = ({ examPlanId }) => {
+    /** 是否已锁定抽样 */
+    isFixedExamSample?: boolean;
+    /** 抽样状态 */
+    sampleStatus?: ExamSampleStatus;
+}> = ({ examPlanId, isFixedExamSample }) => {
     const { token } = theme.useToken();
 
     const actionRef = useRef<ActionType>();
@@ -24,6 +31,25 @@ const ExamSampleList: React.FC<{
 
     const { modal, message } = App.useApp();
 
+    const [sampleCount, setSampleCount] = useState(0);
+    const [executing, setExecuting] = useState(false);
+    const [currentSampleStatus, setCurrentSampleStatus] = useState<ExamSampleStatus>();
+    const { runAsync: reloadGenStatus, cancel: cancelGenStatus } = useRequest(async () => {
+        const res = await ExamPlanController.getSampleStatusById({ id: examPlanId });
+        const s = res?.sampleStatus;
+        if (s !== ExamSampleStatus.RUNNING) {
+            cancelGenStatus();
+            actionRef.current?.reload();
+        }
+        setCurrentSampleStatus(s);
+    }, {
+        pollingInterval: 2000,
+        pollingErrorRetryCount: 3,
+        manual: true,
+        ready: true,
+    });
+    useEffect(() => { reloadGenStatus(); }, []);
+
     // 复制
     const handleDuplicate = useCallback((id: number) => {
         modal.confirm({
@@ -84,6 +110,12 @@ const ExamSampleList: React.FC<{
         });
     }, [actionRef.current]);
 
+    // 查看统计表
+    const handleViewCount = useCallback((id: number) => {
+        // window.open(`/exam-c/plan/sample/count/${id}`);
+        history.push(`/exam-c/plan/sample/count/${id}`);
+    }, []);
+
     // 下载存档
     const handleDownloadArchived = useCallback(async (id: number) => {
         try {
@@ -129,6 +161,290 @@ const ExamSampleList: React.FC<{
         }
     }, []);
 
+    // 执行抽样
+    const handleExecute = useCallback(async () => {
+        setExecuting(true);
+        try {
+            await ExamPlanController.executeSample({ id: examPlanId });
+            await reloadGenStatus();
+            actionRef.current?.reload();
+        }
+        catch { }
+        finally {
+            setExecuting(false);
+        }
+    }, []);
+
+    const opDisabled = !currentSampleStatus || currentSampleStatus === ExamSampleStatus.RUNNING || isFixedExamSample;
+    const columns: ProColumns<API.ExamSampleOutput>[] = [
+        {
+            title: '序',
+            dataIndex: 'sequence',
+            width: 32,
+            align: 'center',
+            hideInSearch: true,
+            fixed: 'left',
+        },
+        {
+            title: '方案简称',
+            dataIndex: 'shortName',
+            width: 352,
+            fixed: 'left',
+            render: (v, r) => {
+                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'],
+            valueType: () => ({
+                type: 'percent',
+                precision: 1,
+            }),
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '年级限制',
+            tooltip: '年级仅有一个班,且该班学生人数小于等于X人,该班全抽',
+            dataIndex: ['config', 'onlyOneClassStudentMin'],
+            width: 96,
+            align: 'center',
+            render: (v, r) => {
+                return (
+                    <>
+                        <StatusIcon status={r.config.isEnabledOnlyOneClassStudentMin ? 'success' : 'forbid'} />
+                        <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
+                    </>
+                );
+            },
+        },
+        {
+            title: '最少未抽',
+            tooltip: '年级多于一个班,且年级未抽样部分学生人数小于X人,该年级全抽',
+            dataIndex: ['config', 'gradeNoSampleStudentMin'],
+            width: 96,
+            align: 'center',
+            render: (v, r) => {
+                return (
+                    <>
+                        <StatusIcon status={r.config.isEnabledGradeNoSampleStudentMin ? 'success' : 'forbid'} />
+                        <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
+                    </>
+                );
+            },
+        },
+        {
+            title: '班级限制',
+            tooltip: '班级学生人数小于等于X人,该班全抽',
+            dataIndex: ['config', 'classStudentMin'],
+            width: 96,
+            align: 'center',
+            render: (v, r) => {
+                return (
+                    <>
+                        <StatusIcon status={r.config.isEnabledClassStudentMin ? 'success' : 'forbid'} />
+                        <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
+                    </>
+                );
+            },
+        },
+        {
+            title: '特殊排除',
+            tooltip: '抽样时排除特殊学生',
+            dataIndex: ['config', 'isExcludeSpecialStudent'],
+            width: 96,
+            align: 'center',
+            render: (_, r) => {
+                const s = TrueOrFalseValueEnum[`${r.config.isExcludeSpecialStudent ?? 'false'}`];
+                return (<TagStatus status={s.status}>{s.text}</TagStatus>);
+            },
+        },
+        {
+            title: '随机序号',
+            tooltip: '在年级内随机打乱监测号顺序',
+            dataIndex: ['config', 'isGradeSeatNumberRandom'],
+            width: 96,
+            align: 'center',
+            render: (_, r) => {
+                const s = TrueOrFalseValueEnum[`${r.config.isGradeSeatNumberRandom ?? 'false'}`];
+                return (<TagStatus status={s.status}>{s.text}</TagStatus>);
+            },
+        },
+        {
+            title: '开始位置',
+            dataIndex: ['config', 'startPosition'],
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '间距',
+            dataIndex: ['config', 'interval'],
+            width: 64,
+            align: 'center',
+        },
+        {
+            title: '全抽班级',
+            dataIndex: ['config', 'sampleAllSchoolClassIds'],
+            width: 96,
+            align: 'center',
+            render: (_, r) => `${r.config.sampleAllSchoolClassIds?.length ?? 0}个`,
+        },
+        {
+            title: '方案状态',
+            dataIndex: 'status',
+            valueEnum: getDictValueEnum('exam_sample_status', true),
+            width: 96,
+            align: 'center',
+            render: (v, r) => {
+                return (<Tooltip title={r.executeLog}><span>{v}</span></Tooltip>);
+            },
+        },
+        {
+            title: '已选定',
+            dataIndex: 'isSelected',
+            width: 96,
+            // align: 'center',
+            render: (_, r) => {
+                const s = TrueOrFalseValueEnum[`${r.isSelected}`];
+                return (
+                    <Space>
+                        <TagStatus status={r.isSelected ? 'success' : 'error'}>{s.text}</TagStatus>
+                        {!r.isFixedExamSample && r.status === ExamSampleStatus.SUCCESSFUL &&
+                            <Button
+                                type="link"
+                                size="small"
+                                disabled={opDisabled}
+                                onClick={() => handleSelect(r.id)}
+                            >选定</Button>
+                        }
+                    </Space>
+                )
+            },
+        },
+        {
+            title: '名单',
+            valueType: 'option',
+            width: 48,
+            align: 'center',
+            fixed: 'right',
+            render: (_, r) => {
+                return (
+                    <Button
+                        type="link"
+                        size="small"
+                        disabled={r.status !== ExamSampleStatus.SUCCESSFUL}
+                        onClick={() => {
+                            history.push(`/exam-c/plan/sample/stu-list/${r.id}`);
+                        }}
+                    >查看</Button>
+                );
+            },
+        },
+        {
+            title: '操作',
+            valueType: 'option',
+            width: isFixedExamSample ? 80 : 112,
+            fixed: 'right',
+            align: 'center',
+            render: (_, r) => {
+                let menuItems: any[] = [];
+                if (!r.isFixedExamSample) {
+                    menuItems = [
+                        { key: 'delete', name: '删除' },
+                        { key: 'copy', name: '复制' },
+                        // { key: 'd-1', type: 'divider' },
+                        // { key: 'generate', name: r.status === ExamSampleStatus.INITIAL ? '生成' : '重新生成' },
+                    ];
+                }
+                if (r.status === ExamSampleStatus.SUCCESSFUL) {
+                    if (menuItems.length > 0) {
+                        menuItems.push({ key: 'd-2', type: 'divider' });
+                    }
+                    menuItems = [
+                        ...menuItems,
+                        { key: 'view-count', name: '查看统计表' },
+                        { key: 'd-3', type: 'divider' },
+                        { key: 'download-archived', name: '下载发归档文件' },
+                        { key: 'download-print-shop', name: '下载发印刷厂文件' },
+                        { key: 'download-count', name: '下载统计表' },
+                    ];
+                }
+                if (opDisabled || r.status === ExamSampleStatus.RUNNING) {
+                    menuItems.forEach(t => {
+                        if (t.type !== 'divider') {
+                            t.disabled = true;
+                        }
+                    });
+                }
+                return (
+                    <Space>
+                        {!r.isFixedExamSample &&
+                            <Button
+                                type="link"
+                                size="small"
+                                disabled={opDisabled || r.status === ExamSampleStatus.RUNNING}
+                                onClick={() => { currentRef.current = r; setEditOpen(true); }}
+                            >修改</Button>
+                        }
+                        <TableDropdown
+                            onSelect={(key) => {
+                                switch (key) {
+                                    case 'delete':
+                                        handleDelete(r.id);
+                                        break;
+                                    case 'copy':
+                                        handleDuplicate(r.id);
+                                        break;
+                                    case 'generate':
+                                        handleGenerate(r.id);
+                                        break;
+                                    case 'view-count':
+                                        handleViewCount(r.id);
+                                        break;
+                                    case 'download-archived':
+                                        handleDownloadArchived(r.id);
+                                        break;
+                                    case 'download-print-shop':
+                                        handleDownloadPrintshop(r.id);
+                                        break;
+                                    case 'download-count':
+                                        handleDownloadCount(r.id);
+                                        break;
+                                }
+                            }}
+                            menus={menuItems}
+                        >
+                            更多<DownOutlined />
+                        </TableDropdown>
+                    </Space>
+                );
+            },
+        }
+    ];
+
     return (
         <>
             <ProTable<API.ExamSampleOutput>
@@ -142,220 +458,7 @@ const ExamSampleList: React.FC<{
                 options={{ density: false, fullScreen: false, setting: false }}
                 scroll={{ x: 'max-content' }}
                 pagination={false}
-                columns={[
-                    {
-                        title: '序',
-                        dataIndex: 'sequence',
-                        width: 32,
-                        align: 'center',
-                        hideInSearch: true,
-                    },
-                    {
-                        title: '方案简称',
-                        dataIndex: 'name',
-                        render: (v, r) => {
-                            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'],
-                        valueType: () => ({
-                            type: 'percent',
-                            precision: 1,
-                        }),
-                        width: 80,
-                        align: 'center',
-                    },
-                    {
-                        title: '年级限制',
-                        tooltip: '年级仅有一个班,且该班学生人数小于等于X人,该班全抽',
-                        dataIndex: ['config', 'onlyOneClassStudentMin'],
-                        width: 96,
-                        align: 'center',
-                        render: (v, r) => {
-                            return (
-                                <>
-                                    <StatusIcon status={r.config.isEnabledOnlyOneClassStudentMin ? 'success' : 'forbid'} />
-                                    <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
-                                </>
-                            );
-                        },
-                    },
-                    {
-                        title: '最少未抽',
-                        tooltip: '年级多于一个班,且年级未抽样部分学生人数小于X人,该年级全抽',
-                        dataIndex: ['config', 'gradeNoSampleStudentMin'],
-                        width: 96,
-                        align: 'center',
-                        render: (v, r) => {
-                            return (
-                                <>
-                                    <StatusIcon status={r.config.isEnabledGradeNoSampleStudentMin ? 'success' : 'forbid'} />
-                                    <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
-                                </>
-                            );
-                        },
-                    },
-                    {
-                        title: '班级限制',
-                        tooltip: '班级学生人数小于等于X人,该班全抽',
-                        dataIndex: ['config', 'classStudentMin'],
-                        width: 96,
-                        align: 'center',
-                        render: (v, r) => {
-                            return (
-                                <>
-                                    <StatusIcon status={r.config.isEnabledClassStudentMin ? 'success' : 'forbid'} />
-                                    <span style={{ display: 'inline-block', width: 32, textAlign: 'right' }}>{v}</span>
-                                </>
-                            );
-                        },
-                    },
-                    {
-                        title: '特殊排除',
-                        tooltip: '抽样时排除特殊学生',
-                        dataIndex: ['config', 'isExcludeSpecialStudent'],
-                        width: 96,
-                        align: 'center',
-                        render: (_, r) => {
-                            const s = TrueOrFalseValueEnum[`${r.config.isExcludeSpecialStudent ?? 'false'}`];
-                            return (<TagStatus status={s.status}>{s.text}</TagStatus>);
-                        },
-                    },
-                    {
-                        title: '随机序号',
-                        tooltip: '在年级内随机打乱监测号顺序',
-                        dataIndex: ['config', 'isGradeSeatNumberRandom'],
-                        width: 96,
-                        align: 'center',
-                        render: (_, r) => {
-                            const s = TrueOrFalseValueEnum[`${r.config.isGradeSeatNumberRandom ?? 'false'}`];
-                            return (<TagStatus status={s.status}>{s.text}</TagStatus>);
-                        },
-                    },
-                    {
-                        title: '开始位置',
-                        dataIndex: ['config', 'startPosition'],
-                        width: 80,
-                        align: 'center',
-                    },
-                    {
-                        title: '间距',
-                        dataIndex: ['config', 'interval'],
-                        width: 64,
-                        align: 'center',
-                    },
-                    {
-                        title: '全抽班级',
-                        dataIndex: ['config', 'sampleAllSchoolClassIds'],
-                        width: 96,
-                        align: 'center',
-                        render: (_, r) => `${r.config.sampleAllSchoolClassIds?.length ?? 0}个`,
-                    },
-                    {
-                        title: '方案状态',
-                        dataIndex: 'status',
-                        valueEnum: getDictValueEnum('exam_sample_status', true),
-                        width: 96,
-                        align: 'center',
-                    },
-                    {
-                        title: '已选定',
-                        dataIndex: 'isSelected',
-                        width: 96,
-                        // align: 'center',
-                        render: (_, r) => {
-                            const s = TrueOrFalseValueEnum[`${r.isSelected}`];
-                            return (
-                                <Space>
-                                    <TagStatus status={r.isSelected ? 'success' : 'error'}>{s.text}</TagStatus>
-                                    {!r.isFixedExamSample && r.status === ExamSampleStatus.SUCCESSFUL &&
-                                        <a onClick={() => handleSelect(r.id)}>选定</a>
-                                    }
-                                </Space>
-                            )
-                        },
-                    },
-                    {
-                        title: '操作',
-                        valueType: 'option',
-                        width: 96,
-                        fixed: 'right',
-                        render: (_, r) => {
-                            let menuItems: any[] = [];
-                            if (!r.isFixedExamSample) {
-                                menuItems = [
-                                    { key: 'delete', name: '删除' },
-                                    { key: 'copy', name: '复制' },
-                                    { key: 'generate', name: r.status === ExamSampleStatus.INITIAL ? '生成' : '重新生成' },
-                                ];
-                            }
-                            if (r.status === ExamSampleStatus.SUCCESSFUL) {
-                                menuItems = [
-                                    ...menuItems,
-                                    { key: 'download-archived', name: '下载发归档文件' },
-                                    { key: 'download-print-shop', name: '下载发印刷厂文件' },
-                                    { key: 'download-count', name: '下载统计表' },
-                                ];
-                            }
-                            return (
-                                <Space>
-                                    {!r.isFixedExamSample &&
-                                        <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
-                                    }
-                                    <TableDropdown
-                                        onSelect={(key) => {
-                                            switch (key) {
-                                                case 'delete':
-                                                    handleDelete(r.id);
-                                                    break;
-                                                case 'copy':
-                                                    handleDuplicate(r.id);
-                                                    break;
-                                                case 'generate':
-                                                    handleGenerate(r.id);
-                                                    break;
-                                                case 'download-archived':
-                                                    handleDownloadArchived(r.id);
-                                                    break;
-                                                case 'download-print-shop':
-                                                    handleDownloadPrintshop(r.id);
-                                                    break;
-                                                case 'download-count':
-                                                    handleDownloadCount(r.id);
-                                                    break;
-                                            }
-                                        }}
-                                        menus={menuItems}
-                                    />
-                                </Space>
-                            );
-                        },
-                    }
-                ]}
+                columns={columns}
                 rowKey="id"
                 toolbar={{
                     subTitle: (<Typography.Text type="warning">发布前重点核对<Typography.Text type="danger" strong>成绩引用</Typography.Text>、人数和特殊学生!</Typography.Text>),
@@ -363,6 +466,7 @@ const ExamSampleList: React.FC<{
                         <Button
                             key="setting"
                             type="primary"
+                            disabled={opDisabled}
                             onClick={() => {
                                 currentRef.current = {
                                     id: 0,
@@ -370,11 +474,19 @@ const ExamSampleList: React.FC<{
                                 };
                                 setEditOpen(true);
                             }}
-                        >添加抽样</Button>,
+                        >添加方案</Button>,
+                        <Button
+                            key="btnExecute"
+                            icon={<ThunderboltOutlined />}
+                            disabled={opDisabled || sampleCount === 0}
+                            loading={executing || currentSampleStatus === ExamSampleStatus.RUNNING}
+                            onClick={handleExecute}
+                        >执行抽样</Button>
                     ],
                 }}
                 request={async () => {
                     const res = await ExamSampleController.getListByExamPlanId({ examplanid: examPlanId });
+                    setSampleCount(res?.length ?? 0);
                     return {
                         data: res ?? [],
                         success: true,

+ 36 - 11
YBEE.EQM.Admin/src/pages/exam-center/ExamPlanDetail/index.tsx

@@ -5,7 +5,7 @@ import { PageContainer, ProCard, ProDescriptions } from "@ant-design/pro-compone
 import { history, useModel, useParams } from "@umijs/max";
 import { useRequest } from "ahooks";
 import { App, Button, FloatButton, Space, Tag } from "antd";
-import { useCallback } from "react";
+import { useCallback, useEffect } from "react";
 import ExamDataPublishList from "./components/ExamDataPublishList";
 import ExamDataReportList from "./components/ExamDataReportList";
 import ExamGradeList from "./components/ExamGradeList";
@@ -13,6 +13,8 @@ import ExamOrgList from "./components/ExamOrgList";
 import ExamResultList from "./components/ExamResultList";
 import ExamSampleList from "./components/ExamSampleList";
 
+const SCROLL_POSITION_KEY = 'ybee_eqm_center_exam_plan_detail_position';
+
 const ExamPlanDetail: React.FC = () => {
     const reqParams = useParams() as unknown as { id: number };
     const { data, run, loading } = useRequest(() => {
@@ -80,6 +82,31 @@ const ExamPlanDetail: React.FC = () => {
     //     });
     // }, []);
 
+
+    // 在离开页面时,记录当前的滚动位置
+    const savePosition = () => {
+        sessionStorage.setItem(SCROLL_POSITION_KEY, `${window.scrollY}`);
+    };
+
+    useEffect(() => {
+        // 在页面加载时检查是否有记录的滚动位置
+        const scrollPosition = JSON.parse(sessionStorage.getItem(SCROLL_POSITION_KEY) || '0');
+        if (scrollPosition) {
+            setTimeout(() => {
+                window.scrollTo(0, scrollPosition);
+            }, 1000);
+        }
+        sessionStorage.removeItem(SCROLL_POSITION_KEY);
+    }, []);
+
+    useEffect(() => {
+        history.listen(e => {
+            if (e.action === 'PUSH') {
+                savePosition();
+            }
+        });
+    }, []);
+
     return (
         <PageContainer
             title={data?.fullName}
@@ -139,19 +166,17 @@ const ExamPlanDetail: React.FC = () => {
                 </ProDescriptions>
             </ProCard>
 
-            {data?.educationStage &&
-                <ExamGradeList
-                    examPlanId={reqParams.id}
-                    examPlanStatus={data?.status}
-                    educationStage={data?.educationStage}
-                    semesterId={data?.semesterId}
-                />
-            }
-
+            {/* 监测年级 */}
+            {data?.educationStage && <ExamGradeList examPlanId={reqParams.id} examPlanStatus={data?.status} educationStage={data?.educationStage} semesterId={data?.semesterId} />}
+            {/* 数据上报 */}
             <ExamDataReportList examPlanId={reqParams.id} examPlanStatus={data?.status} />
+            {/* 监测机构 */}
             <ExamOrgList examPlanId={reqParams.id} examDataReports={data?.examDataReports ?? []} />
-            <ExamSampleList examPlanId={reqParams.id} />
+            {/* 监测抽样 */}
+            <ExamSampleList examPlanId={reqParams.id} isFixedExamSample={data?.isFixedExamSample} sampleStatus={data?.sampleStatus} />
+            {/* 结果反馈 */}
             <ExamResultList examPlanId={reqParams.id} semesterId={data?.semesterId ?? 99999} />
+            {/* 结果反馈 */}
             <ExamDataPublishList examPlanId={reqParams.id} examPlanStatus={data?.status} />
 
             <FloatButton.BackTop visibilityHeight={100} />

+ 305 - 0
YBEE.EQM.Admin/src/pages/exam-center/absent-replace/AbsentReplaceList/index.tsx

@@ -0,0 +1,305 @@
+import { toValueEnum } from "@/common/converter";
+import { downloadFileByBlob } from "@/common/net/download";
+import { FileLink, SuperTable, TabBadge } from "@/components";
+import ExamAbsentReplaceCenterController from "@/services/apis/ExamAbsentReplaceCenterController";
+import ExamGradeController from "@/services/apis/ExamGradeController";
+import { DownloadOutlined } from "@ant-design/icons";
+import { ActionType, PageContainer, ProColumns, ProDescriptionsItemProps } from "@ant-design/pro-components";
+import { history, useModel, useParams } from "@umijs/max";
+import { useRequest } from "ahooks";
+import { App, Button, Space, Tag, Typography, theme } from "antd";
+import { useCallback, useRef, useState } from "react";
+import ExamAbsentReplaceDetailDrawer from "../../../exam-org/absent-replace/OrgExamAbsentReplaceReport/components/ExamAbsentReplaceDetailDrawer";
+
+const ExamAbsentReplaceList: React.FC = () => {
+    const reqParams = useParams() as unknown as { examPlanId: number };
+
+    const { token } = theme.useToken();
+    const { getDictValueEnum, getKeyDict, getDict } = useModel('useDict');
+    const auditStatusDict = getKeyDict('audit_status');
+
+    const [detailOpen, setDetailOpen] = useState(false);
+    const actionRef = useRef<ActionType>();
+    const currentRef = useRef<Partial<API.ExamAbsentReplaceOutput>>();
+
+    const [activeKey, setActiveKey] = useState<React.Key>('0');
+    const [statusCount, seStatusCount] = useState<Record<number, number>>({});
+
+    const { message } = App.useApp();
+
+    const { data: gradeBranchData } = useRequest(async () => {
+        const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId });
+        return {
+            examGrades: res1 ?? [],
+        };
+    });
+
+    // 加载数量统计
+    const loadCount = useCallback(async (params: API.ExamAbsentReplacePageInput) => {
+        const m = await ExamAbsentReplaceCenterController.queryStatusCount(params);
+        const tc = m?.reduce((a, b) => a + b.count, 0) ?? 0;
+
+        const d: Record<number, number> = { 0: tc };
+        m?.forEach((t) => { d[t.status] = t.count; });
+
+        seStatusCount(d);
+    }, []);
+
+    // 导出简表
+    const handleExportSimple = useCallback(async () => {
+        const res = await ExamAbsentReplaceCenterController.exportSimple({
+            examPlanId: reqParams.examPlanId,
+            pageIndex: 1,
+            pageSize: 99999999,
+        });
+        if (res) {
+            downloadFileByBlob(res.data, res.fileName);
+        }
+        else {
+            message.error('下载失败');
+        }
+    }, []);
+    // 导出详表
+    const handleExportFull = useCallback(async () => {
+        const res = await ExamAbsentReplaceCenterController.exportFull({
+            examPlanId: reqParams.examPlanId,
+            pageIndex: 1,
+            pageSize: 99999999,
+        });
+        if (res) {
+            downloadFileByBlob(res.data, res.fileName);
+        }
+        else {
+            message.error('下载失败');
+        }
+    }, []);
+
+    // 明细表列定义
+    const detailColumns: ProColumns<API.ExamAbsentReplaceOutput>[] = [
+        {
+            title: '学校',
+            dataIndex: ['sysOrg', 'name'],
+            width: 180,
+            hideInSearch: true,
+        },
+        {
+            title: '校区',
+            dataIndex: ['sysOrgBranch', 'name'],
+            width: 96,
+            align: 'center',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '状态',
+            dataIndex: 'status',
+            hideInSearch: true,
+            width: 80,
+            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>);
+            },
+        },
+        {
+            title: '有无替补',
+            dataIndex: 'isReplaced',
+            // hideInSearch: true,
+            width: 80,
+            align: 'center',
+            render: (_, r) => {
+                return (
+                    <Typography.Text
+                        type={r.isReplaced ? 'warning' : 'danger'}
+                        style={{ fontSize: token.fontSizeSM }}
+                    >
+                        {r.isReplaced ? '有' : '无'}
+                    </Typography.Text>
+                );
+            },
+        },
+        {
+            title: '年级',
+            dataIndex: 'gradeId',
+            width: 80,
+            align: 'center',
+            valueEnum: toValueEnum(gradeBranchData?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
+            render: (_, r) => {
+                return (
+                    <Space size="small" direction="vertical" style={{ lineHeight: 1 }}>
+                        {r.examGrade?.grade.name}
+                        {/* <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>({r.examGrade?.gradeBeginName})</Typography.Text> */}
+                    </Space>
+                );
+            },
+        },
+        {
+            title: '班级',
+            dataIndex: 'classNumber',
+            width: 64,
+            align: 'center',
+            renderText: (v) => `${v}班`,
+        },
+        {
+            title: '姓名',
+            dataIndex: 'absentName',
+            width: 112,
+            align: 'center',
+            render: (v, r) => <a onClick={() => { currentRef.current = r; setDetailOpen(true); }}>{v}</a>,
+        },
+        {
+            title: '监测号',
+            dataIndex: 'absentExamNumber',
+            width: 128,
+            align: 'center',
+        },
+        {
+            title: '缺测替补原因',
+            dataIndex: 'absentReason',
+            hideInSearch: true,
+            hideInTable: true,
+            width: 240,
+        },
+        {
+            title: '缺测科目',
+            dataIndex: 'absentCourseList',
+            width: 320,
+            render: (_, r) => {
+                return r.absentCourseList?.map(t => t.name).join('、');
+            },
+        },
+        {
+            title: '家长电话',
+            dataIndex: 'patriarchTel',
+            hideInSearch: true,
+            hideInTable: true,
+            width: 128,
+            align: 'center',
+        },
+        {
+            title: '替补学生姓名',
+            dataIndex: 'replaceName',
+            width: 112,
+            align: 'center',
+        },
+        {
+            title: '替补学生监测号',
+            dataIndex: 'replaceExamNumber',
+            width: 160,
+            align: 'center',
+        },
+        {
+            title: '备注',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            ellipsis: true,
+            width: 120,
+        },
+        {
+            title: '佐证材料',
+            hideInSearch: true,
+            hideInTable: true,
+            render: (_, r) => {
+                const li = r.attachmentList?.map((t, i) => {
+                    return (
+                        <FileLink
+                            key={i}
+                            fileExtName={t.fileExtName}
+                            fileName={t.fileName}
+                            url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
+                            thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
+                            card
+                        />
+                    );
+                });
+                return (
+                    <Space direction="vertical" style={{ width: '100%' }}>
+                        {li?.length === 0 ? '未上传' : li}
+                    </Space>
+                );
+            },
+        },
+    ];
+
+    // 呈现状态 tab
+    const renderTabItems = useCallback(() => {
+        let items: { key: string; label: React.ReactNode }[] = [
+            {
+                key: '0',
+                label: (<span>全部<TabBadge count={statusCount[0]} active={activeKey === 0} /></span>),
+            }
+        ];
+        items = items.concat(
+            getDict('audit_status')?.filter(t => t.value > 1)?.sort((a, b) => a.sort - b.sort)?.map((t) => ({
+                key: `${t.value}`,
+                label: (
+                    <span>
+                        {t.name}
+                        <TabBadge color={t.antColor} count={statusCount[t.value] ?? 0} active={activeKey === `${t.value}`} />
+                    </span>
+                ),
+            })),
+        );
+        return items;
+    }, [activeKey, statusCount]);
+
+    return (
+        <PageContainer
+            // title={`${reportData?.examPlan?.fullName ?? ''} - 缺测替补学生名单`}
+            onBack={() => history.back()}
+        >
+            <SuperTable<API.ExamAbsentReplaceOutput>
+                actionRef={actionRef}
+                scroll={{ x: '100%' }}
+                rowKey="id"
+                columns={detailColumns}
+                options={{ setting: false, fullScreen: false }}
+                toolbar={{
+                    title: '缺测替补学生明细',
+                    menu: {
+                        type: 'tab',
+                        activeKey: activeKey,
+                        items: renderTabItems(),
+                        onChange: (key) => {
+                            setActiveKey(key as React.Key);
+                            actionRef.current?.reload();
+                        },
+                    },
+                    actions: [
+                        <Button
+                            key="btnExportSimple"
+                            icon={<DownloadOutlined />}
+                            onClick={handleExportSimple}
+                        >导出简表</Button>,
+                        <Button
+                            key="btnExportFull"
+                            icon={<DownloadOutlined />}
+                            onClick={handleExportFull}
+                        >导出详表</Button>,
+                    ],
+                }}
+                request={async (params, sort) => {
+                    return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                        const qparams = { ...p, examPlanId: reqParams.examPlanId };
+                        await loadCount(qparams);
+                        const res = await ExamAbsentReplaceCenterController.queryPageList({
+                            ...qparams,
+                            status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
+                        });
+                        return res;
+                    });
+                }}
+            />
+            {detailOpen && currentRef.current &&
+                <ExamAbsentReplaceDetailDrawer
+                    data={currentRef.current}
+                    columns={detailColumns as ProDescriptionsItemProps<Partial<API.ExamAbsentReplaceOutput>>[]}
+                    onClose={() => setDetailOpen(false)}
+                />
+            }
+        </PageContainer>
+    );
+}
+
+export default ExamAbsentReplaceList;

+ 96 - 0
YBEE.EQM.Admin/src/pages/exam-center/absent-replace/index.tsx

@@ -0,0 +1,96 @@
+import { toSelectOptions } from '@/common/converter';
+import { SuperTable } from '@/components';
+import ExamDataReportController from '@/services/apis/ExamDataReportController';
+import { DataReportType } from '@/services/enums';
+import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
+import { history, useModel } from '@umijs/max';
+import { useRef } from 'react';
+
+/** 监测计划 */
+const ExamAbsentReplaceList: React.FC = () => {
+    const actionRef = useRef<ActionType>();
+
+    const { getDictValueEnum } = useModel('useDict');
+    const { baseData } = useModel('useBaseData');
+
+    const columns: ProColumns<API.ExamDataReportPlanOutput>[] = [
+        {
+            title: '监测计划',
+            dataIndex: ['examPlan', 'fullName'],
+            renderText: (v, r) => <a onClick={() => history.push(`/exam-c/absent/list/${r.examPlanId}`)}>{v} - 缺测替补</a>,
+            hideInDescriptions: true,
+            search: {
+                transform: (v) => ({ name: v }),
+            },
+        },
+        {
+            title: '监测学期',
+            dataIndex: ['examPlan', 'semester', 'nickShortName'],
+            search: {
+                transform: (v) => v ? ({ semesterId: JSON.parse(v) }) : v,
+            },
+            valueType: 'select',
+            fieldProps: {
+                options: toSelectOptions(baseData?.semesters ?? [], (item) => ({ key: item.id, label: item.nickShortName, value: item.id })),
+            },
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '监测学段',
+            dataIndex: ['examPlan', 'educationStage'],
+            valueEnum: getDictValueEnum('education_stage'),
+            width: 80,
+            align: 'center',
+            search: {
+                transform: (v) => ({ educationStage: v }),
+            },
+        },
+        {
+            title: '计划状态',
+            dataIndex: ['examPlan', 'status'],
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInDescriptions: true,
+        },
+        {
+            title: '上报状态',
+            dataIndex: 'status',
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInDescriptions: true,
+        },
+        {
+            title: '备注说明',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            className: 'minw-120',
+        },
+    ];
+
+    return (
+        <PageContainer title={false}>
+            <SuperTable<API.ExamDataReportPlanOutput>
+                actionRef={actionRef}
+                columns={columns}
+                scroll={{ x: 'max-content' }}
+                toolbar={{
+                    title: '缺测替补上报计划列表',
+                }}
+                request={async (params, sort) => {
+                    try {
+                        return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                            const res = await ExamDataReportController.queryPlanPageList({ ...p, type: DataReportType.ABSENT_REPLACE });
+                            return res;
+                        });
+                    }
+                    catch (ex) { return {}; }
+                }}
+            />
+        </PageContainer>
+    );
+};
+
+export default ExamAbsentReplaceList;

+ 153 - 0
YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleCount/index.tsx

@@ -0,0 +1,153 @@
+import { downloadFileByBlob } from "@/common/net/download";
+import { SuperTable } from "@/components";
+import ExamSampleController from "@/services/apis/ExamSampleController";
+import { ArrowLeftOutlined, DownloadOutlined } from "@ant-design/icons";
+import { ActionType, PageContainer, ProColumns } from "@ant-design/pro-components";
+import { history as umiHistory, useParams } from "@umijs/max";
+import { useRequest } from "ahooks";
+import { App, Button, FloatButton, Spin, theme } from "antd";
+import { useCallback, useRef } from "react";
+import ExamSampleBaseInfo from "../components/ExamSampleBaseInfo";
+
+const ExamSampleCount: React.FC = () => {
+    const reqParams = useParams() as unknown as { id: number };
+    const { token } = theme.useToken();
+    const { message } = App.useApp();
+
+    const actionRef = useRef<ActionType>();
+
+    const { data, loading } = useRequest(async () => {
+        const sampleRes = await ExamSampleController.getById({ id: reqParams.id });
+        return {
+            examSample: sampleRes,
+        };
+    });
+
+    // 明细表列定义
+    const columns: ProColumns<API.ExamSampleCountOutput>[] = [
+        {
+            title: '数据类型',
+            dataIndex: 'typeName',
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '学校代码',
+            dataIndex: 'sysOrgCode',
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '学校名称',
+            dataIndex: 'sysOrgName',
+            width: 160,
+            align: 'center',
+        },
+        // {
+        //     title: '校区',
+        //     dataIndex: 'sysOrgBranchName',
+        //     width: 88,
+        //     align: 'center',
+        //     hideInSearch: true,
+        // },
+        {
+            title: '年级',
+            dataIndex: 'gradeName',
+            width: 80,
+            align: 'center',
+            renderText: (v) => v === 9999 ? null : v,
+        },
+        {
+            title: '班级',
+            dataIndex: 'classNumber',
+            width: 64,
+            align: 'center',
+            renderText: (v) => v === 9999 ? null : `${v}班`,
+        },
+        {
+            title: '特殊学生',
+            dataIndex: 'totalSpecialStudentCount',
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '区级监测',
+            dataIndex: 'centerStudentCount',
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '校内考试',
+            dataIndex: 'schoolStudentCount',
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '合计',
+            dataIndex: 'totalStudentCount',
+            width: 80,
+            align: 'center',
+        },
+    ];
+
+    // 下载统计表
+    const handleDownloadCount = useCallback(async () => {
+        try {
+            message.loading('正在生成文件,请稍侯');
+            const res = await ExamSampleController.exportSampleCount({ id: reqParams.id });
+            if (res) {
+                downloadFileByBlob(res.data, res.fileName);
+            }
+        }
+        catch { }
+        finally {
+            message.destroy();
+        }
+    }, []);
+
+    return (
+        <PageContainer
+            title={data?.examSample?.fullName}
+            onBack={() => umiHistory.back()}
+            backIcon={window.history.length > 1 ? <ArrowLeftOutlined /> : false}
+        >
+            <ExamSampleBaseInfo data={data?.examSample} />
+            <Spin spinning={loading}>
+                {data?.examSample?.id &&
+                    <SuperTable<API.ExamSampleCountOutput>
+                        headerTitle="抽样统计表"
+                        style={{ marginBlockStart: token.margin }}
+                        actionRef={actionRef}
+                        search={false}
+                        scroll={{ x: 'max-content' }}
+                        rowKey="id"
+                        columnEmptyText=""
+                        columns={columns}
+                        pagination={false}
+                        request={async () => {
+                            const res = await ExamSampleController.getSampleCountListById({ id: reqParams.id });
+                            return {
+                                data: res,
+                                success: true,
+                            }
+                        }}
+                        toolbar={{
+                            actions: [
+                                <Button key="btnDownload" icon={<DownloadOutlined />} onClick={handleDownloadCount}>下载表格</Button>
+                            ],
+                        }}
+                        rowClassName={(r) => {
+                            if (r.classNumber === 9999) {
+                                return 'yb-row-summary';
+                            }
+                            return '';
+                        }}
+                    />
+                }
+            </Spin>
+            <FloatButton.BackTop />
+        </PageContainer>
+    );
+}
+
+export default ExamSampleCount;

+ 44 - 86
YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleDetail/index.tsx

@@ -1,14 +1,15 @@
-import { CardStepTitle, SuperTable, TagStatus } from "@/components";
+import { CardStepTitle, SuperTable } from "@/components";
 import ExamSampleController from "@/services/apis/ExamSampleController";
 import ExamStudentController from "@/services/apis/ExamStudentController";
 import { CheckCircleFilled } from "@ant-design/icons";
-import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions } from "@ant-design/pro-components";
+import { ActionType, PageContainer, ProColumns } from "@ant-design/pro-components";
 import { useEmotionCss } from "@ant-design/use-emotion-css";
 import { history, useModel, useParams } from "@umijs/max";
 import { useRequest } from "ahooks";
-import { App, FloatButton, Tag, theme, Typography } from "antd";
+import { App, Divider, FloatButton, Space, theme, Typography } from "antd";
 import lodash from 'lodash';
-import { useCallback, useRef } from "react";
+import { useCallback, useRef, useState } from "react";
+import ExamSampleBaseInfo from "../components/ExamSampleBaseInfo";
 
 type ClassItem = API.ExamStudentCountItem & {
     isFirstGrade: boolean;
@@ -22,10 +23,6 @@ type ClassItem = API.ExamStudentCountItem & {
     }
 };
 
-const EnabledStatus: React.FC<{ enabled: boolean; enabledText?: string; disabledText?: string; }> = ({ enabled, enabledText, disabledText }) => {
-    return (<Tag color={enabled ? 'success' : 'error'}>{enabled ? enabledText ?? '启用' : disabledText ?? '禁用'}</Tag>);
-}
-
 const ExamSampleDetail: React.FC = () => {
     const reqParams = useParams() as unknown as { id: number };
 
@@ -52,20 +49,19 @@ const ExamSampleDetail: React.FC = () => {
     });
 
     const { token } = theme.useToken();
-    const { getDictValueEnum, getKeyDict } = useModel('useDict');
-    const examSampleStatusDict = getKeyDict('exam_sample_status');
+    const { getDictValueEnum } = useModel('useDict');
 
     const { message, modal } = App.useApp();
 
     const actionRef = useRef<ActionType>();
     // const currentRef = useRef<Partial<API.ExamStudentCountItem>>();
 
-    const { data, run, loading } = useRequest(async () => {
-        const examSample = await ExamSampleController.getById({ id: reqParams.id });
-        const res = await ExamStudentController.queryStudentCountPageList({ pageIndex: 1, pageSize: 9999, examPlanId: examSample?.examPlanId ?? 0 });
+    const [classInfo, setClassInfo] = useState<{ orgCount: number; classCount: number; studentCount: number; allSampleClassCount: number; }>();
 
+    const { data, run: loadData, loading } = useRequest(async () => {
+        const examSample = await ExamSampleController.getById({ id: reqParams.id });
+        const res = await ExamStudentController.queryStudentCountPageList({ pageIndex: 1, pageSize: 9999, examPlanId: examSample?.examPlanId ?? 0, isRequiredExam: true });
         const maxClassNumber = (lodash.maxBy(res?.items, 'classNumber') as any)?.classNumber ?? 0;
-
         const hasInAll = (schoolClassId: string) => examSample?.config.sampleAllSchoolClassIds?.some(t => t === schoolClassId) ?? false;
 
         let list: ClassItem[] = [];
@@ -94,6 +90,12 @@ const ExamSampleDetail: React.FC = () => {
             }
         });
 
+        const orgCount = lodash.uniqBy(res?.items, 'sysOrgId').length;
+        const classCount = res?.items?.length ?? 0;
+        const studentCount = lodash.sumBy(res?.items, 'studentCount');
+        const allSampleClassCount = examSample?.config.sampleAllSchoolClassIds?.length ?? 0;
+        setClassInfo({ orgCount, classCount, studentCount, allSampleClassCount });
+
         return {
             examSample,
             list,
@@ -116,7 +118,7 @@ const ExamSampleDetail: React.FC = () => {
                     isAdd,
                 });
                 message.success(`${isAdd ? '加入' : '取消'}`);
-                run();
+                loadData();
                 actionRef.current?.reload();
             },
         });
@@ -220,80 +222,12 @@ const ExamSampleDetail: React.FC = () => {
         ...classColumns,
     ];
 
-    const status = data?.examSample?.status ? examSampleStatusDict[data?.examSample?.status] : undefined;
-
     return (
         <PageContainer
             title={data?.examSample?.fullName}
             onBack={() => history.back()}
         >
-            <ProCard
-                title={<CardStepTitle>基本信息</CardStepTitle>}
-            >
-                <ProDescriptions size="small">
-                    <ProDescriptions.Item label="监测名称">
-                        {data?.examSample?.name}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item label="监测简称">
-                        {data?.examSample?.shortName}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item label="抽样比例">
-                        {data?.examSample?.config?.percent}%
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item
-                        label="年级限制"
-                        tooltip="年级仅有一个班,且该班学生人数小于等于X人,该班全抽"
-                    >
-                        <EnabledStatus enabled={data?.examSample?.config?.isEnabledOnlyOneClassStudentMin ?? false} />
-                        {data?.examSample?.config?.isEnabledOnlyOneClassStudentMin ? data?.examSample?.config?.onlyOneClassStudentMin : ''}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item
-                        label="最少未抽"
-                        tooltip="年级多于一个班,且年级未抽样部分学生人数小于X人,该年级全抽"
-                    >
-                        <EnabledStatus enabled={data?.examSample?.config?.isEnabledGradeNoSampleStudentMin ?? false} />
-                        {data?.examSample?.config?.isEnabledGradeNoSampleStudentMin ? data?.examSample?.config?.gradeNoSampleStudentMin : ''}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item
-                        label="班级限制"
-                        tooltip="班级学生人数小于等于X人,该班全抽"
-                    >
-                        <EnabledStatus enabled={data?.examSample?.config?.isEnabledClassStudentMin ?? false} />
-                        {data?.examSample?.config?.isEnabledClassStudentMin ? data?.examSample?.config?.classStudentMin : ''}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item
-                        label="特殊排除"
-                        tooltip="抽样时排除特殊学生"
-                    >
-                        <EnabledStatus enabled={data?.examSample?.config?.isExcludeSpecialStudent ?? false} />
-                        {data?.examSample?.config?.specialStudentMustApproved ? '(仅审核已通过不参与抽样)' : '(待审核和审核已通过均不参与抽样)'}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item
-                        label="随机序号"
-                        tooltip="在年级内随机打乱监测号顺序"
-                    >
-                        <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>
-                    <ProDescriptions.Item label="抽样间距">
-                        {data?.examSample?.config.interval}
-                    </ProDescriptions.Item>
-                    <ProDescriptions.Item label="方案状态">
-                        <TagStatus status={status?.antStatus} fill>{status?.name}</TagStatus>
-                    </ProDescriptions.Item>
-                    <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>
+            <ExamSampleBaseInfo data={data?.examSample} />
             <SuperTable<ClassItem>
                 style={{ marginTop: token.margin }}
                 actionRef={actionRef}
@@ -302,10 +236,34 @@ const ExamSampleDetail: React.FC = () => {
                 // search={{ filterType: 'light' }}
                 scroll={{ x: 'max-content' }}
                 toolbar={{
-                    title: <CardStepTitle>全抽班级</CardStepTitle>,
+                    title: <CardStepTitle>额外指定全抽班级</CardStepTitle>,
                     subTitle: !data?.examSample?.isFixedExamSample ? '点击下面班级单元格切换全抽班级' : '',
+                    actions: [
+                        <Space key="count" split={<Divider type="vertical" />}>
+                            <Typography.Text>
+                                <Typography.Text type="secondary">学校数:</Typography.Text>
+                                <Typography.Text strong>{classInfo?.orgCount}</Typography.Text>
+                                <Typography.Text type="secondary">所</Typography.Text>
+                            </Typography.Text>
+                            <Typography.Text>
+                                <Typography.Text type="secondary">班级数:</Typography.Text>
+                                <Typography.Text strong>{classInfo?.classCount}</Typography.Text>
+                                <Typography.Text type="secondary">个</Typography.Text>
+                            </Typography.Text>
+                            <Typography.Text>
+                                <Typography.Text type="secondary">学生数:</Typography.Text>
+                                <Typography.Text strong>{classInfo?.studentCount}</Typography.Text>
+                                <Typography.Text type="secondary">个</Typography.Text>
+                            </Typography.Text>
+                            <Typography.Text>
+                                <Typography.Text type="secondary">指定全抽班级数:</Typography.Text>
+                                <Typography.Text type="danger" strong>{classInfo?.allSampleClassCount}</Typography.Text>
+                                <Typography.Text type="secondary">个</Typography.Text>
+                            </Typography.Text>
+                        </Space>
+                    ],
                 }}
-                options={{ setting: false, fullScreen: false }}
+                options={{ setting: false, fullScreen: false, reload: () => loadData() }}
                 rowKey="schoolClassId"
                 columns={columns}
                 dataSource={data?.list ?? []}

+ 300 - 0
YBEE.EQM.Admin/src/pages/exam-center/sample/ExamSampleStudent/index.tsx

@@ -0,0 +1,300 @@
+import { toValueEnum } from "@/common/converter";
+import { buildSortOrders } from "@/common/helper";
+import { downloadFileByBlob } from "@/common/net/download";
+import { CardStepTitle, SuperTable, TagStatus } from "@/components";
+import ExamGradeController from "@/services/apis/ExamGradeController";
+import ExamOrgController from "@/services/apis/ExamOrgController";
+import ExamSampleController from "@/services/apis/ExamSampleController";
+import ExamSampleStudentController from "@/services/apis/ExamSampleStudentController";
+import { ExamSampleType } from "@/services/enums";
+import { DownloadOutlined, DownOutlined } from "@ant-design/icons";
+import { ActionType, PageContainer, ProCard, ProColumns } from "@ant-design/pro-components";
+import { useModel, useParams } from "@umijs/max";
+import { useRequest } from "ahooks";
+import { App, Button, Dropdown, MenuProps, Space, Spin, theme } from "antd";
+import { useCallback, useRef } from "react";
+import ExamSampleBaseInfo from "../components/ExamSampleBaseInfo";
+
+const ExamSampleStudent: React.FC = () => {
+    const reqParams = useParams() as unknown as { id: number };
+    const { token } = theme.useToken();
+    const { message } = App.useApp();
+    const { getDictValueEnum } = useModel('useDict');
+    const actionRef = useRef<ActionType>();
+
+    const { data, loading } = useRequest(async () => {
+        const sampleRes = await ExamSampleController.getById({ id: reqParams.id });
+        const egRes = await ExamGradeController.getListByExamPlanId({ examplanid: sampleRes?.examPlanId ?? 0 });
+        const examOrgRes = await ExamOrgController.getLiteListByExamPlanId({ examplanid: sampleRes?.examPlanId ?? 0 })
+        return {
+            examGrades: egRes ?? [],
+            examSample: sampleRes,
+            examOrg: examOrgRes,
+        };
+    });
+
+    // 明细表列定义
+    const columns: ProColumns<API.ExamSampleStudentOrgOutput>[] = [
+        {
+            title: '名单类型',
+            dataIndex: 'examSampleType',
+            valueEnum: getDictValueEnum('exam_sample_type', false, [ExamSampleType.SCHOOL]),
+            width: 80,
+            align: 'center',
+            search: {
+                transform: (v) => ({ examSampleType: JSON.parse(v) }),
+            },
+        },
+        {
+            title: '学校',
+            // dataIndex: 'sysOrgName',
+            dataIndex: ['examStudent', 'sysOrgId'],
+            valueEnum: toValueEnum(data?.examOrg?.map(t => ({ id: t.sysOrgId, name: t.sysOrg?.name })) ?? []),
+            fieldProps: { showSearch: true },
+            width: 160,
+            align: 'center',
+            // hideInSearch: true,
+            search: {
+                transform: (v) => ({ sysOrgId: JSON.parse(v) }),
+            },
+            order: 100,
+        },
+        {
+            title: '校区',
+            dataIndex: 'sysOrgBranchName',
+            width: 88,
+            align: 'center',
+            hideInSearch: true,
+        },
+        {
+            title: '年级',
+            dataIndex: ['examStudent', 'examGrade', 'gradeId'],
+            width: 144,
+            align: 'center',
+            valueEnum: toValueEnum(data?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
+            search: {
+                transform: (v) => v ? ({ gradeId: JSON.parse(v) }) : null,
+            },
+            order: 90,
+        },
+        {
+            title: '班级',
+            dataIndex: ['examStudent', 'classNumber'],
+            width: 64,
+            align: 'center',
+            renderText: (v) => `${v}班`,
+            search: {
+                transform: (v) => v ? ({ classNumber: JSON.parse(v) }) : null,
+            },
+            sorter: { multiple: 9 },
+            order: 80,
+        },
+        {
+            title: '姓名',
+            dataIndex: ['examStudent', 'name'],
+            width: 160,
+            align: 'center',
+            search: {
+                transform: (v) => ({ name: v }),
+            },
+            order: 70,
+        },
+        {
+            title: '证件类型',
+            dataIndex: ['examStudent', 'certificateType'],
+            valueEnum: getDictValueEnum('certificate_type'),
+            width: 112,
+            align: 'center',
+            search: {
+                transform: (v) => ({ certificateType: JSON.parse(v) }),
+            },
+            order: 50,
+        },
+        {
+            title: '证件号码',
+            dataIndex: ['examStudent', 'idNumber'],
+            width: 160,
+            align: 'center',
+            search: {
+                transform: (v) => ({ idNumber: v }),
+            },
+            order: 60,
+        },
+        {
+            title: '特殊学生',
+            dataIndex: 'isSpecialStudent',
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+            render: (v, r) => {
+                if (!r.isSpecialStudent) {
+                    return null;
+                }
+                return (<TagStatus status="error">特殊</TagStatus>);
+            },
+        },
+        {
+            title: '参照成绩',
+            dataIndex: 'preTotalScore',
+            width: 96,
+            align: 'center',
+            sorter: { multiple: 8 },
+            hideInSearch: true,
+            render: (v) => v ? v : null,
+        },
+        {
+            title: '是否抽中',
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+            render: (v, r) => {
+                if (r.examSampleType === ExamSampleType.DISTRICT) {
+                    return (<TagStatus fill status="success">是</TagStatus>);
+                }
+                return null;
+            },
+        },
+        {
+            title: '抽中轮',
+            dataIndex: 'cyclicNumber',
+            width: 64,
+            align: 'center',
+            hideInSearch: true,
+            renderText: v => v > 0 ? v : null,
+        },
+        {
+            title: '监测号',
+            dataIndex: 'examNumber',
+            width: 128,
+            align: 'center',
+            sorter: { multiple: 1 },
+            order: 40,
+            // sorter: true,
+        },
+    ];
+
+    // 查看统计表
+    const handleViewCount = useCallback(() => {
+        window.open(`/exam-c/plan/sample/count/${reqParams.id}`);
+    }, []);
+
+    // 下载存档
+    const handleDownloadArchived = useCallback(async () => {
+        try {
+            message.loading('正在生成文件,请稍侯');
+            const res = await ExamSampleController.exportToArchived({ id: reqParams.id }, { timeout: 600000 });
+            if (res) {
+                downloadFileByBlob(res.data, res.fileName);
+            }
+        }
+        catch { }
+        finally {
+            message.destroy();
+        }
+    }, []);
+
+    // 下载发印刷厂
+    const handleDownloadPrintshop = useCallback(async () => {
+        try {
+            message.loading('正在生成文件,请稍侯');
+            const res = await ExamSampleController.exportToPrintshop({ id: reqParams.id }, { timeout: 600000 });
+            if (res) {
+                downloadFileByBlob(res.data, res.fileName);
+            }
+        }
+        catch { }
+        finally {
+            message.destroy();
+        }
+    }, []);
+
+    // 下载统计表
+    const handleDownloadCount = useCallback(async () => {
+        try {
+            message.loading('正在生成文件,请稍侯');
+            const res = await ExamSampleController.exportSampleCount({ id: reqParams.id });
+            if (res) {
+                downloadFileByBlob(res.data, res.fileName);
+            }
+        }
+        catch { }
+        finally {
+            message.destroy();
+        }
+    }, []);
+
+    const downloadMenuProps: MenuProps = {
+        items: [
+            { key: 'download-archived', label: '下载发归档文件' },
+            { key: 'download-print-shop', label: '下载发印刷厂文件' },
+            { key: 'download-count', label: '下载统计表' }
+        ],
+        onClick: (e) => {
+            switch (e.key) {
+                case 'download-archived':
+                    handleDownloadArchived();
+                    break;
+                case 'download-print-shop':
+                    handleDownloadPrintshop();
+                    break;
+                case 'download-count':
+                    handleDownloadCount();
+                    break;
+            }
+        },
+    };
+
+    return (
+        <PageContainer
+            title={data?.examSample?.fullName}
+            onBack={() => history.back()}
+        >
+            <ExamSampleBaseInfo data={data?.examSample} />
+            <Spin spinning={loading}>
+                {data?.examSample?.id &&
+                    <ProCard
+                        title={<CardStepTitle>监测抽样学生名单</CardStepTitle>}
+                        style={{ marginBlockStart: token.margin }}
+                        bodyStyle={{ padding: 0 }}
+                    >
+                        <SuperTable<API.ExamSampleStudentOrgOutput>
+                            headerTitle="抽样明细表"
+                            actionRef={actionRef}
+                            scroll={{ x: 'max-content' }}
+                            rowKey="id"
+                            columnEmptyText=""
+                            columns={columns}
+                            request={async (params, sort) => {
+                                return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                                    let sortOrders = buildSortOrders(sort, false) ?? [];
+                                    sortOrders.push('examStudentId asc');
+                                    const res = await ExamSampleStudentController.queryCenterPageList({
+                                        ...p,
+                                        sortOrders,
+                                        examSampleId: reqParams.id ?? 0,
+                                    });
+                                    return res;
+                                });
+                            }}
+                            toolbar={{
+                                actions: [
+                                    <Button key="btnCount" type="link" onClick={handleViewCount}>查看统计表</Button>,
+                                    <Dropdown key="btnDownload" menu={downloadMenuProps}>
+                                        <Button icon={<DownloadOutlined />}>
+                                            <Space>
+                                                下载
+                                                <DownOutlined />
+                                            </Space>
+                                        </Button>
+                                    </Dropdown>
+                                ],
+                            }}
+                        />
+                    </ProCard>
+                }
+            </Spin>
+        </PageContainer>
+    );
+}
+
+export default ExamSampleStudent;

+ 108 - 0
YBEE.EQM.Admin/src/pages/exam-center/sample/components/ExamSampleBaseInfo.tsx

@@ -0,0 +1,108 @@
+import { CardStepTitle, TagStatus } from "@/components";
+import { ProCard, ProDescriptions } from "@ant-design/pro-components";
+import { useModel } from "@umijs/max";
+import { Tag, Typography } from "antd";
+import RcResizeObserver from 'rc-resize-observer';
+import { useState } from "react";
+
+const EnabledStatus: React.FC<{
+    enabled: boolean;
+    enabledText?: string;
+    disabledText?: string;
+}> = ({ enabled, enabledText, disabledText }) => {
+    return (
+        <Tag color={enabled ? 'success' : 'error'}>
+            {enabled ? enabledText ?? '启用' : disabledText ?? '禁用'}
+        </Tag>
+    );
+}
+
+/**
+ * 抽样方案基本信息
+ */
+const ExamSampleBaseInfo: React.FC<{ data?: API.ExamSampleOutput }> = ({ data }) => {
+    const { getKeyDict } = useModel('useDict');
+    const examSampleStatusDict = getKeyDict('exam_sample_status');
+    const status = data?.status ? examSampleStatusDict[data?.status] : undefined;
+
+    const [column, setColumn] = useState(3);
+
+    return (
+        <RcResizeObserver
+            key="resize-observer"
+            onResize={(offset) => {
+                setColumn(offset.width < 1320 ? 2 : 3);
+            }}
+        >
+            <ProCard
+                title={<CardStepTitle>基本信息</CardStepTitle>}
+            >
+                <ProDescriptions size="small" column={column}>
+                    <ProDescriptions.Item label="监测名称">
+                        {data?.name}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="监测简称">
+                        {data?.shortName}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="抽样比例">
+                        {data?.config?.percent}%
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item
+                        label="年级限制"
+                        tooltip="年级仅有一个班,且该班学生人数小于等于X人,该班全抽"
+                    >
+                        <EnabledStatus enabled={data?.config?.isEnabledOnlyOneClassStudentMin ?? false} />
+                        {data?.config?.isEnabledOnlyOneClassStudentMin ? data?.config?.onlyOneClassStudentMin : ''}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item
+                        label="最少未抽"
+                        tooltip="年级多于一个班,且年级未抽样部分学生人数小于X人,该年级全抽"
+                    >
+                        <EnabledStatus enabled={data?.config?.isEnabledGradeNoSampleStudentMin ?? false} />
+                        {data?.config?.isEnabledGradeNoSampleStudentMin ? data?.config?.gradeNoSampleStudentMin : ''}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item
+                        label="班级限制"
+                        tooltip="班级学生人数小于等于X人,该班全抽"
+                    >
+                        <EnabledStatus enabled={data?.config?.isEnabledClassStudentMin ?? false} />
+                        {data?.config?.isEnabledClassStudentMin ? data?.config?.classStudentMin : ''}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item
+                        label="特殊排除"
+                        tooltip="抽样时排除特殊学生"
+                    >
+                        <EnabledStatus enabled={data?.config?.isExcludeSpecialStudent ?? false} />
+                        {data?.config?.specialStudentMustApproved ? '(仅审核已通过不参与抽样)' : '(待审核和审核已通过均不参与抽样)'}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item
+                        label="随机序号"
+                        tooltip="在年级内随机打乱监测号顺序"
+                    >
+                        <EnabledStatus enabled={data?.config?.isGradeSeatNumberRandom ?? false} />
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="抽样方式">
+                        {data?.config.isRandomSampling ? '随机抽样' : '等距抽样'}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="开始位置">
+                        {data?.config.startPosition}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="抽样间距">
+                        {data?.config.interval}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="方案状态">
+                        <TagStatus status={status?.antStatus} fill>{status?.name}</TagStatus>
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="方案选定">
+                        <EnabledStatus enabled={data?.isSelected ?? false} enabledText="已选定" disabledText="未选定" />
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="成绩引用">
+                        {data?.examScoreRefExamPlan?.name ?? <Typography.Text type="danger">未设置</Typography.Text>}
+                    </ProDescriptions.Item>
+                </ProDescriptions>
+            </ProCard>
+        </RcResizeObserver>
+    );
+}
+
+export default ExamSampleBaseInfo;

+ 316 - 0
YBEE.EQM.Admin/src/pages/exam-center/special-student/SpecialStudentList/index.tsx

@@ -0,0 +1,316 @@
+import { toValueEnum } from "@/common/converter";
+import { downloadFileByBlob } from "@/common/net/download";
+import { FileLink, SuperTable, TabBadge } from "@/components";
+import ExamGradeController from "@/services/apis/ExamGradeController";
+import ExamSpecialStudentCenterController from "@/services/apis/ExamSpecialStudentCenterController";
+import { DownloadOutlined } from "@ant-design/icons";
+import { ActionType, PageContainer, ProColumns, ProDescriptionsItemProps, ProFormInstance } from "@ant-design/pro-components";
+import { history, useModel, useParams } from "@umijs/max";
+import { useRequest } from "ahooks";
+import { App, Button, Space, Tag, Typography, theme } from "antd";
+import { useCallback, useRef, useState } from "react";
+import ExamSpecialStudentDetailDrawer from "../../../exam-org/special-student/OrgExamSpecialStudentReport/components/ExamSpecialStudentDetailDrawer";
+
+const ExamSpecialStudentList: React.FC = () => {
+    const reqParams = useParams() as unknown as { examPlanId: number };
+
+    const { token } = theme.useToken();
+    const { getDictValueEnum, getKeyDict, getDict } = useModel('useDict');
+    const auditStatusDict = getKeyDict('audit_status');
+
+    const [detailOpen, setDetailOpen] = useState(false);
+    const actionRef = useRef<ActionType>();
+    const formRef = useRef<ProFormInstance>();
+    const currentRef = useRef<Partial<API.ExamAbsentReplaceOutput>>();
+
+    const [activeKey, setActiveKey] = useState<React.Key>('0');
+    const [statusCount, seStatusCount] = useState<Record<number, number>>({});
+
+    const { message } = App.useApp();
+
+    const { data: gradeBranchData } = useRequest(async () => {
+        const res1 = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId });
+        return {
+            examGrades: res1 ?? [],
+        };
+    });
+
+    // 加载数量统计
+    const loadCount = useCallback(async (params: API.ExamAbsentReplacePageInput) => {
+        const m = await ExamSpecialStudentCenterController.queryStatusCount(params);
+        const tc = m?.reduce((a: any, b: any) => a + b.count, 0) ?? 0;
+
+        const d: Record<number, number> = { 0: tc };
+        m?.forEach((t: any) => { d[t.status] = t.count; });
+
+        seStatusCount(d);
+    }, []);
+
+    const getExportParams = useCallback(async () => {
+        let p = {
+            examPlanId: reqParams.examPlanId,
+            status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
+            pageIndex: 1,
+            pageSize: 99999999,
+        };
+
+        try {
+            const ps = await formRef.current?.validateFields();
+            return {
+                ...p,
+                ...ps,
+            };
+        }
+        catch (ex) {
+            console.error(ex);
+            return p;
+        }
+    }, [activeKey]);
+
+    // 导出简表
+    const handleExportSimple = useCallback(async () => {
+        const p = await getExportParams();
+        const res = await ExamSpecialStudentCenterController.exportSimple(p);
+        if (res) {
+            downloadFileByBlob(res.data, res.fileName);
+        }
+        else {
+            message.error('下载失败');
+        }
+    }, [activeKey]);
+    // 导出详表
+    const handleExportFull = useCallback(async () => {
+        const p = await getExportParams();
+        const res = await ExamSpecialStudentCenterController.exportFull(p);
+        if (res) {
+            downloadFileByBlob(res.data, res.fileName);
+        }
+        else {
+            message.error('下载失败');
+        }
+    }, [activeKey]);
+
+    // 明细表列定义
+    const detailColumns: ProColumns<API.ExamSpecialStudentFullOutput>[] = [
+        {
+            title: '学校',
+            dataIndex: ['sysOrg', 'name'],
+            width: 180,
+            hideInSearch: true,
+        },
+        {
+            title: '校区',
+            dataIndex: ['sysOrgBranch', 'name'],
+            width: 96,
+            align: 'center',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '状态',
+            dataIndex: 'status',
+            hideInSearch: true,
+            width: 80,
+            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>);
+            },
+        },
+        {
+            title: '前期认定',
+            dataIndex: 'isPreIdentified',
+            // hideInSearch: true,
+            width: 80,
+            align: 'center',
+            render: (_, r) => {
+                return (
+                    <Typography.Text
+                        type={r.isPreIdentified ? 'warning' : 'danger'}
+                        style={{ fontSize: token.fontSizeSM }}
+                    >
+                        {r.isPreIdentified ? '是' : '否'}
+                    </Typography.Text>
+                );
+            },
+        },
+        {
+            title: '前测科数',
+            dataIndex: 'preTotalCourse',
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+        },
+        {
+            title: '前测成绩',
+            dataIndex: 'preTotalScore',
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+        },
+        {
+            title: '年级',
+            dataIndex: 'gradeId',
+            width: 80,
+            align: 'center',
+            valueEnum: toValueEnum(gradeBranchData?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
+            render: (_, r) => {
+                return (
+                    <Space size="small" direction="vertical" style={{ lineHeight: 1 }}>
+                        {r.examGrade?.grade.name}
+                        {/* <Typography.Text type="secondary" style={{ fontSize: token.fontSizeSM }}>({r.examGrade?.gradeBeginName})</Typography.Text> */}
+                    </Space>
+                );
+            },
+        },
+        {
+            title: '班级',
+            dataIndex: 'classNumber',
+            width: 64,
+            align: 'center',
+            renderText: (v) => `${v}班`,
+        },
+        {
+            title: '证件类型',
+            dataIndex: 'certificateType',
+            valueEnum: getDictValueEnum('certificate_type'),
+            width: 112,
+            align: 'center',
+        },
+        {
+            title: '证件号码',
+            dataIndex: 'idNumber',
+            width: 160,
+            align: 'center',
+        },
+        {
+            title: '姓名',
+            dataIndex: 'absentName',
+            width: 112,
+            align: 'center',
+            render: (v, r) => <a onClick={() => { currentRef.current = r; setDetailOpen(true); }}>{v}</a>,
+        },
+        {
+            title: '家长电话',
+            dataIndex: 'patriarchTel',
+            hideInSearch: true,
+            hideInTable: true,
+            width: 128,
+            align: 'center',
+        },
+        {
+            title: '备注',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            ellipsis: true,
+            width: 120,
+        },
+        {
+            title: '佐证材料',
+            hideInSearch: true,
+            hideInTable: true,
+            render: (_, r) => {
+                const li = r.attachmentList?.map((t, i) => {
+                    return (
+                        <FileLink
+                            key={i}
+                            fileExtName={t.fileExtName}
+                            fileName={t.fileName}
+                            url={`${AppConfig.fileViewRoot}?id=${t.fileId}`}
+                            thumbUrl={t.thumbFileId && t.thumbFileId !== '0' ? `${AppConfig.fileViewRoot}?id=${t.thumbFileId}` : undefined}
+                            card
+                        />
+                    );
+                });
+                return (
+                    <Space direction="vertical" style={{ width: '100%' }}>
+                        {li?.length === 0 ? '未上传' : li}
+                    </Space>
+                );
+            },
+        },
+    ];
+
+    // 呈现状态 tab
+    const renderTabItems = useCallback(() => {
+        let items: { key: string; label: React.ReactNode }[] = [
+            {
+                key: '0',
+                label: (<span>全部<TabBadge count={statusCount[0]} active={activeKey === 0} /></span>),
+            }
+        ];
+        items = items.concat(
+            getDict('audit_status')?.filter(t => t.value > 1)?.sort((a, b) => a.sort - b.sort)?.map((t) => ({
+                key: `${t.value}`,
+                label: (
+                    <span>
+                        {t.name}
+                        <TabBadge color={t.antColor} count={statusCount[t.value] ?? 0} active={activeKey === `${t.value}`} />
+                    </span>
+                ),
+            })),
+        );
+        return items;
+    }, [activeKey, statusCount]);
+
+    return (
+        <PageContainer
+            // title={`${reportData?.examPlan?.fullName ?? ''} - 缺测替补学生名单`}
+            onBack={() => history.back()}
+        >
+            <SuperTable<API.ExamSpecialStudentFullOutput>
+                actionRef={actionRef}
+                formRef={formRef}
+                scroll={{ x: '100%' }}
+                rowKey="id"
+                columns={detailColumns}
+                options={{ setting: false, fullScreen: false }}
+                toolbar={{
+                    title: '特殊学生明细',
+                    menu: {
+                        type: 'tab',
+                        activeKey: activeKey,
+                        items: renderTabItems(),
+                        onChange: (key) => {
+                            setActiveKey(key as React.Key);
+                            actionRef.current?.reload();
+                        },
+                    },
+                    actions: [
+                        <Button
+                            key="btnExportSimple"
+                            icon={<DownloadOutlined />}
+                            onClick={handleExportSimple}
+                        >导出简表</Button>,
+                        <Button
+                            key="btnExportFull"
+                            icon={<DownloadOutlined />}
+                            onClick={handleExportFull}
+                        >导出详表</Button>,
+                    ],
+                }}
+                request={async (params, sort) => {
+                    return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                        const qparams = { ...p, examPlanId: reqParams.examPlanId };
+                        await loadCount(qparams);
+                        const res = await ExamSpecialStudentCenterController.queryPageList({
+                            ...qparams,
+                            status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
+                        });
+                        return res;
+                    });
+                }}
+            />
+            {detailOpen && currentRef.current &&
+                <ExamSpecialStudentDetailDrawer
+                    data={currentRef.current}
+                    columns={detailColumns as ProDescriptionsItemProps<Partial<API.ExamSpecialStudentFullOutput>>[]}
+                    onClose={() => setDetailOpen(false)}
+                />
+            }
+        </PageContainer>
+    );
+}
+
+export default ExamSpecialStudentList;

+ 96 - 0
YBEE.EQM.Admin/src/pages/exam-center/special-student/index.tsx

@@ -0,0 +1,96 @@
+import { toSelectOptions } from '@/common/converter';
+import { SuperTable } from '@/components';
+import ExamDataReportController from '@/services/apis/ExamDataReportController';
+import { DataReportType } from '@/services/enums';
+import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
+import { history, useModel } from '@umijs/max';
+import { useRef } from 'react';
+
+/** 特殊学生上报计划 */
+const ExamSpecialStudentList: React.FC = () => {
+    const actionRef = useRef<ActionType>();
+
+    const { getDictValueEnum } = useModel('useDict');
+    const { baseData } = useModel('useBaseData');
+
+    const columns: ProColumns<API.ExamDataReportPlanOutput>[] = [
+        {
+            title: '监测计划',
+            dataIndex: ['examPlan', 'fullName'],
+            renderText: (v, r) => <a onClick={() => history.push(`/exam-c/sp-stu/list/${r.examPlanId}`)}>{v} - 特殊学生</a>,
+            hideInDescriptions: true,
+            search: {
+                transform: (v) => ({ name: v }),
+            },
+        },
+        {
+            title: '监测学期',
+            dataIndex: ['examPlan', 'semester', 'nickShortName'],
+            search: {
+                transform: (v) => v ? ({ semesterId: JSON.parse(v) }) : v,
+            },
+            valueType: 'select',
+            fieldProps: {
+                options: toSelectOptions(baseData?.semesters ?? [], (item) => ({ key: item.id, label: item.nickShortName, value: item.id })),
+            },
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '监测学段',
+            dataIndex: ['examPlan', 'educationStage'],
+            valueEnum: getDictValueEnum('education_stage'),
+            width: 80,
+            align: 'center',
+            search: {
+                transform: (v) => ({ educationStage: v }),
+            },
+        },
+        {
+            title: '计划状态',
+            dataIndex: ['examPlan', 'status'],
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInDescriptions: true,
+        },
+        {
+            title: '上报状态',
+            dataIndex: 'status',
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInDescriptions: true,
+        },
+        {
+            title: '备注说明',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            className: 'minw-120',
+        },
+    ];
+
+    return (
+        <PageContainer title={false}>
+            <SuperTable<API.ExamDataReportPlanOutput>
+                actionRef={actionRef}
+                columns={columns}
+                scroll={{ x: 'max-content' }}
+                toolbar={{
+                    title: '特殊学生上报计划列表',
+                }}
+                request={async (params, sort) => {
+                    try {
+                        return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                            const res = await ExamDataReportController.queryPlanPageList({ ...p, type: DataReportType.SP_STUDENT });
+                            return res;
+                        });
+                    }
+                    catch (ex) { return {}; }
+                }}
+            />
+        </PageContainer>
+    );
+};
+
+export default ExamSpecialStudentList;

+ 3 - 0
YBEE.EQM.Admin/src/pages/exam-org/OrgExamPlan/index.tsx

@@ -18,6 +18,9 @@ const OrgExamPlanList: React.FC = () => {
             dataIndex: 'fullName',
             renderText: (v, r) => <a onClick={() => history.push(`/exam-s/plan/detail/${r.id}`)}>{v}</a>,
             hideInDescriptions: true,
+            search: {
+                transform: v => ({ name: v }),
+            },
         },
         {
             title: '监测名称',

+ 7 - 2
YBEE.EQM.Admin/src/pages/exam-org/absent-replace/OrgExamAbsentReplaceReport/components/ExamAbsentReplaceDetailDrawer.tsx

@@ -26,11 +26,16 @@ const ExamAbsentReplaceDetailDrawer: React.FC<ExamAbsentReplaceDetailDrawerProps
                 columns={columns}
                 column={1}
                 dataSource={data}
-                labelStyle={{ width: 96 }}
+                labelStyle={{ width: 128 }}
             >
                 <ProDescriptions.Item label="审核记录">
                     {data.auditList && data.auditList.length > 0 ?
-                        <Card style={{ width: '100%' }} bodyStyle={{ paddingBottom: 0 }}>
+                        <Card
+                            styles={{
+                                cover: { width: '100%' },
+                                body: { paddingBottom: 0 },
+                            }}
+                        >
                             <AuditTimeline auditList={data.auditList} />
                         </Card> : '无'
                     }

+ 174 - 0
YBEE.EQM.Admin/src/pages/exam-org/sample-replace/OrgExamSampleReplaceList/components/OrgExamSampleReplaceSampleDrawer.tsx

@@ -0,0 +1,174 @@
+import { toValueEnum } from '@/common/converter';
+import { SuperTable } from '@/components';
+import ExamSampleReplaceController from '@/services/apis/ExamSampleReplaceController';
+import ExamSampleStudentController from '@/services/apis/ExamSampleStudentController';
+import { ExamSampleType } from '@/services/enums';
+import { ActionType, ProColumns } from '@ant-design/pro-components';
+import { useModel } from '@umijs/max';
+import { Alert, App, Button, Drawer, Space, theme, Typography } from 'antd';
+import { useCallback, useRef, useState } from 'react';
+import { OrgExamSampleReplaceType } from '..';
+
+/** 创建/修改监测计划 */
+const OrgExamSampleReplaceSampleModal: React.FC<{
+    data?: OrgExamSampleReplaceType;
+    onFinish: () => void;
+    onClose?: () => void;
+}> = ({ data, onFinish, onClose }) => {
+    const { message } = App.useApp();
+    const { token } = theme.useToken();
+
+    const [open, setOpen] = useState(true);
+    const handleClose = () => { setOpen(false); setTimeout(() => onClose?.(), 300); };
+
+    const { getDictValueEnum } = useModel('useDict');
+    const actionRef = useRef<ActionType>();
+
+    // 明细表列定义
+    const columns: ProColumns<API.ExamSampleStudentOutput>[] = [
+        ...(data?.hasDistrict ? [{
+            title: '校区',
+            dataIndex: ['examStudent', 'sysOrgBranchId'],
+            width: 96,
+            align: 'center',
+            hideInSearch: !data?.hasDistrict,
+            valueEnum: toValueEnum(data?.branches ?? []),
+            search: {
+                transform: (v) => ({ sysOrgBranchId: v }),
+            },
+        } as ProColumns] : []),
+        {
+            title: '年级',
+            dataIndex: ['examStudent', 'examGrade', 'gradeId'],
+            width: 144,
+            align: 'center',
+            valueEnum: toValueEnum(data?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
+            search: {
+                transform: (v) => ({ gradeId: v }),
+            },
+        },
+        {
+            title: '班级',
+            dataIndex: ['examStudent', 'classNumber'],
+            width: 64,
+            align: 'center',
+            renderText: (v) => `${v}班`,
+            search: {
+                transform: (v) => ({ classNumber: v }),
+            },
+        },
+        {
+            title: '监测号',
+            dataIndex: 'examNumber',
+            width: 128,
+            align: 'center',
+        },
+        {
+            title: '姓名',
+            dataIndex: ['examStudent', 'name'],
+            width: 160,
+            align: 'center',
+            search: {
+                transform: (v) => ({ name: v }),
+            },
+        },
+        {
+            title: '证件类型',
+            dataIndex: ['examStudent', 'certificateType'],
+            valueEnum: getDictValueEnum('certificate_type'),
+            width: 112,
+            align: 'center',
+            search: {
+                transform: (v) => ({ certificateType: JSON.parse(v) }),
+            },
+        },
+        {
+            title: '证件号码',
+            dataIndex: ['examStudent', 'idNumber'],
+            width: 160,
+            align: 'center',
+            search: {
+                transform: (v) => ({ idNumber: v }),
+            },
+        },
+    ];
+
+    // 添加
+    const handleAdd = useCallback(async (ids: string[]) => {
+        if (!ids || ids.length === 0) {
+            return;
+        }
+        await ExamSampleReplaceController.sample({ absentExamSampleStudentId: ids[0] });
+        message.success('替补抽取成功');
+        onFinish();
+        handleClose();
+    }, []);
+
+    return (
+        <Drawer
+            title="添加缺测学生并抽取替补学生"
+            width={1080}
+            open={open}
+            onClose={handleClose}
+            styles={{ body: { padding: 0 } }}
+        >
+            <Alert
+                type="info"
+                showIcon
+                closable
+                message="从【区级监测学生列表】中勾选缺测学生(一次只能选择一个),勾选后点击【确定并抽取替补】按钮即可。"
+                style={{
+                    marginInline: token.paddingLG,
+                    marginBlockStart: token.marginLG,
+                }}
+            />
+            <SuperTable<API.ExamSampleStudentOutput>
+                actionRef={actionRef}
+                scroll={{ x: 'max-content' }}
+                sticky
+                rowKey="id"
+                columnEmptyText=""
+                columns={columns}
+                toolbar={{
+                    title: '区级监测学生列表',
+                }}
+                request={async (params, sort) => {
+                    return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                        const res = await ExamSampleStudentController.queryPageList({
+                            ...p,
+                            examSampleType: ExamSampleType.DISTRICT,
+                            examSampleId: data?.examSample?.id ?? 0
+                        });
+                        return res;
+                    });
+                }}
+                search={{
+                    defaultCollapsed: false,
+                    span: 6,
+                }}
+                rowSelection={{ type: 'radio' }}
+                tableAlertRender={({ selectedRows, onCleanSelected }) => {
+                    return (
+                        <Space>
+                            <Typography.Text>
+                                已选择
+                                <Typography.Text mark>
+                                    &nbsp;
+                                    {selectedRows.map(t => `${t.examStudent?.examGrade?.grade.name} > ${t.examStudent?.classNumber}班 > ${t.examStudent?.name},监测号为:${t.examNumber}`)}
+                                    &nbsp;
+                                </Typography.Text>
+                            </Typography.Text>
+                            <Button type="link" onClick={() => { onCleanSelected(); }}>取消选择</Button>
+                        </Space>
+                    );
+                }}
+                tableAlertOptionRender={({ selectedRowKeys }) => {
+                    return (<Button type="primary" onClick={() => handleAdd(selectedRowKeys as string[])}>确定并抽取替补</Button>);
+                }}
+                pagination={{ pageSize: 10 }}
+            />
+        </Drawer >
+    );
+};
+
+export default OrgExamSampleReplaceSampleModal;

+ 303 - 0
YBEE.EQM.Admin/src/pages/exam-org/sample-replace/OrgExamSampleReplaceList/index.tsx

@@ -0,0 +1,303 @@
+import { toValueEnum } from '@/common/converter';
+import { downloadFileByBlob } from '@/common/net/download';
+import { SuperTable, TagStatus } from '@/components';
+import ExamGradeController from '@/services/apis/ExamGradeController';
+import ExamSampleController from '@/services/apis/ExamSampleController';
+import ExamSampleReplaceController from '@/services/apis/ExamSampleReplaceController';
+import SysOrgController from '@/services/apis/SysOrgController';
+import { DataPublishType } from '@/services/enums';
+import { DownloadOutlined, PlusOutlined } from '@ant-design/icons';
+import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
+import { history, useModel, useParams } from '@umijs/max';
+import { useRequest } from 'ahooks';
+import { Alert, App, Button, Space, Typography } from 'antd';
+import { useCallback, useRef, useState } from 'react';
+import OrgExamSampleReplaceSampleDrawer from './components/OrgExamSampleReplaceSampleDrawer';
+
+export type OrgExamSampleReplaceType = {
+    branches: API.SysOrgLiteOutput[];
+    hasDistrict: boolean;
+    examGrades: API.ExamGradeOutput[];
+    examSample?: API.ExamSamplePlanOutput;
+};
+
+/** 缺测替补抽取列表 */
+const OrgExamSampleReplaceList: React.FC = () => {
+    const reqParams = useParams() as unknown as { examPlanId: number; examDataPublishId: number; };
+    const actionRef = useRef<ActionType>();
+
+    const { message, modal } = App.useApp();
+    const [open, setOpen] = useState(false);
+
+    const { initialState } = useModel('@@initialState');
+    const { currentUser } = initialState ?? {};
+    const { data } = useRequest(async () => {
+        const sampleRes = await ExamSampleController.getByExamDataPublishId({ examdatapublishid: reqParams.examDataPublishId, type: DataPublishType.STUDENT_SAMPLE_LIST });
+        const orgRes = await SysOrgController.getOrgBranchByOrgId({ orgid: currentUser?.sysOrgId ?? 0 });
+        const egRes = await ExamGradeController.getListByExamPlanId({ examplanid: reqParams.examPlanId ?? 0 });
+        return {
+            branches: orgRes ?? [],
+            hasDistrict: (orgRes?.length ?? 0) > 0,
+            examGrades: egRes ?? [],
+            examSample: sampleRes,
+        } as OrgExamSampleReplaceType;
+    });
+
+    // 软删除缺测生
+    const handleDelete = useCallback(async (id: number) => {
+        modal.confirm({
+            title: '警告',
+            content: '确定立即删除吗',
+            okText: '确定',
+            cancelText: '取消',
+            centered: true,
+            onOk: async () => {
+                try {
+                    await ExamSampleReplaceController.fakeDelete({ id });
+                    actionRef.current?.reload();
+                    message.success('已删除');
+                }
+                catch {
+                    message.error('删除失败');
+                }
+            },
+        });
+    }, []);
+
+    // 标注或取消替补学生也缺测
+    const handleMarkedAbsent = useCallback(async (id: number, isReplaceAbsent: boolean) => {
+        const prefix = isReplaceAbsent ? '取消' : '标注';
+        modal.confirm({
+            title: '警告',
+            content: `确定${prefix}该替补学生同样缺测吗?`,
+            okText: '确定',
+            cancelText: '取消',
+            centered: true,
+            onOk: async () => {
+                try {
+                    await ExamSampleReplaceController.markedReplaceAbsent({ id });
+                    actionRef.current?.reload();
+                    message.success(`已${prefix}`);
+                }
+                catch {
+                    message.error(`${prefix}失败`);
+                }
+            },
+        });
+    }, []);
+
+    const columns: ProColumns<API.ExamSampleReplaceOutput>[] = [
+        {
+            title: '序',
+            width: 48,
+            align: 'center',
+            renderText: (v, r, ri) => ri + 1,
+        },
+        ...(data?.hasDistrict ? [{
+            title: '校区',
+            dataIndex: ['sysOrgBranchId'],
+            width: 96,
+            align: 'center',
+            hideInSearch: !data?.hasDistrict,
+            valueEnum: toValueEnum(data?.branches ?? []),
+            search: {
+                transform: (v) => ({ sysOrgBranchId: v }),
+            },
+        } as ProColumns] : []),
+        {
+            title: '年级',
+            dataIndex: ['examGrade', 'gradeId'],
+            width: 144,
+            align: 'center',
+            valueEnum: toValueEnum(data?.examGrades?.map(t => ({ id: t.gradeId, name: `${t.grade.name}(${t.gradeBeginName})` })) ?? []),
+            search: {
+                transform: (v) => ({ gradeId: v }),
+            },
+        },
+        {
+            title: '班级',
+            dataIndex: 'classNumber',
+            width: 64,
+            align: 'center',
+            renderText: (v) => `${v}班`,
+        },
+
+        {
+            title: '缺测学生',
+            align: 'center',
+            children: [
+                {
+                    title: '姓名',
+                    dataIndex: ['absentExamSampleStudent', 'examStudent', 'name'],
+                    width: 120,
+                    align: 'center',
+                },
+                {
+                    title: '证件号码',
+                    dataIndex: ['absentExamSampleStudent', 'examStudent', 'idNumber'],
+                    width: 160,
+                    align: 'center',
+                },
+                {
+                    title: '监测号',
+                    dataIndex: ['absentExamSampleStudent', 'examNumber'],
+                    width: 128,
+                    align: 'center',
+                },
+            ],
+        },
+
+        {
+            title: '替补学生',
+            align: 'center',
+            children: [
+                {
+                    title: '姓名',
+                    dataIndex: ['replaceExamSampleStudent', 'examStudent', 'name'],
+                    width: 120,
+                    align: 'center',
+                },
+                {
+                    title: '证件号码',
+                    dataIndex: ['replaceExamSampleStudent', 'examStudent', 'idNumber'],
+                    width: 160,
+                    align: 'center',
+                },
+                {
+                    title: '监测号',
+                    dataIndex: ['replaceExamSampleStudent', 'examNumber'],
+                    width: 128,
+                    align: 'center',
+                },
+                {
+                    title: '是否同样缺测',
+                    dataIndex: 'isReplaceAbsent',
+                    // align: 'center',
+                    width: 120,
+                    render: (_, r) => {
+                        return (
+                            <Space>
+                                {/* <Typography.Text>{r.isReplaceAbsent ? '是' : '否'}</Typography.Text> */}
+                                <TagStatus status={r.isReplaceAbsent ? 'error' : 'default'}>{r.isReplaceAbsent ? '是' : '否'}</TagStatus>
+                                <Button
+                                    type="link"
+                                    size="small"
+                                    disabled={r.isReplaceAbsentLocked}
+                                    onClick={() => handleMarkedAbsent(r.id, r.isReplaceAbsent)}
+                                >{r.isReplaceAbsent ? '取消缺测' : '标注缺测'}</Button>
+                            </Space>
+                        );
+                    },
+                },
+            ],
+        },
+        {
+            title: '抽取时间',
+            dataIndex: 'createTime',
+            align: 'center',
+            width: 144,
+        },
+        // {},
+        {
+            title: '操作',
+            valueType: 'option',
+            width: 64,
+            align: 'center',
+            fixed: 'right',
+            render: (_, r) => {
+                return (<a onClick={() => handleDelete(r.id)}>删除</a>);
+            }
+        }
+        // {
+        //     title: '备注',
+        //     dataIndex: 'remark',
+        //     hideInSearch: true,
+        //     className: 'minw-120',
+        // },
+    ];
+
+    const handleDownload = useCallback(async () => {
+        try {
+            message.loading('正在生成文件,请稍侯');
+            if (!reqParams.examPlanId) {
+                return;
+            }
+            const res = await ExamSampleReplaceController.exportToOrg({ examplanid: reqParams.examPlanId });
+            if (res) {
+                downloadFileByBlob(res.data, res.fileName);
+            }
+        }
+        catch { }
+        finally {
+            message.destroy();
+        }
+    }, []);
+
+    return (
+        <PageContainer
+            title={`${data?.examSample?.examPlan?.fullName ?? '监测计划'} - 缺测替补学生名单`}
+            onBack={() => history.back()}
+            content={
+                <Typography.Paragraph>
+                    <Typography.Title level={5}>操作说明:</Typography.Title>
+                    <ul>
+                        <li>第一步:点击【添加缺测并抽取替补】按钮打开</li>
+                        <li>第二步:根据条件查询缺测学生</li>
+                        <li>第三步:勾选缺测学生后,点击【确定并抽取替补】即可</li>
+                    </ul>
+                    <Alert
+                        type="warning"
+                        showIcon
+                        message={
+                            <Typography.Text>
+                                <Typography.Text>特别说明:若有抽取的替补学生也有同样缺测的情况,先在</Typography.Text>
+                                <Typography.Text mark>&nbsp;替补学生/是否同样缺测&nbsp;</Typography.Text>
+                                <Typography.Text>列中点击【标注缺测】把替补学生标注为缺测,然后再添加缺测学生抽取新的替补学生!</Typography.Text>
+                            </Typography.Text>
+                        }
+                    />
+                </Typography.Paragraph>
+            }
+        >
+            <SuperTable<API.ExamSampleReplaceOutput>
+                actionRef={actionRef}
+                search={false}
+                columns={columns}
+                scroll={{ x: 'max-content' }}
+                request={async (params, sort) => {
+                    return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                        const res = await ExamSampleReplaceController.queryOrgPageList({
+                            ...p,
+                            examPlanId: reqParams.examPlanId ?? 0,
+                        });
+                        return res;
+                    });
+                }}
+                toolbar={{
+                    title: '缺测替补学生名单列表',
+                    actions: [
+                        <Button
+                            key="btnSample"
+                            type="primary"
+                            icon={<PlusOutlined />}
+                            onClick={() => setOpen(true)}
+                        >添加缺测并抽取替补</Button>,
+                        <Button
+                            key="btnDownload"
+                            icon={<DownloadOutlined />}
+                            onClick={handleDownload}
+                        >下载名单</Button>
+                    ],
+                }}
+            // pagination={false}
+            />
+            {open && <OrgExamSampleReplaceSampleDrawer
+                data={data}
+                onFinish={() => actionRef.current?.reload()}
+                onClose={() => setOpen(false)}
+            />}
+        </PageContainer>
+    );
+};
+
+export default OrgExamSampleReplaceList;

+ 105 - 0
YBEE.EQM.Admin/src/pages/exam-org/sample-replace/index.tsx

@@ -0,0 +1,105 @@
+import { toSelectOptions } from '@/common/converter';
+import { SuperTable } from '@/components';
+import ExamDataPublishController from '@/services/apis/ExamDataPublishController';
+import { DataPublishType, ExamStatus } from '@/services/enums';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
+import { history, useModel } from '@umijs/max';
+import { theme } from 'antd';
+import { useRef } from 'react';
+
+/** 缺测替补抽取列表 */
+const OrgExamSampleReplacePlanList: React.FC = () => {
+    const actionRef = useRef<ActionType>();
+    const { token } = theme.useToken();
+
+    const { getDictValueEnum } = useModel('useDict');
+    const { baseData } = useModel('useBaseData');
+
+    const columns: ProColumns<API.ExamDataPublishOrgOutput>[] = [
+        {
+            title: '抽样名单',
+            dataIndex: 'fullName',
+            search: {
+                transform: v => ({ name: v }),
+            },
+            render: (_, r) => {
+                const name = `${r.examPlanFullName} - ${r.examDataPublishName}`;
+                if (r.examPlanStatus !== ExamStatus.ACTIVE) {
+                    return name;
+                }
+                return (<a onClick={() => history.push(`/exam-s/replace/list/${r.examPlanId}/${r.examDataPublishId}`)}>{name}</a>);
+            },
+        },
+        {
+            title: '监测状态',
+            dataIndex: 'examPlanStatus',
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+        },
+        {
+            title: '监测学期',
+            dataIndex: 'semesterNickShortName',
+            search: {
+                transform: (v) => v ? ({ semesterId: JSON.parse(v) }) : v,
+            },
+            valueType: 'select',
+            fieldProps: {
+                options: toSelectOptions(baseData?.semesters ?? [], (item) => ({ key: item.id, label: item.nickShortName, value: item.id })),
+            },
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '监测学段',
+            dataIndex: 'educationStage',
+            valueEnum: getDictValueEnum('education_stage'),
+            width: 80,
+            align: 'center',
+            hideInSearch: true,
+        },
+        {
+            title: '备注说明',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            className: 'minw-120',
+        },
+    ];
+
+    return (
+        <PageContainer
+            title="缺测替补学生抽取管理"
+            content="在区级监测工作开展前,或是监测进行的过程中,如果出现被抽选参与区级监测的学生缺测的情况,需在此抽取替补学生。若某个班级的全体学生均已被纳入区级监测抽样范围,那么该班级则无需再另行抽取替补学生。"
+            extra={
+                <a onClick={() => { window.open('/handbook/缺测替补学生抽取操作说明.pdf'); }}>
+                    <QuestionCircleOutlined style={{ marginRight: token.marginXS }} />
+                    操作说明
+                </a>
+            }
+        >
+            <SuperTable<API.ExamDataPublishOrgOutput>
+                toolbar={{
+                    title: '抽样名单列表',
+                    subTitle: '点击下方抽样名单进入替补学生抽取',
+                }}
+                actionRef={actionRef}
+                columns={columns}
+                scroll={{ x: 'max-content' }}
+                rowKey="rowNumber"
+                request={async (params, sort) => {
+                    return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                        const res = await ExamDataPublishController.queryOrgPageList({
+                            ...p,
+                            type: DataPublishType.STUDENT_SAMPLE_LIST,
+                        });
+                        return res;
+                    });
+                }}
+            />
+        </PageContainer>
+    );
+};
+
+export default OrgExamSampleReplacePlanList;

+ 1 - 1
YBEE.EQM.Admin/src/pages/exam-org/sample/OrgExamSampleList/index.tsx

@@ -44,7 +44,7 @@ const OrgExamSampleList: React.FC = () => {
             title: '名单类型',
             dataIndex: 'examSampleType',
             valueEnum: getDictValueEnum('exam_sample_type', false, [ExamSampleType.SCHOOL]),
-            width: 72,
+            width: 80,
             align: 'center',
             search: {
                 transform: (v) => ({ examSampleType: JSON.parse(v) }),

+ 6 - 2
YBEE.EQM.Admin/src/pages/exam-org/special-student/OrgExamSpecialStudentReport/components/ExamSpecialStudentEditModal.tsx

@@ -15,7 +15,7 @@ export type GradeBranchData = {
 /** 修改监测特殊学生信息 */
 const ExamSpecialStudentEditModal: React.FC<{
     examPlanId: number;
-    data: Partial<API.ExamSpecialStudentOutput>;
+    data: Partial<API.ExamSpecialStudentOutput> & { isRejectedReaudit?: boolean; };
     gradeBranch: GradeBranchData;
     onFinish: () => void;
     onClose?: () => void;
@@ -68,7 +68,11 @@ const ExamSpecialStudentEditModal: React.FC<{
                         gender: JSON.parse(gender as any),
                     };
                     if (data.id !== 0) {
-                        const up = { id: data.id ?? 0, ...p } as API.UpdateExamSpecialStudentInput;
+                        const up = {
+                            ...p,
+                            id: data.id ?? 0,
+                            isRejectedReaudit: data.isRejectedReaudit,
+                        } as API.UpdateExamSpecialStudentInput;
                         await ExamSpecialStudentController.update(up);
                     }
                     else {

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

@@ -11,7 +11,7 @@ import { BulbOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UploadOut
 import { ActionType, PageContainer, ProCard, ProColumns, ProDescriptions, ProDescriptionsItemProps, ProTable } from "@ant-design/pro-components";
 import { history, useModel, useParams } from "@umijs/max";
 import { useRequest } from "ahooks";
-import { Alert, App, Badge, Button, Card, Drawer, Space, Tag, Tour, Typography, theme } from "antd";
+import { Alert, App, Badge, Button, Card, Divider, Drawer, Space, Tag, Tooltip, Tour, Typography, theme } from "antd";
 import { useCallback, useRef, useState } from "react";
 import ExamSpecialStudentDetailDrawer from "./components/ExamSpecialStudentDetailDrawer";
 import ExamSpecialStudentEditModal from "./components/ExamSpecialStudentEditModal";
@@ -34,7 +34,7 @@ const OrgExamSpecialStudentReport: React.FC = () => {
     const [remarkOpen, setRemarkOpen] = useState(false);
     const [editOpen, setEditOpen] = useState(false);
     const actionRef = useRef<ActionType>();
-    const currentRef = useRef<Partial<API.ExamSpecialStudentOutput>>();
+    const currentRef = useRef<Partial<API.ExamSpecialStudentOutput & { isRejectedReaudit?: boolean; }>>();
 
     const [activeKey, setActiveKey] = useState<React.Key>('2');
     const [statusCount, seStatusCount] = useState<Record<number, number>>({});
@@ -126,12 +126,14 @@ const OrgExamSpecialStudentReport: React.FC = () => {
     // 上报
     const handleSubmit = useCallback(() => {
         return new Promise<void>((resolve, reject) => {
-            if ((studentCountData?.total ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
+            // if ((studentCountData?.total ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
+            if ((statusCount?.[AuditStatus.AUDIT] ?? 0) > 0 && (reportData?.examOrgDataReport?.attachmentList?.length ?? 0) === 0) {
                 message.error('未上传《特殊学生明细表》和《会议记录》打印盖章的扫描电子文件');
                 reject();
                 return;
             }
-            let content = `共 ${studentCountData?.total ?? 0} 个学生,上报后不能再修改,确定立即上报吗?`;
+            // let content = `共 ${studentCountData?.total ?? 0} 个学生,上报后不能再修改,确定立即上报吗?`;
+            let content = `共上报【${statusCount?.[AuditStatus.AUDIT] ?? 0}】个待审核特殊学生,上报后不能再修改,确定立即上报吗?`;
             modal.confirm({
                 title: '警告',
                 content,
@@ -157,7 +159,8 @@ const OrgExamSpecialStudentReport: React.FC = () => {
                 onCancel: () => reject(),
             });
         });
-    }, [studentCountData, reportData]);
+        // }, [studentCountData, reportData]);
+    }, [statusCount, reportData]);
 
     // 删除佐证材料
     const handleDeleteAttachment = useCallback((id: number, fileId: string) => {
@@ -198,6 +201,9 @@ const OrgExamSpecialStudentReport: React.FC = () => {
         if (res) {
             downloadFileByBlob(res.data, res.fileName);
         }
+        else {
+            message.error('下载失败');
+        }
     }, []);
 
     // // 提交单个学生审核
@@ -223,6 +229,7 @@ const OrgExamSpecialStudentReport: React.FC = () => {
             reportData?.examOrgDataReport?.status === DataReportStatus.REJECTED));
 
     // 工具栏按定义
+    const hasAuditCount = (statusCount?.[AuditStatus.AUDIT] ?? 0) === 0;
     let detailActions = [];
     if (reportable) {
         detailActions.push(
@@ -247,13 +254,15 @@ const OrgExamSpecialStudentReport: React.FC = () => {
             >批量导入</Button>
         );
         detailActions.push(
-            <Button
-                key="download"
-                ref={tourDownloadRef}
-                disabled={!reportable}
-                icon={<DownloadOutlined />}
-                onClick={handleDownloadPrintFile}
-            >下载打印表格文件</Button>
+            <Tooltip color={token.colorError} title={hasAuditCount ? '无待审核特殊学生,请先添加' : null}>
+                <Button
+                    key="download"
+                    ref={tourDownloadRef}
+                    disabled={!reportable || hasAuditCount}
+                    icon={<DownloadOutlined />}
+                    onClick={handleDownloadPrintFile}
+                >下载打印表格文件</Button>
+            </Tooltip>
         );
     }
 
@@ -490,8 +499,12 @@ const OrgExamSpecialStudentReport: React.FC = () => {
             render: (_, r) => {
                 if (reportable && [AuditStatus.UNSUBMIT, AuditStatus.REJECTED, AuditStatus.AUDIT].includes(r.status)) {
                     return (
-                        <Space>
-                            <a onClick={() => { currentRef.current = r; setEditOpen(true); }}>修改</a>
+                        <Space split={<Divider type="vertical" />}>
+                            <a onClick={() => {
+                                currentRef.current = r;
+                                currentRef.current.isRejectedReaudit = r.status === AuditStatus.REJECTED;
+                                setEditOpen(true);
+                            }}>{r.status === AuditStatus.REJECTED ? '重报' : '修改'}</a>
                             <a onClick={() => handleDelete(r.id)}>删除</a>
                         </Space>
                     );

+ 179 - 0
YBEE.EQM.Admin/src/pages/ncee/NceePlan/components/NceePlanEditModal.tsx

@@ -0,0 +1,179 @@
+import { MovableModalForm } from '@/components';
+import NceePlanController from '@/services/apis/NceePlanController';
+import { EducationStage } from '@/services/enums';
+import { FormInstance, ProFormCheckbox, ProFormItem, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
+import { useModel } from '@umijs/max';
+import { App, Space, Tag } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+/** 创建/修改高中分析计划 */
+const NceePlanEditModal: React.FC<{
+    data: Partial<API.NceePlanOutput>;
+    onFinish: () => void;
+    onClose?: () => void;
+}> = ({ data, onFinish, onClose }) => {
+    const formRef = useRef<FormInstance>();
+    const { message } = App.useApp();
+
+    const [open, setOpen] = useState(true);
+
+    const handleClose = () => { setOpen(false); setTimeout(() => onClose?.(), 300); };
+    const isEdit = (data.id ?? 0) > 0;
+
+    const { baseData, fetchBaseData } = useModel('useBaseData');
+    useEffect(() => {
+        (async () => {
+            if (!baseData) {
+                await fetchBaseData();
+            }
+            formRef.current?.setFieldsValue({
+                semesterId: data.semesterId ? data.semesterId : undefined,
+            });
+        })();
+    }, [baseData, formRef.current]);
+
+    return (
+        <MovableModalForm
+            title={`${isEdit ? '修改' : '创建'}高中分析计划`}
+            width={800}
+            open={open}
+            formRef={formRef}
+            initialValues={{
+                ...data,
+            }}
+            modalProps={{
+                centered: true,
+                maskClosable: false,
+                onCancel: () => {
+                    formRef?.current?.resetFields();
+                    handleClose();
+                },
+            }}
+            onFinish={async (values: any) => {
+                try {
+                    const { ...restValues } = values;
+                    let p: API.AddNceePlanInput = {
+                        ...restValues
+                    };
+                    if ((data.id ?? 0) > 0) {
+                        const up = { id: data.id ?? 0, ...p } as API.UpdateNceePlanInput;
+                        await NceePlanController.update(up);
+                    }
+                    else {
+                        await NceePlanController.add(p);
+                    }
+                    message.success('操作成功!');
+                    formRef?.current?.resetFields();
+                    onFinish();
+                    handleClose();
+                    return true;
+                } catch (ex) {
+                    console.error(ex);
+                    message.error('操作失败!');
+                    return false;
+                }
+            }}
+            onFieldsChange={(changedFields) => {
+                if (!['semesterId', 'gradeId', 'planName'].includes(changedFields[0]?.name[0])) {
+                    return;
+                }
+
+                let fsn = '';
+                let sn = '';
+                let short_sn = '';
+                const sn_e = baseData?.semesters?.find(t => t.id === formRef.current?.getFieldValue('semesterId'));
+                if (sn_e) {
+                    fsn = `${sn_e.name}(${sn_e.nickShortName})`;
+                    sn = `${sn_e.shortName}(${sn_e.nickShortName})`;
+                    short_sn = `${sn_e.nickShortName}`;
+                }
+
+                let gn = '';
+                const gn_e = baseData?.grades?.find(t => t.id === formRef.current?.getFieldValue('gradeId'));
+                if (gn_e) {
+                    gn = gn_e.shortName;
+                }
+
+                const planName = formRef.current?.getFieldValue('planName') ?? '';
+                formRef.current?.setFieldsValue({
+                    fullName: `${fsn}${gn}${planName}`,
+                    name: `${sn}${gn}${planName}`,
+                    shortName: `${short_sn}${gn}${planName}`,
+                });
+            }}
+        >
+            <ProFormSelect
+                label="学期"
+                name="semesterId"
+                options={(baseData?.semesters ?? []).map(t => ({
+                    value: t.id,
+                    label: (
+                        <span>
+                            {t.name}({t.nickShortName})
+                            {t.isCurrent && <Tag color="success">当前学期</Tag>}
+                        </span>
+                    ),
+                }))}
+                required
+                rules={[{ required: true }]}
+            />
+            <ProFormRadio.Group
+                label="年级"
+                name="gradeId"
+                options={(baseData?.grades?.filter(t => t.educationStage === EducationStage.SENIOR_HIGH_SCHOOL_STAGE) ?? []).map(t => ({
+                    value: t.id,
+                    label: t.shortName,
+                }))}
+                required
+                rules={[{ required: true }]}
+            />
+            <ProFormText
+                label="名称"
+                name="planName"
+                required
+                rules={[{ required: true }]}
+                fieldProps={{
+                    maxLength: 50,
+                    showCount: true,
+                }}
+            />
+            <ProFormText label="全称" name="fullName" readonly />
+            <ProFormText label="名称" name="name" readonly />
+            <ProFormText label="简称" name="shortName" readonly />
+
+            <ProFormItem label="选项">
+                <Space>
+                    <ProFormCheckbox name={['config', 'convertEnabled']} noStyle                >
+                        转换赋分
+                    </ProFormCheckbox>
+                    <ProFormCheckbox name={['config', 'calcTotalLineScoreEnabled']} noStyle                >
+                        计算总有效分
+                    </ProFormCheckbox>
+                    <ProFormCheckbox name={['config', 'calcCourseLineScoreEnabled']} noStyle                >
+                        计算单科有效分
+                    </ProFormCheckbox>
+                    <ProFormCheckbox name={['config', 'courseCombStatEnabled']} noStyle                >
+                        选科组合统计
+                    </ProFormCheckbox>
+                    <ProFormCheckbox name={['config', 'cxportConvertScoreEnabled']} noStyle                >
+                        导出赋分
+                    </ProFormCheckbox>
+                    <ProFormCheckbox name={['config', 'exportOrderEnabled']} noStyle                >
+                        导出排名
+                    </ProFormCheckbox>
+                </Space>
+            </ProFormItem>
+
+            <ProFormTextArea
+                label="备注"
+                name="remark"
+                fieldProps={{
+                    maxLength: 200,
+                    showCount: true,
+                }}
+            />
+        </MovableModalForm >
+    );
+};
+
+export default NceePlanEditModal;

+ 234 - 0
YBEE.EQM.Admin/src/pages/ncee/NceePlan/index.tsx

@@ -0,0 +1,234 @@
+import { toSelectOptions } from '@/common/converter';
+import { SuperTable, TabBadge } from '@/components';
+import NceePlanController from '@/services/apis/NceePlanController';
+import { ExamStatus } from '@/services/enums';
+import { PlusOutlined } from '@ant-design/icons';
+import { ActionType, PageContainer, ProColumns } from '@ant-design/pro-components';
+import { history, useModel } from '@umijs/max';
+import { App, Button } from 'antd';
+import { useCallback, useRef, useState } from 'react';
+import NceePlanEditModal from './components/NceePlanEditModal';
+
+/** 高中分析计划 */
+const NceePlanList: React.FC = () => {
+    const actionRef = useRef<ActionType>();
+    const currentRef = useRef<Partial<API.NceePlanOutput>>();
+
+    const [activeKey, setActiveKey] = useState<React.Key>('0');
+    const [statusCount, seStatusCount] = useState<Record<number, number>>({});
+
+    const [editShow, setEditShow] = useState(false);
+    const { message, modal } = App.useApp();
+
+    const { getDictValueEnum, getDict } = useModel('useDict');
+    const { baseData } = useModel('useBaseData');
+
+    // 加载数量统计
+    const loadCount = useCallback(async (params: API.NceePlanPageInput) => {
+        const m = await NceePlanController.queryStatusCount(params);
+        const tc = m?.reduce((a, b) => a + b.count, 0) ?? 0;
+
+        const d: Record<number, number> = { 0: tc };
+        m?.forEach((t) => { d[t.status] = t.count; });
+
+        seStatusCount(d);
+    }, []);
+
+    // 呈现状态 tab
+    const renderTabItems = useCallback(() => {
+        let items: { key: string; label: React.ReactNode }[] = [{
+            key: '0',
+            label: (<span>全部<TabBadge count={statusCount[0]} active={activeKey === 0} /></span>),
+        }];
+        items = items.concat(
+            getDict('exam_status')?.map((t) => ({
+                key: `${t.value}`,
+                label: (
+                    <span>
+                        {t.name}
+                        <TabBadge color={t.antColor} count={statusCount[t.value] ?? 0} active={activeKey === `${t.value}`} />
+                    </span>
+                ),
+            })),
+        );
+        return items;
+    }, [activeKey, statusCount]);
+
+    // 删除
+    const handleDelete = useCallback((id: number) => {
+        modal.confirm({
+            title: '警告',
+            content: '确定立即删除吗',
+            okText: '确定',
+            cancelText: '取消',
+            centered: true,
+            onOk: async () => {
+                await NceePlanController.del({ id });
+                message.success('已删除');
+                actionRef.current?.reload();
+            },
+        });
+    }, []);
+
+    const columns: ProColumns<API.NceePlanOutput>[] = [
+        {
+            title: '计划全称',
+            dataIndex: 'fullName',
+            renderText: (v, r) => <a onClick={() => history.push(`/ncee/plan/detail/${r.id}`)}>{v}</a>,
+            hideInDescriptions: true,
+        },
+        {
+            title: '计划全称',
+            dataIndex: 'fullName',
+            hideInTable: true,
+            hideInSearch: true,
+        },
+        {
+            title: '计划名称',
+            dataIndex: 'name',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '计划简称',
+            dataIndex: 'shortName',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '年级',
+            dataIndex: ['grade', 'shortName'],
+            align: 'center',
+            search: {
+                transform: v => ({ gradeId: v }),
+            },
+            hideInSearch: true,
+        },
+        {
+            title: '计划状态',
+            dataIndex: 'status',
+            valueEnum: getDictValueEnum('exam_status', true),
+            width: 80,
+            align: 'center',
+            hideInDescriptions: true,
+        },
+        {
+            title: '学期',
+            dataIndex: ['semester', 'nickShortName'],
+            search: {
+                transform: (v) => v ? ({ semesterId: JSON.parse(v) }) : v,
+            },
+            valueType: 'select',
+            fieldProps: {
+                options: toSelectOptions(baseData?.semesters ?? [], (item) => ({ key: item.id, label: item.nickShortName, value: item.id })),
+            },
+            width: 80,
+            align: 'center',
+        },
+        {
+            title: '开始时间',
+            dataIndex: 'beginTime',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '结束时间',
+            dataIndex: 'endTime',
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '备注说明',
+            dataIndex: 'remark',
+            hideInSearch: true,
+            className: 'minw-120',
+        },
+        {
+            title: '创建人',
+            dataIndex: ['createSysUser', 'name'],
+            hideInSearch: true,
+            hideInTable: true,
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            align: 'center',
+            width: 144,
+            valueType: 'dateRange',
+            colSize: 2,
+            render: (_, r) => r.createTime,
+            search: {
+                transform: (v) => v ? ({
+                    searchBeginTime: v[0],
+                    searchEndTime: v[1],
+                }) : ({}),
+            },
+        },
+        {
+            title: '操作',
+            valueType: 'option',
+            width: 112,
+            align: 'center',
+            fixed: 'right',
+            render: (_, r) => {
+                let ops: React.ReactNode[] = [];
+                if (r.status === ExamStatus.READY) {
+                    ops.push(<Button key="edit" type="link" size="small" onClick={() => { currentRef.current = r; setEditShow(true); }}>修改</Button>);
+                    ops.push(<Button key="del" type="link" size="small" onClick={() => handleDelete(r.id)}>删除</Button>);
+                }
+                return <>{ops}</>;
+            },
+        },
+    ];
+
+    return (
+        <PageContainer title={false}>
+            <SuperTable<API.NceePlanOutput>
+                actionRef={actionRef}
+                columns={columns}
+                scroll={{ x: 'max-content' }}
+                request={async (params, sort) => {
+                    try {
+                        return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                            await loadCount(p);
+                            const res = await NceePlanController.queryPageList({
+                                ...p,
+                                status: activeKey !== '0' ? parseInt(activeKey as string) : undefined,
+                            });
+                            return res;
+                        });
+                    }
+                    catch (ex) { return {}; }
+                }}
+                toolbar={{
+                    menu: {
+                        type: 'tab',
+                        activeKey: activeKey,
+                        items: renderTabItems(),
+                        onChange: (key) => {
+                            setActiveKey(key as React.Key);
+                            actionRef.current?.reload();
+                        },
+                    },
+                    actions: [
+                        <Button
+                            key="add"
+                            type="primary"
+                            icon={<PlusOutlined />}
+                            onClick={() => { currentRef.current = { id: 0 }; setEditShow(true); }}
+                        >创建分析计划</Button>,
+                    ],
+                }}
+            />
+            {editShow && currentRef.current &&
+                <NceePlanEditModal
+                    data={currentRef.current}
+                    onFinish={() => actionRef.current?.reload()}
+                    onClose={() => setEditShow(false)}
+                />
+            }
+        </PageContainer>
+    );
+};
+
+export default NceePlanList;

+ 81 - 0
YBEE.EQM.Admin/src/pages/ncee/NceePlanDetail/index.tsx

@@ -0,0 +1,81 @@
+import { CardStepTitle } from "@/components";
+import NceePlanController from "@/services/apis/NceePlanController";
+import { ReloadOutlined } from "@ant-design/icons";
+import { PageContainer, ProCard, ProDescriptions } from "@ant-design/pro-components";
+import { history, useModel, useParams } from "@umijs/max";
+import { useRequest } from "ahooks";
+import { Button, FloatButton, Tag, theme } from "antd";
+
+const NceePlanDetail: React.FC = () => {
+    const reqParams = useParams() as unknown as { id: number };
+    const { data, run, loading } = useRequest(() => {
+        return NceePlanController.getById({ id: reqParams.id });
+    });
+
+    const { token } = theme.useToken();
+
+    const { getKeyDict } = useModel('useDict');
+    const examStatus = getKeyDict('exam_status');
+
+    const status = data?.status ? examStatus[data?.status] : undefined;
+
+    return (
+        <PageContainer
+            title={data?.fullName}
+            loading={loading}
+            onBack={() => history.back()}
+            extra={<Button key="reload" icon={<ReloadOutlined />} onClick={run}>刷新</Button>}
+        >
+            <ProCard
+                title={<CardStepTitle>基本信息</CardStepTitle>}
+                extra={<Tag color={status?.antColor} style={{ marginRight: 0 }}>{status?.name}</Tag>}
+            >
+                <ProDescriptions size="small">
+                    <ProDescriptions.Item label="计划名称">
+                        {data?.name}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="计划简称">
+                        {data?.shortName}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="学期名称">
+                        {data?.semester?.name}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="创建人员">
+                        {data?.createSysUser?.name}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="创建时间">
+                        {data?.createTime}
+                    </ProDescriptions.Item>
+                    <ProDescriptions.Item label="备注说明">
+                        {data?.remark}
+                    </ProDescriptions.Item>
+                </ProDescriptions>
+            </ProCard>
+
+            <ProCard
+                title={<CardStepTitle>一、成绩导入</CardStepTitle>}
+                style={{ marginBlockStart: token.margin }}
+            >
+                <div>模板</div>
+                <Button>导入成绩</Button>
+                <ul>
+                    <li>1.原始选科划线,只有原始分,有方向,有或无选科。设置划线比例或绝对人数,计算有效分。导出转换分、排名、有效分、均分、上线统计、分机构</li>
+                    <li>2.原始未选划线,只有原始分,无方向,无选科。设置划线比例或绝对人数,计算有效分。导出排名、有效分、均分、上线统计、分机构</li>
+                    <li>3.赋分选科划线,有原始分,有赋分,有或无等级,有方向,有选科。设置有效分。导出排名、有效分、均分、上线统计、分机构</li>
+                </ul>
+            </ProCard>
+
+            <ProCard
+                title={<CardStepTitle>二、模拟划线</CardStepTitle>}
+                style={{ marginBlockStart: token.margin }}
+            >
+                <div>模板</div>
+                <Button>导入成绩</Button>
+            </ProCard>
+
+            <FloatButton.BackTop visibilityHeight={100} />
+        </PageContainer>
+    );
+}
+
+export default NceePlanDetail;

+ 2 - 2
YBEE.EQM.Admin/src/pages/system/Role/components/RoleUserAddModal.tsx

@@ -22,7 +22,7 @@ const RoleUserAddModal: React.FC<RoleUserAddModalProps> = ({ role, onFinish, onC
             setLoading(false);
             const list = (res ?? []).map((t) => ({
                 key: `${t.id}`,
-                title: t.name,
+                title: `${t.name}(${t.account})`,
                 description: t.sysOrg?.name,
             }));
             setUserList(list);
@@ -71,7 +71,7 @@ const RoleUserAddModal: React.FC<RoleUserAddModalProps> = ({ role, onFinish, onC
                     operations={['加入', '移出']}
                     showSearch
                     filterOption={(inputValue: string, option: any) =>
-                        option.title.indexOf(inputValue) > -1
+                        option.title.toUpperCase().indexOf(inputValue?.toUpperCase()) > -1
                     }
                     dataSource={userList}
                     targetKeys={targetKeys}

+ 5 - 0
YBEE.EQM.Admin/src/pages/system/Role/components/UserList.tsx

@@ -55,6 +55,11 @@ const UserList = forwardRef<UserListHandles, UserListProps>(({ role }, ref) => {
     }));
 
     const columns: ProColumns<API.SysUserOutput>[] = [
+        {
+            title: '账号',
+            dataIndex: 'account',
+            width: 120,
+        },
         {
             title: '姓名',
             dataIndex: 'name',

+ 124 - 36
YBEE.EQM.Admin/src/pages/system/User/index.tsx

@@ -2,17 +2,21 @@ import { buildTreeNodes } from '@/common/converter';
 import { calcElementTop } from '@/common/helper';
 import { SuperTable } from '@/components';
 import SysOrgController from '@/services/apis/SysOrgController';
-import { PageContainer, ProCard } from '@ant-design/pro-components';
-import { ActionType, ProColumns } from '@ant-design/pro-table';
+import SysUserController from '@/services/apis/SysUserController';
+import { CommonStatus } from '@/services/enums';
+import { PageContainer, ProCard, ProColumns } from '@ant-design/pro-components';
+import { ActionType } from '@ant-design/pro-table';
 import { useEmotionCss } from '@ant-design/use-emotion-css';
-import { useRequest } from '@umijs/max';
-import { Tree } from 'antd';
-import { Key, useEffect, useRef, useState } from 'react';
+import { useModel, useRequest } from '@umijs/max';
+import { App, Button, message, Space, Tree, Typography } from 'antd';
+import { Key, useCallback, useEffect, useRef, useState } from 'react';
 
 const ROOT_KEY = 0;
 
 /** 用户管理 */
 const OrgMember: React.FC = () => {
+    const { modal, notification } = App.useApp();
+
     const [expandedKeys, setExpandedKeys] = useState<Key[]>([ROOT_KEY]);
     const [selectedKeys, setSelectedKeys] = useState<Key[]>([ROOT_KEY]);
 
@@ -20,6 +24,9 @@ const OrgMember: React.FC = () => {
     const treeRef = useRef(null);
     const [treeTop, setTreeTop] = useState(148);
 
+    const { getDictValueEnum } = useModel('useDict');
+    const commonStatusValueEnum = getDictValueEnum('common_status', true);
+
     const treeClassName = useEmotionCss(({ token }) => {
         return {
             '.ant-tree-list-holder-inner': {
@@ -51,7 +58,14 @@ const OrgMember: React.FC = () => {
         formatResult: (res) => {
             setExpandedKeys([ROOT_KEY]);
             const items = buildTreeNodes<API.SysOrgLiteOutput>((res ?? []) as unknown as DICTS.ParentTreeNode[], ROOT_KEY);
-            return items;
+            return [
+                {
+                    key: 0,
+                    title: '全部',
+                    isLeaf: false,
+                    children: items,
+                }
+            ];
         }
     });
 
@@ -62,36 +76,89 @@ const OrgMember: React.FC = () => {
             return;
         }
         setSelectedKeys(sks);
-        actionRef?.current?.reload();
+        actionRef?.current?.reload?.();
     }
 
-    const columns: ProColumns<API.SysUserOutput>[] = [
-        {
-            title: '姓名',
-            dataIndex: ['user', 'name'],
-            search: {
-                transform: (v) => v ? ({ name: v }) : v,
+    const handleResetPassword = useCallback((record: API.SysUserSimpleOutput) => {
+        modal.confirm({
+            title: '警告',
+            content: '重置将生成新的初始密码,同时账户需要重新激活,确定立即重置吗?',
+            okText: '确定',
+            cancelText: '取消',
+            centered: true,
+            onOk: async () => {
+                const newPwd = await SysUserController.resetPassword({ id: record.id });
+                notification.success({
+                    message: '重置成功',
+                    description: (
+                        <Typography.Paragraph>
+                            <Typography.Text>账户{record.account},密码已重置为:</Typography.Text>
+                            <Typography.Text type="danger" strong>{newPwd}</Typography.Text>
+                            <Typography.Text
+                                copyable={{
+                                    text: `你的账户${record.account},密码已重置为${newPwd},请重设置新密码激活账户!`,
+                                }}
+                            >,请提醒用户登录后及时修改!</Typography.Text>
+                        </Typography.Paragraph>
+                    ),
+                    duration: null,
+                });
             },
-            width: 80,
+        });
+    }, []);
+
+    const handleSwitchStatus = useCallback((record: API.SysUserSimpleOutput) => {
+        const tip = record.status === CommonStatus.ENABLE ? '禁用' : '启用';
+        modal.confirm({
+            title: '警告',
+            content: `确定立即【${tip}】该用户吗?`,
+            okText: '确定',
+            cancelText: '取消',
+            centered: true,
+            onOk: async () => {
+                await SysUserController.updateStatus({
+                    id: record.id,
+                    status: record.status === CommonStatus.ENABLE ? CommonStatus.DISABLE : CommonStatus.ENABLE,
+                });
+                message.success(`已${tip}`);
+                actionRef.current?.reload();
+            },
+        });
+    }, []);
+
+
+    const columns: ProColumns<API.SysUserSimpleOutput>[] = [
+        {
+            title: '账号',
+            dataIndex: 'account',
+
+            width: 120,
         },
         {
-            title: '手机号码',
-            dataIndex: ['user', 'mobile'],
-            search: {
-                transform: (v) => v ? ({ mobile: v }) : v,
-            },
+            title: '姓名',
+            dataIndex: 'name',
             width: 120,
-            align: 'center',
-            hideInSearch: true,
-            // renderText: (v) => {
-            //     const p = /(\d{3})\d*(\d{4})/;
-            //     return v.replace(p, '$1****$2');
-            // },
         },
+        // {
+        //     title: '手机号码',
+        //     dataIndex: ['user', 'mobile'],
+        //     search: {
+        //         transform: (v) => v ? ({ mobile: v }) : v,
+        //     },
+        //     width: 120,
+        //     align: 'center',
+        //     hideInSearch: true,
+        //     // renderText: (v) => {
+        //     //     const p = /(\d{3})\d*(\d{4})/;
+        //     //     return v.replace(p, '$1****$2');
+        //     // },
+        // },
         {
             title: '所属机构',
             dataIndex: ['sysOrg', 'name'],
-            hideInSearch: true,
+            search: {
+                transform: (v) => v ? ({ sysOrgName: v }) : v,
+            },
         },
         {
             title: '所属角色',
@@ -101,13 +168,33 @@ const OrgMember: React.FC = () => {
                 return r.sysRoles?.map((t: any) => t.name).join('、');
             },
         },
-
+        {
+            title: '状态',
+            dataIndex: 'status',
+            width: 144,
+            align: 'center',
+            valueEnum: commonStatusValueEnum,
+            render: (v, r) => {
+                return (
+                    <Space>
+                        {v}
+                        <Button
+                            type="link"
+                            size="small"
+                            onClick={() => handleSwitchStatus(r)}
+                        >
+                            {r.status === CommonStatus.ENABLE ? '禁用' : '启用'}
+                        </Button>
+                    </Space>
+                );
+            },
+        },
         {
             title: '操作',
-            width: 100,
+            width: 120,
             align: 'center',
             valueType: 'option',
-            // render: (_, record) => <ResetPassword userId={record.id} trigger={<a>重置密码</a>} />,
+            render: (_, r) => (<Button type="link" size="small" onClick={() => handleResetPassword(r)}>重置密码</Button>),
         },
 
     ];
@@ -140,6 +227,7 @@ const OrgMember: React.FC = () => {
                         showIcon={false}
                         autoExpandParent
                         blockNode={true}
+                        expandAction={false}
                         expandedKeys={expandedKeys}
                         selectedKeys={selectedKeys}
                         onExpand={(expandedKeys) => setExpandedKeys(expandedKeys as number[])}
@@ -152,16 +240,16 @@ const OrgMember: React.FC = () => {
                     bodyStyle={{ padding: 0 }}
                     headerBordered
                 >
-                    <SuperTable
+                    <SuperTable<API.SysUserSimpleOutput>
                         columns={columns}
                         actionRef={actionRef}
                         sticky
-                        // request={async (params = {}, sort) => {
-                        //     return SuperTable.requestPageAgent({ params, sort }, async (p) => {
-                        //         const res = await SysOrgUserController.queryPageList({ ...p, sysOrgId: (selectedKeys?.[0] as number) ?? undefined });
-                        //         return res;
-                        //     });
-                        // }}
+                        request={async (params = {}, sort) => {
+                            return SuperTable.requestPageAgent({ params, sort }, async (p) => {
+                                const res = await SysUserController.queryUserSimplePageList({ ...p, sysOrgId: (selectedKeys?.[0] as number) ?? undefined });
+                                return res;
+                            });
+                        }}
                         options={{ reload: false, fullScreen: false, setting: false }}
                     />
                 </ProCard>

+ 29 - 0
YBEE.EQM.Admin/src/services/apis/EsaProcessingController.ts

@@ -0,0 +1,29 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// 有效分分析处理服务
+// --------------------------------------------------------------------------
+
+import { request } from '@umijs/max';
+
+/** 分析处理 POST /api/esa/processing/execute */
+export async function execute(
+    params: {
+        /**  */
+        esaplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/esa/processing/execute';
+    const config = { method: 'POST', params, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
+export default {
+    /** 分析处理 POST /api/esa/processing/execute */
+    execute,
+};

+ 99 - 0
YBEE.EQM.Admin/src/services/apis/ExamAbsentReplaceCenterController.ts

@@ -0,0 +1,99 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// 缺测替补管理服务(用于中心管理)
+// --------------------------------------------------------------------------
+
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
+
+/** 分页查询监测缺测替补列表 POST /api/exam/absent/replace/center/query-page-list */
+export async function queryPageList(
+    data: API.ExamAbsentReplacePageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/absent/replace/center/query-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamAbsentReplaceFullOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
+/** 获取状态数量 POST /api/exam/absent/replace/center/query-status-count */
+export async function queryStatusCount(
+    data: API.ExamAbsentReplacePageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/absent/replace/center/query-status-count';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.StatusCount[]>>(url, config);
+    return res?.data;
+}
+
+/** 导出数据表格(简表) POST /api/exam/absent/replace/center/export-simple */
+export async function exportSimple(
+    data: API.ExamAbsentReplacePageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/absent/replace/center/export-simple';
+    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;
+}
+
+/** 导出数据表格(完整) POST /api/exam/absent/replace/center/export-full */
+export async function exportFull(
+    data: API.ExamAbsentReplacePageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/absent/replace/center/export-full';
+    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/absent/replace/center/query-page-list */
+    queryPageList,
+    /** 获取状态数量 POST /api/exam/absent/replace/center/query-status-count */
+    queryStatusCount,
+    /** 导出数据表格(简表) POST /api/exam/absent/replace/center/export-simple */
+    exportSimple,
+    /** 导出数据表格(完整) POST /api/exam/absent/replace/center/export-full */
+    exportFull,
+};

+ 22 - 3
YBEE.EQM.Admin/src/services/apis/ExamDataPublishController.ts

@@ -8,6 +8,7 @@
 // --------------------------------------------------------------------------
 
 import { request } from '@umijs/max';
+import { DataPublishType } from '../enums';
 
 /** 添加发布内容 POST /api/exam-data-publish/add */
 export async function add(data: API.AddExamDataPublishInput, options?: { [key: string]: any }) {
@@ -56,7 +57,7 @@ export async function unpublish(data: API.BaseId, options?: { [key: string]: any
 export async function getById(
     params: {
         /**  */
-        id?: number;
+        id: number;
     },
     options?: { [key: string]: any },
 ) {
@@ -69,8 +70,10 @@ export async function getById(
 /** 根据监测计划ID获取数据发布内容列表 GET /api/exam-data-publish/get-list-by-exam-plan-id */
 export async function getListByExamPlanId(
     params: {
-        /**  */
-        examplanid?: number;
+        /** 监测计划ID */
+        examplanid: number;
+        /** 发布类型 */
+        type?: DataPublishType;
     },
     options?: { [key: string]: any },
 ) {
@@ -80,6 +83,20 @@ export async function getListByExamPlanId(
     return res?.data;
 }
 
+/** 分页查询面向机构发布的内容列表 POST /api/exam-data-publish/query-org-page-list */
+export async function queryOrgPageList(
+    data: API.ExamDataPublishOrgPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam-data-publish/query-org-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamDataPublishOrgOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
 export default {
     /** 添加发布内容 POST /api/exam-data-publish/add */
     add,
@@ -95,4 +112,6 @@ export default {
     getById,
     /** 根据监测计划ID获取数据发布内容列表 GET /api/exam-data-publish/get-list-by-exam-plan-id */
     getListByExamPlanId,
+    /** 分页查询面向机构发布的内容列表 POST /api/exam-data-publish/query-org-page-list */
+    queryOrgPageList,
 };

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

@@ -107,6 +107,20 @@ export async function getListByExamPlanId(
     return res?.data;
 }
 
+/** 获取数据上报计划列表 POST /api/exam/data/report/query-plan-page-list */
+export async function queryPlanPageList(
+    data: API.ExamDataReportPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/data/report/query-plan-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamDataReportPlanOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
 export default {
     /** 添加上报类型 POST /api/exam/data/report/add */
     add,
@@ -128,4 +142,6 @@ export default {
     getById,
     /** 根据监测计划ID获取数据上报类型列表 GET /api/exam/data/report/get-list-by-exam-plan-id */
     getListByExamPlanId,
+    /** 获取数据上报计划列表 POST /api/exam/data/report/query-plan-page-list */
+    queryPlanPageList,
 };

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

@@ -50,6 +50,20 @@ export async function getListByExamPlanId(
     return res?.data;
 }
 
+/** 根据监测计划ID获取监测机构简要列表 GET /api/exam/org/get-lite-list-by-exam-plan-id */
+export async function getLiteListByExamPlanId(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/org/get-lite-list-by-exam-plan-id';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<API.ExamOrgLiteOutput[]>>(url, config);
+    return res?.data;
+}
+
 /** 分页查询监测机构列表 POST /api/exam/org/query-page-list */
 export async function queryPageList(data: API.ExamOrgPageInput, options?: { [key: string]: any }) {
     const url = '/api/exam/org/query-page-list';
@@ -92,6 +106,8 @@ export default {
     switchRequiredSample,
     /** 根据监测计划ID获取监测机构列表 GET /api/exam/org/get-list-by-exam-plan-id */
     getListByExamPlanId,
+    /** 根据监测计划ID获取监测机构简要列表 GET /api/exam/org/get-lite-list-by-exam-plan-id */
+    getLiteListByExamPlanId,
     /** 分页查询监测机构列表 POST /api/exam/org/query-page-list */
     queryPageList,
     /** 分页查询未加入的机构 POST /api/exam/org/query-not-in-sys-org-page-list */

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

@@ -71,6 +71,20 @@ export async function getById(
     return res?.data;
 }
 
+/** 获取监测计划抽样状态 GET /api/exam/plan/get-sample-status-by-id */
+export async function getSampleStatusById(
+    params: {
+        /**  */
+        id?: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/plan/get-sample-status-by-id';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<API.ExamPlanSampleStatusOutput>>(url, config);
+    return res?.data;
+}
+
 /** 分页查询监测计划列表 POST /api/exam/plan/query-page-list */
 export async function queryPageList(data: API.ExamPlanPageInput, options?: { [key: string]: any }) {
     const url = '/api/exam/plan/query-page-list';
@@ -104,6 +118,14 @@ export async function getSampleRefPlanList(
     return res?.data;
 }
 
+/** 按监测计划执行抽样 POST /api/exam/plan/execute-sample */
+export async function executeSample(data: API.BaseId, options?: { [key: string]: any }) {
+    const url = '/api/exam/plan/execute-sample';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
 export default {
     /** 添加监测计划 POST /api/exam/plan/add */
     add,
@@ -119,10 +141,14 @@ export default {
     cancel,
     /** 根据ID获取监测计划 GET /api/exam/plan/get-by-id */
     getById,
+    /** 获取监测计划抽样状态 GET /api/exam/plan/get-sample-status-by-id */
+    getSampleStatusById,
     /** 分页查询监测计划列表 POST /api/exam/plan/query-page-list */
     queryPageList,
     /** 获取我的单据状态数量 POST /api/exam/plan/query-status-count */
     queryStatusCount,
     /** 获取最近5个抽测参照成绩监测计划 GET /api/exam/plan/get-sample-ref-plan-list */
     getSampleRefPlanList,
+    /** 按监测计划执行抽样 POST /api/exam/plan/execute-sample */
+    executeSample,
 };

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

@@ -8,7 +8,7 @@
 // --------------------------------------------------------------------------
 
 import { request, RequestOptions } from '@umijs/max';
-import { DataPublishType } from '../enums';
+import { ExamSampleStatus, DataPublishType } from '../enums';
 import contentDisposition from 'content-disposition';
 
 /** 添加监测抽样方案 POST /api/exam/sample/add */
@@ -73,6 +73,28 @@ export async function selectSample(data: API.BaseId, options?: { [key: string]:
     return res?.data;
 }
 
+/** 取消选定 POST /api/exam/sample/unselect-sample */
+export async function unselectSample(data: API.BaseId, options?: { [key: string]: any }) {
+    const url = '/api/exam/sample/unselect-sample';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
+/** 更新机构是否需要上报校考成绩状态 POST /api/exam/sample/update-org-report-school-exam-score-status */
+export async function updateOrgReportSchoolExamScoreStatus(
+    params: {
+        /** 监测计划ID */
+        examplanid?: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/update-org-report-school-exam-score-status';
+    const config = { method: 'POST', params, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
 /** 执行抽样 POST /api/exam/sample/execute-sample */
 export async function executeSample(data: API.BaseId, options?: { [key: string]: any }) {
     const url = '/api/exam/sample/execute-sample';
@@ -199,7 +221,7 @@ export async function exportSampleCountToOrg(data: API.BaseId, options?: { [key:
 /** 根据ID获取抽样方案 GET /api/exam/sample/get-by-id */
 export async function getById(
     params: {
-        /**  */
+        /** 抽样方案ID */
         id: number;
     },
     options?: { [key: string]: any },
@@ -224,6 +246,20 @@ export async function getListByExamPlanId(
     return res?.data;
 }
 
+/** 根据监测计划ID获取全部抽样方案的状态 GET /api/exam/sample/get-status-list-by-exam-plan-id */
+export async function getStatusListByExamPlanId(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/get-status-list-by-exam-plan-id';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<API.ExamSampleStatusOutput[]>>(url, config);
+    return res?.data;
+}
+
 /** 查询已发布抽样 GET /api/exam/sample/get-by-exam-data-publish-id */
 export async function getByExamDataPublishId(
     params: {
@@ -243,7 +279,7 @@ export async function getByExamDataPublishId(
 /** 获取抽样统计表 GET /api/exam/sample/get-sample-count-list-by-id */
 export async function getSampleCountListById(
     params: {
-        /**  */
+        /** 抽样方案ID */
         id: number;
     },
     options?: { [key: string]: any },
@@ -283,6 +319,10 @@ export default {
     switchExamSampleAllClass,
     /** 选定方案 POST /api/exam/sample/select-sample */
     selectSample,
+    /** 取消选定 POST /api/exam/sample/unselect-sample */
+    unselectSample,
+    /** 更新机构是否需要上报校考成绩状态 POST /api/exam/sample/update-org-report-school-exam-score-status */
+    updateOrgReportSchoolExamScoreStatus,
     /** 执行抽样 POST /api/exam/sample/execute-sample */
     executeSample,
     /** 导出抽样方案存档文件 POST /api/exam/sample/export-to-archived */
@@ -299,6 +339,8 @@ export default {
     getById,
     /** 根据监测计划ID获取全部抽样方案 GET /api/exam/sample/get-list-by-exam-plan-id */
     getListByExamPlanId,
+    /** 根据监测计划ID获取全部抽样方案的状态 GET /api/exam/sample/get-status-list-by-exam-plan-id */
+    getStatusListByExamPlanId,
     /** 查询已发布抽样 GET /api/exam/sample/get-by-exam-data-publish-id */
     getByExamDataPublishId,
     /** 获取抽样统计表 GET /api/exam/sample/get-sample-count-list-by-id */

+ 100 - 0
YBEE.EQM.Admin/src/services/apis/ExamSampleReplaceController.ts

@@ -0,0 +1,100 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// 缺测替补抽样服务
+// --------------------------------------------------------------------------
+
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
+
+/** 抽取 POST /api/exam/sample/replace/sample */
+export async function sample(
+    data: API.SampleExamSampleReplaceInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/replace/sample';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
+/** 标记替补为缺测 GET /api/exam/sample/replace/marked-replace-absent */
+export async function markedReplaceAbsent(
+    params: {
+        /**  */
+        id: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/replace/marked-replace-absent';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
+/** 软删除 POST /api/exam/sample/replace/fake-delete */
+export async function fakeDelete(data: API.BaseId, options?: { [key: string]: any }) {
+    const url = '/api/exam/sample/replace/fake-delete';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
+/** 导出缺测替补名单 POST /api/exam/sample/replace/export-to-org */
+export async function exportToOrg(
+    params: {
+        /**  */
+        examplanid: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/replace/export-to-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;
+}
+
+/** 分页查询缺测替补抽样列表 POST /api/exam/sample/replace/query-org-page-list */
+export async function queryOrgPageList(
+    data: API.ExamSampleReplacePageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/replace/query-org-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamSampleReplaceOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
+export default {
+    /** 抽取 POST /api/exam/sample/replace/sample */
+    sample,
+    /** 标记替补为缺测 GET /api/exam/sample/replace/marked-replace-absent */
+    markedReplaceAbsent,
+    /** 软删除 POST /api/exam/sample/replace/fake-delete */
+    fakeDelete,
+    /** 导出缺测替补名单 POST /api/exam/sample/replace/export-to-org */
+    exportToOrg,
+    /** 分页查询缺测替补抽样列表 POST /api/exam/sample/replace/query-org-page-list */
+    queryOrgPageList,
+};

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

@@ -50,6 +50,20 @@ export async function queryExamSampleStudent(
     return res?.data;
 }
 
+/** 中心查询抽样名单 POST /api/exam/sample/student/query-center-page-list */
+export async function queryCenterPageList(
+    data: API.ExamSampleStudentPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/sample/student/query-center-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamSampleStudentOrgOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
 export default {
     /** 分页查询抽样学生信息 POST /api/exam/sample/student/query-page-list */
     queryPageList,
@@ -57,4 +71,6 @@ export default {
     getByExamNumber,
     /** 查询监测学生信息 POST /api/exam/sample/student/query-exam-sample-student */
     queryExamSampleStudent,
+    /** 中心查询抽样名单 POST /api/exam/sample/student/query-center-page-list */
+    queryCenterPageList,
 };

+ 99 - 0
YBEE.EQM.Admin/src/services/apis/ExamSpecialStudentCenterController.ts

@@ -0,0 +1,99 @@
+// @ts-ignore
+/* eslint-disable */
+
+// 该文件自动生成,请勿手动修改!
+
+// --------------------------------------------------------------------------
+// 特殊学生管理服务(用于中心管理)
+// --------------------------------------------------------------------------
+
+import { request, RequestOptions } from '@umijs/max';
+import contentDisposition from 'content-disposition';
+
+/** 分页查询列表 POST /api/exam/special/student/center/query-page-list */
+export async function queryPageList(
+    data: API.ExamSpecialStudentPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/special/student/center/query-page-list';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.PageResponse<API.ExamSpecialStudentFullOutput>>>(
+        url,
+        config,
+    );
+    return res?.data;
+}
+
+/** 获取状态数量 POST /api/exam/special/student/center/query-status-count */
+export async function queryStatusCount(
+    data: API.ExamSpecialStudentPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/special/student/center/query-status-count';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<API.StatusCount[]>>(url, config);
+    return res?.data;
+}
+
+/** 导出数据表格(简表) POST /api/exam/special/student/center/export-simple */
+export async function exportSimple(
+    data: API.ExamSpecialStudentPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/special/student/center/export-simple';
+    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;
+}
+
+/** 导出数据表格(完整) POST /api/exam/special/student/center/export-full */
+export async function exportFull(
+    data: API.ExamSpecialStudentPageInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/exam/special/student/center/export-full';
+    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/special/student/center/query-page-list */
+    queryPageList,
+    /** 获取状态数量 POST /api/exam/special/student/center/query-status-count */
+    queryStatusCount,
+    /** 导出数据表格(简表) POST /api/exam/special/student/center/export-simple */
+    exportSimple,
+    /** 导出数据表格(完整) POST /api/exam/special/student/center/export-full */
+    exportFull,
+};

+ 13 - 0
YBEE.EQM.Admin/src/services/apis/NceePlanController.ts

@@ -25,6 +25,17 @@ export async function update(data: API.UpdateNceePlanInput, options?: { [key: st
     return res?.data;
 }
 
+/** 更新配置 POST /api/ncee/plan/update-config */
+export async function updateConfig(
+    data: API.UpdateNceePlanConfigInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/ncee/plan/update-config';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
 /** 删除计划 POST /api/ncee/plan/del */
 export async function del(data: API.BaseId, options?: { [key: string]: any }) {
     const url = '/api/ncee/plan/del';
@@ -95,6 +106,8 @@ export default {
     add,
     /** 更新计划 POST /api/ncee/plan/update */
     update,
+    /** 更新配置 POST /api/ncee/plan/update-config */
+    updateConfig,
     /** 删除计划 POST /api/ncee/plan/del */
     del,
     /** 开始监测 POST /api/ncee/plan/start */

+ 29 - 0
YBEE.EQM.Admin/src/services/apis/SysUserController.ts

@@ -34,9 +34,38 @@ export async function queryUserSimplePageList(
     return res?.data;
 }
 
+/** 重置密码 GET /api/sys/user/reset-password */
+export async function resetPassword(
+    params: {
+        /** 用户ID */
+        id: number;
+    },
+    options?: { [key: string]: any },
+) {
+    const url = '/api/sys/user/reset-password';
+    const config = { method: 'GET', params, ...(options || {}) };
+    const res = await request<API.ResponseType<string>>(url, config);
+    return res?.data;
+}
+
+/** 修改用户状态 POST /api/sys/user/update-status */
+export async function updateStatus(
+    data: API.UpdateSysUserStatusInput,
+    options?: { [key: string]: any },
+) {
+    const url = '/api/sys/user/update-status';
+    const config = { method: 'POST', data, ...(options || {}) };
+    const res = await request<API.ResponseType<any>>(url, config);
+    return res?.data;
+}
+
 export default {
     /** 修改密码 POST /api/sys/user/change-password */
     changePassword,
     /** 查询简要用户列表 POST /api/sys/user/query-user-simple-page-list */
     queryUserSimplePageList,
+    /** 重置密码 GET /api/sys/user/reset-password */
+    resetPassword,
+    /** 修改用户状态 POST /api/sys/user/update-status */
+    updateStatus,
 };

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

@@ -8,8 +8,10 @@ import * as BaseEducationStageYearsController from './BaseEducationStageYearsCon
 import * as BaseGradeController from './BaseGradeController';
 import * as BaseSchoolClassController from './BaseSchoolClassController';
 import * as BaseSemesterController from './BaseSemesterController';
+import * as EsaProcessingController from './EsaProcessingController';
 import * as ExamAbsentReplaceController from './ExamAbsentReplaceController';
 import * as ExamAbsentReplaceAuditController from './ExamAbsentReplaceAuditController';
+import * as ExamAbsentReplaceCenterController from './ExamAbsentReplaceCenterController';
 import * as ExamCourseController from './ExamCourseController';
 import * as ExamDataPublishController from './ExamDataPublishController';
 import * as ExamDataReportController from './ExamDataReportController';
@@ -26,12 +28,14 @@ import * as ExamPatriarchQuestionnaireProgressController from './ExamPatriarchQu
 import * as ExamReportingAvgRangeController from './ExamReportingAvgRangeController';
 import * as ExamReportingTqesController from './ExamReportingTqesController';
 import * as ExamResultController from './ExamResultController';
+import * as ExamSampleReplaceController from './ExamSampleReplaceController';
 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';
+import * as ExamSpecialStudentCenterController from './ExamSpecialStudentCenterController';
 import * as ExamStudentController from './ExamStudentController';
 import * as ExamTeacherCourseController from './ExamTeacherCourseController';
 import * as ExamTeacherController from './ExamTeacherController';
@@ -63,10 +67,14 @@ export default {
     BaseSchoolClassController,
     /** 学期信息管理服务 */
     BaseSemesterController,
+    /** 有效分分析处理服务 */
+    EsaProcessingController,
     /** 缺测替补管理服务 */
     ExamAbsentReplaceController,
     /** 缺测替补审核服务 */
     ExamAbsentReplaceAuditController,
+    /** 缺测替补管理服务(用于中心管理) */
+    ExamAbsentReplaceCenterController,
     /** 监测科目管理服务 */
     ExamCourseController,
     /** 监测发布内容管理服务 */
@@ -99,6 +107,8 @@ export default {
     ExamReportingTqesController,
     /** 反馈结果管理服务 */
     ExamResultController,
+    /** 缺测替补抽样服务 */
+    ExamSampleReplaceController,
     /** 监测抽样学生管理服务 */
     ExamSampleStudentController,
     /** 监测抽样方案管理服务 */
@@ -111,6 +121,8 @@ export default {
     ExamSpecialStudentController,
     /** 特殊学生审核服务 */
     ExamSpecialStudentAuditController,
+    /** 特殊学生管理服务(用于中心管理) */
+    ExamSpecialStudentCenterController,
     /** 监测学生管理服务 */
     ExamStudentController,
     /** 监测教师任教科目管理服务 */

+ 519 - 3
YBEE.EQM.Admin/src/services/typing.d.ts

@@ -298,6 +298,8 @@ declare global {
             examPlanId: number;
             /** 备注 */
             remark?: string;
+            /** 成绩引用监测计划ID */
+            examScoreRefExamPlanId?: number;
             config: ExamSampleConfig;
         };
 
@@ -845,6 +847,67 @@ declare global {
             total?: number;
         };
 
+        /** 监测缺测替补上报输出参数 */
+        type ExamAbsentReplaceFullOutput = {
+            /** 主键 */
+            id: number;
+            /** 创建时间 */
+            createTime: string;
+            /** 更新时间 */
+            updateTime?: string;
+            /** 创建人ID */
+            createSysUserId: number;
+            /** 修改者Id */
+            updateSysUserId?: number;
+            createSysUser: SysUserLiteOutput;
+            updateSysUser?: SysUserLiteOutput;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 机构ID */
+            sysOrgId: number;
+            /** 校区ID */
+            sysOrgBranchId?: number;
+            /** 监测年级ID */
+            examGradeId: number;
+            /** 年级ID */
+            gradeId: number;
+            /** 班级ID */
+            schoolClassId: string;
+            /** 班号 */
+            classNumber: number;
+            /** 缺测学生姓名 */
+            absentName: string;
+            /** 缺测学生监测号 */
+            absentExamNumber: string;
+            /** 缺测科目 */
+            absentCourses: string;
+            /** 缺测原因 */
+            absentReason: string;
+            /** 是否有替补 */
+            isReplaced: boolean;
+            /** 替补学生姓名 */
+            replaceName?: string;
+            /** 替补学生监测号 */
+            replaceExamNumber?: string;
+            /** 家长姓名 */
+            patriarchName?: string;
+            /** 家长电话 */
+            patriarchTel: string;
+            /** 备注 */
+            remark?: string;
+            status: AuditStatus;
+            schoolClass?: SchoolClassLiteOutput;
+            sysOrgBranch?: SysOrgLiteOutput;
+            examGrade?: ExamGradeOutput;
+            /** 缺测科目列表 */
+            absentCourseList?: CourseMiniOutput[];
+            /** 佐证材料列表 */
+            attachmentList?: AttachmentItem[];
+            /** 审核记录 */
+            auditList?: AuditItem[];
+            sysOrg?: SysOrgLiteOutput;
+        };
+
         /** 监测缺测替补上报输出参数 */
         type ExamAbsentReplaceOutput = {
             /** 主键 */
@@ -923,6 +986,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -1017,6 +1082,63 @@ declare global {
             remark?: string;
         };
 
+        /** 监测机构发布内容列表输出参数 */
+        type ExamDataPublishOrgOutput = {
+            /** 行号 */
+            rowNumber: number;
+            /** 发布名称 */
+            examDataPublishName: string;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 监测计划全称 */
+            examPlanFullName: string;
+            /** 监测计划名称 */
+            examPlanName: string;
+            /** 监测计划简称 */
+            examPlanShortName: string;
+            examPlanStatus: ExamStatus;
+            educationStage: EducationStage;
+            /** 学期ID */
+            semesterId: number;
+            /** 学期简别称 */
+            semesterNickShortName: string;
+            /** 机构ID */
+            sysOrgId: number;
+            /** 数据发布ID */
+            examDataPublishId: number;
+            type: DataPublishType;
+            examStatus: ExamStatus;
+        };
+
+        /** 分页查询面向机构发布数据输入参数 */
+        type ExamDataPublishOrgPageInput = {
+            /** 当前页码 */
+            pageIndex: number;
+            /** 每页大小 */
+            pageSize: number;
+            /** 搜索值 */
+            searchValue?: string;
+            /** 搜索开始时间 */
+            searchBeginTime?: string;
+            /** 搜索结束时间 */
+            searchEndTime?: string;
+            /** 排序字段 */
+            sortField?: string;
+            /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
+            sortOrder?: string;
+            /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
+            descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
+            /** 复杂查询条件 */
+            searchParameters?: Condition[];
+            type: DataPublishType;
+            /** 监测名称 */
+            name?: string;
+            /** 监测学期 */
+            semesterId?: number;
+        };
+
         /** 监测机构反馈结果文件输出参数 */
         type ExamDataPublishOrgResultOutput = {
             /** 主键 */
@@ -1098,6 +1220,72 @@ declare global {
             attachmentList?: AttachmentItem[];
         };
 
+        /** 分页查询数据上报列表输入参数 */
+        type ExamDataReportPageInput = {
+            /** 当前页码 */
+            pageIndex: number;
+            /** 每页大小 */
+            pageSize: number;
+            /** 搜索值 */
+            searchValue?: string;
+            /** 搜索开始时间 */
+            searchBeginTime?: string;
+            /** 搜索结束时间 */
+            searchEndTime?: string;
+            /** 排序字段 */
+            sortField?: string;
+            /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
+            sortOrder?: string;
+            /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
+            descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
+            /** 复杂查询条件 */
+            searchParameters?: Condition[];
+            educationStage?: EducationStage;
+            /** 学期ID */
+            semesterId?: number;
+            /** 名称 */
+            name?: string;
+            status?: ExamStatus;
+            type?: DataReportType;
+        };
+
+        /** 监测数据上报类型输出参数 */
+        type ExamDataReportPlanOutput = {
+            /** 主键 */
+            id: number;
+            /** 创建时间 */
+            createTime: string;
+            /** 更新时间 */
+            updateTime?: string;
+            /** 创建人ID */
+            createSysUserId: number;
+            /** 修改者Id */
+            updateSysUserId?: number;
+            createSysUser: SysUserLiteOutput;
+            updateSysUser?: SysUserLiteOutput;
+            /** 监测计划ID */
+            examPlanId: number;
+            type: DataReportType;
+            status: ExamStatus;
+            /** 开始时间 */
+            beginTime: string;
+            /** 结束时间 */
+            endTime: string;
+            /** 上报说明 */
+            remark?: string;
+            /** 应上报机构数量 */
+            count?: number;
+            /** 已上报机构数量 */
+            reportedCount?: number;
+            /** 未上报机构数量 */
+            unreportCount?: number;
+            /** 佐证材料列表 */
+            attachmentList?: AttachmentItem[];
+            examPlan?: ExamPlanOutput;
+        };
+
         /** 监测年级输出参数 */
         type ExamGradeOutput = {
             /** 主键 */
@@ -1163,6 +1351,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -1217,6 +1407,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             type: DataReportType;
@@ -1242,6 +1434,21 @@ declare global {
             isExpired: boolean;
         };
 
+        /** 监测机构简要输出参数 */
+        type ExamOrgLiteOutput = {
+            /** 主键 */
+            id: number;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 监测机构ID */
+            sysOrgId: number;
+            /** 是否参与监测 */
+            isRequiredExam: boolean;
+            /** 是否需要上报校考成绩 */
+            isReportSchoolExamScore: boolean;
+            sysOrg?: SysOrgLiteOutput;
+        };
+
         /** 分页查询未加入的被监测机构输入参数 */
         type ExamOrgNotInPageInput = {
             /** 当前页码 */
@@ -1260,6 +1467,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -1282,6 +1491,8 @@ declare global {
             sysOrgId: number;
             /** 是否参与监测 */
             isRequiredExam: boolean;
+            /** 是否需要上报校考成绩 */
+            isReportSchoolExamScore: boolean;
             sysOrg?: SysOrgLiteOutput;
             /** 上报状态 */
             dataReports?: any;
@@ -1307,6 +1518,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -1458,6 +1671,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             educationStage?: EducationStage;
@@ -1735,6 +1950,7 @@ declare global {
             /** 是否已固定监测抽样方案 */
             isFixedExamSample: boolean;
             status: ExamStatus;
+            sampleStatus: ExamSampleStatus;
             /** 开始时间 */
             beginTime?: string;
             /** 结束时间 */
@@ -1784,11 +2000,11 @@ declare global {
             rowNumber: number;
             /** 监测计划ID */
             examPlanId: number;
-            /** 监测计划ID */
+            /** 监测计划全称 */
             examPlanFullName: string;
-            /** 监测计划ID */
+            /** 监测计划名称 */
             examPlanName: string;
-            /** 监测计划ID */
+            /** 监测计划简称 */
             examPlanShortName: string;
             examPlanStatus: ExamStatus;
             educationStage: EducationStage;
@@ -1843,6 +2059,7 @@ declare global {
             /** 是否已固定监测抽样方案 */
             isFixedExamSample: boolean;
             status: ExamStatus;
+            sampleStatus: ExamSampleStatus;
             /** 开始时间 */
             beginTime?: string;
             /** 结束时间 */
@@ -1874,6 +2091,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             educationStage?: EducationStage;
@@ -1884,6 +2103,13 @@ declare global {
             status?: ExamStatus;
         };
 
+        /** 监测计划抽样状态输出参数 */
+        type ExamPlanSampleStatusOutput = {
+            /** 主键Id */
+            id: number;
+            sampleStatus: ExamSampleStatus;
+        };
+
         /** 监测反馈结果输出参数 */
         type ExamResultOutput = {
             /** 主键 */
@@ -1938,6 +2164,8 @@ declare global {
 
         /** 抽样数量统计输出参数 */
         type ExamSampleCountOutput = {
+            /** 行号主键 */
+            id?: number;
             /** 类型ID */
             typeId?: number;
             /** 类型名称 */
@@ -1964,6 +2192,30 @@ declare global {
             centerStudentCount?: number;
             /** 校测学生数 */
             schoolStudentCount?: number;
+            /** 特殊学生数 */
+            totalSpecialStudentCount?: number;
+        };
+
+        /** 监测方案简要输出参数 */
+        type ExamSampleLiteOutput = {
+            /** 主键Id */
+            id: number;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 序,同一监测从1开始计数 */
+            sequence: number;
+            /** 名称 */
+            name: string;
+            /** 全称 */
+            fullName: string;
+            /** 简称 */
+            shortName: string;
+            status: ExamSampleStatus;
+            /** 是否已固定监测抽样方案 */
+            isFixedExamSample: boolean;
+            educationStage: EducationStage;
+            /** 是否选中使用的方案 */
+            isSelected: boolean;
         };
 
         /** 监测方案输出参数 */
@@ -2006,6 +2258,8 @@ declare global {
             /** 选中使用操作用户ID */
             selectedSysUserId?: number;
             selectedSysUser?: SysUserLiteOutput;
+            /** 执行日志 */
+            executeLog?: string;
             examScoreRefExamPlan?: ExamPlanLiteOutput;
         };
 
@@ -2049,10 +2303,139 @@ declare global {
             /** 选中使用操作用户ID */
             selectedSysUserId?: number;
             selectedSysUser?: SysUserLiteOutput;
+            /** 执行日志 */
+            executeLog?: string;
             examScoreRefExamPlan?: ExamPlanLiteOutput;
             examPlan?: ExamPlanOutput;
         };
 
+        /** 缺测替补抽样输出参数 */
+        type ExamSampleReplaceOutput = {
+            /** 主键 */
+            id: number;
+            /** 创建时间 */
+            createTime: string;
+            /** 更新时间 */
+            updateTime?: string;
+            /** 创建人ID */
+            createSysUserId: number;
+            /** 修改者Id */
+            updateSysUserId?: number;
+            createSysUser: SysUserLiteOutput;
+            updateSysUser?: SysUserLiteOutput;
+            /** 抽样方案ID */
+            examSampleId: number;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 机构ID */
+            sysOrgId: number;
+            /** 校区ID */
+            sysOrgBranchId?: number;
+            /** 监测年级ID */
+            examGradeId: number;
+            /** 年级ID */
+            gradeId: number;
+            /** 班级ID */
+            schoolClassId: string;
+            /** 班号 */
+            classNumber: number;
+            /** 缺测学生抽样ID */
+            absentExamSampleStudentId: string;
+            /** 替补学生抽样ID */
+            replaceExamSampleStudentId: string;
+            /** 备注 */
+            remark?: string;
+            /** 替补学生也缺测 */
+            isReplaceAbsent: boolean;
+            /** 替补学生标注缺测是否已锁定 */
+            isReplaceAbsentLocked: boolean;
+            examSample?: ExamSampleLiteOutput;
+            absentExamSampleStudent?: ExamSampleStudentOutput;
+            replaceExamSampleStudent?: ExamSampleStudentOutput;
+            schoolClass?: SchoolClassLiteOutput;
+            sysOrgBranch?: SysOrgLiteOutput;
+            examGrade?: ExamGradeOutput;
+        };
+
+        /** 分页查询抽取替补抽样列表输入参数 */
+        type ExamSampleReplacePageInput = {
+            /** 当前页码 */
+            pageIndex: number;
+            /** 每页大小 */
+            pageSize: number;
+            /** 搜索值 */
+            searchValue?: string;
+            /** 搜索开始时间 */
+            searchBeginTime?: string;
+            /** 搜索结束时间 */
+            searchEndTime?: string;
+            /** 排序字段 */
+            sortField?: string;
+            /** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
+            sortOrder?: string;
+            /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
+            descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
+            /** 复杂查询条件 */
+            searchParameters?: Condition[];
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 年级ID */
+            gradeId?: number;
+            /** 班级号 */
+            classNumber?: number;
+            /** 学生姓名 */
+            name?: string;
+            /** 证件号码 */
+            idNumber?: string;
+            /** 监测号 */
+            examNumber?: string;
+            /** 校区ID */
+            sysOrgBranchId?: number;
+            /** 机构ID */
+            sysOrgId?: number;
+            /** 替补学生也缺测 */
+            isReplaceAbsent?: boolean;
+            /** 替补学生标注缺测是否已锁定 */
+            isReplaceAbsentLocked?: boolean;
+        };
+
+        /** 抽样状态输出参数 */
+        type ExamSampleStatusOutput = {
+            /** 主键Id */
+            id: number;
+            status: ExamSampleStatus;
+            /** 执行日志 */
+            executeLog?: string;
+        };
+
+        /** 抽样学生输出参数 */
+        type ExamSampleStudentOrgOutput = {
+            /** 主键 */
+            id: string;
+            /** 抽样方案ID */
+            examSampleId: number;
+            /** 监测样本学生ID */
+            examStudentId: string;
+            /** 监测号 */
+            examNumber: string;
+            /** 序号 */
+            sequence: number;
+            examSampleType: ExamSampleType;
+            /** 是否为特殊学生 */
+            isSpecialStudent: boolean;
+            /** 前期总成绩 */
+            preTotalScore: number;
+            /** 循环次数 */
+            cyclicNumber: number;
+            examStudent?: ExamStudentOutput;
+            /** 学校名称 */
+            sysOrgName: string;
+            /** 校区名称 */
+            sysOrgBranchName?: string;
+        };
+
         /** 抽样学生输出参数 */
         type ExamSampleStudentOutput = {
             /** 主键 */
@@ -2070,6 +2453,8 @@ declare global {
             isSpecialStudent: boolean;
             /** 前期总成绩 */
             preTotalScore: number;
+            /** 循环次数 */
+            cyclicNumber: number;
             examStudent?: ExamStudentOutput;
         };
 
@@ -2091,6 +2476,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测抽样ID */
@@ -2108,6 +2495,8 @@ declare global {
             examNumber?: string;
             /** 校区ID */
             sysOrgBranchId?: number;
+            /** 机构ID */
+            sysOrgId?: number;
             examSampleType?: ExamSampleType;
         };
 
@@ -2139,6 +2528,69 @@ declare global {
             total?: number;
         };
 
+        /** 监测特殊学生上报输出参数 */
+        type ExamSpecialStudentFullOutput = {
+            /** 主键 */
+            id: number;
+            /** 创建时间 */
+            createTime: string;
+            /** 更新时间 */
+            updateTime?: string;
+            /** 创建人ID */
+            createSysUserId: number;
+            /** 修改者Id */
+            updateSysUserId?: number;
+            createSysUser: SysUserLiteOutput;
+            updateSysUser?: SysUserLiteOutput;
+            /** 监测计划ID */
+            examPlanId: number;
+            /** 机构ID */
+            sysOrgId: number;
+            /** 校区ID */
+            sysOrgBranchId?: number;
+            /** 监测年级ID */
+            examGradeId: number;
+            /** 年级ID */
+            gradeId: number;
+            /** 班级ID */
+            schoolClassId: string;
+            /** 班号 */
+            classNumber: number;
+            /** 学籍号 */
+            studentNumber?: string;
+            /** 姓名 */
+            name: string;
+            certificateType: CertificateType;
+            /** 证件号码 */
+            idNumber?: string;
+            /** 出生日期 */
+            birthDate?: string;
+            gender: Gender;
+            /** 申请原因 */
+            applyReason: string;
+            /** 家长姓名 */
+            patriarchName?: string;
+            /** 家长电话 */
+            patriarchTel: string;
+            /** 备注 */
+            remark?: string;
+            status: AuditStatus;
+            schoolClass?: SchoolClassLiteOutput;
+            sysOrgBranch?: SysOrgLiteOutput;
+            examGrade?: ExamGradeOutput;
+            /** 佐证材料列表 */
+            attachmentList?: AttachmentItem[];
+            /** 审核记录 */
+            auditList?: AuditItem[];
+            /** 往期是否已认定 */
+            isPreIdentified: boolean;
+            /** 上一期总分科目数量 */
+            preTotalCourse?: number;
+            /** 上一期总分 */
+            preTotalScore?: number;
+            sysOrg?: SysOrgLiteOutput;
+        };
+
         /** 监测特殊学生上报输出参数 */
         type ExamSpecialStudentOutput = {
             /** 主键 */
@@ -2219,6 +2671,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -2301,6 +2755,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -2326,6 +2782,8 @@ declare global {
             excludeSchoolClassIds?: string[];
             /** 必须包含的班级ID列表 */
             includeSchoolClassIds?: string[];
+            /** 是否必须参与监测 */
+            isRequiredExam?: boolean;
         };
 
         /** 监测学生统计输出参数 */
@@ -2399,6 +2857,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -2482,6 +2942,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -2578,6 +3040,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -2640,6 +3104,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 监测计划ID */
@@ -3093,6 +3559,8 @@ declare global {
             updateSysUser?: SysUserLiteOutput;
             /** 学期ID */
             semesterId: number;
+            /** 年级ID */
+            gradeId: number;
             /** 名称 */
             name: string;
             /** 全称 */
@@ -3131,6 +3599,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 学期ID */
@@ -3236,6 +3706,14 @@ declare global {
             twoWayTables?: TwoWayTable[];
         };
 
+        /** 抽取替补抽样输入参数 */
+        type SampleExamSampleReplaceInput = {
+            /** 缺测学生抽样ID */
+            absentExamSampleStudentId: string;
+            /** 备注 */
+            remark?: string;
+        };
+
         /** 保存学科问题建议输入参数 */
         type SaveExamPaperSuggestion = {
             /** 主键Id */
@@ -3337,6 +3815,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 学校ID */
@@ -3488,6 +3968,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 名称 */
@@ -3555,6 +4037,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 名称 */
@@ -3607,6 +4091,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 名称 */
@@ -3809,6 +4295,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 名称 */
@@ -3894,6 +4382,8 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 角色ID */
@@ -4012,10 +4502,14 @@ declare global {
             sortOrder?: string;
             /** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
             descStr?: string;
+            /** 排序字段列表 ["reportData asc", "userCount desc"] */
+            sortOrders?: string[];
             /** 复杂查询条件 */
             searchParameters?: Condition[];
             /** 机构ID */
             sysOrgId?: number;
+            /** 机构ID列表 */
+            sysOrgIds?: number[];
             /** 姓名 */
             name?: string;
             /** 账户 */
@@ -4024,6 +4518,8 @@ declare global {
             mobile?: string;
             /** 电子邮箱 */
             email?: string;
+            /** 机构名称 */
+            sysOrgName?: string;
         };
 
         type SysUserSimpleOutput = {
@@ -4039,9 +4535,12 @@ declare global {
             email?: string;
             /** 工号 */
             jobNumber?: string;
+            status?: CommonStatus;
             /** 机构ID */
             sysOrgId: number;
             sysOrg: SysOrgLiteOutput;
+            /** 所属角色 */
+            sysRoles: SysRoleOutput[];
         };
 
         /** 双向细目表 */
@@ -4221,6 +4720,8 @@ declare global {
             examPlanId: number;
             /** 备注 */
             remark?: string;
+            /** 成绩引用监测计划ID */
+            examScoreRefExamPlanId?: number;
             config: ExamSampleConfig;
             /** 主键 */
             id: number;
@@ -4258,6 +4759,8 @@ declare global {
             patriarchTel?: string;
             /** 备注 */
             remark?: string;
+            /** 已驳回的重新提交 */
+            isRejectedReaudit?: boolean;
         };
 
         /** 更新监测学生输入参数 */
@@ -4338,6 +4841,13 @@ declare global {
             remark?: string;
         };
 
+        /** 更新高考模拟划线计划配置输入参数 */
+        type UpdateNceePlanConfigInput = {
+            /** 主键 */
+            id: number;
+            config: NceePlanConfig;
+        };
+
         /** 更新高考模拟划线计划输入参数 */
         type UpdateNceePlanInput = {
             /** 学期ID */
@@ -4448,6 +4958,12 @@ declare global {
             remark?: string;
         };
 
+        type UpdateSysUserStatusInput = {
+            /** 主键Id */
+            id: number;
+            status?: CommonStatus;
+        };
+
         /** 监测缺测替补批量上传行数据类型 */
         type UploadExamAbsentReplaceOutput = {
             /** ID */

+ 1 - 1
YBEE.EQM.Application/Base/Semester/Services/SemesterService.cs

@@ -38,7 +38,7 @@ public class SemesterService : ISemesterService, ITransient
         {
             item.SemesterType = SemesterType.FIRST_SEMESTER;
             item.BeginYear = lastItem.EndYear;
-            item.EndYear = (short)(lastItem.BeginYear + 1);
+            item.EndYear = (short)(item.BeginYear + 1);
             item.Id = (short)(item.BeginYear * 10 + (short)SemesterType.SECOND_SEMESTER);
             item.Name = $"{item.BeginYear}至{item.EndYear}学年上学期";
             item.ShortName = $"{item.BeginYear}~{item.EndYear}上";

+ 21 - 0
YBEE.EQM.Application/Esa/Dtos/EsaProcessingDto.cs

@@ -0,0 +1,21 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 总上线
+/// </summary>
+public class EsaProcessingTotalLineDto
+{
+    /// <summary>
+    /// 上线分
+    /// </summary>
+    public decimal LineScore { get; set; }
+    /// <summary>
+    /// 上线平均分
+    /// </summary>
+    public decimal LineAvgScore { get; set; }
+    /// <summary>
+    /// 有效系数
+    /// </summary>
+    public decimal Factor { get; set; }
+}
+

+ 23 - 0
YBEE.EQM.Application/Esa/EsaProcessingAppService.cs

@@ -0,0 +1,23 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 有效分分析处理服务
+/// </summary>
+[ApiDescriptionSettings(Name = "esa-processing")]
+[Route("esa/processing")]
+public class EsaProcessingAppService(IEsaProcessingService esaProcessingService) : IDynamicApiController
+{
+    /// <summary>
+    /// 分析处理
+    /// </summary>
+    /// <param name="esaPlanId"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisableOpLog]
+    public async Task Execute([FromQuery][Required] int esaPlanId)
+    {
+        await esaProcessingService.Execute(esaPlanId);
+    }
+}

+ 719 - 0
YBEE.EQM.Application/Esa/Services/EsaProcessingService.cs

@@ -0,0 +1,719 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 有效分分析处理服务
+/// </summary>
+public class EsaProcessingService(IRepository<EsaPlan> rep) : IEsaProcessingService, ITransient
+{
+    /// <summary>
+    /// 分析处理
+    /// </summary>
+    /// <param name="esaPlanId"></param>
+    /// <returns></returns>
+    public async Task Execute(int esaPlanId)
+    {
+        var plan = await rep.DetachedEntities
+                            .Include(t => t.EsaBaseLines).ThenInclude(t => t.EsaBaseLineCourses)
+                            .FirstOrDefaultAsync(t => t.Id == esaPlanId) 
+                            ?? throw Oops.Oh(ErrorCode.E2001);
+        // 更新状态
+        await rep.SqlNonQueryAsync($"UPDATE esa_plan SET status = {(short)ProcessingStatus.RUNNING} WHERE id = {esaPlanId}");
+        try
+        {
+            foreach (var esaBaseLine in plan.EsaBaseLines)
+            {
+                try
+                {
+                    // 更新状态
+                    await rep.SqlNonQueryAsync($"UPDATE esa_base_line SET status = {(short)ProcessingStatus.RUNNING} WHERE id = {esaBaseLine.Id}");
+
+                    // 构造分析学科
+                    var courseIds = string.Join(", ", esaBaseLine.EsaBaseLineCourses.OrderBy(t => t.CourseId).Select(t => t.CourseId));
+                    // 构造取数条件
+                    var whereBaseSql = $"WHERE T1.exam_plan_id = {plan.ExamPlanId} AND exam_sample_type = 1 AND T1.grade_id = {esaBaseLine.GradeId} AND T1.score > 0";
+                    var whereSql = $"{whereBaseSql} AND T1.course_id IN ({courseIds})";
+
+                    // 删除数据
+                    var deleteWhereSql = $"WHERE esa_plan_id = {esaPlanId} AND esa_base_line_id = {esaBaseLine.Id} AND grade_id = {esaBaseLine.GradeId}";
+                    await rep.SqlNonQueryAsync(@$"
+DELETE FROM esa_line_total {deleteWhereSql};
+DELETE FROM esa_line_course {deleteWhereSql};
+DELETE FROM esa_line_course_score {deleteWhereSql};
+");
+
+                    #region 区级分析
+                    #region 总分上线
+                    // 总分上线分
+                    var totalLineScore = await rep.SqlScalarAsync<decimal>(@$"
+SELECT MIN(T1.total_score) AS line_score
+FROM
+(
+    SELECT total_score
+    FROM
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T1
+    ORDER BY T1.total_score DESC
+    LIMIT {esaBaseLine.LineCount}
+) AS T1;
+");
+
+                    // 总分上线均分
+                    var totalLineAvgScore = Math.Round(await rep.SqlScalarAsync<decimal>(@$"
+SELECT AVG(T1.total_score) AS line_avg_score
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+WHERE T1.total_score >= {totalLineScore};
+"), 2);
+
+                    // 更新有效系数
+                    var factor = Math.Round(totalLineScore / totalLineAvgScore, 8);
+                    await rep.SqlNonQueryAsync($"UPDATE esa_base_line SET line_score = {totalLineScore}, factor = {factor} WHERE id = {esaBaseLine.Id}");
+
+                    // 实际上线人数
+                    var factLineCount = await rep.SqlScalarAsync($@"
+SELECT COUNT(1) AS line_count
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+WHERE T1.total_score >= {totalLineScore};
+");
+
+                    // 总均分
+                    var totalAvgScore = Math.Round(await rep.SqlScalarAsync<decimal>(@$"
+SELECT AVG(total_score) AS avg_score
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.exam_student_id
+) AS T1;
+"), 2);
+
+                    // 学科有效分
+                    var totalRelativeDiff = Math.Round((totalLineScore - totalAvgScore) / totalLineScore, 8);
+                    await rep.SqlNonQueryAsync(@$"
+-- 总分
+INSERT INTO esa_line_course_score(esa_level, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, course_id, avg_score, line_avg_score, line_score, relative_diff)
+VALUES({(short)EsaLevel.DISTRICT}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, {(short)MultCourse.TOTAL}, {totalAvgScore}, {totalLineAvgScore}, {totalLineScore}, {totalRelativeDiff});
+
+-- 单科
+INSERT INTO esa_line_course_score(esa_level, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, course_id, avg_score, line_avg_score, line_score, relative_diff)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, T1.course_id, T2.avg_course_score, T1.avg_line_course_score, T1.course_line_score, ROUND((T1.course_line_score - T2.avg_course_score) / T1.course_line_score, 8) AS relative_diff
+FROM
+(
+    SELECT T1.course_id, ROUND(AVG(T1.score), 2) AS avg_line_course_score, ROUND(ROUND(AVG(T1.score), 2) * {factor}, 2) AS course_line_score
+    FROM
+    (
+        SELECT T1.exam_student_id, T1.course_id, T1.score
+        FROM exam_score AS T1
+        {whereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.exam_student_id
+        FROM
+        (
+            SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+            FROM exam_score AS T1
+            {whereSql}
+            GROUP BY T1.exam_student_id
+        ) AS T1
+        WHERE T1.total_score >= {totalLineScore}
+    ) AS T2 ON T1.exam_student_id = T2.exam_student_id
+    GROUP BY T1.course_id
+) AS T1
+JOIN
+(
+    SELECT T1.course_id, ROUND(AVG(T1.score), 2) AS avg_course_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.course_id
+) AS T2 ON T1.course_id = T2.course_id;
+");
+
+                    // 全区
+                    await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_total(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, line_count, total_count, line_rate, factor)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.TOTAL}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, COUNT(1) AS line_count, T2.total_count, COUNT(1) / T2.total_count, 0
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T1
+) AS T2 ON 1 = 1
+WHERE T1.total_score >= {totalLineScore};
+");
+
+                    // 学校
+                    await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_total(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, line_count, total_count, line_rate, factor)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, T1.sys_org_id, COUNT(1) AS line_count, T2.total_count, COUNT(1) / T2.total_count, 0
+FROM
+(
+    SELECT T1.sys_org_id, T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.sys_org_id, T1.exam_student_id
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, COUNT(1) as total_count
+    FROM
+    (
+        SELECT T1.sys_org_id, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.sys_org_id, T1.exam_student_id
+    ) AS T1
+    GROUP BY T1.sys_org_id
+) AS T2 ON T1.sys_org_id = T2.sys_org_id
+WHERE T1.total_score >= {totalLineScore}
+GROUP BY T1.sys_org_id;
+");
+
+                    // 班级
+                    await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_total(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, factor)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) AS line_count, T2.total_count, COUNT(1) / T2.total_count, 0
+FROM
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {whereSql}
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM
+    (
+        SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id
+    ) AS T1
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number
+) AS T2 ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number
+WHERE T1.total_score >= {totalLineScore}
+GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number;
+");
+                    #endregion
+
+                    #region 学科上线
+                    var courseLines = await rep.Change<EsaLineCourseScore>().DetachedEntities
+                                                                            .Where(t => t.EsaPlanId == esaPlanId &&
+                                                                                        t.EsaBaseLineId == esaBaseLine.Id &&
+                                                                                        t.EsaLevel == EsaLevel.DISTRICT &&
+                                                                                        t.EsaLineLevel == esaBaseLine.EsaLineLevel &&
+                                                                                        t.GradeId == esaBaseLine.GradeId &&
+                                                                                        t.CourseId != (short)MultCourse.TOTAL
+                                                                             ).ToListAsync();
+                    foreach (var courseLine in courseLines)
+                    {
+                        // 单科条件
+                        var courseWhereSql = $"{whereBaseSql} AND T1.course_id = {courseLine.CourseId}";
+
+                        // 全区 - 单上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.TOTAL}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 0
+FROM
+(
+    SELECT COUNT(1) AS line_count
+    FROM exam_score AS T1
+    {courseWhereSql} AND T1.score >= {courseLine.LineScore}
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+) AS T2 ON 1 = 1;
+");
+
+                        // 全区 - 双上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.TOTAL}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 1
+FROM
+(
+    SELECT COUNT(1) AS line_count
+    FROM
+    (
+        SELECT T1.exam_student_id, T1.score
+        FROM exam_score AS T1
+        {courseWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T2 ON T1.exam_student_id = T2.exam_student_id
+    WHERE T1.score >= {courseLine.LineScore} AND T2.total_score >= {totalLineScore}
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+) AS T2 ON 1 = 1;
+");
+
+                        // 学校 - 单上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.sys_org_id, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 0
+FROM
+(
+    SELECT T1.sys_org_id, COUNT(1) AS line_count
+    FROM exam_score AS T1
+    {courseWhereSql} AND T1.score >= {courseLine.LineScore}
+    GROUP BY T1.sys_org_id
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+    GROUP BY T1.sys_org_id
+) AS T2 ON T1.sys_org_id = T2.sys_org_id;
+");
+
+                        // 学校 - 双上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.sys_org_id, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 1
+FROM
+(
+    SELECT T1.sys_org_id, COUNT(1) AS line_count
+    FROM
+    (
+        SELECT T1.sys_org_id, T1.exam_student_id, T1.score
+        FROM exam_score AS T1
+        {courseWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.sys_org_id, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.sys_org_id, T1.exam_student_id
+    ) AS T2 ON T1.sys_org_id = T2.sys_org_id AND T1.exam_student_id = T2.exam_student_id
+    WHERE T1.score >= {courseLine.LineScore} AND T2.total_score >= {totalLineScore}
+    GROUP BY T1.sys_org_id
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+    GROUP BY T1.sys_org_id
+) AS T2 ON T1.sys_org_id = T2.sys_org_id;
+");
+
+                        // 班级 - 单上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.sys_org_id, T1.school_class_id, T1.class_number, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 0
+FROM
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) AS line_count
+    FROM exam_score AS T1
+    {courseWhereSql} AND T1.score >= {courseLine.LineScore}
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number
+) AS T2 ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number;
+");
+
+                        // 班级 - 双上线
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.DISTRICT}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {courseLine.CourseId}, {esaBaseLine.GradeId}, T1.sys_org_id, T1.school_class_id, T1.class_number, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 1
+FROM
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) AS line_count
+    FROM
+    (
+        SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id, T1.score
+        FROM exam_score AS T1
+        {courseWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {whereSql}
+        GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number, T1.exam_student_id
+    ) AS T2 ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number AND T1.exam_student_id = T2.exam_student_id
+    WHERE T1.score >= {courseLine.LineScore} AND T2.total_score >= {totalLineScore}
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number
+) AS T1
+JOIN 
+(
+    SELECT T1.sys_org_id, T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {courseWhereSql}
+    GROUP BY T1.sys_org_id, T1.school_class_id, T1.class_number
+) AS T2 ON T1.sys_org_id = T2.sys_org_id AND T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number;
+");
+                    }
+                    #endregion
+                    #endregion
+
+                    #region 学校分析
+                    var orgTotalLines = await rep.Change<EsaLineTotal>().DetachedEntities
+                                                                        .Where(t => t.EsaLevel == EsaLevel.DISTRICT &&
+                                                                                    t.EsaLineLevel == esaBaseLine.EsaLineLevel &&
+                                                                                    t.EsaDataScopeType == EsaDataScopeType.ORG &&
+                                                                                    t.GradeId == esaBaseLine.GradeId
+                                                                         ).OrderBy(t => t.SysOrgId).ToListAsync();
+                    foreach (var orgTotalLine in orgTotalLines)
+                    {
+                        // 构造取数条件
+                        var orgWhereBaseSql = $"WHERE T1.exam_plan_id = {plan.ExamPlanId} AND T1.grade_id = {esaBaseLine.GradeId} AND T1.score > 0 AND T1.sys_org_id = {orgTotalLine.SysOrgId}";
+                        var orgWhereSql = $"{orgWhereBaseSql} AND T1.course_id IN ({courseIds})";
+
+                        #region 总分划线
+                        // 总学生数量
+                        var orgTotalCount = await rep.SqlScalarAsync<int>($@"
+SELECT COUNT(1) AS total_count
+FROM
+(
+    SELECT T1.exam_student_id
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.exam_student_id
+) AS T1;
+");
+
+                        // 总分上线分
+                        var orgTotalLineScore = await rep.SqlScalarAsync<decimal>($@"
+SELECT MIN(total_score) AS line_score
+FROM
+(
+    SELECT ROW_NUMBER() OVER(ORDER BY total_score DESC) AS rn, total_score
+    FROM
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {orgWhereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T1
+) AS T1
+WHERE T1.rn <= ROUND({orgTotalCount} * {orgTotalLine.LineRate}, 0);
+");
+
+                        // 总分上线均分
+                        var orgTotalLineAvgScore = Math.Round(await rep.SqlScalarAsync<decimal>(@$"
+SELECT AVG(T1.total_score) AS line_avg_score
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+WHERE T1.total_score >= {orgTotalLineScore};
+"), 2);
+
+                        // 更新有效系数
+                        var orgFactor = Math.Round(orgTotalLineScore / orgTotalLineAvgScore, 8);
+                        await rep.SqlNonQueryAsync($"UPDATE esa_line_total SET factor = {orgFactor} WHERE id = {orgTotalLine.Id}");
+
+                        // 实际上线人数
+                        var orgFactLineCount = await rep.SqlScalarAsync($@"
+SELECT COUNT(1) AS line_count
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+WHERE T1.total_score >= {orgTotalLineScore};
+");
+
+                        // 总均分
+                        var orgTotalAvgScore = Math.Round(await rep.SqlScalarAsync<decimal>(@$"
+SELECT AVG(total_score) AS avg_score
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.exam_student_id
+) AS T1;
+"), 2);
+
+                        // 学科有效分
+                        var orgTotalRelativeDiff = Math.Round((orgTotalLineScore - orgTotalAvgScore) / orgTotalLineScore, 8);
+                        await rep.SqlNonQueryAsync(@$"
+-- 总分
+INSERT INTO esa_line_course_score(esa_level, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, course_id, avg_score, line_avg_score, line_score, relative_diff)
+VALUES({(short)EsaLevel.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, {(short)MultCourse.TOTAL}, {orgTotalAvgScore}, {orgTotalLineAvgScore}, {orgTotalLineScore}, {orgTotalRelativeDiff});
+
+-- 单科
+INSERT INTO esa_line_course_score(esa_level, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, course_id, avg_score, line_avg_score, line_score, relative_diff)
+SELECT {(short)EsaLevel.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.course_id, T2.avg_course_score, T1.avg_line_course_score, T1.course_line_score, ROUND((T1.course_line_score - T2.avg_course_score) / T1.course_line_score, 8) AS relative_diff
+FROM
+(
+    SELECT T1.course_id, ROUND(AVG(T1.score), 2) AS avg_line_course_score, ROUND(ROUND(AVG(T1.score), 2) * {orgFactor}, 2) AS course_line_score
+    FROM
+    (
+        SELECT T1.exam_student_id, T1.course_id, T1.score
+        FROM exam_score AS T1
+        {orgWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.exam_student_id
+        FROM
+        (
+            SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+            FROM exam_score AS T1
+            {orgWhereSql}
+            GROUP BY T1.exam_student_id
+        ) AS T1
+        WHERE T1.total_score >= {orgTotalLineScore}
+    ) AS T2 ON T1.exam_student_id = T2.exam_student_id
+    GROUP BY T1.course_id
+) AS T1
+JOIN
+(
+    SELECT T1.course_id, ROUND(AVG(T1.score), 2) AS avg_course_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.course_id
+) AS T2 ON T1.course_id = T2.course_id;
+");
+
+                        // 学校
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_total(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, line_count, total_count, line_rate, factor)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, COUNT(1) AS line_count, T2.total_count, COUNT(1) / T2.total_count, 0
+FROM
+(
+    SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.exam_student_id
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {orgWhereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T1
+) AS T2 ON 1 = 1
+WHERE T1.total_score >= {orgTotalLineScore};
+");
+
+                        // 班级
+                        await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_total(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, factor)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.school_class_id, T1.class_number, COUNT(1) AS line_count, T2.total_count, COUNT(1) / T2.total_count, 0
+FROM
+(
+    SELECT T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+    FROM exam_score AS T1
+    {orgWhereSql}
+    GROUP BY T1.school_class_id, T1.class_number, T1.exam_student_id
+) AS T1
+JOIN 
+(
+    SELECT T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM
+    (
+        SELECT T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {orgWhereSql}
+        GROUP BY T1.school_class_id, T1.class_number, T1.exam_student_id
+    ) AS T1
+    GROUP BY T1.school_class_id, T1.class_number
+) AS T2 ON T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number
+WHERE T1.total_score >= {orgTotalLineScore}
+GROUP BY T1.school_class_id, T1.class_number;
+");
+                        #endregion
+
+                        #region 学科划线
+                        var orgCourseLines = await rep.Change<EsaLineCourseScore>().DetachedEntities
+                                                                                   .Where(t => t.EsaPlanId == esaPlanId &&
+                                                                                               t.EsaBaseLineId == esaBaseLine.Id &&
+                                                                                               t.EsaLevel == EsaLevel.ORG &&
+                                                                                               t.EsaLineLevel == esaBaseLine.EsaLineLevel &&
+                                                                                               t.GradeId == esaBaseLine.GradeId &&
+                                                                                               t.SysOrgId == orgTotalLine.SysOrgId &&
+                                                                                               t.CourseId != (short)MultCourse.TOTAL
+                                                                                    ).ToListAsync();
+                        foreach (var orgCourseLine in orgCourseLines)
+                        {
+                            // 单科条件
+                            var orgCourseWhereSql = $"{orgWhereBaseSql} AND T1.course_id = {orgCourseLine.CourseId}";
+
+                            // 学校 - 单上线
+                            await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {orgCourseLine.CourseId}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 0
+FROM
+(
+    SELECT COUNT(1) AS line_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql} AND T1.score >= {orgCourseLine.LineScore}
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql}
+) AS T2 ON 1 = 1;
+");
+
+                            // 学校 - 双上线
+                            await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.ORG}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {orgCourseLine.CourseId}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 1
+FROM
+(
+    SELECT COUNT(1) AS line_count
+    FROM
+    (
+        SELECT T1.exam_student_id, T1.score
+        FROM exam_score AS T1
+        {orgCourseWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {orgWhereSql}
+        GROUP BY T1.exam_student_id
+    ) AS T2 ON T1.exam_student_id = T2.exam_student_id
+    WHERE T1.score >= {orgCourseLine.LineScore} AND T2.total_score >= {orgTotalLineScore}
+) AS T1
+JOIN 
+(
+    SELECT COUNT(1) as total_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql}
+) AS T2 ON 1 = 1;
+");
+
+                            // 班级 - 单上线
+                            await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {orgCourseLine.CourseId}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.school_class_id, T1.class_number, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 0
+FROM
+(
+    SELECT T1.school_class_id, T1.class_number, COUNT(1) AS line_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql} AND T1.score >= {orgCourseLine.LineScore}
+    GROUP BY T1.school_class_id, T1.class_number
+) AS T1
+JOIN 
+(
+    SELECT T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql}
+    GROUP BY T1.school_class_id, T1.class_number
+) AS T2 ON T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number;
+");
+
+                            // 班级 - 双上线
+                            await rep.SqlNonQueryAsync(@$"
+INSERT INTO esa_line_course(esa_level, esa_data_scope_type, esa_line_level, esa_plan_id, esa_base_line_id, course_id, grade_id, sys_org_id, school_class_id, class_number, line_count, total_count, line_rate, is_double_line)
+SELECT {(short)EsaLevel.ORG}, {(short)EsaDataScopeType.CLASS}, {(short)esaBaseLine.EsaLineLevel}, {esaPlanId}, {esaBaseLine.Id}, {orgCourseLine.CourseId}, {esaBaseLine.GradeId}, {orgTotalLine.SysOrgId}, T1.school_class_id, T1.class_number, T1.line_count, T2.total_count, T1.line_count / T2.total_count, 1
+FROM
+(
+    SELECT T1.school_class_id, T1.class_number, COUNT(1) AS line_count
+    FROM
+    (
+        SELECT T1.school_class_id, T1.class_number, T1.exam_student_id, T1.score
+        FROM exam_score AS T1
+        {orgCourseWhereSql}
+    ) AS T1
+    JOIN
+    (
+        SELECT T1.school_class_id, T1.class_number, T1.exam_student_id, SUM(T1.score) AS total_score
+        FROM exam_score AS T1
+        {orgWhereSql}
+        GROUP BY T1.school_class_id, T1.class_number, T1.exam_student_id
+    ) AS T2 ON T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number AND T1.exam_student_id = T2.exam_student_id
+    WHERE T1.score >= {orgCourseLine.LineScore} AND T2.total_score >= {orgTotalLineScore}
+    GROUP BY T1.school_class_id, T1.class_number
+) AS T1
+JOIN 
+(
+    SELECT T1.school_class_id, T1.class_number, COUNT(1) as total_count
+    FROM exam_score AS T1
+    {orgCourseWhereSql}
+    GROUP BY T1.school_class_id, T1.class_number
+) AS T2 ON T1.school_class_id = T2.school_class_id AND T1.class_number = T2.class_number;
+");
+                        }
+                        #endregion
+                    }
+                    #endregion
+
+                    // 更新状态
+                    await rep.SqlNonQueryAsync($"UPDATE esa_base_line SET status = {(short)ProcessingStatus.SUCCESSFUL} WHERE id = {esaBaseLine.Id}");
+                }
+                catch (Exception)
+                {
+                    // 更新状态
+                    await rep.SqlNonQueryAsync($"UPDATE esa_base_line SET status = {(short)ProcessingStatus.FAILED} WHERE id = {esaBaseLine.Id}");
+                    throw;
+                }
+            }
+
+            // 更新状态
+            await rep.SqlNonQueryAsync($"UPDATE esa_plan SET status = {(short)ProcessingStatus.SUCCESSFUL} WHERE id = {esaPlanId}");
+        }
+        catch (Exception)
+        {
+            // 更新状态
+            await rep.SqlNonQueryAsync($"UPDATE esa_plan SET status = {(short)ProcessingStatus.FAILED} WHERE id = {esaPlanId}");
+            throw;
+        }
+    }
+}

+ 14 - 0
YBEE.EQM.Application/Esa/Services/IEsaProcessingService.cs

@@ -0,0 +1,14 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 有效分分析处理服务
+/// </summary>
+public interface IEsaProcessingService
+{
+    /// <summary>
+    /// 分析处理
+    /// </summary>
+    /// <param name="esaPlanId"></param>
+    /// <returns></returns>
+    Task Execute(int esaPlanId);
+}

+ 1 - 0
YBEE.EQM.Application/Exam/ExamAbsentReplace/Dtos/ExamAbsentReplaceMapper.cs

@@ -8,5 +8,6 @@ public class ExamAbsentReplaceMapper : IRegister
     public void Register(TypeAdapterConfig config)
     {
         config.ForType<ExamAbsentReplace, ExamAbsentReplaceOutput>().Map(d => d.AbsentCourseList, s => JSON.Deserialize<List<CourseMiniOutput>>(s.AbsentCourses, null));
+        config.ForType<ExamAbsentReplace, ExamAbsentReplaceFullOutput>().Map(d => d.AbsentCourseList, s => JSON.Deserialize<List<CourseMiniOutput>>(s.AbsentCourses, null));
     }
 }

+ 13 - 2
YBEE.EQM.Application/Exam/ExamAbsentReplace/Dtos/ExamAbsentReplaceOutput.cs

@@ -119,11 +119,22 @@ public class ExamAbsentReplaceOutput : DEntityOutput
     /// <summary>
     /// 佐证材料列表
     /// </summary>
-    public List<AttachmentItem> AttachmentList { get; set; } = new();
+    public List<AttachmentItem> AttachmentList { get; set; } = [];
     /// <summary>
     /// 审核记录
     /// </summary>
-    public List<AuditItem> AuditList { get; set; } = new();
+    public List<AuditItem> AuditList { get; set; } = [];
+}
+
+/// <summary>
+/// 监测缺测替补上报输出参数
+/// </summary>
+public class ExamAbsentReplaceFullOutput : ExamAbsentReplaceOutput
+{
+    /// <summary>
+    /// 学校
+    /// </summary>
+    public SysOrgLiteOutput SysOrg { get; set; }
 }
 
 /// <summary>

+ 14 - 23
YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceAppService.cs

@@ -7,17 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-absent-replace")]
 [Route("exam/absent/replace")]
-public class ExamAbsentReplaceAppService : IDynamicApiController
+public class ExamAbsentReplaceAppService(IExamAbsentReplaceService examAbsentReplaceService, IResourceFileService resourceFileService) : IDynamicApiController
 {
-    private readonly IExamAbsentReplaceService _examAbsentReplaceService;
-    private readonly IResourceFileService _resourceFileService;
-
-    public ExamAbsentReplaceAppService(IExamAbsentReplaceService examAbsentReplaceService, IResourceFileService resourceFileService)
-    {
-        _examAbsentReplaceService = examAbsentReplaceService;
-        _resourceFileService = resourceFileService;
-    }
-
     #region 批量导入
     /// <summary>
     /// 上传批量导入文件
@@ -40,7 +31,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
         await fs.FlushAsync();
         fs.Close();
 
-        var ret = await _examAbsentReplaceService.Upload(filePath, input.ExamPlanId);
+        var ret = await examAbsentReplaceService.Upload(filePath, input.ExamPlanId);
         return ret;
     }
     /// <summary>
@@ -50,7 +41,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<int> Import(ImportExamAbsentReplaceInput input)
     {
-        return await _examAbsentReplaceService.Import(input);
+        return await examAbsentReplaceService.Import(input);
     }
     #endregion
 
@@ -62,7 +53,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Add(AddExamAbsentReplaceInput input)
     {
-        await _examAbsentReplaceService.Add(input);
+        await examAbsentReplaceService.Add(input);
     }
     /// <summary>
     /// 更新监测缺测替补
@@ -71,7 +62,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Update(UpdateExamAbsentReplaceInput input)
     {
-        await _examAbsentReplaceService.Update(input);
+        await examAbsentReplaceService.Update(input);
     }
     /// <summary>
     /// 上传特殊学生佐证材料
@@ -82,12 +73,12 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadAttachment([FromForm] UploadResourceFileInput input)
     {
-        var rfile = await _resourceFileService.Upload(input);
+        var rfile = await resourceFileService.Upload(input);
         var addParams = rfile.Adapt<AddAttachmentInput>();
         //addParams.SourceId = (int)input.SourceId;
         //addParams.FileId = rfile.Id;
         //addParams.ThumbFileId = rfile.ThumbResourceFile?.Id;
-        await _examAbsentReplaceService.AddAttachment(addParams);
+        await examAbsentReplaceService.AddAttachment(addParams);
     }
     /// <summary>
     /// 删除特殊学生佐证材料
@@ -96,7 +87,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task DelAttachment(DeleteAttachmentInput input)
     {
-        await _examAbsentReplaceService.DelAttachment(input);
+        await examAbsentReplaceService.DelAttachment(input);
     }
     /// <summary>
     /// 删除监测缺测替补
@@ -105,7 +96,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        await _examAbsentReplaceService.Del(input);
+        await examAbsentReplaceService.Del(input);
     }
     /// <summary>
     /// 清空监测缺测替补
@@ -114,7 +105,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Clear(ClearExamAbsentReplaceInput input)
     {
-        await _examAbsentReplaceService.Clear(input);
+        await examAbsentReplaceService.Clear(input);
     }
     /// <summary>
     /// 导出监测缺测替补上报打印表格
@@ -123,7 +114,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportPrintTable(int examPlanId)
     {
-        var bs = await _examAbsentReplaceService.ExportPrintTable(examPlanId);
+        var bs = await examAbsentReplaceService.ExportPrintTable(examPlanId);
         return new FileContentResult(bs, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
         {
             FileDownloadName = "缺测替补学生明细表.docx"
@@ -139,7 +130,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamAbsentReplaceOutput>> QueryPageList(ExamAbsentReplacePageInput input)
     {
-        return await _examAbsentReplaceService.QueryPageList(input);
+        return await examAbsentReplaceService.QueryPageList(input);
     }
     /// <summary>
     /// 获取机构班级特殊学生上报人数统计列表
@@ -149,7 +140,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<ExamAbsentReplaceCountOutput> GetOrgGradeClassStudentCount(int examPlanId, short? sysOrgId = null)
     {
-        return await _examAbsentReplaceService.GetOrgGradeClassStudentCount(examPlanId, sysOrgId);
+        return await examAbsentReplaceService.GetOrgGradeClassStudentCount(examPlanId, sysOrgId);
     }
     /// <summary>
     /// 获取状态数量
@@ -157,7 +148,7 @@ public class ExamAbsentReplaceAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<StatusCount>> QueryStatusCount(ExamAbsentReplacePageInput input)
     {
-        return await _examAbsentReplaceService.QueryStatusCount(input);
+        return await examAbsentReplaceService.QueryStatusCount(input);
     }
     #endregion
 }

+ 6 - 13
YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceAuditAppService.cs

@@ -7,15 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-absent-replace-audit")]
 [Route("exam/absent/replace/audit")]
-public class ExamAbsentReplaceAuditAppService : IDynamicApiController
+public class ExamAbsentReplaceAuditAppService(IExamAbsentReplaceAuditService examAbsentReplaceAuditService) : IDynamicApiController
 {
-    private readonly IExamAbsentReplaceAuditService _examAbsentReplaceAuditService;
-
-    public ExamAbsentReplaceAuditAppService(IExamAbsentReplaceAuditService examAbsentReplaceAuditService)
-    {
-        _examAbsentReplaceAuditService = examAbsentReplaceAuditService;
-    }
-
     /// <summary>
     /// 提交审核
     /// </summary>
@@ -23,7 +16,7 @@ public class ExamAbsentReplaceAuditAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Submit(BaseId input)
     {
-        await _examAbsentReplaceAuditService.Submit(input);
+        await examAbsentReplaceAuditService.Submit(input);
     }
     /// <summary>
     /// 审核
@@ -32,7 +25,7 @@ public class ExamAbsentReplaceAuditAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Audit(ExamAbsentReplaceAuditInput input)
     {
-        await _examAbsentReplaceAuditService.Audit(input);
+        await examAbsentReplaceAuditService.Audit(input);
     }
     /// <summary>
     /// 反审
@@ -41,7 +34,7 @@ public class ExamAbsentReplaceAuditAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Reaudit(BaseId input)
     {
-        await _examAbsentReplaceAuditService.Reaudit(input);
+        await examAbsentReplaceAuditService.Reaudit(input);
     }
 
     /// <summary>
@@ -51,7 +44,7 @@ public class ExamAbsentReplaceAuditAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamPlanAuditOutput>> QueryExamPlanPageList(ExamPlanPageInput input)
     {
-        return await _examAbsentReplaceAuditService.QueryExamPlanPageList(input);
+        return await examAbsentReplaceAuditService.QueryExamPlanPageList(input);
     }
     /// <summary>
     /// 获取待审核机构列表
@@ -60,6 +53,6 @@ public class ExamAbsentReplaceAuditAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamPlanOrgAuditOutput>> QueryOrgAuditPageList(ExamOrgDataReportAuditPageInput input)
     {
-        return await _examAbsentReplaceAuditService.QueryOrgAuditPageList(input);
+        return await examAbsentReplaceAuditService.QueryOrgAuditPageList(input);
     }
 }

+ 55 - 0
YBEE.EQM.Application/Exam/ExamAbsentReplace/ExamAbsentReplaceCenterAppService.cs

@@ -0,0 +1,55 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补管理服务(用于中心管理)
+/// </summary>
+[ApiDescriptionSettings(Name = "exam-absent-replace-center")]
+[Route("exam/absent/replace/center")]
+public class ExamAbsentReplaceCenterAppService(IExamAbsentReplaceCenterService examAbsentReplaceCenterService) : IDynamicApiController
+{
+    /// <summary>
+    /// 分页查询监测缺测替补列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamAbsentReplaceFullOutput>> QueryPageList(ExamAbsentReplacePageInput input)
+    {
+        return await examAbsentReplaceCenterService.QueryPageList(input);
+    }
+    /// <summary>
+    /// 获取状态数量
+    /// </summary>
+    /// <returns></returns>
+    public async Task<List<StatusCount>> QueryStatusCount(ExamAbsentReplacePageInput input)
+    {
+        return await examAbsentReplaceCenterService.QueryStatusCount(input);
+    }
+    /// <summary>
+    /// 导出数据表格(简表)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<IActionResult> ExportSimple(ExamAbsentReplacePageInput input)
+    {
+        var (fileName, fileBytes) = await examAbsentReplaceCenterService.ExportSimple(input);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
+    /// <summary>
+    /// 导出数据表格(完整)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<IActionResult> ExportFull(ExamAbsentReplacePageInput input)
+    {
+        var (fileName, fileBytes) = await examAbsentReplaceCenterService.ExportFull(input);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
+}

+ 27 - 30
YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceAuditService.cs

@@ -8,7 +8,6 @@ namespace YBEE.EQM.Application;
 /// </summary>
 public class ExamAbsentReplaceAuditService(IRepository<ExamAbsentReplace> rep, ISysRoleService sysRoleService) : IExamAbsentReplaceAuditService, ITransient
 {
-
     /// <summary>
     /// 提交审核
     /// </summary>
@@ -74,7 +73,7 @@ public class ExamAbsentReplaceAuditService(IRepository<ExamAbsentReplace> rep, I
             return new();
         }
 
-        List<string> whereCause = new();
+        List<string> whereCause = [];
         if (roleDataScope.EducationStages.Count == 0)
         {
             whereCause.Add($"T1.education_stage = {(short)roleDataScope.EducationStages[0]}");
@@ -118,21 +117,19 @@ public class ExamAbsentReplaceAuditService(IRepository<ExamAbsentReplace> rep, I
 SELECT COUNT(1) AS total_count
 FROM
 (
-	SELECT T1.id, COUNT(T1.sys_org_id) AS org_count
-	FROM
-	(
-		SELECT T1.id, T2.sys_org_id
-		FROM exam_plan AS T1
+    SELECT T1.id, COUNT(T1.sys_org_id) AS org_count
+    FROM
+    (
+        SELECT T1.id, T2.sys_org_id
+        FROM exam_plan AS T1
         JOIN exam_data_report AS EDR ON T1.id = EDR.exam_plan_id
-		LEFT JOIN exam_org AS T2 ON T1.id = T2.exam_plan_id
-		{whereSql}
-		GROUP BY T1.id, t2.sys_org_id
-	) AS T1
-	GROUP BY T1.id
+        LEFT JOIN exam_org AS T2 ON T1.id = T2.exam_plan_id
+        {whereSql}
+        GROUP BY T1.id, t2.sys_org_id
+    ) AS T1
+    GROUP BY T1.id
 ) AS T
 ", p);
-
-
         var items = await rep.SqlQueriesAsync<ExamPlanAuditOutput>($@"
 SELECT
     T1.id, 
@@ -269,22 +266,22 @@ SELECT ROW_NUMBER() OVER (ORDER BY T.exam_plan_id DESC, T.audit_count DESC, T.re
 FROM
 (
     SELECT
-	    T2.exam_plan_id, 
-	    T1.full_name AS exam_plan_full_name, 
-	    T1.`name` AS exam_plan_name, 
-	    T1.semester_id, 
-	    T1.education_stage, 
-	    T1.`status` AS exam_status,
-	    T2.sys_org_id,
-	    ORG.full_name AS sys_org_full_name,
-	    ORG.`name` AS sys_org_name,
-	    ORG.`code` AS sys_org_code,
-	    IFNULL(T3.`status`, 1) AS data_report_status,
-	    T3.report_time,	
-	    COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) THEN T4.id ELSE NULL END) AS total_count,
-	    COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND (T4.`status` = {(short)AuditStatus.AUDIT} OR T4.`status` = {(short)AuditStatus.APPROVE_CANCELED}) THEN T4.id ELSE NULL END) AS audit_count,
-	    COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND T4.`status` = {(short)AuditStatus.APPROVED} THEN T4.id ELSE NULL END) AS approved_count,
-	    COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND T4.`status` = {(short)AuditStatus.REJECTED} THEN T4.id ELSE NULL END) AS rejected_count
+        T2.exam_plan_id, 
+        T1.full_name AS exam_plan_full_name, 
+        T1.`name` AS exam_plan_name, 
+        T1.semester_id, 
+        T1.education_stage, 
+        T1.`status` AS exam_status,
+        T2.sys_org_id,
+        ORG.full_name AS sys_org_full_name,
+        ORG.`name` AS sys_org_name,
+        ORG.`code` AS sys_org_code,
+        IFNULL(T3.`status`, 1) AS data_report_status,
+        T3.report_time,    
+        COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) THEN T4.id ELSE NULL END) AS total_count,
+        COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND (T4.`status` = {(short)AuditStatus.AUDIT} OR T4.`status` = {(short)AuditStatus.APPROVE_CANCELED}) THEN T4.id ELSE NULL END) AS audit_count,
+        COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND T4.`status` = {(short)AuditStatus.APPROVED} THEN T4.id ELSE NULL END) AS approved_count,
+        COUNT(CASE WHEN (T3.`status` = {(short)DataReportStatus.REPORTED} OR T3.`status` = {(short)DataReportStatus.REJECTED}) AND T4.`status` = {(short)AuditStatus.REJECTED} THEN T4.id ELSE NULL END) AS rejected_count
     {fromSql}
     {whereSql}
     {groupSql}

+ 225 - 0
YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/ExamAbsentReplaceCenterService.cs

@@ -0,0 +1,225 @@
+using Furion.JsonSerialization;
+using NPOI.SS.UserModel;
+using NPOI.XSSF.UserModel;
+using System.Data;
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补管理服务(用于中心管理)
+/// </summary>
+public class ExamAbsentReplaceCenterService(
+    IRepository<ExamAbsentReplace> rep,
+    IExportExcelService exportExcelService,
+    ICourseService courseService,
+    IExamPlanService examPlanService
+) : IExamAbsentReplaceCenterService, ITransient
+{
+    /// <summary>
+    /// 分页查询监测缺测替补列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamAbsentReplaceFullOutput>> QueryPageList(ExamAbsentReplacePageInput input)
+    {
+        var query = GetQueryBase(input);
+        var ret = await query.OrderBy(t => t.SysOrgId).ThenBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id)
+                             .ProjectToType<ExamAbsentReplaceFullOutput>()
+                             .ToADPagedListAsync(input.PageIndex, input.PageSize);
+        return ret;
+    }
+    /// <summary>
+    /// 获取状态数量
+    /// </summary>
+    /// <returns></returns>
+    public async Task<List<StatusCount>> QueryStatusCount(ExamAbsentReplacePageInput input)
+    {
+        var query = GetQueryBase(input);
+        if (query == null)
+        {
+            return [];
+        }
+        var counts = await query.GroupBy(t => t.Status).Select(t => new StatusCount { Status = (int)t.Key, Count = t.Count() }).ToListAsync();
+        return counts;
+    }
+    /// <summary>
+    /// 导出数据表格(简表)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<(string fileName, byte[] fileBytes)> ExportSimple(ExamAbsentReplacePageInput input)
+    {
+        var examPlan = await examPlanService.GetById(input.ExamPlanId);
+
+        var query = GetQueryBase(input);
+        var items = await query.OrderBy(t => t.SysOrgId).ThenBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id)
+                               .ProjectToType<ExamAbsentReplaceFullOutput>()
+                               .ToListAsync();
+
+        // 科目列表
+        var courses = await courseService.GetAllLiteList();
+        // 科目字典
+        var courseDicts = courses.ToDictionary(t => t.Name);
+
+        XSSFWorkbook wb = new();
+        ISheet sheet = wb.CreateSheet("缺测替补名单");
+        sheet.DisplayGridlines = false;
+
+        var cellStyle = exportExcelService.GetCellStyle(wb);
+
+        // 行索引号
+        int rowNum = 0;
+
+        #region 列头
+        IRow headerRow = sheet.CreateRow(rowNum++);
+        int ci = 0;
+        exportExcelService.AddCell("学校", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("缺测姓名", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("缺测考号", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 16);
+        exportExcelService.AddCell("有无替补", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("替补姓名", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("替补考号", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 16);
+        exportExcelService.AddCell("缺测替补科目", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 50);
+        exportExcelService.AddCell("状态", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        sheet.CreateFreezePane(0, rowNum);
+        #endregion
+
+        #region 数据
+        foreach (var item in items)
+        {
+            IRow row = sheet.CreateRow(rowNum++);
+            int rci = 0;
+            exportExcelService.AddCell(item.SysOrg.Name, row, rci++, cellStyle.LeftCellStyle);
+            exportExcelService.AddCell(item.ExamGrade.Grade.Name, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ClassNumber, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.AbsentName, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.AbsentExamNumber, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.IsReplaced ? "是" : "否", row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ReplaceName, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ReplaceExamNumber, row, rci++, cellStyle.CenterCellStyle);
+            var cs = JSON.Deserialize<List<CourseMiniOutput>>(item.AbsentCourses);
+            var cns = string.Join("、", cs.Select(t => t.Name));
+            exportExcelService.AddCell(cns, row, rci++, cellStyle.LeftCellStyle);
+            exportExcelService.AddCell(item.Status.GetDescription(), row, rci++, cellStyle.CenterCellStyle);
+        }
+        #endregion
+
+        MemoryStream ms = new();
+        wb.Write(ms, false);
+        ms.Flush();
+
+        return ($"{examPlan.Name}-缺测替补名单.xlsx", ms.ToArray());
+    }
+    /// <summary>
+    /// 导出数据表格(完整)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<(string fileName, byte[] fileBytes)> ExportFull(ExamAbsentReplacePageInput input)
+    {
+        var examPlan = await examPlanService.GetById(input.ExamPlanId);
+
+        var query = GetQueryBase(input);
+        var items = await query.OrderBy(t => t.SysOrgId).ThenBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id)
+                               .ProjectToType<ExamAbsentReplaceFullOutput>()
+                               .ToListAsync();
+
+        // 科目列表
+        var courses = await courseService.GetAllLiteList();
+        // 科目字典
+        var courseDicts = courses.ToDictionary(t => t.Name);
+
+        XSSFWorkbook wb = new();
+        ISheet sheet = wb.CreateSheet("缺测替补名单");
+        sheet.DisplayGridlines = false;
+
+        var cellStyle = exportExcelService.GetCellStyle(wb);
+
+        // 行索引号
+        int rowNum = 0;
+
+        #region 列头
+        IRow headerRow = sheet.CreateRow(rowNum++);
+        int ci = 0;
+        exportExcelService.AddCell("学校", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 20);
+        exportExcelService.AddCell("年级", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("班级", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("缺测姓名", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("缺测考号", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 16);
+        exportExcelService.AddCell("有无替补", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("替补姓名", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 12);
+        exportExcelService.AddCell("替补考号", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 16);
+        exportExcelService.AddCell("缺测替补科目", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 50);
+        exportExcelService.AddCell("状态", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 8);
+        exportExcelService.AddCell("缺测原因", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 40);
+        exportExcelService.AddCell("家长电话", headerRow, ci++, cellStyle.ColumnFillHeaderStyle, sheet, 12);
+        sheet.CreateFreezePane(0, rowNum);
+        #endregion
+
+        #region 数据
+        foreach (var item in items)
+        {
+            IRow row = sheet.CreateRow(rowNum++);
+            int rci = 0;
+            exportExcelService.AddCell(item.SysOrg.Name, row, rci++, cellStyle.LeftCellStyle);
+            exportExcelService.AddCell(item.ExamGrade.Grade.Name, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ClassNumber, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.AbsentName, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.AbsentExamNumber, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.IsReplaced ? "是" : "否", row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ReplaceName, row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.ReplaceExamNumber, row, rci++, cellStyle.CenterCellStyle);
+            var cs = JSON.Deserialize<List<CourseMiniOutput>>(item.AbsentCourses);
+            var cns = string.Join("、", cs.Select(t => t.Name));
+            exportExcelService.AddCell(cns, row, rci++, cellStyle.LeftCellStyle);
+            exportExcelService.AddCell(item.Status.GetDescription(), row, rci++, cellStyle.CenterCellStyle);
+            exportExcelService.AddCell(item.AbsentReason, row, rci++, cellStyle.LeftWrapCellStyle);
+            exportExcelService.AddCell(item.PatriarchTel, row, rci++, cellStyle.CenterCellStyle);
+        }
+        #endregion
+
+        MemoryStream ms = new();
+        wb.Write(ms, false);
+        ms.Flush();
+
+        return ($"{examPlan.Name}-缺测替补名单.xlsx", ms.ToArray());
+    }
+
+    #region 私有方法
+    /// <summary>
+    /// 构建查询
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    private IQueryable<ExamAbsentReplace> GetQueryBase(ExamAbsentReplacePageInput input)
+    {
+        var absentName = !string.IsNullOrEmpty(input.AbsentName?.Trim());
+        var absentExamNumber = !string.IsNullOrEmpty(input.AbsentExamNumber?.Trim());
+        var absentReason = !string.IsNullOrEmpty(input.AbsentReason?.Trim());
+        var replaceName = !string.IsNullOrEmpty(input.ReplaceName?.Trim());
+        var replaceExamNumber = !string.IsNullOrEmpty(input.ReplaceExamNumber?.Trim());
+        var patriarchTel = !string.IsNullOrEmpty(input.PatriarchTel?.Trim());
+        var remark = !string.IsNullOrEmpty(input.Remark?.Trim());
+
+        var query = rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId)
+                                        .Where((absentName, u => EF.Functions.Like(u.AbsentName, $"%{input.AbsentName.Trim()}%")))
+                                        .Where((absentExamNumber, u => EF.Functions.Like(u.AbsentExamNumber, $"%{input.AbsentExamNumber.Trim()}%")))
+                                        .Where((absentReason, u => EF.Functions.Like(u.AbsentReason, $"%{input.AbsentReason.Trim()}%")))
+                                        .Where((replaceName, u => EF.Functions.Like(u.ReplaceName, $"%{input.ReplaceName.Trim()}%")))
+                                        .Where((replaceExamNumber, u => EF.Functions.Like(u.ReplaceExamNumber, $"%{input.ReplaceExamNumber.Trim()}%")))
+                                        .Where((patriarchTel, u => EF.Functions.Like(u.PatriarchTel, $"%{input.PatriarchTel.Trim()}%")))
+                                        .Where((remark, u => EF.Functions.Like(u.Remark, $"%{input.Remark.Trim()}%")))
+                                        .Where((input.AbentCourseId.HasValue, u => EF.Functions.Like(u.AbsentCourses, $"%{input.AbentCourseId}%")))
+                                        .Where(input.Status.HasValue, t => t.Status == input.Status)
+                                        .Where(input.IsReplaced.HasValue, t => t.IsReplaced == input.IsReplaced)
+                                        .Where(input.GradeId.HasValue, t => t.GradeId == input.GradeId)
+                                        .Where(input.ClassNumber.HasValue, t => t.ClassNumber == input.ClassNumber)
+                                        .Where(input.SysOrgId.HasValue, t => t.SysOrgId == input.SysOrgId)
+                                        .Where(input.SysOrgBranchId.HasValue, t => t.SchoolClass.SysOrgBranchId == input.SysOrgBranchId);
+        return query;
+    }
+    #endregion
+}

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

@@ -11,25 +11,15 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 缺测替补管理服务
 /// </summary>
-public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITransient
+public partial class ExamAbsentReplaceService(
+    IRepository<ExamAbsentReplace> rep,
+    IExamGradeService examGradeService,
+    ISysDictDataService sysDictDataService,
+    ISchoolClassService schoolClassService,
+    IResourceFileService resourceFileService,
+    ICourseService courseService
+) : IExamAbsentReplaceService, ITransient
 {
-    private readonly IRepository<ExamAbsentReplace> _rep;
-    private readonly IExamGradeService _examGradeService;
-    private readonly ISysDictDataService _sysDictDataService;
-    private readonly ISchoolClassService _schoolClassService;
-    private readonly IResourceFileService _resourceFileService;
-    private readonly ICourseService _courseService;
-
-    public ExamAbsentReplaceService(IRepository<ExamAbsentReplace> rep, IExamGradeService examGradeService, ISysDictDataService sysDictDataService, ISchoolClassService schoolClassService, IResourceFileService resourceFileService, ICourseService courseService)
-    {
-        _rep = rep;
-        _examGradeService = examGradeService;
-        _sysDictDataService = sysDictDataService;
-        _schoolClassService = schoolClassService;
-        _resourceFileService = resourceFileService;
-        _courseService = courseService;
-    }
-
     #region 批量导入
     /// <summary>
     /// 上传监测缺测替补批量导入文件
@@ -105,9 +95,9 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
 
             #region 处理数据
             // 监测年级
-            var examGrades = await _examGradeService.GetListByExamPlanId(examPlanId);
+            var examGrades = await examGradeService.GetListByExamPlanId(examPlanId);
             // 科目列表
-            var courses = await _courseService.GetAllLiteList();
+            var courses = await courseService.GetAllLiteList();
             // 科目字典
             var courseDicts = courses.ToDictionary(t => t.Name);
 
@@ -260,19 +250,19 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     {
         var orgId = input.SysOrgId ?? CurrentSysUserInfo.SysOrgId;
 
-        var existsIdNumberList = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId).Select(t => t.AbsentExamNumber).Distinct().ToListAsync() ?? new List<string>();
+        var existsIdNumberList = await rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId).Select(t => t.AbsentExamNumber).Distinct().ToListAsync() ?? new List<string>();
 
         List<ExamAbsentReplace> items = new();
 
         var spStus = input.Items.Where(t => !existsIdNumberList.Contains(t.AbsentExamNumber)).ToList();
         var gs = spStus.Select(t => t.ExamGradeId).Distinct().ToList();
-        var examGrades = await _rep.Change<ExamGrade>().DetachedEntities.Where(t => gs.Contains(t.Id)).Select(t => t.Adapt<ExamGradeOutput>()).ToListAsync();
+        var examGrades = await rep.Change<ExamGrade>().DetachedEntities.Where(t => gs.Contains(t.Id)).Select(t => t.Adapt<ExamGradeOutput>()).ToListAsync();
 
         int c = 0;
         foreach (var eg in examGrades)
         {
             var classNumbers = spStus.Where(t => t.ExamGradeId == eg.Id).Select(t => t.ClassNumber).Distinct().ToList();
-            var classDict = await _schoolClassService.GetImportSchoolClassList(new()
+            var classDict = await schoolClassService.GetImportSchoolClassList(new()
             {
                 SysOrgId = orgId,
                 SysOrgBranchId = input.SysOrgBranchId,
@@ -296,7 +286,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
             }
         }
 
-        await _rep.InsertAsync(items);
+        await rep.InsertAsync(items);
         return c;
     }
     #endregion
@@ -312,14 +302,14 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         var orgId = CurrentSysUserInfo.SysOrgId;
 
         // 检测同一监测计划中同机构内是否有相同监测号的学生
-        var sameItems = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.AbsentExamNumber.ToUpper() == input.AbsentExamNumber.ToUpper()).ProjectToType<ExamAbsentReplaceOutput>().ToListAsync();
-        if (sameItems.Any())
+        var sameItems = await rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId && t.AbsentExamNumber.ToUpper() == input.AbsentExamNumber.ToUpper()).ProjectToType<ExamAbsentReplaceOutput>().ToListAsync();
+        if (sameItems.Count != 0)
         {
             throw Oops.Oh(ErrorCode.E2003, string.Join("、", sameItems.Select(t => $"{t.ExamGrade.Grade.Name}{t.ClassNumber}班{t.AbsentName}")), "监测号");
         }
 
-        var examGrade = await _examGradeService.GetById(input.ExamGradeId);
-        var schoolClass = await _schoolClassService.GetSchoolClass(orgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
+        var examGrade = await examGradeService.GetById(input.ExamGradeId);
+        var schoolClass = await schoolClassService.GetSchoolClass(orgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
 
         var item = input.Adapt<ExamAbsentReplace>();
         item.SysOrgId = orgId;
@@ -338,10 +328,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task Update(UpdateExamAbsentReplaceInput input)
     {
-        var oitem = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var oitem = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.Id) ?? throw Oops.Oh(ErrorCode.E2001);
 
-        var examGrade = await _examGradeService.GetById(oitem.ExamGradeId);
-        var schoolClass = await _schoolClassService.GetSchoolClass(oitem.SysOrgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
+        var examGrade = await examGradeService.GetById(oitem.ExamGradeId);
+        var schoolClass = await schoolClassService.GetSchoolClass(oitem.SysOrgId, input.SysOrgBranchId, examGrade, input.ClassNumber);
 
         var item = input.Adapt<ExamAbsentReplace>();
         item.SchoolClassId = schoolClass.Id;
@@ -373,9 +363,9 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task AddAttachment(AddAttachmentInput input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
         item.Attachments = AttachmentUtil.InsertInto(item.Attachments, input.Adapt<AttachmentItem>());
-        await item.UpdateIncludeAsync(new[] { nameof(item.Attachments) });
+        await item.UpdateIncludeAsync([nameof(item.Attachments)]);
     }
     /// <summary>
     /// 删除缺测替补佐证材料
@@ -384,7 +374,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task DelAttachment(DeleteAttachmentInput input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
         var attachments = AttachmentUtil.GetList(item.Attachments);
         var a = attachments.FirstOrDefault(t => t.FileId == input.FileId);
         if (a != null)
@@ -393,10 +383,10 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
             item.Attachments = JSON.Serialize(attachments);
             await item.UpdateIncludeAsync(new[] { nameof(item.Attachments) });
 
-            await _resourceFileService.Del(new() { Id = a.FileId });
+            await resourceFileService.Del(new() { Id = a.FileId });
             if (a.ThumbFileId.HasValue && a.ThumbFileId > 0)
             {
-                await _resourceFileService.Del(new() { Id = a.ThumbFileId.Value });
+                await resourceFileService.Del(new() { Id = a.ThumbFileId.Value });
             }
         }
     }
@@ -407,7 +397,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task<bool> VerifyAttachment(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ProjectToType<ExamAbsentReplaceOutput>().ToListAsync();
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ProjectToType<ExamAbsentReplaceOutput>().ToListAsync();
         if (items.Any(t => t.AttachmentList.Count == 0))
         {
             return false;
@@ -421,16 +411,16 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <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);
         var attachments = JSON.Deserialize<List<AttachmentItem>>(item.Attachments);
-        if (attachments != null && attachments.Any())
+        if (attachments != null && attachments.Count != 0)
         {
             foreach (var attachment in attachments)
             {
-                await _resourceFileService.Del(new() { Id = attachment.FileId });
+                await resourceFileService.Del(new() { Id = attachment.FileId });
                 if (attachment.ThumbFileId.HasValue && attachment.ThumbFileId > 0)
                 {
-                    await _resourceFileService.Del(new() { Id = attachment.ThumbFileId.Value });
+                    await resourceFileService.Del(new() { Id = attachment.ThumbFileId.Value });
                 }
             }
         }
@@ -444,7 +434,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     public async Task Clear(ClearExamAbsentReplaceInput 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).ExecuteDeleteAsync();
     }
     /// <summary>
     /// 导出监测缺测替补上报打印表格
@@ -453,14 +443,14 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task<byte[]> ExportPrintTable(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Include(t => t.Grade).Include(t => t.SysOrg).Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ToListAsync();
-        if (!items.Any())
+        var items = await rep.DetachedEntities.Include(t => t.Grade).Include(t => t.SysOrg).Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId).ToListAsync();
+        if (items.Count == 0)
         {
             throw Oops.Oh(ErrorCode.E2001);
         }
 
         // 获取证件类型
-        var cts = await _sysDictDataService.GetListByDictTypeId(304);
+        var cts = await sysDictDataService.GetListByDictTypeId(304);
         var certificateTypes = cts.ToDictionary(x => (CertificateType)x.Value, y => y.Name);
 
         XWPFDocument doc = new();
@@ -623,23 +613,6 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     /// <returns></returns>
     public async Task<PageResult<ExamAbsentReplaceOutput>> QueryPageList(ExamAbsentReplacePageInput input)
     {
-        var orgId = input.SysOrgId ?? CurrentSysUserInfo.SysOrgId;
-
-        var query = GetQueryBase(input);
-        var ret = await query.OrderBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id)
-                             .ProjectToType<ExamAbsentReplaceOutput>()
-                             .ToADPagedListAsync(input.PageIndex, input.PageSize);
-        return ret;
-    }
-    /// <summary>
-    /// 分页查询监测缺测替补列表
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    public async Task<PageResult<ExamAbsentReplaceOutput>> QueryAuditPageList(ExamAbsentReplacePageInput input)
-    {
-        var orgId = input.SysOrgId ?? CurrentSysUserInfo.SysOrgId;
-
         var query = GetQueryBase(input);
         var ret = await query.OrderBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenBy(t => t.Id)
                              .ProjectToType<ExamAbsentReplaceOutput>()
@@ -655,7 +628,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
     public async Task<ExamAbsentReplaceCountOutput> GetOrgGradeClassStudentCount(int examPlanId, short? sysOrgId = null)
     {
         var orgId = sysOrgId ?? CurrentSysUserInfo.SysOrgId;
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == orgId)
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId && t.SysOrgId == orgId)
                                                .GroupBy(t => new { t.GradeId, t.ClassNumber })
                                                .Select(t => new
                                                {
@@ -694,13 +667,43 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         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;
     }
     #endregion
 
+    #region 公有静态方法
+    /// <summary>
+    /// 根据科目字符串组合获取科目列表
+    /// </summary>
+    /// <param name="courseDict">科目字典</param>
+    /// <param name="courses">科目组合</param>
+    /// <returns></returns>
+    public static (List<string> errorMessage, List<CourseMiniOutput> courses) GetCourses(Dictionary<string, CourseLiteOutput> courseDict, string courses)
+    {
+        List<CourseMiniOutput> ret = [];
+        List<string> errs = [];
+
+        var cns = courses.ClearWhitespace().Split(['、', ',', ',', ';', ';']);
+        foreach (var cn in cns)
+        {
+            if (courseDict.TryGetValue(cn, out CourseLiteOutput c))
+            {
+                if (ret.Any(t => t.Id != c.Id))
+                {
+                    ret.Add(c.Adapt<CourseMiniOutput>());
+                }
+            }
+            else
+            {
+                errs.Add(cn);
+            }
+        }
+        return (errs, ret);
+    }
+    #endregion
 
     #region 私有方法
     /// <summary>
@@ -720,7 +723,7 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         var patriarchTel = !string.IsNullOrEmpty(input.PatriarchTel?.Trim());
         var remark = !string.IsNullOrEmpty(input.Remark?.Trim());
 
-        var query = _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId)
+        var query = rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == orgId)
                                          .Where((absentName, u => EF.Functions.Like(u.AbsentName, $"%{input.AbsentName.Trim()}%")))
                                          .Where((absentExamNumber, u => EF.Functions.Like(u.AbsentExamNumber, $"%{input.AbsentExamNumber.Trim()}%")))
                                          .Where((absentReason, u => EF.Functions.Like(u.AbsentReason, $"%{input.AbsentReason.Trim()}%")))
@@ -737,34 +740,6 @@ public partial class ExamAbsentReplaceService : IExamAbsentReplaceService, ITran
         return query;
     }
     /// <summary>
-    /// 根据科目字符串组合获取科目列表
-    /// </summary>
-    /// <param name="courseDict">科目字典</param>
-    /// <param name="courses">科目组合</param>
-    /// <returns></returns>
-    private static (List<string> errorMessage, List<CourseMiniOutput> courses) GetCourses(Dictionary<string, CourseLiteOutput> courseDict, string courses)
-    {
-        List<CourseMiniOutput> ret = [];
-        List<string> errs = [];
-
-        var cns = courses.ClearWhitespace().Split(['、', ',', ',', ';', ';']);
-        foreach (var cn in cns)
-        {
-            if (courseDict.TryGetValue(cn, out CourseLiteOutput c))
-            {
-                if (ret.Any(t => t.Id != c.Id))
-                {
-                    ret.Add(c.Adapt<CourseMiniOutput>());
-                }
-            }
-            else
-            {
-                errs.Add(cn);
-            }
-        }
-        return (errs, ret);
-    }
-    /// <summary>
     /// 设置单元格文本
     /// </summary>
     /// <param name="table"></param>

+ 33 - 0
YBEE.EQM.Application/Exam/ExamAbsentReplace/Services/IExamAbsentReplaceCenterService.cs

@@ -0,0 +1,33 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补管理服务(用于中心管理)
+/// </summary>
+public interface IExamAbsentReplaceCenterService
+{
+    /// <summary>
+    /// 分页查询监测缺测替补列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<PageResult<ExamAbsentReplaceFullOutput>> QueryPageList(ExamAbsentReplacePageInput input);
+    /// <summary>
+    /// 获取状态数量
+    /// </summary>
+    /// <returns></returns>
+    Task<List<StatusCount>> QueryStatusCount(ExamAbsentReplacePageInput input);
+    /// <summary>
+    /// 导出数据表格(简表)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<(string fileName, byte[] fileBytes)> ExportSimple(ExamAbsentReplacePageInput input);
+    /// <summary>
+    /// 导出数据表格(完整)
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<(string fileName, byte[] fileBytes)> ExportFull(ExamAbsentReplacePageInput input);
+}

+ 19 - 0
YBEE.EQM.Application/Exam/ExamDataPublish/Dtos/ExamDataPublishInput.cs

@@ -44,4 +44,23 @@ public class UpdateExamDataPublishInput: BaseId
     /// </summary>
     [StringLength(2000)]
     public string Remark { get; set; }
+}
+/// <summary>
+/// 分页查询面向机构发布数据输入参数
+/// </summary>
+public class ExamDataPublishOrgPageInput : PageInputBase
+{
+    /// <summary>
+    /// 发布类型
+    /// </summary>
+    [Required]
+    public DataPublishType Type { get; set; }
+    /// <summary>
+    /// 监测名称
+    /// </summary>
+    public string Name { get; set; }
+    /// <summary>
+    /// 监测学期
+    /// </summary>
+    public short? SemesterId { get; set; }
 }

+ 77 - 0
YBEE.EQM.Application/Exam/ExamDataPublish/Dtos/ExamDataPublishOutput.cs

@@ -90,4 +90,81 @@ public class ExamDataPublishOrgResultOutput
     /// 监测机构反馈文件列表
     /// </summary>
     public List<ExamOrgResultOutput> ExamOrgResultList { get; set; } = new();
+}
+
+/// <summary>
+/// 监测机构发布内容列表输出参数
+/// </summary>
+public class ExamDataPublishOrgOutput
+{
+    /// <summary>
+    /// 行号
+    /// </summary>
+    [Required]
+    public int RowNumber { get; set; }
+    /// <summary>
+    /// 发布名称
+    /// </summary>
+    [Required]
+    public string ExamDataPublishName { get; set; }
+    /// <summary>
+    /// 监测计划ID
+    /// </summary>
+    [Required]
+    public int ExamPlanId { get; set; }
+    /// <summary>
+    /// 监测计划全称
+    /// </summary>
+    [Required]
+    public string ExamPlanFullName { get; set; }
+    /// <summary>
+    /// 监测计划名称
+    /// </summary>
+    [Required]
+    public string ExamPlanName { get; set; }
+    /// <summary>
+    /// 监测计划简称
+    /// </summary>
+    [Required]
+    public string ExamPlanShortName { get; set; }
+    /// <summary>
+    /// 监测计划状态
+    /// </summary>
+    [Required]
+    public ExamStatus ExamPlanStatus { get; set; }
+    /// <summary>
+    /// 学段
+    /// </summary>
+    [Required]
+    public EducationStage EducationStage { get; set; }
+    /// <summary>
+    /// 学期ID
+    /// </summary>
+    [Required]
+    public short SemesterId { get; set; }
+    /// <summary>
+    /// 学期简别称
+    /// </summary>
+    [Required]
+    public string SemesterNickShortName { get; set; }
+    /// <summary>
+    /// 机构ID
+    /// </summary>
+    [Required]
+    public short SysOrgId { get; set; }
+    /// <summary>
+    /// 数据发布ID
+    /// </summary>
+    [Required]
+    public int ExamDataPublishId { get; set; }
+    /// <summary>
+    /// 发布类型
+    /// </summary>
+    [Required]
+    public DataPublishType Type { get; set; }
+    /// <summary>
+    /// 监测状态
+    /// </summary>
+    [Required]
+    public ExamStatus ExamStatus { get; set; }
 }

+ 21 - 18
YBEE.EQM.Application/Exam/ExamDataPublish/ExamDataPublishAppService.cs

@@ -5,15 +5,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 监测发布内容管理服务
 /// </summary>
-public class ExamDataPublishAppService : IDynamicApiController
+public class ExamDataPublishAppService(IExamDataPublishService examDataPublishService) : IDynamicApiController
 {
-    private readonly IExamDataPublishService _examDataPublishService;
-
-    public ExamDataPublishAppService(IExamDataPublishService examDataPublishService)
-    {
-        _examDataPublishService = examDataPublishService;
-    }
-
     #region 创建更新
     /// <summary>
     /// 添加发布内容
@@ -22,7 +15,7 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Add(AddExamDataPublishInput input)
     {
-        await _examDataPublishService.Add(input);
+        await examDataPublishService.Add(input);
     }
     /// <summary>
     /// 更新发布内容
@@ -31,7 +24,7 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Update(UpdateExamDataPublishInput input)
     {
-        await _examDataPublishService.Update(input);
+        await examDataPublishService.Update(input);
     }
     /// <summary>
     /// 删除发布内容
@@ -40,7 +33,7 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        await _examDataPublishService.Del(input);
+        await examDataPublishService.Del(input);
     }
     #endregion
 
@@ -52,7 +45,7 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Publish(BaseId input)
     {
-        await _examDataPublishService.Publish(input);
+        await examDataPublishService.Publish(input);
     }
     /// <summary>
     /// 取消
@@ -61,7 +54,7 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Unpublish(BaseId input)
     {
-        await _examDataPublishService.Unpublish(input);
+        await examDataPublishService.Unpublish(input);
     }
     #endregion
 
@@ -71,18 +64,28 @@ public class ExamDataPublishAppService : IDynamicApiController
     /// </summary>
     /// <param name="id"></param>
     /// <returns></returns>
-    public async Task<ExamDataPublishOutput> GetById(int id)
+    public async Task<ExamDataPublishOutput> GetById([FromQuery][Required] int id)
     {
-        return await _examDataPublishService.GetById(id);
+        return await examDataPublishService.GetById(id);
     }
     /// <summary>
     /// 根据监测计划ID获取数据发布内容列表
     /// </summary>
-    /// <param name="examPlanId"></param>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <param name="type">发布类型</param>
+    /// <returns></returns>
+    public async Task<List<ExamDataPublishOutput>> GetListByExamPlanId([FromQuery][Required] int examPlanId, DataPublishType? type)
+    {
+        return await examDataPublishService.GetListByExamPlanId(examPlanId, type);
+    }
+    /// <summary>
+    /// 分页查询面向机构发布的内容列表
+    /// </summary>
+    /// <param name="input"></param>
     /// <returns></returns>
-    public async Task<List<ExamDataPublishOutput>> GetListByExamPlanId(int examPlanId)
+    public async Task<PageResult<ExamDataPublishOrgOutput>> QueryOrgPageList(ExamDataPublishOrgPageInput input)
     {
-        return await _examDataPublishService.GetListByExamPlanId(examPlanId);
+        return await examDataPublishService.QueryOrgPageList(input);
     }
     #endregion
 }

+ 96 - 18
YBEE.EQM.Application/Exam/ExamDataPublish/Services/ExamDataPublishService.cs

@@ -6,14 +6,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 监测发布内容管理服务
 /// </summary>
-public class ExamDataPublishService : IExamDataPublishService, ITransient
+public class ExamDataPublishService(IRepository<ExamDataPublish> rep, IExamSampleService examSampleService) : IExamDataPublishService, ITransient
 {
-    private readonly IRepository<ExamDataPublish> _rep;
-    public ExamDataPublishService(IRepository<ExamDataPublish> rep)
-    {
-        _rep = rep;
-    }
-
     #region 创建更新
     /// <summary>
     /// 添加发布内容
@@ -36,10 +30,10 @@ public class ExamDataPublishService : IExamDataPublishService, ITransient
     /// <returns></returns>
     public async Task Update(UpdateExamDataPublishInput 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);
         item.Name = input.Name;
         item.Remark = input.Remark;
-        await item.UpdateIncludeAsync(new[] { nameof(item.Name), nameof(item.Remark) });
+        await item.UpdateIncludeAsync([nameof(item.Name), nameof(item.Remark)]);
     }
     /// <summary>
     /// 删除发布内容
@@ -48,7 +42,7 @@ public class ExamDataPublishService : IExamDataPublishService, 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 != PublishStatus.UNPUBLISH)
         {
             throw Oops.Oh(ErrorCode.E3001);
@@ -65,15 +59,26 @@ public class ExamDataPublishService : IExamDataPublishService, ITransient
     /// <returns></returns>
     public async Task Publish(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 == PublishStatus.PUBLISHED)
         {
             throw Oops.Oh(ErrorCode.E2006);
         }
+
+        if (item.Type == DataPublishType.STUDENT_SAMPLE_LIST || item.Type == DataPublishType.STUDENT_SAMPLE_COUNT_LIST)
+        {
+            var selected = await examSampleService.CheckSelectedByExamPlanId(item.ExamPlanId);
+            // 未选定抽样方案不能发布
+            if (!selected)
+            {
+                throw Oops.Oh(ErrorCode.E3008);
+            }
+        }
+
         item.Status = PublishStatus.PUBLISHED;
         item.PublishTime = DateTime.Now;
         item.PublishSysUserId = CurrentSysUserInfo.SysUserId;
-        await item.UpdateIncludeAsync(new[] { nameof(item.Status), nameof(item.PublishTime), nameof(item.PublishSysUserId) });
+        await item.UpdateIncludeAsync([nameof(item.Status), nameof(item.PublishTime), nameof(item.PublishSysUserId)]);
     }
     /// <summary>
     /// 取消
@@ -82,13 +87,13 @@ public class ExamDataPublishService : IExamDataPublishService, ITransient
     /// <returns></returns>
     public async Task Unpublish(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 != PublishStatus.PUBLISHED)
         {
             throw Oops.Oh(ErrorCode.E2006);
         }
         item.Status = PublishStatus.UNPUBLISH; // 设置为未发布
-        await item.UpdateIncludeAsync(new[] { nameof(item.Status) });
+        await item.UpdateIncludeAsync([nameof(item.Status)]);
     }
     #endregion
 
@@ -100,19 +105,92 @@ public class ExamDataPublishService : IExamDataPublishService, ITransient
     /// <returns></returns>
     public async Task<ExamDataPublishOutput> GetById(int id)
     {
-        var item = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
         return item.Adapt<ExamDataPublishOutput>();
     }
     /// <summary>
     /// 根据监测计划ID获取数据发布内容列表
     /// </summary>
-    /// <param name="examPlanId"></param>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <param name="type">发布类型</param>
     /// <returns></returns>
-    public async Task<List<ExamDataPublishOutput>> GetListByExamPlanId(int examPlanId)
+    public async Task<List<ExamDataPublishOutput>> GetListByExamPlanId(int examPlanId, DataPublishType? type)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamDataPublishOutput>().OrderBy(t => t.Type).ToListAsync();
+        var items = await rep.DetachedEntities
+                             .Where(t => t.ExamPlanId == examPlanId)
+                             .Where(type.HasValue, t => t.Type == type)
+                             .ProjectToType<ExamDataPublishOutput>()
+                             .OrderBy(t => t.Type)
+                             .ToListAsync();
         return items;
     }
+    /// <summary>
+    /// 分页查询面向机构发布的内容列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamDataPublishOrgOutput>> QueryOrgPageList(ExamDataPublishOrgPageInput input)
+    {
+        string where = $"T1.sys_org_id = @sysOrgId AND T2.type = @type AND T2.`status` = {(int)PublishStatus.PUBLISHED} AND T2.is_deleted = 0 AND T3.is_deleted = 0";
+        if (input.SemesterId.HasValue)
+        {
+            where = $"{where} AND T3.semester_id = {input.SemesterId.Value}";
+        }
+        if (!string.IsNullOrEmpty(input.Name?.Trim()))
+        {
+            where = $"{where} AND T3.full_name LIKE '%{input.Name.Trim()}%'";
+        }
+
+        var p = new
+        {
+            CurrentSysUserInfo.SysOrgId,
+            input.PageSize,
+            PageOffset = (input.PageIndex - 1) * input.PageSize,
+            input.Type,
+            input.Name,
+            input.SemesterId,
+        };
+
+        var totalCount = await rep.SqlScalarAsync<int>($@"
+SELECT COUNT(1) AS total_count
+FROM exam_org AS T1
+JOIN exam_data_publish AS T2 ON T1.exam_plan_id = T2.exam_plan_id
+JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
+JOIN base_semester AS T4 ON T3.semester_id = T4.id
+WHERE {where}
+", p);
+
+        var items = await rep.SqlQueriesAsync<ExamDataPublishOrgOutput>($@"
+SELECT ROW_NUMBER() OVER (ORDER BY T1.exam_plan_id DESC, T2.type) AS `row_number`, 
+    T1.exam_plan_id,
+	T1.sys_org_id,
+    T2.id AS exam_data_publish_id,
+	T2.type,
+	T2.`name` AS exam_data_publish_name,
+    T3.full_name AS exam_plan_full_name, 
+	T3.`name` AS exam_plan_name, 
+	T3.short_name AS exam_plan_short_name,
+    T3.`status` AS exam_plan_status, 
+	T3.education_stage, 
+	T3.semester_id,
+    T4.nick_short_name AS semesterNickShortName
+FROM exam_org AS T1
+JOIN exam_data_publish AS T2 ON T1.exam_plan_id = T2.exam_plan_id
+JOIN exam_plan AS T3 ON T1.exam_plan_id = T3.id
+JOIN base_semester AS T4 ON T3.semester_id = T4.id
+WHERE {where}
+LIMIT @pageSize OFFSET @pageOffset;
+", p);
+        PageResult<ExamDataPublishOrgOutput> ret = new()
+        {
+            PageIndex = input.PageIndex,
+            PageSize = input.PageSize,
+            TotalCount = totalCount,
+            Items = items
+        };
+        return ret;
+    }
+
     #endregion
 
     #region 上传匹配

+ 9 - 2
YBEE.EQM.Application/Exam/ExamDataPublish/Services/IExamDataPublishService.cs

@@ -53,8 +53,15 @@ public interface IExamDataPublishService
     /// <summary>
     /// 根据监测计划ID获取数据发布内容列表
     /// </summary>
-    /// <param name="examPlanId"></param>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <param name="type">发布类型</param>
     /// <returns></returns>
-    Task<List<ExamDataPublishOutput>> GetListByExamPlanId(int examPlanId);
+    Task<List<ExamDataPublishOutput>> GetListByExamPlanId(int examPlanId, DataPublishType? type);
+    /// <summary>
+    /// 分页查询面向机构发布的内容列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<PageResult<ExamDataPublishOrgOutput>> QueryOrgPageList(ExamDataPublishOrgPageInput input);
     #endregion
 }

+ 11 - 0
YBEE.EQM.Application/Exam/ExamDataReport/Dtos/ExamDataReportInput.cs

@@ -54,4 +54,15 @@ public class UpdateExamDataReportInput : BaseId
     /// </summary>
     [StringLength(2000)]
     public string Remark { get; set; } = "";
+}
+
+/// <summary>
+/// 分页查询数据上报列表输入参数
+/// </summary>
+public class ExamDataReportPageInput : ExamPlanPageInput
+{
+    /// <summary>
+    /// 数据上报类型
+    /// </summary>
+    public DataReportType? Type { get; set; }
 }

+ 11 - 0
YBEE.EQM.Application/Exam/ExamDataReport/Dtos/ExamDataReportOutput.cs

@@ -55,3 +55,14 @@ public class ExamDataReportOutput : DEntityOutput
     /// </summary>
     public List<AttachmentItem> AttachmentList { get; set; } = new();
 }
+
+/// <summary>
+/// 监测数据上报类型输出参数
+/// </summary>
+public class ExamDataReportPlanOutput : ExamDataReportOutput
+{
+    /// <summary>
+    /// 监测计划
+    /// </summary>
+    public ExamPlanOutput ExamPlan { get; set; }
+}

+ 21 - 21
YBEE.EQM.Application/Exam/ExamDataReport/ExamDataReportAppService.cs

@@ -7,17 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-data-report")]
 [Route("exam/data/report")]
-public class ExamDataReportAppService : IDynamicApiController
+public class ExamDataReportAppService(IExamDataReportService examDataReportService, IResourceFileService resourceFileService) : IDynamicApiController
 {
-    private readonly IExamDataReportService _examDataReportService;
-    private readonly IResourceFileService _resourceFileService;
-
-    public ExamDataReportAppService(IExamDataReportService examDataReportService, IResourceFileService resourceFileService)
-    {
-        _examDataReportService = examDataReportService;
-        _resourceFileService = resourceFileService;
-    }
-
     #region 创建更新
     /// <summary>
     /// 添加上报类型
@@ -26,7 +17,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Add(AddExamDataReportInput input)
     {
-        await _examDataReportService.Add(input);
+        await examDataReportService.Add(input);
     }
     /// <summary>
     /// 更新上报类型
@@ -35,7 +26,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Update(UpdateExamDataReportInput input)
     {
-        await _examDataReportService.Update(input);
+        await examDataReportService.Update(input);
     }
     /// <summary>
     /// 删除上报类型
@@ -44,7 +35,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        await _examDataReportService.Del(input);
+        await examDataReportService.Del(input);
     }
 
     /// <summary>
@@ -56,9 +47,9 @@ public class ExamDataReportAppService : IDynamicApiController
     [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
     public async Task UploadAttachment([FromForm] UploadResourceFileInput input)
     {
-        var rfile = await _resourceFileService.Upload(input);
+        var rfile = await resourceFileService.Upload(input);
         var addParams = rfile.Adapt<AddAttachmentInput>();
-        await _examDataReportService.AddAttachment(addParams);
+        await examDataReportService.AddAttachment(addParams);
     }
     /// <summary>
     /// 删除附件
@@ -67,7 +58,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task DelAttachment(DeleteAttachmentInput input)
     {
-        await _examDataReportService.DelAttachment(input);
+        await examDataReportService.DelAttachment(input);
     }
     #endregion
 
@@ -79,7 +70,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Start(BaseId input)
     {
-        await _examDataReportService.Start(input);
+        await examDataReportService.Start(input);
     }
     /// <summary>
     /// 结束
@@ -88,7 +79,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Stop(BaseId input)
     {
-        await _examDataReportService.Stop(input);
+        await examDataReportService.Stop(input);
     }
     /// <summary>
     /// 取消
@@ -97,7 +88,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Cancel(BaseId input)
     {
-        await _examDataReportService.Cancel(input);
+        await examDataReportService.Cancel(input);
     }
     #endregion
 
@@ -109,7 +100,7 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<ExamDataReportOutput> GetById([FromQuery][Required] int id)
     {
-        return await _examDataReportService.GetById(id);
+        return await examDataReportService.GetById(id);
     }
     /// <summary>
     /// 根据监测计划ID获取数据上报类型列表
@@ -118,7 +109,16 @@ public class ExamDataReportAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<ExamDataReportOutput>> GetListByExamPlanId([FromQuery][Required] int examPlanId)
     {
-        return await _examDataReportService.GetListByExamPlanId(examPlanId);
+        return await examDataReportService.GetListByExamPlanId(examPlanId);
+    }
+    /// <summary>
+    /// 获取数据上报计划列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamDataReportPlanOutput>> QueryPlanPageList(ExamDataReportPageInput input)
+    {
+        return await examDataReportService.QueryPlanPageList(input);
     }
     #endregion
 }

+ 35 - 24
YBEE.EQM.Application/Exam/ExamDataReport/Services/ExamDataReportService.cs

@@ -7,17 +7,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 监测数据上报类型管理服务
 /// </summary>
-public class ExamDataReportService : IExamDataReportService, ITransient
+public class ExamDataReportService(IRepository<ExamDataReport> rep, IResourceFileService resourceFileService) : IExamDataReportService, ITransient
 {
-    private readonly IRepository<ExamDataReport> _rep;
-    private readonly IResourceFileService _resourceFileService;
-
-    public ExamDataReportService(IRepository<ExamDataReport> rep, IResourceFileService resourceFileService)
-    {
-        _rep = rep;
-        _resourceFileService = resourceFileService; 
-    }
-
     #region 创建更新
     /// <summary>
     /// 添加上报类型
@@ -26,7 +17,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task Add(AddExamDataReportInput input)
     {
-        if (await _rep.AnyAsync(t => t.ExamPlanId == input.ExamPlanId && t.Type == input.Type))
+        if (await rep.AnyAsync(t => t.ExamPlanId == input.ExamPlanId && t.Type == input.Type))
         {
             throw Oops.Oh(ErrorCode.E2002, "该类型");
         }
@@ -40,7 +31,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task Update(UpdateExamDataReportInput 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);
         item.Remark = input.Remark;
         await item.UpdateIncludeAsync(new[] { nameof(item.BeginTime), nameof(item.EndTime), nameof(item.Remark) });
     }
@@ -51,7 +42,7 @@ public class ExamDataReportService : IExamDataReportService, 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.READY)
         {
             throw Oops.Oh(ErrorCode.E3001);
@@ -68,7 +59,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task Start(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.READY)
         {
             throw Oops.Oh(ErrorCode.E2006);
@@ -84,7 +75,7 @@ public class ExamDataReportService : IExamDataReportService, 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);
@@ -100,7 +91,7 @@ public class ExamDataReportService : IExamDataReportService, 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);
@@ -117,7 +108,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task AddAttachment(AddAttachmentInput input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
         item.Attachments = AttachmentUtil.InsertInto(item.Attachments, input.Adapt<AttachmentItem>());
         await item.UpdateIncludeAsync(new[] { nameof(item.Attachments) });
     }
@@ -128,7 +119,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task DelAttachment(DeleteAttachmentInput input)
     {
-        var item = await _rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.FirstOrDefaultAsync(t => t.Id == input.SourceId) ?? throw Oops.Oh(ErrorCode.E2001);
         var attachments = AttachmentUtil.GetList(item.Attachments);
         var a = attachments.FirstOrDefault(t => t.FileId == input.FileId);
         if (a != null)
@@ -137,10 +128,10 @@ public class ExamDataReportService : IExamDataReportService, ITransient
             item.Attachments = JSON.Serialize(attachments);
             await item.UpdateIncludeAsync(new[] { nameof(item.Attachments) });
 
-            await _resourceFileService.Del(new() { Id = a.FileId });
+            await resourceFileService.Del(new() { Id = a.FileId });
             if (a.ThumbFileId.HasValue && a.ThumbFileId > 0)
             {
-                await _resourceFileService.Del(new() { Id = a.ThumbFileId.Value });
+                await resourceFileService.Del(new() { Id = a.ThumbFileId.Value });
             }
         }
     }
@@ -155,7 +146,7 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task<ExamDataReportOutput> GetById(int id)
     {
-        var item = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
+        var item = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id) ?? throw Oops.Oh(ErrorCode.E2001);
         return item.Adapt<ExamDataReportOutput>();
     }
     /// <summary>
@@ -165,16 +156,36 @@ public class ExamDataReportService : IExamDataReportService, ITransient
     /// <returns></returns>
     public async Task<List<ExamDataReportOutput>> GetListByExamPlanId(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamDataReportOutput>().OrderBy(t => t.Type).ToListAsync();
-        var orgCount = await _rep.Change<ExamOrg>().DetachedEntities.Where(t => t.ExamPlanId == examPlanId).CountAsync();
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamDataReportOutput>().OrderBy(t => t.Type).ToListAsync();
+        var orgCount = await rep.Change<ExamOrg>().DetachedEntities.Where(t => t.ExamPlanId == examPlanId).CountAsync();
 
         foreach (var item in items)
         {
-            var reportedCount = await _rep.Change<ExamOrgDataReport>().DetachedEntities.Where(t => t.Type == item.Type && t.ExamOrg.ExamPlanId == examPlanId && t.Status == DataReportStatus.REPORTED).CountAsync();
+            var reportedCount = await rep.Change<ExamOrgDataReport>().DetachedEntities.Where(t => t.Type == item.Type && t.ExamOrg.ExamPlanId == examPlanId && t.Status == DataReportStatus.REPORTED).CountAsync();
             item.ReportedCount = reportedCount;
             item.Count = orgCount;
         }
         return items;
     }
+    /// <summary>
+    /// 获取数据上报计划列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamDataReportPlanOutput>> QueryPlanPageList(ExamDataReportPageInput input)
+    {
+        var _rep = rep.Change<ExamDataReport>();
+        var name = !string.IsNullOrEmpty(input.Name?.Trim());
+        var ret = await _rep.DetachedEntities
+                            .Where(input.Type.HasValue, t => t.Type == input.Type)
+                            .Where(input.EducationStage.HasValue, t => t.ExamPlan.EducationStage == input.EducationStage)
+                            .Where(input.SemesterId.HasValue, t => t.ExamPlan.SemesterId == input.SemesterId)
+                            .Where(input.Status.HasValue, t => t.ExamPlan.Status == input.Status)
+                            .Where(name, t => EF.Functions.Like(t.ExamPlan.Name, $"%{input.Name.ClearWhitespace()}%") || EF.Functions.Like(t.ExamPlan.FullName, $"%{input.Name.ClearWhitespace()}%") || EF.Functions.Like(t.ExamPlan.ShortName, $"%{input.Name.ClearWhitespace()}%"))
+                            .ProjectToType<ExamDataReportPlanOutput>()
+                            .OrderByDescending(t => t.ExamPlan.CreateTime).ThenByDescending(t => t.CreateTime)
+                            .ToADPagedListAsync(input.PageIndex, input.PageSize);
+        return ret;
+    }
     #endregion
 }

+ 7 - 2
YBEE.EQM.Application/Exam/ExamDataReport/Services/IExamDataReportService.cs

@@ -1,5 +1,4 @@
-using Furion.JsonSerialization;
-using YBEE.EQM.Core;
+using YBEE.EQM.Core;
 
 namespace YBEE.EQM.Application;
 
@@ -76,5 +75,11 @@ public interface IExamDataReportService
     /// <param name="examPlanId"></param>
     /// <returns></returns>
     Task<List<ExamDataReportOutput>> GetListByExamPlanId(int examPlanId);
+    /// <summary>
+    /// 获取数据上报计划列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<PageResult<ExamDataReportPlanOutput>> QueryPlanPageList(ExamDataReportPageInput input);
     #endregion
 }

+ 20 - 10
YBEE.EQM.Application/Exam/ExamOrg/Dtos/ExamOrgOutput.cs

@@ -3,9 +3,9 @@
 namespace YBEE.EQM.Application;
 
 /// <summary>
-/// 监测机构输出参数
+/// 监测机构简要输出参数
 /// </summary>
-public class ExamOrgOutput
+public class ExamOrgLiteOutput
 {
     /// <summary>
     /// 主键
@@ -28,7 +28,23 @@ public class ExamOrgOutput
     /// </summary>
     [Required]
     public bool IsRequiredExam { get; set; }
+    /// <summary>
+    /// 是否需要上报校考成绩
+    /// </summary>
+    [Required]
+    public bool IsReportSchoolExamScore { get; set; }
+
+    /// <summary>
+    /// 机构
+    /// </summary>
+    public SysOrgLiteOutput SysOrg { get; set; }
+}
 
+/// <summary>
+/// 监测机构输出参数
+/// </summary>
+public class ExamOrgOutput : ExamOrgLiteOutput
+{
     ///// <summary>
     ///// 监测方案下载次数
     ///// </summary>
@@ -43,18 +59,12 @@ public class ExamOrgOutput
     ///// </summary>
     //public int? ExamSampleDownloadSysUserId { get; set; }
 
-
-    /// <summary>
-    /// 机构
-    /// </summary>
-    public SysOrgLiteOutput SysOrg { get; set; }
-
     /// <summary>
     /// 上报状态
     /// </summary>
-    public Dictionary<short, DataReportStatus> DataReports { get; set; } = new();
+    public Dictionary<short, DataReportStatus> DataReports { get; set; } = [];
     /// <summary>
     /// 上报列表
     /// </summary>
-    public List<ExamOrgDataReportOutput> DataReportList { get; set; } = new();
+    public List<ExamOrgDataReportOutput> DataReportList { get; set; } = [];
 }

+ 18 - 16
YBEE.EQM.Application/Exam/ExamOrg/ExamOrgAppService.cs

@@ -1,5 +1,4 @@
-using YBEE.EQM.Application;
-using YBEE.EQM.Core;
+using YBEE.EQM.Core;
 
 namespace YBEE.EQM.Application;
 
@@ -8,14 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-org")]
 [Route("exam/org")]
-public class ExamOrgAppService : IDynamicApiController
+public class ExamOrgAppService(IExamOrgService examOrgService) : IDynamicApiController
 {
-    private readonly IExamOrgService _examOrgService;
-    public ExamOrgAppService(IExamOrgService examOrgService)
-    {
-        _examOrgService = examOrgService;
-    }
-
     /// <summary>
     /// 添加机构
     /// </summary>
@@ -23,7 +16,7 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task AddList(AddExamOrgListInput input)
     {
-        await _examOrgService.AddList(input);
+        await examOrgService.AddList(input);
     }
     /// <summary>
     /// 移出机构
@@ -32,7 +25,7 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Remove(DelExamOrgInput input)
     {
-        await _examOrgService.Remove(input);
+        await examOrgService.Remove(input);
     }
     /// <summary>
     /// 切换机构是否参与区统一监测
@@ -41,7 +34,7 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task SwitchRequiredSample(SwitchExamOrgRequiredSampleInput input)
     {
-        await _examOrgService.SwitchRequiredSample(input);
+        await examOrgService.SwitchRequiredSample(input);
     }
     /// <summary>
     /// 根据监测计划ID获取监测机构列表
@@ -50,7 +43,16 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<ExamOrgOutput>> GetListByExamPlanId([FromQuery][Required] int examPlanId)
     {
-        return await _examOrgService.GetListByExamPlanId(examPlanId);
+        return await examOrgService.GetListByExamPlanId(examPlanId);
+    }
+    /// <summary>
+    /// 根据监测计划ID获取监测机构简要列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<List<ExamOrgLiteOutput>> GetLiteListByExamPlanId([FromQuery][Required] int examPlanId)
+    {
+        return await examOrgService.GetLiteListByExamPlanId(examPlanId);
     }
     /// <summary>
     /// 分页查询监测机构列表
@@ -59,7 +61,7 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamOrgOutput>> QueryPageList(ExamOrgPageInput input)
     {
-        return await _examOrgService.QueryPageList(input);
+        return await examOrgService.QueryPageList(input);
     }
     /// <summary>
     /// 分页查询未加入的机构
@@ -68,7 +70,7 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<SysOrgLiteOutput>> QueryNotInSysOrgPageList(ExamOrgNotInPageInput input)
     {
-        return await _examOrgService.QueryNotInSysOrgPageList(input);
+        return await examOrgService.QueryNotInSysOrgPageList(input);
     }
 
     /// <summary>
@@ -78,6 +80,6 @@ public class ExamOrgAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<PageResult<ExamPlanOutput>> QueryExamPlanPageList(ExamPlanPageInput input)
     {
-        return await _examOrgService.QueryExamPlanPageList(input);
+        return await examOrgService.QueryExamPlanPageList(input);
     }
 }

+ 37 - 29
YBEE.EQM.Application/Exam/ExamOrg/Services/ExamOrgService.cs

@@ -6,13 +6,8 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 被监测机构管理服务
 /// </summary>
-public class ExamOrgService : IExamOrgService, ITransient
+public class ExamOrgService(IRepository<ExamOrg> rep) : IExamOrgService, ITransient
 {
-    private readonly IRepository<ExamOrg> _rep;
-    public ExamOrgService(IRepository<ExamOrg> rep)
-    {
-        _rep = rep;
-    }
 
     /// <summary>
     /// 添加机构
@@ -21,14 +16,14 @@ public class ExamOrgService : IExamOrgService, ITransient
     /// <returns></returns>
     public async Task AddList(AddExamOrgListInput input)
     {
-        var existsOrgIdList = await _rep.Where(t => t.ExamPlanId == input.ExamPlanId).Select(t => t.SysOrgId).ToListAsync();
+        var existsOrgIdList = await rep.Where(t => t.ExamPlanId == input.ExamPlanId).Select(t => t.SysOrgId).ToListAsync();
         var addOrgIdList = from t in input.SysOrgIdList where !(from rid in existsOrgIdList select rid).Contains(t) select t;
         var roles = addOrgIdList.Select(u => new ExamOrg
         {
             ExamPlanId = input.ExamPlanId,
             SysOrgId = u,
         }).ToList();
-        await _rep.InsertAsync(roles);
+        await rep.InsertAsync(roles);
     }
     /// <summary>
     /// 移出机构
@@ -37,8 +32,8 @@ public class ExamOrgService : IExamOrgService, ITransient
     /// <returns></returns>
     public async Task Remove(DelExamOrgInput input)
     {
-        var roleUsers = await _rep.Where(t => input.IdList.Contains(t.Id)).ToListAsync();
-        await _rep.DeleteAsync(roleUsers);
+        var roleUsers = await rep.Where(t => input.IdList.Contains(t.Id)).ToListAsync();
+        await rep.DeleteAsync(roleUsers);
     }
     /// <summary>
     /// 切换机构是否参与区统一监测
@@ -47,7 +42,7 @@ public class ExamOrgService : IExamOrgService, ITransient
     /// <returns></returns>
     public async Task SwitchRequiredSample(SwitchExamOrgRequiredSampleInput 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);
         item.IsRequiredExam = input.IsRequiredExam;
         await item.UpdateIncludeAsync(new[] { nameof(item.IsRequiredExam) });
     }
@@ -58,9 +53,9 @@ public class ExamOrgService : IExamOrgService, ITransient
     /// <returns></returns>
     public async Task<List<ExamOrgOutput>> GetListByExamPlanId(int examPlanId)
     {
-        var items = await _rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamOrgOutput>().ToListAsync();
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamOrgOutput>().ToListAsync();
 
-        var odrs = await _rep.Change<ExamOrgDataReport>().Where(t => t.ExamOrg.ExamPlanId == examPlanId).ToListAsync();
+        var odrs = await rep.Change<ExamOrgDataReport>().Where(t => t.ExamOrg.ExamPlanId == examPlanId).ToListAsync();
         foreach (var item in items)
         {
             item.DataReports = odrs.Where(t => t.ExamOrgId == item.Id).ToDictionary(t => (short)t.Type, u => u.Status);
@@ -68,6 +63,16 @@ public class ExamOrgService : IExamOrgService, ITransient
         return items;
     }
     /// <summary>
+    /// 根据监测计划ID获取监测机构简要列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<List<ExamOrgLiteOutput>> GetLiteListByExamPlanId(int examPlanId)
+    {
+        var items = await rep.DetachedEntities.Where(t => t.ExamPlanId == examPlanId).ProjectToType<ExamOrgLiteOutput>().ToListAsync();
+        return items;
+    }
+    /// <summary>
     /// 分页查询监测机构列表
     /// </summary>
     /// <param name="input"></param>
@@ -76,12 +81,12 @@ public class ExamOrgService : IExamOrgService, ITransient
     {
         var orgName = !string.IsNullOrEmpty(input.OrgName?.Trim());
 
-        var ret = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId)
+        var ret = await rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId)
                                              .Where((orgName, u => EF.Functions.Like(u.SysOrg.FullName, $"%{input.OrgName.Trim()}%")))
                                              .ProjectToType<ExamOrgOutput>()
                                              .ToADPagedListAsync(input.PageIndex, input.PageSize);
 
-        var odrs = await _rep.Change<ExamOrgDataReport>().Where(t => t.ExamOrg.ExamPlanId == input.ExamPlanId).ProjectToType<ExamOrgDataReportOutput>().ToListAsync();
+        var odrs = await rep.Change<ExamOrgDataReport>().Where(t => t.ExamOrg.ExamPlanId == input.ExamPlanId).ProjectToType<ExamOrgDataReportOutput>().ToListAsync();
         foreach (var item in ret.Items)
         {
             item.DataReportList = odrs.Where(t => t.ExamOrgId == item.Id).ToList();
@@ -100,9 +105,9 @@ public class ExamOrgService : IExamOrgService, ITransient
         var code = !string.IsNullOrEmpty(input.Code?.Trim());
         var name = !string.IsNullOrEmpty(input.Name?.Trim());
 
-        var examPlan = await _rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.ExamPlanId);
-        var inOrgIds = await _rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).Select(t => t.SysOrgId).ToListAsync();
-        var ret = await _rep.Change<SysOrg>().DetachedEntities
+        var examPlan = await rep.Change<ExamPlan>().DetachedEntities.FirstOrDefaultAsync(t => t.Id == input.ExamPlanId);
+        var inOrgIds = await rep.DetachedEntities.Where(t => t.ExamPlanId == input.ExamPlanId).Select(t => t.SysOrgId).ToListAsync();
+        var ret = await rep.Change<SysOrg>().DetachedEntities
                                                    .Where((code, u => EF.Functions.Like(u.Code, $"%{input.Code.Trim()}%")))
                                                    .Where((name, u => EF.Functions.Like(u.FullName, $"%{input.Name.Trim()}%")))
                                                    .Where(input.UrbanRuralType.HasValue, t => t.UrbanRuralType == input.UrbanRuralType)
@@ -122,17 +127,20 @@ public class ExamOrgService : IExamOrgService, ITransient
         var name = !string.IsNullOrEmpty(input.Name?.Trim());
         var searchBeginTime = !string.IsNullOrEmpty(input.SearchBeginTime?.Trim());
         var searchEndTime = !string.IsNullOrEmpty(input.SearchEndTime?.Trim());
-        var ret = await _rep.DetachedEntities.Where(t => t.SysOrgId == CurrentSysUserInfo.SysOrgId && (t.ExamPlan.Status == ExamStatus.ACTIVE || t.ExamPlan.Status == ExamStatus.STOPPED) && t.ExamPlan.IsDeleted == false)
-                                             .Where((name, u => EF.Functions.Like(u.ExamPlan.Name, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ExamPlan.FullName, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ExamPlan.ShortName, $"%{input.Name.Trim()}%")))
-                                             .Where(input.EducationStage.HasValue, t => t.ExamPlan.EducationStage == input.EducationStage)
-                                             //.Where(input.ExamPeriodType.HasValue, t => t.ExamPlan.ExamPeriodType == input.ExamPeriodType)
-                                             //.Where(input.ExamType.HasValue, t => t.ExamPlan.ExamType == input.ExamType)
-                                             .Where(input.SemesterId.HasValue, t => t.ExamPlan.SemesterId == input.SemesterId)
-                                             .Where(t => (t.ExamPlan.Status == ExamStatus.ACTIVE || t.ExamPlan.Status == ExamStatus.STOPPED) && (!input.Status.HasValue || input.Status.HasValue && t.ExamPlan.Status == input.Status))
-                                             .Where((searchBeginTime, u => u.ExamPlan.CreateTime >= DateTime.Parse(input.SearchBeginTime)))
-                                             .Where((searchEndTime, u => u.ExamPlan.CreateTime <= DateTime.Parse(input.SearchEndTime)))
-                                             .OrderByDescending(t => t.ExamPlanId)
-                                             .Select(t => t.ExamPlan).ProjectToType<ExamPlanOutput>().ToADPagedListAsync(input.PageIndex, input.PageSize);
+        var ret = await rep.DetachedEntities
+                           .Where(t => t.SysOrgId == CurrentSysUserInfo.SysOrgId && (t.ExamPlan.Status == ExamStatus.ACTIVE || t.ExamPlan.Status == ExamStatus.STOPPED) && t.ExamPlan.IsDeleted == false)
+                           .Where((name, u => EF.Functions.Like(u.ExamPlan.Name, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ExamPlan.FullName, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ExamPlan.ShortName, $"%{input.Name.Trim()}%")))
+                           .Where(input.EducationStage.HasValue, t => t.ExamPlan.EducationStage == input.EducationStage)
+                           //.Where(input.ExamPeriodType.HasValue, t => t.ExamPlan.ExamPeriodType == input.ExamPeriodType)
+                           //.Where(input.ExamType.HasValue, t => t.ExamPlan.ExamType == input.ExamType)
+                           .Where(input.SemesterId.HasValue, t => t.ExamPlan.SemesterId == input.SemesterId)
+                           .Where(t => (t.ExamPlan.Status == ExamStatus.ACTIVE || t.ExamPlan.Status == ExamStatus.STOPPED) && (!input.Status.HasValue || input.Status.HasValue && t.ExamPlan.Status == input.Status))
+                           .Where((searchBeginTime, u => u.ExamPlan.CreateTime >= DateTime.Parse(input.SearchBeginTime)))
+                           .Where((searchEndTime, u => u.ExamPlan.CreateTime <= DateTime.Parse(input.SearchEndTime)))
+                           .OrderByDescending(t => t.ExamPlanId)
+                           .Select(t => t.ExamPlan)
+                           .ProjectToType<ExamPlanOutput>()
+                           .ToADPagedListAsync(input.PageIndex, input.PageSize);
 
         return ret;
     }

+ 6 - 0
YBEE.EQM.Application/Exam/ExamOrg/Services/IExamOrgService.cs

@@ -33,6 +33,12 @@ public interface IExamOrgService
     /// <returns></returns>
     Task<List<ExamOrgOutput>> GetListByExamPlanId(int examPlanId);
     /// <summary>
+    /// 根据监测计划ID获取监测机构简要列表
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<List<ExamOrgLiteOutput>> GetLiteListByExamPlanId(int examPlanId);
+    /// <summary>
     /// 分页查询监测机构列表
     /// </summary>
     /// <param name="input"></param>

+ 4 - 4
YBEE.EQM.Application/Exam/ExamOrgDataReport/Dtos/ExamOrgDataReportOutput.cs

@@ -118,22 +118,22 @@ public class ExamPlanOrgDataReportOutput
     [Required]
     public int ExamPlanId { get; set; }
     /// <summary>
-    /// 监测计划ID
+    /// 监测计划全称
     /// </summary>
     [Required]
     public string ExamPlanFullName { get; set; }
     /// <summary>
-    /// 监测计划ID
+    /// 监测计划名称
     /// </summary>
     [Required]
     public string ExamPlanName { get; set; }
     /// <summary>
-    /// 监测计划ID
+    /// 监测计划简称
     /// </summary>
     [Required]
     public string ExamPlanShortName { get; set; }
     /// <summary>
-    /// 监测计划ID
+    /// 监测计划状态
     /// </summary>
     [Required]
     public ExamStatus ExamPlanStatus { get; set; }

+ 21 - 19
YBEE.EQM.Application/Exam/ExamOrgDataReport/Services/ExamOrgDataReportService.cs

@@ -211,7 +211,7 @@ public class ExamOrgDataReportService : IExamOrgDataReportService, ITransient
         }
         if (input.SemesterId.HasValue)
         {
-            where = $"{where} AND T4.semester_id = {(short)input.SemesterId.Value}";
+            where = $"{where} AND T4.semester_id = {input.SemesterId.Value}";
         }
         if (!string.IsNullOrEmpty(input.Name?.Trim()))
         {
@@ -230,26 +230,28 @@ public class ExamOrgDataReportService : IExamOrgDataReportService, ITransient
         };
 
         var totalCount = await _rep.SqlScalarAsync<int>($@"
-                            SELECT COUNT(1) AS total_count
-                            FROM exam_org AS T1
-                            JOIN exam_data_report AS T2 ON T1.exam_plan_id = T2.exam_plan_id
-                            INNER JOIN exam_plan AS T4 ON T1.exam_plan_id = T4.id
-                            LEFT JOIN exam_org_data_report AS T3 ON T1.id = T3.exam_org_id AND T2.type = T3.type
-                            WHERE {where}", p);
+SELECT COUNT(1) AS total_count
+FROM exam_org AS T1
+JOIN exam_data_report AS T2 ON T1.exam_plan_id = T2.exam_plan_id
+INNER JOIN exam_plan AS T4 ON T1.exam_plan_id = T4.id
+LEFT JOIN exam_org_data_report AS T3 ON T1.id = T3.exam_org_id AND T2.type = T3.type
+WHERE {where}
+", p);
 
         var items = await _rep.SqlQueriesAsync<ExamPlanOrgDataReportOutput>($@"
-                            SELECT ROW_NUMBER() OVER (ORDER BY T1.exam_plan_id DESC, T2.type) AS `row_number`, 
-	                            T1.exam_plan_id, T1.sys_org_id, T2.type, T2.begin_time, T2.end_time, T2.`status` AS exam_status, 
-	                            IFNULL(T3.`status`, 1) AS `status`, T3.report_sys_user_id, T5.`name` AS report_sys_user_name, T3.report_time,
-	                            T4.full_name AS exam_plan_full_name, T4.`name` AS exam_plan_name, T4.short_name AS exam_plan_short_name,
-	                            T4.`status` AS exam_plan_status, T4.education_stage, T4.semester_id
-                            FROM exam_org AS T1
-                            JOIN exam_data_report AS T2 ON T1.exam_plan_id = T2.exam_plan_id
-                            JOIN exam_plan AS T4 ON T1.exam_plan_id = T4.id
-                            LEFT JOIN exam_org_data_report AS T3 ON T1.id = T3.exam_org_id AND T2.type = T3.type
-                            LEFT JOIN sys_user AS T5 ON T3.report_sys_user_id = T5.id
-                            WHERE {where}
-                            LIMIT @pageSize OFFSET @pageOffset;", p);
+SELECT ROW_NUMBER() OVER (ORDER BY T1.exam_plan_id DESC, T2.type) AS `row_number`, 
+    T1.exam_plan_id, T1.sys_org_id, T2.type, T2.begin_time, T2.end_time, T2.`status` AS exam_status, 
+    IFNULL(T3.`status`, 1) AS `status`, T3.report_sys_user_id, T5.`name` AS report_sys_user_name, T3.report_time,
+    T4.full_name AS exam_plan_full_name, T4.`name` AS exam_plan_name, T4.short_name AS exam_plan_short_name,
+    T4.`status` AS exam_plan_status, T4.education_stage, T4.semester_id
+FROM exam_org AS T1
+JOIN exam_data_report AS T2 ON T1.exam_plan_id = T2.exam_plan_id
+JOIN exam_plan AS T4 ON T1.exam_plan_id = T4.id
+LEFT JOIN exam_org_data_report AS T3 ON T1.id = T3.exam_org_id AND T2.type = T3.type
+LEFT JOIN sys_user AS T5 ON T3.report_sys_user_id = T5.id
+WHERE {where}
+LIMIT @pageSize OFFSET @pageOffset;
+", p);
         PageResult<ExamPlanOrgDataReportOutput> ret = new()
         {
             PageIndex = input.PageIndex,

+ 68 - 77
YBEE.EQM.Application/Exam/ExamOrgScoreReport/Services/ExamOrgScoreReportService.cs

@@ -11,18 +11,9 @@ namespace YBEE.EQM.Application;
 /// <summary>
 /// 校考成绩上报管理服务
 /// </summary>
-public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
+public class ExamOrgScoreReportService(IRepository<ExamOrgScoreReport> rep, IOptions<EqmSiteOptions> options, IExportExcelService exportExcelService) : IExamOrgScoreReportService, ITransient
 {
-    private readonly IRepository<ExamOrgScoreReport> _rep;
-    private readonly EqmSiteOptions _eqmSiteOptions;
-    private readonly IExportExcelService _exportExcelService;
-
-    public ExamOrgScoreReportService(IRepository<ExamOrgScoreReport> rep, IOptions<EqmSiteOptions> options, IExportExcelService exportExcelService)
-    {
-        _rep = rep;
-        _eqmSiteOptions = options.Value;
-        _exportExcelService = exportExcelService;
-    }
+    private readonly EqmSiteOptions _eqmSiteOptions = options.Value;
 
     /// <summary>
     /// 上传校考成绩
@@ -54,7 +45,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
 
         try
         {
-            var examCourse = await _rep.Change<ExamCourse>().DetachedEntities.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId) ?? throw Oops.Oh(ErrorCode.E2001);
+            var examCourse = await rep.Change<ExamCourse>().DetachedEntities.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId) ?? throw Oops.Oh(ErrorCode.E2001);
             ret.ScoreReportConfig = JSON.Deserialize<ExamCourseScoreReportConfig>(examCourse.ScoreReportConfig);
 
             using FileStream fsv = new(filePath, FileMode.Open, FileAccess.Read);
@@ -70,7 +61,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
 
             rows.MoveNext();
             var headerRow = (IRow)rows.Current;
-            List<string> columns = new();
+            List<string> columns = [];
             ret.ColumnsCount = headerRow.LastCellNum;
             if (ret.ColumnsCount != ret.ScoreReportConfig.HeaderColumnNames.Count)
             {
@@ -162,7 +153,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
             // 验证成功写入
             if (ret.IsSuccess)
             {
-                var item = await _rep.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId && t.SysOrgId == CurrentSysUserInfo.SysOrgId);
+                var item = await rep.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId && t.SysOrgId == CurrentSysUserInfo.SysOrgId);
                 if (item != null)
                 {
                     var delPath = Path.Combine(_eqmSiteOptions.ResourceFileRoot, item.FilePath);
@@ -172,7 +163,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
                     }
                     await item.DeleteAsync();
                 }
-                var examOrg = await _rep.Change<ExamOrg>().FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId) ?? throw Oops.Oh(ErrorCode.E2009);
+                var examOrg = await rep.Change<ExamOrg>().FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId) ?? throw Oops.Oh(ErrorCode.E2009);
                 item = input.Adapt<ExamOrgScoreReport>();
                 item.ExamOrgId = examOrg.Id;
                 item.SysOrgId = CurrentSysUserInfo.SysOrgId;
@@ -206,7 +197,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
     /// <returns></returns>
     public async Task<ExamOrgScoreReportOutput> QueryOne(QueryExamOrgScoreReportInput input)
     {
-        var item = await _rep.DetachedEntities.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId && t.SysOrgId == CurrentSysUserInfo.SysOrgId);
+        var item = await rep.DetachedEntities.FirstOrDefaultAsync(t => t.ExamPlanId == input.ExamPlanId && t.ExamGradeId == input.ExamGradeId && t.CourseId == input.CourseId && t.SysOrgId == CurrentSysUserInfo.SysOrgId);
         return item.Adapt<ExamOrgScoreReportOutput>();
     }
 
@@ -217,7 +208,7 @@ public class ExamOrgScoreReportService : IExamOrgScoreReportService, ITransient
     /// <returns></returns>
     public async Task<List<ExamOrgScoreReportItem>> GetListByExamPlanId(int examPlanId)
     {
-        var items = await _rep.SqlQueriesAsync<ExamOrgScoreReportItem>($@"
+        var items = await rep.SqlQueriesAsync<ExamOrgScoreReportItem>($@"
 SELECT 
     T1.id AS exam_course_id, 
     T1.exam_plan_id, 
@@ -241,15 +232,15 @@ SELECT
     T6.`name` AS create_sys_user_name,
     T2.update_sys_user_id
 FROM exam_course AS T1
-LEFT JOIN (SELECT * FROM exam_org_score_report WHERE sys_org_id = {CurrentSysUserInfo.SysOrgId}) AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.exam_grade_id =  T2.exam_grade_id AND T1.course_id = T2.course_id
+LEFT JOIN (SELECT * FROM exam_org_score_report WHERE sys_org_id = @sysOrgId) AS T2 ON T1.exam_plan_id = T2.exam_plan_id AND T1.exam_grade_id = T2.exam_grade_id AND T1.course_id = T2.course_id
 LEFT JOIN exam_grade AS T3 ON T1.exam_grade_id = T3.id
 LEFT JOIN base_course AS T4 ON T1.course_id = T4.id
 LEFT JOIN base_grade AS T5 ON T1.grade_id = T5.id
 LEFT JOIN sys_user AS T6 ON T2.create_sys_user_id = T6.id
-JOIN (SELECT * FROM exam_org WHERE sys_org_id = {CurrentSysUserInfo.SysOrgId} AND is_required_exam = 1)AS T7 ON T1.exam_plan_id = T7.exam_plan_id
-WHERE T1.exam_plan_id = {examPlanId}
+JOIN (SELECT * FROM exam_org WHERE sys_org_id = @sysOrgId AND is_required_exam = 1)AS T7 ON T1.exam_plan_id = T7.exam_plan_id
+WHERE T1.exam_plan_id = @examPlanId AND T3.is_required_sample = 1
 ORDER BY T1.grade_id, T1.course_id
-");
+", new { ExamPlanId = examPlanId, CurrentSysUserInfo.SysOrgId });
         return items;
     }
 
@@ -260,8 +251,8 @@ ORDER BY T1.grade_id, T1.course_id
     /// <returns></returns>
     public async Task<(string, byte[])> MergeMinorScore(int examPlanId)
     {
-        var examPlan = await _rep.Change<ExamPlan>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
-        var orgs = await _rep.Change<SysOrg>().DetachedEntities.Where(t => t.EducationStage == examPlan.EducationStage).ToListAsync();
+        var examPlan = await rep.Change<ExamPlan>().FirstOrDefaultAsync(t => t.Id == examPlanId) ?? throw Oops.Oh(ErrorCode.E2001, "监测计划");
+        var orgs = await rep.Change<SysOrg>().DetachedEntities.Where(t => t.EducationStage == examPlan.EducationStage).ToListAsync();
         var orgDict = orgs.ToDictionary(t => t.Code);
 
         // 临时存放目录
@@ -271,7 +262,7 @@ ORDER BY T1.grade_id, T1.course_id
         Directory.CreateDirectory(filePath);
         try
         {
-            var examCourses = await _rep.Change<ExamCourse>()
+            var examCourses = await rep.Change<ExamCourse>()
                                     .DetachedEntities
                                     .Where(t => t.ExamPlanId == examPlanId)
                                     .ProjectToType<ExamCourseOutput>()
@@ -282,23 +273,23 @@ ORDER BY T1.grade_id, T1.course_id
 
             IWorkbook twb = new HSSFWorkbook();
             ISheet tsheet = twb.CreateSheet();
-            var tCellStyles = _exportExcelService.GetCellStyle(twb);
+            var tCellStyles = exportExcelService.GetCellStyle(twb);
 
             int tRowNum = 0;
             IRow tHeaderRow = tsheet.CreateRow(tRowNum++);
             int tci = 0;
-            _exportExcelService.AddCell("学校", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 20);
-            _exportExcelService.AddCell("学校ID", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 8);
-            _exportExcelService.AddCell("考号", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 12);
-            _exportExcelService.AddCell("年级", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 6);
-            _exportExcelService.AddCell("班级", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 6);
-            _exportExcelService.AddCell("身份证号码", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 20);
-            _exportExcelService.AddCell("姓名", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 12);
+            exportExcelService.AddCell("学校", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 20);
+            exportExcelService.AddCell("学校ID", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 8);
+            exportExcelService.AddCell("考号", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 12);
+            exportExcelService.AddCell("年级", tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 6);
+            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 = [];
             foreach (var ac in allCourses)
             {
                 courseIndexes.Add(ac.Id, tci);
-                _exportExcelService.AddCell(ac.Name, tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 8);
+                exportExcelService.AddCell(ac.Name, tHeaderRow, tci++, tCellStyles.ColumnFillHeaderStyle, tsheet, 8);
             }
 
             var ecgs = examCourses.GroupBy(t => t.GradeId);
@@ -307,24 +298,24 @@ ORDER BY T1.grade_id, T1.course_id
                 var fecg = ecg.First();
 
                 // 获取抽样学生信息
-                var students = await _rep.SqlQueriesAsync<ExamSampleStudentExportDto>($@"
+                var students = await rep.SqlQueriesAsync<ExamSampleStudentExportDto>($@"
 SET @esid = (SELECT id FROM exam_sample WHERE exam_plan_id = @examPlanId AND is_selected = 1);
 SELECT 
-	T2.sys_org_id,
+    T2.sys_org_id,
     T3.tqes_id AS sys_org_tqes_id,
-	T3.`name` AS sys_org_name,
-	T2.sys_org_branch_id,
-	T4.`name` AS sys_org_branch_name,
-	T2.grade_id,
+    T3.`name` AS sys_org_name,
+    T2.sys_org_branch_id,
+    T4.`name` AS sys_org_branch_name,
+    T2.grade_id,
     G.grade_number,
-	T2.school_class_id,
-	T2.class_number,
-	T2.certificate_type,
-	T2.id_number,
-	T1.exam_student_id,
-	REPLACE(REPLACE(T2.`name`, '.', ''), '·', '') AS exam_student_name,
-	T1.exam_number,
-	T1.exam_sample_type
+    T2.school_class_id,
+    T2.class_number,
+    T2.certificate_type,
+    T2.id_number,
+    T1.exam_student_id,
+    REPLACE(REPLACE(T2.`name`, '.', ''), '·', '') AS exam_student_name,
+    T1.exam_number,
+    T1.exam_sample_type
 FROM exam_sample_student AS T1
 JOIN exam_student AS T2 ON T1.exam_student_id = T2.id
 JOIN sys_org AS T3 ON T2.sys_org_id = T3.id
@@ -339,14 +330,14 @@ WHERE T1.exam_sample_id = @esid AND T2.exam_plan_id = @examPlanId AND T2.grade_i
                 var ecs = ecg.OrderBy(t => t.CourseId).ToList();
                 foreach (var examCourse in ecs)
                 {
-                    var mergeItems = await _rep.SqlQueriesAsync<ExamOrgScoreReportMergeDto>(@$"
+                    var mergeItems = await rep.SqlQueriesAsync<ExamOrgScoreReportMergeDto>(@$"
 SELECT T1.id, t1.sys_org_id, T2.`name` AS sys_org_name, grade_id, course_id, file_path
 FROM exam_org_score_report AS T1
 JOIN sys_org AS T2 ON T1.sys_org_id = T2.id
 WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @courseId AND T1.sys_org_id IN (
-	SELECT sys_org_id 
-	FROM exam_org_data_report
-	WHERE exam_plan_id = @examPlanId AND type = 6 and `status` = 3
+    SELECT sys_org_id 
+    FROM exam_org_data_report
+    WHERE exam_plan_id = @examPlanId AND type = 6 and `status` = 3
 )
 ", new { ExamPlanId = examPlanId, examCourse.GradeId, examCourse.CourseId });
 
@@ -436,7 +427,7 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                                 else // 提取姓名
                                 if (ci == 0)
                                 {
-                                    string n = cell?.ToString()?.Trim() ?? "";
+                                    string n = cell?.ToString()?.ClearWhitespace() ?? "";
                                     ncell.SetCellValue(n);
                                     totalItem.StudentName = n.Replace(".", "").Replace("·", "");
                                 }
@@ -490,21 +481,21 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                 #region 生成年级文件
                 IWorkbook gwb = new HSSFWorkbook();
                 ISheet gsheet = gwb.CreateSheet();
-                var gcellStyles = _exportExcelService.GetCellStyle(gwb);
+                var gcellStyles = exportExcelService.GetCellStyle(gwb);
 
                 int grownum = 0;
                 IRow gHeaderRow = gsheet.CreateRow(grownum++);
                 int gci = 0;
-                _exportExcelService.AddCell("学校", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 20);
-                _exportExcelService.AddCell("学校ID", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 8);
-                _exportExcelService.AddCell("考号", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 12);
-                _exportExcelService.AddCell("年级", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 6);
-                _exportExcelService.AddCell("班级", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 6);
-                _exportExcelService.AddCell("身份证号码", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 20);
-                _exportExcelService.AddCell("姓名", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 12);
+                exportExcelService.AddCell("学校", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 20);
+                exportExcelService.AddCell("学校ID", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 8);
+                exportExcelService.AddCell("考号", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 12);
+                exportExcelService.AddCell("年级", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 6);
+                exportExcelService.AddCell("班级", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 6);
+                exportExcelService.AddCell("身份证号码", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 20);
+                exportExcelService.AddCell("姓名", gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 12);
                 foreach (var ec in ecs)
                 {
-                    _exportExcelService.AddCell(ec.Course.Name, gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 8);
+                    exportExcelService.AddCell(ec.Course.Name, gHeaderRow, gci++, gcellStyles.ColumnFillHeaderStyle, gsheet, 8);
                 }
 
                 var items = (from t in totalItems
@@ -535,26 +526,26 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                     }
 
                     gci = 0;
-                    _exportExcelService.AddCell(stu.SysOrgName, row, gci, gcellStyles.LeftCellStyle, gsheet, 20);
-                    _exportExcelService.AddCell(stu.SysOrgName, trow, gci++, tCellStyles.LeftCellStyle, tsheet, 20);
+                    exportExcelService.AddCell(stu.SysOrgName, row, gci, gcellStyles.LeftCellStyle, gsheet, 20);
+                    exportExcelService.AddCell(stu.SysOrgName, trow, gci++, tCellStyles.LeftCellStyle, tsheet, 20);
 
-                    _exportExcelService.AddCell(stu.SysOrgTqesId, row, gci, gcellStyles.CenterCellStyle, gsheet, 8);
-                    _exportExcelService.AddCell(stu.SysOrgTqesId, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 8);
+                    exportExcelService.AddCell(stu.SysOrgTqesId, row, gci, gcellStyles.CenterCellStyle, gsheet, 8);
+                    exportExcelService.AddCell(stu.SysOrgTqesId, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 8);
 
-                    _exportExcelService.AddCell(stu.ExamNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 12);
-                    _exportExcelService.AddCell(stu.ExamNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 12);
+                    exportExcelService.AddCell(stu.ExamNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 12);
+                    exportExcelService.AddCell(stu.ExamNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 12);
 
-                    _exportExcelService.AddCell(stu.GradeNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 6);
-                    _exportExcelService.AddCell(stu.GradeNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 6);
+                    exportExcelService.AddCell(stu.GradeNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 6);
+                    exportExcelService.AddCell(stu.GradeNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 6);
 
-                    _exportExcelService.AddCell(stu.ClassNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 6);
-                    _exportExcelService.AddCell(stu.ClassNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 6);
+                    exportExcelService.AddCell(stu.ClassNumber, row, gci, gcellStyles.CenterCellStyle, gsheet, 6);
+                    exportExcelService.AddCell(stu.ClassNumber, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 6);
 
-                    _exportExcelService.AddCell(stu.IdNumber ?? "", row, gci, gcellStyles.CenterCellStyle, gsheet, 20);
-                    _exportExcelService.AddCell(stu.IdNumber ?? "", trow, gci++, tCellStyles.CenterCellStyle, tsheet, 20);
+                    exportExcelService.AddCell(stu.IdNumber ?? "", row, gci, gcellStyles.CenterCellStyle, gsheet, 20);
+                    exportExcelService.AddCell(stu.IdNumber ?? "", trow, gci++, tCellStyles.CenterCellStyle, tsheet, 20);
 
-                    _exportExcelService.AddCell(stu.StudentName, row, gci, gcellStyles.CenterCellStyle, gsheet, 12);
-                    _exportExcelService.AddCell(stu.StudentName, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 12);
+                    exportExcelService.AddCell(stu.StudentName, row, gci, gcellStyles.CenterCellStyle, gsheet, 12);
+                    exportExcelService.AddCell(stu.StudentName, trow, gci++, tCellStyles.CenterCellStyle, tsheet, 12);
 
                     foreach (var ec in ecs)
                     {
@@ -565,8 +556,8 @@ WHERE exam_plan_id = @examPlanId AND T1.grade_id = @gradeId AND T1.course_id = @
                             continue;
                         }
 
-                        _exportExcelService.AddCell(s.Score, trow, courseIndexes[s.CourseId], tCellStyles.CenterCellStyle, tsheet, 8);
-                        _exportExcelService.AddCell(s.Score, row, gci++, gcellStyles.CenterCellStyle, gsheet, 8);
+                        exportExcelService.AddCell(s.Score, trow, courseIndexes[s.CourseId], tCellStyles.CenterCellStyle, tsheet, 8);
+                        exportExcelService.AddCell(s.Score, row, gci++, gcellStyles.CenterCellStyle, gsheet, 8);
                     }
                 }
 

+ 25 - 3
YBEE.EQM.Application/Exam/ExamPlan/Dtos/ExamPlanOutput.cs

@@ -61,6 +61,11 @@ public class ExamPlanLiteOutput : DEntityOutput
     [Required]
     public ExamStatus Status { get; set; }
     /// <summary>
+    /// 抽样状态
+    /// </summary>
+    [Required]
+    public ExamSampleStatus SampleStatus { get; set; }
+    /// <summary>
     /// 开始时间
     /// </summary>
     public DateTime? BeginTime { get; set; }
@@ -129,6 +134,11 @@ public class ExamPlanOutput : DEntityOutput
     [Required]
     public ExamStatus Status { get; set; }
     /// <summary>
+    /// 抽样状态
+    /// </summary>
+    [Required]
+    public ExamSampleStatus SampleStatus { get; set; }
+    /// <summary>
     /// 开始时间
     /// </summary>
     public DateTime? BeginTime { get; set; }
@@ -175,7 +185,7 @@ public class ExamPlanAuditOutput
     /// 监测计划全称
     /// </summary>
     [Required]
-    public string FullName { get;set; }
+    public string FullName { get; set; }
     /// <summary>
     /// 监测计划简称
     /// </summary>
@@ -279,7 +289,7 @@ public class ExamPlanOrgAuditOutput
     /// <summary>
     /// 机构代码
     /// </summary>
-    public string SYsOrgCode { get;set; }
+    public string SYsOrgCode { get; set; }
     /// <summary>
     /// 上报时间
     /// </summary>
@@ -294,7 +304,7 @@ public class ExamPlanOrgAuditOutput
     /// 上报状态
     /// </summary>
     public DataReportStatus DataReportStatus { get; set; }
-    
+
     /// <summary>
     /// 总数量
     /// </summary>
@@ -320,4 +330,16 @@ public class ExamPlanOrgAuditOutput
     /// </summary>
     [Required]
     public int PreIdentifiedCount { get; set; } = 0;
+}
+
+/// <summary>
+/// 监测计划抽样状态输出参数
+/// </summary>
+public class ExamPlanSampleStatusOutput : BaseId
+{
+    /// <summary>
+    /// 抽样状态
+    /// </summary>
+    [Required]
+    public ExamSampleStatus SampleStatus { get; set; }
 }

+ 24 - 2
YBEE.EQM.Application/Exam/ExamPlan/ExamPlanAppService.cs

@@ -7,9 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-plan")]
 [Route("exam/plan")]
-public class ExamPlanAppService(IExamPlanService examPlanService) : IDynamicApiController
+public class ExamPlanAppService(IExamPlanService examPlanService, ExamSampleWorker examSampleWorker) : IDynamicApiController
 {
-
     /// <summary>
     /// 添加监测计划
     /// </summary>
@@ -74,6 +73,16 @@ public class ExamPlanAppService(IExamPlanService examPlanService) : IDynamicApiC
         return await examPlanService.GetById(id);
     }
     /// <summary>
+    /// 获取监测计划抽样状态
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [DisableOpLog]
+    public async Task<ExamPlanSampleStatusOutput> GetSampleStatusById(int id)
+    {
+        return await examPlanService.GetSampleStatusById(id);
+    }
+    /// <summary>
     /// 分页查询监测计划列表
     /// </summary>
     /// <param name="input"></param>
@@ -99,4 +108,17 @@ public class ExamPlanAppService(IExamPlanService examPlanService) : IDynamicApiC
     {
         return await examPlanService.GetSampleRefPlanList(id);
     }
+
+    /// <summary>
+    /// 按监测计划执行抽样
+    /// </summary>
+    /// <param name="input"></param>
+    public void ExecuteSample(BaseId input)
+    {
+        var ret = examSampleWorker.StartTask(input.Id);
+        if (ret == -1)
+        {
+            throw Oops.Oh(ErrorCode.E3007);
+        }
+    }
 }

+ 11 - 2
YBEE.EQM.Application/Exam/ExamPlan/Services/ExamPlanService.cs

@@ -8,7 +8,6 @@ namespace YBEE.EQM.Application;
 /// </summary>
 public class ExamPlanService(IRepository<ExamPlan> rep, IEducationStageYearsService educationStageYearsService) : IExamPlanService, ITransient
 {
-
     #region 创建更新
     /// <summary>
     /// 添加监测计划
@@ -38,7 +37,7 @@ public class ExamPlanService(IRepository<ExamPlan> rep, IEducationStageYearsServ
             throw Oops.Oh(ErrorCode.E2001);
         }
         var item = input.Adapt<ExamPlan>();
-        await item.UpdateIncludeAsync(new[] { nameof(item.Name), nameof(item.FullName), nameof(item.ShortName), nameof(item.Remark), nameof(item.Config) });
+        await item.UpdateIncludeAsync([nameof(item.Name), nameof(item.FullName), nameof(item.ShortName), nameof(item.Remark), nameof(item.Config)]);
     }
     /// <summary>
     /// 删除监测计划
@@ -125,6 +124,16 @@ public class ExamPlanService(IRepository<ExamPlan> rep, IEducationStageYearsServ
         //return item.Adapt<ExamPlanOutput>();
     }
     /// <summary>
+    /// 获取监测计划抽样状态
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    public async Task<ExamPlanSampleStatusOutput> GetSampleStatusById(int id)
+    {
+        var sampleStatus = (await rep.DetachedEntities.FirstOrDefaultAsync(t => t.Id == id))?.SampleStatus ?? throw Oops.Oh(ErrorCode.E2001);
+        return new() { Id = id, SampleStatus = sampleStatus };
+    }
+    /// <summary>
     /// 分页查询监测计划列表
     /// </summary>
     /// <param name="input"></param>

+ 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>
+    /// 获取监测计划抽样状态
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    Task<ExamPlanSampleStatusOutput> GetSampleStatusById(int id);
+    /// <summary>
     /// 获取最近5个抽测参照成绩监测计划
     /// </summary>
     /// <param name="id"></param>

+ 195 - 196
YBEE.EQM.Application/Exam/ExamReporting/Services/ExamReportingAvgRangeService.cs

@@ -1,5 +1,4 @@
-using NPOI.SS.Formula.Functions;
-using NPOI.SS.UserModel;
+using NPOI.SS.UserModel;
 using NPOI.SS.Util;
 using NPOI.XSSF.UserModel;
 using System.Data;
@@ -699,14 +698,14 @@ public class ExamReportingAvgRangeService(
 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
-	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.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
+    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
+    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.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
@@ -729,14 +728,14 @@ JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
 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
-	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.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
+    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
+    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.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
@@ -758,14 +757,14 @@ JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
 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
+    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
@@ -787,183 +786,183 @@ JOIN exam_score_range AS T3 ON T1.exam_score_range_id = T3.id
 SELECT T1.*
 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,
-		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 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
-	JOIN 
+    -- 总分
+    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,
+        T1.avg_score - T3.score_max AS avg_score_diff,
+        T3.score_max
+    FROM
     (
-		SELECT MAX(T.avg_score) 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
+        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 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
+    JOIN 
+    (
+        SELECT MAX(T.avg_score) 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
     ) AS T3
 
-    -- 单科	
+    -- 单科    
     UNION ALL
-	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,
-		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 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
-	JOIN 
+    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,
+        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 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
+    JOIN 
     (
-		SELECT T.course_id, MAX(T.avg_score) 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 T3 ON T1.course_id = T3.course_id
+        SELECT T.course_id, MAX(T.avg_score) 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 T3 ON T1.course_id = T3.course_id
 
     -- 同类小计
-	UNION ALL
-	SELECT 2 AS data_scope_type, T1.*, 
-		NULL AS order_in_total,
-		NULL AS order_in_same,
-		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.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 is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
-	) AS T3
-	
-	-- 单科同类小计
-	UNION ALL
-	SELECT 2 AS data_scope_type, T1.*, 
-		NULL AS order_in_total,
-		NULL AS order_in_same,
-		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.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 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 3 AS data_scope_type, T1.*, 
-		NULL AS order_in_total,
-		NULL AS order_in_same,
-		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.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 is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
-	) AS T3
-	
-	-- 单科全部合计
-	UNION ALL
-	SELECT 3 AS data_scope_type, T1.*, 
-		NULL AS order_in_total,
-		NULL AS order_in_same,
-		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.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 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
+    UNION ALL
+    SELECT 2 AS data_scope_type, T1.*, 
+        NULL AS order_in_total,
+        NULL AS order_in_same,
+        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.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 is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+    ) AS T3
+    
+    -- 单科同类小计
+    UNION ALL
+    SELECT 2 AS data_scope_type, T1.*, 
+        NULL AS order_in_total,
+        NULL AS order_in_same,
+        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.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 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 3 AS data_scope_type, T1.*, 
+        NULL AS order_in_total,
+        NULL AS order_in_same,
+        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.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 is_excluded = 0 AND score > 0 AND (exam_sample_type = @examSampleType OR @examSampleType = 0)
+    ) AS T3
+    
+    -- 单科全部合计
+    UNION ALL
+    SELECT 3 AS data_scope_type, T1.*, 
+        NULL AS order_in_total,
+        NULL AS order_in_same,
+        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.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 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.data_scope_type, T1.urban_rural_type, T1.course_id, T1.order_in_total, T1.sys_org_id, T1.total_count
 ;

+ 4 - 0
YBEE.EQM.Application/Exam/ExamSample/Dtos/ExamSampleDto.cs

@@ -75,4 +75,8 @@ public class ExamSampleDto
     /// 监测号
     /// </summary>
     public string ExamNumber { get; set; }
+    /// <summary>
+    /// 循环次数
+    /// </summary>
+    public short CyclicNumber { get; set; } = 0;
 }

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

@@ -19,6 +19,10 @@ public class AddExamSampleInput
     [StringLength(200)]
     public string Remark { get; set; } = "";
 
+    /// <summary>
+    /// 成绩引用监测计划ID
+    /// </summary>
+    public int? ExamScoreRefExamPlanId { get; set; }
 
     /// <summary>
     /// 抽样配置

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

@@ -2,6 +2,61 @@
 
 namespace YBEE.EQM.Application;
 
+/// <summary>
+/// 监测方案简要输出参数
+/// </summary>
+public class ExamSampleLiteOutput : BaseId
+{
+    /// <summary>
+    /// 监测计划ID
+    /// </summary>
+    [Required]
+    public int ExamPlanId { get; set; }
+    /// <summary>
+    /// 序,同一监测从1开始计数
+    /// </summary>
+    [Required]
+    public short Sequence { get; set; }
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [Required]
+    public string Name { get; set; }
+    /// <summary>
+    /// 全称
+    /// </summary>
+    [Required]
+    public string FullName { get; set; }
+    /// <summary>
+    /// 简称
+    /// </summary>
+    [Required]
+    public string ShortName { get; set; }
+
+    /// <summary>
+    /// 状态
+    /// </summary>
+    [Required]
+    public ExamSampleStatus Status { get; set; }
+
+    /// <summary>
+    /// 是否已固定监测抽样方案
+    /// </summary>
+    [Required]
+    public bool IsFixedExamSample { get; set; }
+    /// <summary>
+    /// 学段
+    /// </summary>
+    [Required]
+    public EducationStage EducationStage { get; set; }
+
+    /// <summary>
+    /// 是否选中使用的方案
+    /// </summary>
+    [Required]
+    public bool IsSelected { get; set; }
+}
+
 /// <summary>
 /// 监测方案输出参数
 /// </summary>
@@ -82,6 +137,11 @@ public class ExamSampleOutput : DEntityOutput
     /// </summary>
     public SysUserLiteOutput SelectedSysUser { get; set; }
 
+    /// <summary>
+    /// 执行日志
+    /// </summary>
+    public string ExecuteLog { get; set; }
+
     /// <summary>
     /// 成绩引用监测计划
     /// </summary>
@@ -101,10 +161,14 @@ public class ExamSamplePlanOutput : ExamSampleOutput
 /// </summary>
 public class ExamSampleCountOutput
 {
+    /// <summary>
+    /// 行号主键
+    /// </summary>
+    public int Id { get; set; }
     /// <summary>
     /// 类型ID
     /// </summary>
-    public int TypeId { get;set; }
+    public int TypeId { get; set; }
     /// <summary>
     /// 类型名称
     /// </summary>
@@ -153,4 +217,24 @@ public class ExamSampleCountOutput
     /// 校测学生数
     /// </summary>
     public int SchoolStudentCount { get; set; }
-}
+    /// <summary>
+    /// 特殊学生数
+    /// </summary>
+    public int TotalSpecialStudentCount { get; set; }
+}
+
+/// <summary>
+/// 抽样状态输出参数
+/// </summary>
+public class ExamSampleStatusOutput : BaseId
+{
+    /// <summary>
+    /// 状态
+    /// </summary>
+    [Required]
+    public ExamSampleStatus Status { get; set; }
+    /// <summary>
+    /// 执行日志
+    /// </summary>
+    public string ExecuteLog { get; set; }
+}

+ 48 - 31
YBEE.EQM.Application/Exam/ExamSample/ExamSampleAppService.cs

@@ -7,15 +7,8 @@ namespace YBEE.EQM.Application;
 /// </summary>
 [ApiDescriptionSettings(Name = "exam-sample")]
 [Route("exam/sample")]
-public class ExamSampleAppService : IDynamicApiController
+public class ExamSampleAppService(IExamSampleService examSampleService) : IDynamicApiController
 {
-    private readonly IExamSampleService _examSampleService;
-
-    public ExamSampleAppService(IExamSampleService examSampleService)
-    {
-        _examSampleService = examSampleService;
-    }
-
     /// <summary>
     /// 添加监测抽样方案
     /// </summary>
@@ -23,7 +16,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Add(AddExamSampleInput input)
     {
-        await _examSampleService.Add(input);
+        await examSampleService.Add(input);
     }
     /// <summary>
     /// 更新监测抽样方案
@@ -32,7 +25,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Update(UpdateExamSampleInput input)
     {
-        await _examSampleService.Update(input);
+        await examSampleService.Update(input);
     }
     /// <summary>
     /// 复制抽样方案信息
@@ -41,7 +34,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Duplicate(BaseId input)
     {
-        await _examSampleService.Duplicate(input);
+        await examSampleService.Duplicate(input);
     }
     /// <summary>
     /// 删除监测抽样方案
@@ -50,7 +43,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task Del(BaseId input)
     {
-        await _examSampleService.Del(input);
+        await examSampleService.Del(input);
     }
     /// <summary>
     /// 保存全抽班级ID列表
@@ -59,7 +52,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task SaveExamSampleAllClasses(SaveExamSampleAllClasses input)
     {
-        await _examSampleService.SaveExamSampleAllClasses(input);
+        await examSampleService.SaveExamSampleAllClasses(input);
     }
     /// <summary>
     /// 切换全抽班级
@@ -68,7 +61,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task SwitchExamSampleAllClass(SwitchExamSampleAllClassInput input)
     {
-        await _examSampleService.SwitchExamSampleAllClass(input);
+        await examSampleService.SwitchExamSampleAllClass(input);
     }
     /// <summary>
     /// 选定方案
@@ -77,9 +70,26 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task SelectSample(BaseId input)
     {
-        await _examSampleService.SelectSample(input);
+        await examSampleService.SelectSample(input);
+    }
+    /// <summary>
+    /// 取消选定
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task UnselectSample(BaseId input)
+    {
+        await examSampleService.UnselectSample(input);
+    }
+    /// <summary>
+    /// 更新机构是否需要上报校考成绩状态
+    /// </summary>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <returns></returns>
+    public async Task UpdateOrgReportSchoolExamScoreStatus(int examPlanId)
+    {
+        await examSampleService.UpdateOrgReportSchoolExamScoreStatus(examPlanId);
     }
-
 
     /// <summary>
     /// 执行抽样
@@ -89,10 +99,9 @@ public class ExamSampleAppService : IDynamicApiController
     /// <exception cref="Exception"></exception>
     public async Task ExecuteSample(BaseId input)
     {
-        await _examSampleService.ExecuteSample(input);
+        await examSampleService.ExecuteSample(input);
     }
 
-
     /// <summary>
     /// 导出抽样方案存档文件
     /// </summary>
@@ -100,7 +109,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportToArchived(BaseId input)
     {
-        var (fileName, fileBytes) = await _examSampleService.ExportToArchived(input.Id, false);
+        var (fileName, fileBytes) = await examSampleService.ExportToArchived(input.Id, false, true);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
@@ -113,7 +122,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportToPrintshop(BaseId input)
     {
-        var (fileName, fileBytes) = await _examSampleService.ExportToArchived(input.Id, true);
+        var (fileName, fileBytes) = await examSampleService.ExportToPrintshop(input.Id);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
@@ -126,7 +135,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportToOrg(BaseId input)
     {
-        var (fileName, fileBytes) = await _examSampleService.ExportToOrg(input.Id);
+        var (fileName, fileBytes) = await examSampleService.ExportToOrg(input.Id);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
@@ -139,7 +148,7 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportSampleCount(BaseId input)
     {
-        var (fileName, fileBytes) = await _examSampleService.ExportSampleCount(input.Id);
+        var (fileName, fileBytes) = await examSampleService.ExportSampleCount(input.Id, true);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
@@ -152,22 +161,21 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<IActionResult> ExportSampleCountToOrg(BaseId input)
     {
-        var (fileName, fileBytes) = await _examSampleService.ExportSampleCountToOrg(input.Id);
+        var (fileName, fileBytes) = await examSampleService.ExportSampleCountToOrg(input.Id);
         return new FileContentResult(fileBytes, "application/octet-stream")
         {
             FileDownloadName = fileName,
         };
     }
 
-
     /// <summary>
     /// 根据ID获取抽样方案
     /// </summary>
-    /// <param name="id"></param>
+    /// <param name="id">抽样方案ID</param>
     /// <returns></returns>
     public async Task<ExamSampleOutput> GetById([FromQuery][Required] int id)
     {
-        return await _examSampleService.GetById(id);
+        return await examSampleService.GetById(id);
     }
     /// <summary>
     /// 根据监测计划ID获取全部抽样方案
@@ -176,7 +184,16 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<ExamSampleOutput>> GetListByExamPlanId([FromQuery][Required] int examPlanId)
     {
-        return await _examSampleService.GetListByExamPlanId(examPlanId);
+        return await examSampleService.GetListByExamPlanId(examPlanId);
+    }
+    /// <summary>
+    /// 根据监测计划ID获取全部抽样方案的状态
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<List<ExamSampleStatusOutput>> GetStatusListByExamPlanId([FromQuery][Required] int examPlanId)
+    {
+        return await examSampleService.GetStatusListByExamPlanId(examPlanId);
     }
     /// <summary>
     /// 查询已发布抽样
@@ -186,17 +203,17 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<ExamSamplePlanOutput> GetByExamDataPublishId([FromQuery][Required] int examDataPublishId, [FromQuery][Required] DataPublishType type)
     {
-        return await _examSampleService.GetByExamDataPublishId(examDataPublishId, type);
+        return await examSampleService.GetByExamDataPublishId(examDataPublishId, type);
     }
 
     /// <summary>
     /// 获取抽样统计表
     /// </summary>
-    /// <param name="id"></param>
+    /// <param name="id">抽样方案ID</param>
     /// <returns></returns>
     public async Task<List<ExamSampleCountOutput>> GetSampleCountListById([FromQuery][Required] int id)
     {
-        return await _examSampleService.GetSampleCountListById(id);
+        return await examSampleService.GetSampleCountListById(id);
     }
     /// <summary>
     /// 获取学校抽样统计表
@@ -205,6 +222,6 @@ public class ExamSampleAppService : IDynamicApiController
     /// <returns></returns>
     public async Task<List<ExamSampleCountOutput>> GetOrgSampleCountListById([FromQuery][Required] int id)
     {
-        return await _examSampleService.GetOrgSampleCountListById(id);
+        return await examSampleService.GetOrgSampleCountListById(id);
     }
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 545 - 167
YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleService.cs


+ 114 - 0
YBEE.EQM.Application/Exam/ExamSample/Services/ExamSampleWorker.cs

@@ -0,0 +1,114 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace YBEE.EQM.Application;
+
+public class ExamSampleWorker(ILogger<ExamSampleWorker> logger, IServiceScopeFactory scopeFactory) : IHostedService, IDisposable
+{
+    private readonly Dictionary<int, Task> _executingTasks = [];
+    private readonly Dictionary<int, CancellationTokenSource> _cancellationTokenSources = [];
+
+    /// <summary>
+    /// StartAsync 方法不再直接启动任务,而是保持服务初始化
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task StartAsync(CancellationToken cancellationToken)
+    {
+        logger.LogInformation("抽样执行服务已初始化");
+        return Task.CompletedTask;
+    }
+
+    /// <summary>
+    /// 外部控制:通过参数启动后台任务
+    /// </summary>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <returns></returns>
+    public int StartTask(int examPlanId)
+    {
+        if (_executingTasks.TryGetValue(examPlanId, out Task value) && !value.IsCompleted)
+        {
+            logger.LogInformation("监测计划ID:{examPlanId},执行中,不能重复启动!", examPlanId);
+            return -1;
+        }
+
+        logger.LogInformation("监测计划ID:{examPlanId},开始执行...", examPlanId);
+        var cancellationTokenSource = new CancellationTokenSource();
+        var executingTask = ExecuteAsync(examPlanId, cancellationTokenSource.Token);
+
+        // 保存任务和取消令牌源,以便可以后续控制
+        _executingTasks[examPlanId] = executingTask;
+        _cancellationTokenSources[examPlanId] = cancellationTokenSource;
+
+        return 0;
+    }
+
+    /// <summary>
+    /// 外部控制:通过任务参数停止任务
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public int StopTask(int examPlanId)
+    {
+        if (!_executingTasks.TryGetValue(examPlanId, out Task value) || value.IsCompleted)
+        {
+            logger.LogInformation("监测计划ID:{examPlanId},未运行!", examPlanId);
+            return -1;
+        }
+
+        logger.LogInformation("监测计划ID:{examPlanId},开始停止...", examPlanId);
+
+        // 停止任务
+        _cancellationTokenSources[examPlanId]?.Cancel();
+        _executingTasks.Remove(examPlanId);
+        _cancellationTokenSources.Remove(examPlanId);
+
+        return 0;
+    }
+
+    /// <summary>
+    /// 任务执行逻辑
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    private async Task ExecuteAsync(int examPlanId, CancellationToken cancellationToken)
+    {
+        try
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                using var scope = scopeFactory.CreateScope();
+                var services = scope.ServiceProvider;
+                var examSampleService = services.GetService<IExamSampleService>();
+                await examSampleService.ExecuteSampleByExamPlanId(examPlanId);
+                logger.LogInformation("监测计划ID:{examPlanId},执行完成。", examPlanId);
+                StopTask(examPlanId);
+            }
+        }
+        catch (TaskCanceledException)
+        {
+            logger.LogInformation("监测计划ID:{examPlanId},已取消。", examPlanId);
+        }
+    }
+
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        // 停止所有正在运行的任务
+        foreach (var key in _executingTasks.Keys.ToList())
+        {
+            StopTask(key);
+        }
+
+        return Task.CompletedTask;
+    }
+
+    public void Dispose()
+    {
+        foreach (var cancellationTokenSource in _cancellationTokenSources.Values)
+        {
+            cancellationTokenSource?.Dispose();
+        }
+    }
+}

+ 35 - 3
YBEE.EQM.Application/Exam/ExamSample/Services/IExamSampleService.cs

@@ -49,7 +49,24 @@ public interface IExamSampleService
     /// <param name="input"></param>
     /// <returns></returns>
     Task SelectSample(BaseId input);
-
+    /// <summary>
+    /// 取消选定
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task UnselectSample(BaseId input);
+    /// <summary>
+    /// 检查监测计划中是否已有选定的抽样方案
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<bool> CheckSelectedByExamPlanId(int examPlanId);
+    /// <summary>
+    /// 更新机构是否需要上报校考成绩状态
+    /// </summary>
+    /// <param name="examPlanId">监测计划ID</param>
+    /// <returns></returns>
+    Task UpdateOrgReportSchoolExamScoreStatus(int examPlanId);
 
     /// <summary>
     /// 执行抽样
@@ -58,14 +75,22 @@ public interface IExamSampleService
     /// <returns></returns>
     /// <exception cref="Exception"></exception>
     Task ExecuteSample(BaseId input);
+    /// <summary>
+    /// 启动监测计划所有抽样方案生成
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    Task ExecuteSampleByExamPlanId(int examPlanId);
 
     /// <summary>
     /// 导出抽样方案存档文件
     /// </summary>
     /// <param name="id"></param>
     /// <param name="hideIdNumber"></param>
+    /// <param name="includeSpecialStudentCount">是否包含特殊学生数</param>
     /// <returns></returns>
-    Task<(string fileName, byte[] fileBytes)> ExportToArchived(int id, bool hideIdNumber = false);
+    Task<(string fileName, byte[] fileBytes)> ExportToArchived(int id, bool hideIdNumber = false, bool includeSpecialStudentCount = false);
     /// <summary>
     /// 导出给印刷厂和网阅机构文件
     /// </summary>
@@ -82,8 +107,9 @@ public interface IExamSampleService
     /// 导出抽样统计表
     /// </summary>
     /// <param name="id"></param>
+    /// <param name="includeSpecialStudentCount">是否包含特殊学生数</param>
     /// <returns></returns>
-    Task<(string fileName, byte[] fileBytes)> ExportSampleCount(int id);
+    Task<(string fileName, byte[] fileBytes)> ExportSampleCount(int id, bool includeSpecialStudentCount = false);
     /// <summary>
     /// 导出学校抽样统计表
     /// </summary>
@@ -105,6 +131,12 @@ public interface IExamSampleService
     /// <returns></returns>
     Task<List<ExamSampleOutput>> GetListByExamPlanId(int examPlanId);
     /// <summary>
+    /// 根据监测计划ID获取全部抽样方案的状态
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<List<ExamSampleStatusOutput>> GetStatusListByExamPlanId(int examPlanId);
+    /// <summary>
     /// 查询已发布抽样
     /// </summary>
     /// <param name="examDataPublishId">监测发布内容ID</param>

+ 69 - 0
YBEE.EQM.Application/Exam/ExamSampleReplace/Dtos/ExamSampleReplaceInput.cs

@@ -0,0 +1,69 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 抽取替补抽样输入参数
+/// </summary>
+public class SampleExamSampleReplaceInput
+{
+    /// <summary>
+    /// 缺测学生抽样ID
+    /// </summary>
+    [Required]
+    public long AbsentExamSampleStudentId { get; set; }
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [StringLength(200)]
+    public string Remark { get; set; }
+}
+/// <summary>
+/// 分页查询抽取替补抽样列表输入参数
+/// </summary>
+public class ExamSampleReplacePageInput : PageInputBase
+{
+    /// <summary>
+    /// 监测计划ID
+    /// </summary>
+    [Required]
+    public int ExamPlanId { get; set; }
+    /// <summary>
+    /// 年级ID
+    /// </summary>
+    public short? GradeId { get; set; }
+    /// <summary>
+    /// 班级号
+    /// </summary>
+    public short? ClassNumber { get; set; }
+    /// <summary>
+    /// 学生姓名
+    /// </summary>
+    public string Name { get; set; }
+    /// <summary>
+    /// 证件号码
+    /// </summary>
+    public string IdNumber { get; set; }
+    /// <summary>
+    /// 监测号
+    /// </summary>
+    public string ExamNumber { get; set; }
+    /// <summary>
+    /// 校区ID
+    /// </summary>
+    public short? SysOrgBranchId { get; set; }
+    /// <summary>
+    /// 机构ID
+    /// </summary>
+    public short? SysOrgId { get; set; }
+
+    /// <summary>
+    /// 替补学生也缺测
+    /// </summary>
+    public bool? IsReplaceAbsent { get; set; }
+
+    /// <summary>
+    /// 替补学生标注缺测是否已锁定
+    /// </summary>
+    public bool? IsReplaceAbsentLocked { get; set; }
+}

+ 101 - 0
YBEE.EQM.Application/Exam/ExamSampleReplace/Dtos/ExamSampleReplaceOutput.cs

@@ -0,0 +1,101 @@
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补抽样输出参数
+/// </summary>
+public class ExamSampleReplaceOutput : DEntityOutput
+{
+    /// <summary>
+    /// 抽样方案ID
+    /// </summary>
+    [Required]
+    public int ExamSampleId { get; set; }
+    /// <summary>
+    /// 监测计划ID
+    /// </summary>
+    [Required]
+    public int ExamPlanId { get; set; }
+    /// <summary>
+    /// 机构ID
+    /// </summary>
+    [Required]
+    public short SysOrgId { get; set; }
+    /// <summary>
+    /// 校区ID
+    /// </summary>
+    public short? SysOrgBranchId { get; set; }
+    /// <summary>
+    /// 监测年级ID
+    /// </summary>
+    [Required]
+    public short ExamGradeId { get; set; }
+    /// <summary>
+    /// 年级ID
+    /// </summary>
+    [Required]
+    public short GradeId { get; set; }
+    /// <summary>
+    /// 班级ID
+    /// </summary>
+    [Required]
+    public long SchoolClassId { get; set; }
+    /// <summary>
+    /// 班号
+    /// </summary>
+    [Required]
+    public short ClassNumber { get; set; }
+
+    /// <summary>
+    /// 缺测学生抽样ID
+    /// </summary>
+    [Required]
+    public long AbsentExamSampleStudentId { get; set; }
+    /// <summary>
+    /// 替补学生抽样ID
+    /// </summary>
+    [Required]
+    public long ReplaceExamSampleStudentId { get; set; }
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string Remark { get; set; }
+
+    /// <summary>
+    /// 替补学生也缺测
+    /// </summary>
+    [Required]
+    public bool IsReplaceAbsent { get; set; }
+
+    /// <summary>
+    /// 替补学生标注缺测是否已锁定
+    /// </summary>
+    [Required]
+    public bool IsReplaceAbsentLocked { get; set; }
+
+    /// <summary>
+    /// 抽样方案
+    /// </summary>
+    public ExamSampleLiteOutput ExamSample { get; set; }
+    /// <summary>
+    /// 缺测学生
+    /// </summary>
+    public ExamSampleStudentOutput AbsentExamSampleStudent { get; set; }
+    /// <summary>
+    /// 替补学生
+    /// </summary>
+    public ExamSampleStudentOutput ReplaceExamSampleStudent { get; set; }
+
+    /// <summary>
+    /// 班级
+    /// </summary>
+    public SchoolClassLiteOutput SchoolClass { get; set; }
+    /// <summary>
+    /// 校区
+    /// </summary>
+    public SysOrgLiteOutput SysOrgBranch { get; set; }
+    /// <summary>
+    /// 监测年级
+    /// </summary>
+    public ExamGradeOutput ExamGrade { get; set; }
+}

+ 63 - 0
YBEE.EQM.Application/Exam/ExamSampleReplace/ExamSampleReplaceAppService.cs

@@ -0,0 +1,63 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补抽样服务
+/// </summary>
+[ApiDescriptionSettings(Name = "exam-sample-replace")]
+[Route("exam/sample/replace")]
+public class ExamSampleReplaceAppService(IExamSampleReplaceService examSampleReplaceService) : IDynamicApiController
+{
+    /// <summary>
+    /// 抽取
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task Sample(SampleExamSampleReplaceInput input)
+    {
+        await examSampleReplaceService.Sample(input);
+    }
+    /// <summary>
+    /// 标记替补为缺测
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet]
+    public async Task MarkedReplaceAbsent([FromQuery][Required] int id)
+    {
+        await examSampleReplaceService.MarkedReplaceAbsent(id);
+    }
+    /// <summary>
+    /// 软删除
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task FakeDelete(BaseId input)
+    {
+        await examSampleReplaceService.FakeDelete(input);
+    }
+    /// <summary>
+    /// 导出缺测替补名单
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<IActionResult> ExportToOrg([FromQuery][Required] int examPlanId)
+    {
+        var (fileName, fileBytes) = await examSampleReplaceService.ExportToOrg(examPlanId);
+        return new FileContentResult(fileBytes, "application/octet-stream")
+        {
+            FileDownloadName = fileName,
+        };
+    }
+    /// <summary>
+    /// 分页查询缺测替补抽样列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamSampleReplaceOutput>> QueryOrgPageList(ExamSampleReplacePageInput input)
+    {
+        return await examSampleReplaceService.QueryOrgPageList(input);
+    }
+}

+ 280 - 0
YBEE.EQM.Application/Exam/ExamSampleReplace/Services/ExamSampleReplaceService.cs

@@ -0,0 +1,280 @@
+using Furion.DatabaseAccessor.Extensions;
+using NPOI.SS.UserModel;
+using NPOI.SS.Util;
+using NPOI.XSSF.UserModel;
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补抽样服务
+/// </summary>
+public class ExamSampleReplaceService(IRepository<ExamSampleReplace> replaceRep, IRepository<ExamSampleStudent> stuRep, IExportExcelService exportExcelService) : IExamSampleReplaceService, ITransient
+{
+    /// <summary>
+    /// 抽取
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task Sample(SampleExamSampleReplaceInput input)
+    {
+        var orgId = CurrentSysUserInfo.SysOrgId;
+
+        // 缺测学生
+        var absentStu = await stuRep.DetachedEntities
+                                    .Include(t => t.ExamStudent).Include(t => t.ExamSample)
+                                    .FirstOrDefaultAsync(t => t.Id == input.AbsentExamSampleStudentId && t.ExamStudent.SysOrgId == orgId)
+                                    ?? throw Oops.Oh(ErrorCode.E2001);
+        // 替补列表
+        var rps = await replaceRep.DetachedEntities
+                                  .Where(t => t.ExamSampleId == absentStu.ExamSampleId && t.SysOrgId == orgId && t.IsDeleted == false)
+                                  .Select(t => new { t.AbsentExamSampleStudentId, t.ReplaceExamSampleStudentId, t.IsReplaceAbsent })
+                                  .ToListAsync();
+
+        // 如果已添加缺测,并且替补未被标记为缺测的情况则报错
+        if (rps.Any(t => t.AbsentExamSampleStudentId == absentStu.Id && t.IsReplaceAbsent == false))
+        {
+            throw Oops.Oh(ErrorCode.E3010);
+        }
+
+        // 所有学生
+        var stus = await stuRep.DetachedEntities.Where(t => t.ExamSampleId == absentStu.ExamSampleId &&
+                                                            t.ExamStudent.SysOrgId == orgId &&
+                                                            t.ExamStudent.GradeId == absentStu.ExamStudent.GradeId &&
+                                                            t.ExamStudent.SchoolClassId == absentStu.ExamStudent.SchoolClassId &&
+                                                            t.IsSpecialStudent == false)
+                                                .OrderByDescending(t => t.PreTotalScore).ThenBy(t => t.ExamStudentId)
+                                                .ToListAsync();
+        if (stus.Count == 0)
+        {
+            throw Oops.Oh(ErrorCode.E3009);
+        }
+
+        // 去掉已抽为替补的
+        var ers = rps.Select(t => t.ReplaceExamSampleStudentId).ToList();
+        stus = stus.Where(t => !ers.Contains(t.Id)).ToList();
+
+        ExamSampleStudent rstu = null;
+        // 有前置成绩按顺序抽取,无随机抽取
+        if (stus.Any(t => t.PreTotalScore > 0))
+        {
+            // 向上找紧挨的1位
+            rstu = stus.Where(t => t.PreTotalScore >= absentStu.PreTotalScore && t.Id != absentStu.Id && t.ExamSampleType == ExamSampleType.SCHOOL_EXAM).LastOrDefault();
+            // 向上未找到则向下找紧挨的1位
+            rstu ??= stus.Where(t => t.PreTotalScore <= absentStu.PreTotalScore && t.Id != absentStu.Id && t.ExamSampleType == ExamSampleType.SCHOOL_EXAM).FirstOrDefault();
+        }
+        else
+        {
+            var nstus = stus.Where(t => t.Id != absentStu.Id && t.ExamSampleType == ExamSampleType.SCHOOL_EXAM).ToList();
+            if (nstus.Count > 0)
+            {
+                var rand = new Random();
+                var si = rand.Next(0, nstus.Count);
+                rstu = nstus[si];
+            }
+        }
+        if (rstu is null)
+        {
+            throw Oops.Oh(ErrorCode.E3009);
+        }
+
+        // 同一缺测生抽的其他替补缺测锁定
+        var rps2 = await replaceRep.Where(t => t.ExamSampleId == absentStu.ExamSampleId &&
+                                               t.SysOrgId == CurrentSysUserInfo.SysOrgId &&
+                                               t.AbsentExamSampleStudentId == absentStu.Id &&
+                                               t.IsDeleted == false &&
+                                               t.IsReplaceAbsent == true
+                                   ).ToListAsync();
+        foreach (var rp in rps2)
+        {
+            rp.IsReplaceAbsentLocked = true;
+            await rp.UpdateIncludeAsync([nameof(rp.IsReplaceAbsentLocked)]);
+        }
+
+        ExamSampleReplace item = new()
+        {
+            ExamSampleId = absentStu.ExamSampleId,
+            ExamPlanId = absentStu.ExamSample.ExamPlanId,
+            SysOrgId = absentStu.ExamStudent.SysOrgId,
+            SysOrgBranchId = absentStu.ExamStudent.SysOrgBranchId,
+            SchoolClassId = absentStu.ExamStudent.SchoolClassId,
+            ExamGradeId = absentStu.ExamStudent.ExamGradeId,
+            GradeId = absentStu.ExamStudent.GradeId,
+            ClassNumber = absentStu.ExamStudent.ClassNumber,
+            AbsentExamSampleStudentId = absentStu.Id,
+            ReplaceExamSampleStudentId = rstu.Id,
+            Remark = input.Remark,
+            IsReplaceAbsent = false,
+            IsReplaceAbsentLocked = false,
+        };
+        await replaceRep.InsertAsync(item);
+    }
+    /// <summary>
+    /// 标记替补为缺测
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    public async Task MarkedReplaceAbsent(int id)
+    {
+        var item = await replaceRep.FirstOrDefaultAsync(t => t.Id == id && t.IsReplaceAbsentLocked == false) ?? throw Oops.Oh(ErrorCode.E2001);
+        item.IsReplaceAbsent = !item.IsReplaceAbsent;
+        await item.UpdateIncludeAsync([nameof(item.IsReplaceAbsent)]);
+
+        // 同一缺测生抽的其他替补缺测解除锁定
+        var rps2 = await replaceRep.Where(t => t.ExamSampleId == item.ExamSampleId &&
+                                               t.SysOrgId == CurrentSysUserInfo.SysOrgId &&
+                                               t.AbsentExamSampleStudentId == item.AbsentExamSampleStudentId &&
+                                               t.Id != item.Id &&
+                                               t.IsDeleted == false &&
+                                               t.IsReplaceAbsent == true
+                                   ).ToListAsync();
+        foreach (var rp in rps2)
+        {
+            rp.IsReplaceAbsentLocked = !item.IsReplaceAbsent;
+            await rp.UpdateIncludeAsync([nameof(rp.IsReplaceAbsentLocked)]);
+        }
+    }
+    /// <summary>
+    /// 软删除
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task FakeDelete(BaseId input)
+    {
+        var item = await replaceRep.FirstOrDefaultAsync(t => t.Id == input.Id && t.IsDeleted == false) ?? throw Oops.Oh(ErrorCode.E2001);
+        item.IsDeleted = true;
+        await item.UpdateIncludeNowAsync([nameof(item.IsDeleted)]);
+    }
+    /// <summary>
+    /// 导出缺测替补名单
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    public async Task<(string fileName, byte[] fileBytes)> ExportToOrg(int examPlanId)
+    {
+        var res = await QueryOrgPageList(new()
+        {
+            ExamPlanId = examPlanId,
+            IsReplaceAbsent = false,
+            PageIndex = 1,
+            PageSize = 9999
+        });
+
+        var hasBranch = res.Items.Any(t => t.SysOrgBranchId.HasValue && t.SysOrgBranchId > 0);
+
+        XSSFWorkbook wb = new();
+        ISheet sheet = wb.CreateSheet();
+        sheet.DisplayGridlines = false;
+
+        // 获取样式
+        var cellStyle = exportExcelService.GetCellStyle(wb);
+
+        #region 表头
+        int rowNum = 0;
+
+        IRow headerRow1 = sheet.CreateRow(rowNum);
+        headerRow1.Height = ExportExcelCellStyle.DefaultRowHeight;
+        int ci = 0;
+        exportExcelService.AddCell("序", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 6);
+        if (hasBranch)
+        {
+            exportExcelService.AddCell("校区", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        }
+        exportExcelService.AddCell("年级", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 10);
+        exportExcelService.AddCell("班级", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 8);
+
+        exportExcelService.AddCell("缺测学生", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        //exportExcelService.AddCell(null, headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
+        exportExcelService.AddCell(null, headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 14);
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum, rowNum, ci - 2, ci - 1));
+
+        exportExcelService.AddCell("替补学生", headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 12);
+        //exportExcelService.AddCell(null, headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 20);
+        exportExcelService.AddCell(null, headerRow1, ci++, cellStyle.ColumnHeaderStyle, sheet, 14);
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum, rowNum, ci - 2, ci - 1));
+
+        exportExcelService.AddCell("抽取时间", headerRow1, ci, cellStyle.ColumnHeaderStyle, sheet, 20);
+
+        IRow headerRow2 = sheet.CreateRow(++rowNum);
+        headerRow2.Height = ExportExcelCellStyle.DefaultRowHeight;
+        int lci = hasBranch ? 4 : 3;
+        for (ci = 0; ci < lci; ci++)
+        {
+            exportExcelService.AddCell(null, headerRow2, ci, cellStyle.ColumnHeaderStyle);
+            sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum, ci, ci));
+        }
+        exportExcelService.AddCell("姓名", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+        //exportExcelService.AddCell("证件号码", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+        exportExcelService.AddCell("监测号", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+
+        exportExcelService.AddCell("姓名", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+        //exportExcelService.AddCell("证件号码", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+        exportExcelService.AddCell("监测号", headerRow2, ci++, cellStyle.ColumnHeaderStyle);
+
+        exportExcelService.AddCell(null, headerRow2, ci, cellStyle.ColumnHeaderStyle);
+        sheet.AddMergedRegion(new CellRangeAddress(rowNum - 1, rowNum, ci, ci));
+
+        sheet.CreateFreezePane(0, 2);
+        #endregion
+
+        int rn = 0;
+        foreach (var item in res.Items)
+        {
+            IRow row = sheet.CreateRow(++rowNum);
+            row.Height = ExportExcelCellStyle.DefaultRowHeight;
+
+            int rci = 0;
+            ICellStyle cstyle = cellStyle.CenterCellStyle;
+
+            exportExcelService.AddCell(++rn, row, rci++, cstyle);
+            if (hasBranch)
+            {
+                exportExcelService.AddCell(item.SysOrgBranch?.Name ?? "", row, rci++, cstyle);
+            }
+            exportExcelService.AddCell(item.ExamGrade.Grade.Name, row, rci++, cstyle);
+            exportExcelService.AddCell(item.ClassNumber, row, rci++, cstyle);
+
+            exportExcelService.AddCell(item.AbsentExamSampleStudent.ExamStudent.Name, row, rci++, cstyle);
+            //exportExcelService.AddCell(item.AbsentExamSampleStudent.ExamStudent.IdNumber, row, rci++, cstyle);
+            exportExcelService.AddCell(item.AbsentExamSampleStudent.ExamNumber, row, rci++, cstyle);
+
+            exportExcelService.AddCell(item.ReplaceExamSampleStudent.ExamStudent.Name, row, rci++, cstyle);
+            //exportExcelService.AddCell(item.ReplaceExamSampleStudent.ExamStudent.IdNumber, row, rci++, cstyle);
+            exportExcelService.AddCell(item.ReplaceExamSampleStudent.ExamNumber, row, rci++, cstyle);
+
+            exportExcelService.AddCell(item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"), row, rci++, cstyle);
+        }
+
+        MemoryStream ms = new();
+        wb.Write(ms, false);
+        ms.Flush();
+
+        return ($"监测替补抽取表{DateTime.Now:yyyyMMddHHmmss}.xlsx", ms.ToArray());
+    }
+    /// <summary>
+    /// 分页查询缺测替补抽样列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public async Task<PageResult<ExamSampleReplaceOutput>> QueryOrgPageList(ExamSampleReplacePageInput input)
+    {
+        var name = !string.IsNullOrEmpty(input.Name?.Trim());
+        var idNumber = !string.IsNullOrEmpty(input.IdNumber?.Trim());
+        var examNumber = !string.IsNullOrEmpty(input.ExamNumber?.Trim());
+
+        var ret = await replaceRep.DetachedEntities
+                                  .Where(t => t.ExamPlanId == input.ExamPlanId && t.SysOrgId == CurrentSysUserInfo.SysOrgId && t.IsDeleted == false)
+                                  .Where((name, u => EF.Functions.Like(u.AbsentExamSampleStudent.ExamStudent.Name, $"%{input.Name.Trim()}%") || EF.Functions.Like(u.ReplaceExamSampleStudent.ExamStudent.Name, $"%{input.Name.Trim()}%")))
+                                  .Where((idNumber, u => EF.Functions.Like(u.AbsentExamSampleStudent.ExamStudent.IdNumber, $"%{input.IdNumber.Trim()}%") || EF.Functions.Like(u.ReplaceExamSampleStudent.ExamStudent.IdNumber, $"%{input.IdNumber.Trim()}%")))
+                                  .Where((examNumber, u => EF.Functions.Like(u.AbsentExamSampleStudent.ExamNumber, $"%{input.ExamNumber.Trim()}%") || EF.Functions.Like(u.ReplaceExamSampleStudent.ExamNumber, $"%{input.ExamNumber.Trim()}%")))
+                                  .Where(input.GradeId.HasValue, t => t.GradeId == input.GradeId)
+                                  .Where(input.ClassNumber.HasValue, t => t.ClassNumber == input.ClassNumber)
+                                  .Where(input.SysOrgBranchId.HasValue, t => t.SysOrgBranchId == input.SysOrgBranchId)
+                                  .Where(input.IsReplaceAbsent.HasValue, t => t.IsReplaceAbsent == input.IsReplaceAbsent)
+                                  .Where(input.IsReplaceAbsentLocked.HasValue, t => t.IsReplaceAbsentLocked == input.IsReplaceAbsentLocked)
+                                  .ProjectToType<ExamSampleReplaceOutput>()
+                                  .OrderBy(t => t.SysOrgBranchId).ThenBy(t => t.GradeId).ThenBy(t => t.ClassNumber).ThenByDescending(t => t.CreateTime)
+                                  .ToADPagedListAsync(input.PageIndex, input.PageSize);
+        return ret;
+    }
+}

+ 40 - 0
YBEE.EQM.Application/Exam/ExamSampleReplace/Services/IExamSampleReplaceService.cs

@@ -0,0 +1,40 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+/// <summary>
+/// 缺测替补抽样服务
+/// </summary>
+public interface IExamSampleReplaceService
+{
+    /// <summary>
+    /// 抽取
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task Sample(SampleExamSampleReplaceInput input);
+    /// <summary>
+    /// 标记替补为缺测
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    Task MarkedReplaceAbsent(int id);
+    /// <summary>
+    /// 软删除
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task FakeDelete(BaseId input);
+    /// <summary>
+    /// 导出缺测替补名单
+    /// </summary>
+    /// <param name="examPlanId"></param>
+    /// <returns></returns>
+    Task<(string fileName, byte[] fileBytes)> ExportToOrg(int examPlanId);
+    /// <summary>
+    /// 分页查询缺测替补抽样列表
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    Task<PageResult<ExamSampleReplaceOutput>> QueryOrgPageList(ExamSampleReplacePageInput input);
+}

+ 9 - 0
YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentInput.cs

@@ -42,6 +42,11 @@ public class AddExamSampleStudentInput
     /// </summary>
     [Required]
     public decimal PreTotalScore { get; set; } = 0;
+    /// <summary>
+    /// 循环次数
+    /// </summary>
+    [Required]
+    public short CyclicNumber { get; set; } = 0;
 }
 
 /// <summary>
@@ -83,6 +88,10 @@ public class ExamSampleStudentPageInput : PageInputBase
     /// </summary>
     public short? SysOrgBranchId { get; set; }
     /// <summary>
+    /// 机构ID
+    /// </summary>
+    public short? SysOrgId { get; set; }
+    /// <summary>
     /// 抽样类型
     /// </summary>
     public ExamSampleType? ExamSampleType { get; set;}

+ 14 - 0
YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentMapper.cs

@@ -0,0 +1,14 @@
+using YBEE.EQM.Core;
+
+namespace YBEE.EQM.Application;
+
+public class ExamSampleStudentMapper : IRegister
+{
+    public void Register(TypeAdapterConfig config)
+    {
+        config.ForType<ExamSampleStudent, ExamSampleStudentOrgOutput>()
+              .Map(d => d.SysOrgName, s => s.ExamStudent.SysOrg.Name)
+              .Map(d => d.SysOrgBranchName, s => s.ExamStudent.SysOrgBranch != null ? s.ExamStudent.SysOrgBranch.Name : "")
+              ;
+    }
+}

+ 21 - 0
YBEE.EQM.Application/Exam/ExamSampleStudent/Dtos/ExamSampleStudentOutput.cs

@@ -47,9 +47,30 @@ public class ExamSampleStudentOutput
     /// </summary>
     [Required]
     public decimal PreTotalScore { get; set; }
+    /// <summary>
+    /// 循环次数
+    /// </summary>
+    [Required]
+    public short CyclicNumber { get; set; } = 0;
 
     /// <summary>
     /// 监测学生
     /// </summary>
     public ExamStudentOutput ExamStudent { get; set; }
 }
+
+/// <summary>
+/// 抽样学生输出参数
+/// </summary>
+public class ExamSampleStudentOrgOutput: ExamSampleStudentOutput
+{
+    /// <summary>
+    /// 学校名称
+    /// </summary>
+    [Required]
+    public string SysOrgName { get; set; }
+    /// <summary>
+    /// 校区名称
+    /// </summary>
+    public string SysOrgBranchName { get; set; }
+}

Vissa filer visades inte eftersom för många filer har ändrats