Commit 20a651b9 authored by drigle's avatar drigle

feat: implement immutable problem rectification archives

parent 439b3b69
......@@ -2,6 +2,7 @@ import FormDatePicker from '@/components/FormDatePicker';
import ResourceFilterMultiSelect from '@/components/ResourceFilterMultiSelect';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import {
Archive,
CalendarDaysIcon,
ChevronRightIcon,
Close,
......@@ -71,6 +72,7 @@ import {
FLOW_MODE_BATCH,
FLOW_MODE_DELAY_DEPARTMENT,
FLOW_MODE_DEPARTMENT,
archiveProblemRectificationRecords,
buildDepartmentAssignmentsFromRecord,
buildDepartmentDelayRequestItems,
buildDelayRequestItemsBatchUpdateData,
......@@ -801,6 +803,8 @@ export default function ProblemRectificationListPage() {
const [delayDetailRecord, setDelayDetailRecord] = useState(null);
const [cloneTargetRecord, setCloneTargetRecord] = useState(null);
const [deleteTargetRecords, setDeleteTargetRecords] = useState([]);
const [archiveTargetRecords, setArchiveTargetRecords] = useState([]);
const [archiveError, setArchiveError] = useState('');
const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
......@@ -1213,6 +1217,46 @@ export default function ProblemRectificationListPage() {
}
};
const handleArchive = (targetRecords) => {
if (!targetRecords || targetRecords.length === 0) {
setError('请先选择需要归档的记录。');
return;
}
setError('');
setArchiveError('');
setArchiveTargetRecords(targetRecords);
};
const confirmArchive = async () => {
const targetRecords = archiveTargetRecords;
if (targetRecords.length === 0) {
return;
}
const recordIds = targetRecords.map((record) => record.id).filter(Boolean);
if (recordIds.length !== targetRecords.length) {
setError('选中记录缺少可归档的 ID,请刷新列表后重试。');
return;
}
setActionLoading(true);
setError('');
setArchiveError('');
setSuccessMessage('');
try {
const result = await archiveProblemRectificationRecords(recordIds);
setArchiveTargetRecords([]);
setSelectedIds([]);
setSuccessMessage(`归档成功,已生成“${result.batchTitle}”及 ${result.count} 条独立快照。`);
} catch (requestError) {
setArchiveError(requestError?.message || '问题整改归档失败,请稍后重试。');
} finally {
setActionLoading(false);
}
};
const handleClone = async () => {
if (!cloneTargetRecord) {
return;
......@@ -1887,6 +1931,14 @@ export default function ProblemRectificationListPage() {
actionLoading,
onClick: openDelayPushModal,
},
{
key: 'archive-selected',
label: '一键归档',
icon: Archive,
variant: 'ghost',
disabled: () => selectedIds.length === 0 || actionLoading,
onClick: () => handleArchive(selectedRecords),
},
{
key: 'delete-selected',
label: '删除选中',
......@@ -2558,6 +2610,60 @@ export default function ProblemRectificationListPage() {
</Alert>
</XinyuanModal>
) : null}
{archiveTargetRecords.length > 0 ? (
<XinyuanModal
isOpen={archiveTargetRecords.length > 0}
onOpenChange={(isOpen) => {
if (!isOpen && !actionLoading) {
setArchiveError('');
setArchiveTargetRecords([]);
}
}}
size="md"
title="确认一键归档"
description={`将为选中的 ${archiveTargetRecords.length} 条问题生成独立归档快照,并创建一个归档批次。`}
footer={
<>
<HeroButton
isDisabled={actionLoading}
size="sm"
variant="secondary"
onPress={() => {
setArchiveError('');
setArchiveTargetRecords([]);
}}
>
取消
</HeroButton>
<HeroButton
isDisabled={actionLoading}
isPending={actionLoading}
size="sm"
variant="primary"
onPress={confirmArchive}
>
{actionLoading ? '归档中...' : '确认归档'}
</HeroButton>
</>
}
>
{archiveError ? (
<Alert status="danger">
<Alert.Content>
<Alert.Description>{archiveError}</Alert.Description>
</Alert.Content>
</Alert>
) : null}
<Alert status="warning">
<Alert.Content>
<Alert.Description>
归档不会锁定、删除或修改原问题;原问题后续变化不会更新本次快照。
</Alert.Description>
</Alert.Content>
</Alert>
</XinyuanModal>
) : null}
</>
);
}
......@@ -501,7 +501,8 @@
- 功能入口:`/problem_rectification` 主列表,用户勾选一条或多条问题后点击“一键归档”
- 数据对象:归档明细写入 `problem_rectification_archive_records`;归档批次写入 `issue_rectification_archive`
- 发起校验:必须至少选择 1 条问题;归档动作生成快照,不改变原问题记录状态,也不自动锁定、删除或结束流程
- 明细转换:按 `problem_rectification_archive_records` 字段结构从主记录 `metadata` 映射归档字段;`responsible_department` 转为逗号拼接文本;`fill_in_the_people`、`review_people` 只保存用户 `_id[]`;`rectification_proof_materials` 作为文件对象保留;日期字段继续保存 ISO 8601 字符串
- 明细转换:按 `problem_rectification_archive_records` 的 51 个字段从主记录 `metadata` 生成独立快照;`responsible_department` 转为逗号拼接文本;`fill_in_the_people`、`review_people` 保持 `user` 类型并只保存用户 `_id[]`;`rectification_proof_materials` 作为文件对象保留;日期字段继续保存 ISO 8601 字符串
- 嵌套快照:`department_rectification_items` 与 `problem_rectification_delay_request_items` 在归档明细中均使用多值 `object`,归档时深拷贝整组对象,不使用 `subject` 或 `reference` 关联原问题中的嵌套数据
- 批次写入:归档明细导入成功后创建 1 条 `issue_rectification_archive` 批次记录,标题格式为“问题整改归档 YYYY-MM-DD”,并在 `metadata.reference_records` 中保存本批次所有归档明细 `_id`
- 归档查看:`/issue_rectification_archive` 展示归档批次列表;批次详情通过 `reference_records` 追溯归档明细数量和具体快照内容
- 异常限制:归档是一次性历史快照,原问题记录后续编辑不会反向更新归档明细;若归档明细导入失败,不创建批次;若批次创建失败,需要提示用户本次归档未完整闭环,避免出现孤立快照无人可查
......@@ -714,7 +715,7 @@
1. 在问题整改列表中勾选一条或多条记录
2. 点击“一键归档”
3. 前端按 `problem_rectification_archive_records` 的字段结构转换当前问题数据,调用 `/record/import` 导入归档明细快照
3. 确认弹窗说明归档不修改原问题;用户确认后,前端重新读取所选问题详情及 `problem_rectification_archive_records` 的实时字段定义,仅按归档对象字段白名单转换当前问题数据,并调用 `/record/import` 导入归档明细快照
4. 归档明细导入成功后,创建 1 条 `issue_rectification_archive` 批次记录,写入 `metadata.reference_records = 归档明细 _id[]`
5. 归档列表页展示归档批次,批次详情通过 `reference_records` 追溯批次下明细
6. 归档动作不修改原 `problem_rectification` 记录,不自动锁定,不自动删除,也不改变正在进行中的推送或延期流程
......@@ -724,8 +725,10 @@
- 原主表中的多值选择 `responsible_department` 在旧实现中被转成逗号拼接文本
- `fill_in_the_people`、`review_people` 继续保存为用户 `_id[]`
- `rectification_proof_materials` 按线上字段的单值或多值形态保留文件对象或文件对象数组
- 主记录的治理字段、锁定状态、部门整改明细和延期申请明细均写入 `problem_rectification_archive_records`;两组嵌套明细以多值 `object` 深拷贝保存,不建立到原问题嵌套数据的引用
- 归档是快照,后续原问题记录再变化,不应反向污染归档结果
- 归档明细导入失败时不创建归档批次;归档批次创建失败时应提示用户归档未完整完成,避免出现无法从批次入口追溯的孤立明细
- 归档期间按钮进入加载状态并禁止重复提交;读取原记录条数、导入返回的归档明细 ID 数量与用户选择数必须一致,否则中止创建批次并提示刷新或联系管理员处理
---
......
function toArray(value) {
if (value === undefined || value === null || value === '') {
return [];
}
return Array.isArray(value) ? value : [value];
}
function normalizeText(value) {
return value === undefined || value === null ? '' : String(value).trim();
}
function cloneArchiveValue(value) {
if (value === undefined || value === null || typeof value !== 'object') {
return value;
}
return JSON.parse(JSON.stringify(value));
}
export function normalizeArchiveRecordId(value) {
if (!value) {
return '';
}
if (typeof value === 'object') {
return normalizeText(value._id || value.id || value.name);
}
return normalizeText(value);
}
function normalizeArchiveDate(value, fieldName) {
if (!value) {
return value;
}
const parsedDate = new Date(value);
if (Number.isNaN(parsedDate.getTime())) {
throw new Error(`归档字段“${fieldName}”不是有效日期,请修正后重试。`);
}
return parsedDate.toISOString();
}
function normalizeArchiveFieldValue(value, field) {
if (value === undefined) {
return undefined;
}
if (field.name === 'responsible_department') {
return toArray(value)
.map((item) => normalizeText(item))
.filter(Boolean)
.join(',');
}
if (field.type === 'user') {
const userIds = toArray(value).map(normalizeArchiveRecordId).filter(Boolean);
return field.multiple ? userIds : userIds[0] || null;
}
if (field.type === 'date') {
if (field.multiple) {
return toArray(value).map((item) => normalizeArchiveDate(item, field.label || field.name));
}
return normalizeArchiveDate(value, field.label || field.name);
}
if (field.type === 'object' && typeof value === 'string') {
try {
return cloneArchiveValue(JSON.parse(value));
} catch {
throw new Error(`归档字段“${field.label || field.name}”不是有效 JSON,请修正后重试。`);
}
}
return cloneArchiveValue(value);
}
export function buildProblemRectificationArchiveSnapshot(record, archiveSubject) {
const metadata = record?.metadata || {};
const archiveMetadata = {};
toArray(archiveSubject?.fields).forEach((field) => {
if (!field?.name) {
return;
}
const value = normalizeArchiveFieldValue(metadata[field.name], field);
if (value !== undefined) {
archiveMetadata[field.name] = value;
}
});
return {
title: record?.title || metadata.project_name || metadata.problem_brief || undefined,
metadata: archiveMetadata,
};
}
import dayjs from 'dayjs';
import {
countRecords,
createRecord,
getSubjectView,
getSubjectViewDetail,
importRecords,
loadSubject,
queryRecords,
} from '@/services/DataService';
......@@ -29,8 +31,16 @@ import {
sortRecordsByView,
getViewRestrictionDefaults,
} from '@/utils/subjectView';
import {
buildProblemRectificationArchiveSnapshot,
normalizeArchiveRecordId,
} from './problemRectificationArchiveSnapshot.mjs';
export { buildProblemRectificationArchiveSnapshot };
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
export const ISSUE_RECTIFICATION_ARCHIVE_SUBJECT = 'issue_rectification_archive';
export const PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT = 'problem_rectification_archive_records';
export const PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS = [20, 50, 100];
export const DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE = 20;
export const DEPARTMENT_RECTIFICATION_FIELD = 'department_rectification_items';
......@@ -97,6 +107,73 @@ const FOCUS_FILTER_OPTIONS = [
{ label: '制度/系统整改类', value: 'policy_or_system' },
];
function extractImportedRecordIds(importResult) {
const insertedIds = importResult?.insertedIds || importResult?.data?.insertedIds || {};
return Object.values(insertedIds).map(normalizeArchiveRecordId).filter(Boolean);
}
export async function archiveProblemRectificationRecords(recordIds = []) {
const normalizedRecordIds = Array.from(
new Set(recordIds.map(normalizeArchiveRecordId).filter(Boolean)),
);
if (normalizedRecordIds.length === 0) {
throw new Error('请先选择需要归档的问题整改记录。');
}
const [sourceResult, archiveSubject] = await Promise.all([
queryRecords({
subject: PROBLEM_RECTIFICATION_SUBJECT,
page: 0,
page_size: normalizedRecordIds.length,
filter: { _id: { $in: normalizedRecordIds } },
mode: 'full',
}),
loadSubject(PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT),
]);
const sourceRecords = sourceResult?.records || [];
if (sourceRecords.length !== normalizedRecordIds.length) {
throw new Error(
`归档前只读取到 ${sourceRecords.length}/${normalizedRecordIds.length} 条原记录,请刷新列表后重试。`,
);
}
const snapshots = sourceRecords.map((record) =>
buildProblemRectificationArchiveSnapshot(record, archiveSubject),
);
const importResult = await importRecords(
PROBLEM_RECTIFICATION_ARCHIVE_RECORDS_SUBJECT,
snapshots,
);
const archiveRecordIds = extractImportedRecordIds(importResult);
if (archiveRecordIds.length !== snapshots.length) {
throw new Error(
`归档明细写入不完整:预期 ${snapshots.length} 条,实际返回 ${archiveRecordIds.length} 条,未创建归档批次。`,
);
}
const batchTitle = `问题整改归档 ${dayjs().format('YYYY-MM-DD')}`;
try {
const batchRecord = await createRecord({
subject: ISSUE_RECTIFICATION_ARCHIVE_SUBJECT,
title: batchTitle,
metadata: {
reference_records: archiveRecordIds,
},
});
return {
batchRecord,
batchTitle,
archiveRecordIds,
count: archiveRecordIds.length,
};
} catch (error) {
throw new Error(
`已生成 ${archiveRecordIds.length} 条归档快照,但归档批次创建失败;请联系管理员处理孤立快照。${error?.message ? ` ${error.message}` : ''}`,
);
}
}
function toArray(value) {
if (!value) {
return [];
......
......@@ -99,63 +99,9 @@ type Record = {
<!-- SUBJECT_SCHEMA_SYNC:START -->
> 同步根对象:`am_material`、`issue_rectification_archive`、`message_reference_id`、`problem_rectification`、`problem_rectification_archive_records`
> 同步根对象:`problem_rectification`、`issue_rectification_archive`、`message_reference_id`、`am_material`
> 当前依赖闭包:7 个 Subject。
### `am_material`:审计材料文件
- 类型:`normal`
- 字段数量:14
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 材料名称 | `material_title` | text | 否 | 是 | 否 | |
| 文件对象 | `file_asset` | file | 否 | 是 | 否 | |
| 来源应用 | `source_app` | text | 否 | 否 | 否 | |
| 来源系统 | `source_system` | select | 否 | 否 | 否 | options: 投资交易系统, 估值系统, 注册登记系统, OA系统, 财务系统, 产品系统, 投研系统, 外部导出, 其他业务系统 |
| 材料分类 | `archive_category` | select | 否 | 否 | 否 | options: 审计需求, 纪检需求 |
| 上传人 | `uploader` | user | 否 | 是 | 否 | |
| 上传时间 | `uploaded_at` | date | 否 | 是 | 否 | |
| 备注 | `remark` | text | 否 | 否 | 否 | |
| 标签 | `tag` | text | 否 | 否 | 否 | |
| 提供部门 | `provide_department` | text | 否 | 否 | 否 | |
| 提供时间 | `provide_time` | date | 否 | 否 | 否 | |
| 是否涉密 | `is_confidential` | boolean | 否 | 否 | 否 | |
| 是否审计部材料 | `is_audit_dept_material` | boolean | 否 | 否 | 否 | |
| 所属年份 | `archive_year` | text | 否 | 否 | 否 | |
### `issue_rectification_archive`:问题整改归档
- 类型:`normal`
- 字段数量:1
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 关联问题整改记录 | `reference_records` | reference | 是 | 否 | 否 | Subject: problem_rectification_archive_records |
### `message_reference_id`:信息关联id
- 类型:`normal`
- 字段数量:15
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 发送人 | `sender` | user | 否 | 否 | 否 | |
| 填写人 | `editor` | user | 是 | 否 | 否 | |
| 审核人 | `auditor` | user | 是 | 否 | 否 | |
| 约束key | `restriction_key` | text | 否 | 否 | 否 | |
| 约束值 | `restriction_value` | text | 是 | 否 | 否 | |
| 对象名 | `subject_name` | text | 否 | 否 | 否 | |
| 视图名 | `view_name` | text | 否 | 否 | 否 | |
| 关联记录ID | `reference_record_id` | text | 是 | 否 | 否 | |
| 填写人流程ID | `progress_id_editor` | text | 是 | 否 | 否 | |
| 审核人流程id | `progress_id_auditor` | text | 是 | 否 | 否 | |
| 发送人流程ID | `progress_id_sender` | text | 否 | 否 | 否 | |
| 填写人填写状态 | `editor_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 审批人审批状态 | `auditor_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 发送人审核状态 | `sender_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 部门明细 ID | `department_item_ids` | text | 是 | 否 | 否 | |
### `problem_rectification`:问题整改
- 类型:`normal`
......@@ -214,33 +160,59 @@ type Record = {
| 支持材料质量 | `support_material_quality` | text | 否 | 否 | 否 | |
| 是否重复发生 | `is_recurrence_occurred` | boolean | 否 | 否 | 否 | |
### `problem_rectification_archive_records`:问题整改归档数据
### `issue_rectification_archive`:问题整改归档
- 类型:`normal`
- 字段数量:20
- 字段数量:1
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 检查实施单位 | `inspect_the_implementing_unit` | text | 否 | 否 | 否 | options: 审计部, 风险管理部, 法律合规部, 鑫沅资产 |
| 检查实施时间 | `check_the_implementation_time` | text | 否 | 否 | 否 | |
| 检查报告出具日 | `inspection_report_date` | text | 否 | 否 | 否 | |
| 问题发现情况 | `problem_discovery_situation` | text | 否 | 否 | 否 | |
| 责任部门 | `responsible_department` | text | 否 | 否 | 否 | |
| 整改进度 | `rectification_progress` | text | 否 | 否 | 否 | |
| 整改情况 | `rectification_situation` | long_text | 否 | 否 | 否 | |
| 反馈单位 | `feedback_unit` | text | 否 | 否 | 否 | |
| 备注 | `remark` | long_text | 否 | 否 | 否 | |
| 预计整改完成时间 | `the_time_of_rectification` | date | 否 | 否 | 否 | |
| 填写人 | `fill_in_the_people` | user | 是 | 否 | 否 | |
| 复核人 | `review_people` | user | 是 | 否 | 否 | |
| 问题来源 | `problem_from` | text | 否 | 否 | 否 | |
| 问题类型 | `problem_type` | text | 否 | 否 | 否 | |
| 问题涉及业务领域 | `related_issue_domain` | text | 否 | 否 | 否 | |
| 问题简述 | `problem_brief` | text | 否 | 否 | 否 | |
| 审批状态 | `audit_status` | text | 否 | 否 | 否 | |
| 最终审批状态 | `final_audit_status` | text | 否 | 否 | 否 | |
| 检查项目名称 | `project_name` | text | 否 | 否 | 否 | |
| 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 | |
| 关联问题整改记录 | `reference_records` | reference | 是 | 否 | 否 | Subject: problem_rectification_archive_records |
### `message_reference_id`:信息关联id
- 类型:`normal`
- 字段数量:15
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 发送人 | `sender` | user | 否 | 否 | 否 | |
| 填写人 | `editor` | user | 是 | 否 | 否 | |
| 审核人 | `auditor` | user | 是 | 否 | 否 | |
| 约束key | `restriction_key` | text | 否 | 否 | 否 | |
| 约束值 | `restriction_value` | text | 是 | 否 | 否 | |
| 对象名 | `subject_name` | text | 否 | 否 | 否 | |
| 视图名 | `view_name` | text | 否 | 否 | 否 | |
| 关联记录ID | `reference_record_id` | text | 是 | 否 | 否 | |
| 填写人流程ID | `progress_id_editor` | text | 是 | 否 | 否 | |
| 审核人流程id | `progress_id_auditor` | text | 是 | 否 | 否 | |
| 发送人流程ID | `progress_id_sender` | text | 否 | 否 | 否 | |
| 填写人填写状态 | `editor_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 审批人审批状态 | `auditor_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 发送人审核状态 | `sender_status` | select | 否 | 否 | 否 | options: 未完成, 已完成 |
| 部门明细 ID | `department_item_ids` | text | 是 | 否 | 否 | |
### `am_material`:审计材料文件
- 类型:`normal`
- 字段数量:14
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 材料名称 | `material_title` | text | 否 | 是 | 否 | |
| 文件对象 | `file_asset` | file | 否 | 是 | 否 | |
| 来源应用 | `source_app` | text | 否 | 否 | 否 | |
| 来源系统 | `source_system` | select | 否 | 否 | 否 | options: 投资交易系统, 估值系统, 注册登记系统, OA系统, 财务系统, 产品系统, 投研系统, 外部导出, 其他业务系统 |
| 材料分类 | `archive_category` | select | 否 | 否 | 否 | options: 审计需求, 纪检需求 |
| 上传人 | `uploader` | user | 否 | 是 | 否 | |
| 上传时间 | `uploaded_at` | date | 否 | 是 | 否 | |
| 备注 | `remark` | text | 否 | 否 | 否 | |
| 标签 | `tag` | text | 否 | 否 | 否 | |
| 提供部门 | `provide_department` | text | 否 | 否 | 否 | |
| 提供时间 | `provide_time` | date | 否 | 否 | 否 | |
| 是否涉密 | `is_confidential` | boolean | 否 | 否 | 否 | |
| 是否审计部材料 | `is_audit_dept_material` | boolean | 否 | 否 | 否 | |
| 所属年份 | `archive_year` | text | 否 | 否 | 否 | |
### `department_rectification_items`:部门整改明细
......@@ -267,6 +239,65 @@ type Record = {
| 更新时间 | `updated_at` | text | 否 | 否 | 否 | |
| 整改事由 | `delay_reason` | text | 否 | 否 | 否 | |
### `problem_rectification_archive_records`:问题整改归档数据
- 类型:`normal`
- 字段数量:51
| 字段标签 | 字段别名 | 字段类型 | 多值 | 必填 | 主键 | 关联/配置 |
| --- | --- | --- | --- | --- | --- | --- |
| 检查实施单位 | `inspect_the_implementing_unit` | text | 否 | 否 | 否 | options: 审计部, 风险管理部, 法律合规部, 鑫沅资产 |
| 检查实施时间 | `check_the_implementation_time` | text | 否 | 否 | 否 | |
| 检查报告出具日 | `inspection_report_date` | text | 否 | 否 | 否 | |
| 问题发现情况 | `problem_discovery_situation` | text | 否 | 否 | 否 | |
| 责任部门 | `responsible_department` | text | 否 | 否 | 否 | |
| 整改进度 | `rectification_progress` | text | 否 | 否 | 否 | |
| 整改情况 | `rectification_situation` | long_text | 否 | 否 | 否 | |
| 反馈单位 | `feedback_unit` | text | 否 | 否 | 否 | |
| 备注 | `remark` | long_text | 否 | 否 | 否 | |
| 预计整改完成时间 | `the_time_of_rectification` | date | 否 | 否 | 否 | |
| 填写人 | `fill_in_the_people` | user | 是 | 否 | 否 | |
| 复核人 | `review_people` | user | 是 | 否 | 否 | |
| 问题来源 | `problem_from` | text | 否 | 否 | 否 | |
| 问题类型 | `problem_type` | text | 否 | 否 | 否 | |
| 问题涉及业务领域 | `related_issue_domain` | text | 否 | 否 | 否 | |
| 问题简述 | `problem_brief` | long_text | 否 | 否 | 否 | |
| 审批状态 | `audit_status` | text | 否 | 否 | 否 | |
| 最终审批状态 | `final_audit_status` | text | 否 | 否 | 否 | |
| 检查项目名称 | `project_name` | text | 否 | 否 | 否 | |
| 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 | |
| 问题序号 | `problem_index` | text | 否 | 否 | 否 | |
| 是否造成经济损失 | `has_economic_loss` | text | 否 | 否 | 否 | |
| 对应经济损失 | `economic_loss_amount` | text | 否 | 否 | 否 | |
| 是否造成外部影响 | `has_external_impact` | text | 否 | 否 | 否 | |
| 处罚机构 | `penalty_organization` | text | 否 | 否 | 否 | |
| 是否涉及员工问责 | `has_employee_accountability` | text | 否 | 否 | 否 | |
| 问责人数 | `accountability_person_count` | text | 否 | 否 | 否 | |
| 问题一级分类 | `problem_category_level_1` | text | 否 | 否 | 否 | |
| 问题二级分类 | `problem_category_level_2` | text | 否 | 否 | 否 | |
| 问题三级分类 | `problem_category_level_3` | text | 否 | 否 | 否 | |
| 问题四级分类 | `problem_category_level_4` | text | 否 | 否 | 否 | |
| 问题原因 | `problem_root_cause` | text | 否 | 否 | 否 | |
| 问题综合严重程度 | `problem_severity_level` | text | 否 | 否 | 否 | |
| 整改时间评估 | `rectification_timeliness_level` | text | 否 | 否 | 否 | |
| 整改证明材料质量 | `rectification_material_quality_level` | text | 否 | 否 | 否 | |
| 是否涉及制度修订 | `involves_policy_revision` | text | 否 | 否 | 否 | |
| 制度名称 | `policy_revision_name` | text | 否 | 否 | 否 | |
| 是否涉及系统建设 | `involves_system_construction` | text | 否 | 否 | 否 | |
| 系统名称 | `system_construction_name` | text | 否 | 否 | 否 | |
| 是否为往期发现问题 | `is_historical_problem` | text | 否 | 否 | 否 | |
| 问题是否重复出现 | `is_repeated_problem` | text | 否 | 否 | 否 | |
| 综合整改评定等级 | `rectification_overall_rating` | text | 否 | 否 | 否 | |
| 实际整改完成时间 | `actual_rectification_completed_at` | text | 否 | 否 | 否 | |
| 实际检查完成日期 | `inspection_completed_at` | text | 否 | 否 | 否 | |
| 主责部门 | `primary_responsible_department` | text | 否 | 否 | 否 | |
| 整改时效 | `rectification_time_limit` | text | 否 | 否 | 否 | |
| 支持材料质量 | `support_material_quality` | text | 否 | 否 | 否 | |
| 是否锁定 | `is_locked` | boolean | 否 | 否 | 否 | |
| 是否重复发生 | `is_recurrence_occurred` | boolean | 否 | 否 | 否 | |
| 部门整改明细 | `department_rectification_items` | object | 是 | 否 | 否 | |
| 问题整改延期 | `problem_rectification_delay_request_items` | object | 是 | 否 | 否 | |
### `problem_rectification_delay_request`:问题整改延期申请
- 类型:`embedded`
......@@ -295,9 +326,9 @@ type Record = {
依赖字段通过 `settings.subject` 指向的对象会递归纳入本节;`form` 资源不属于 Subject 依赖。
- `issue_rectification_archive.reference_records` -> `problem_rectification_archive_records`
- `problem_rectification.department_rectification_items` -> `department_rectification_items`
- `problem_rectification.problem_rectification_delay_request_items` -> `problem_rectification_delay_request`
- `issue_rectification_archive.reference_records` -> `problem_rectification_archive_records`
- `problem_rectification_delay_request.flow_record_id` -> `message_reference_id`
<!-- SUBJECT_SCHEMA_SYNC:END -->
......
import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildProblemRectificationArchiveSnapshot,
} from '../src/pages/problem_rectification/problemRectificationArchiveSnapshot.mjs';
const ARCHIVE_SUBJECT = {
fields: [
{ name: 'project_name', type: 'text', multiple: false },
{ name: 'responsible_department', type: 'text', multiple: false },
{ name: 'fill_in_the_people', type: 'user', multiple: true },
{ name: 'the_time_of_rectification', label: '预计整改完成时间', type: 'date', multiple: false },
{ name: 'is_locked', type: 'boolean', multiple: false },
{ name: 'department_rectification_items', type: 'object', multiple: true },
],
};
test('builds an archive-field whitelist snapshot with normalized protocol values', () => {
const source = {
title: '问题 A',
metadata: {
project_name: '检查项目',
responsible_department: ['法律合规部', '审计部'],
fill_in_the_people: [{ _id: 'user-1', display_name: '甲' }, 'user-2'],
the_time_of_rectification: '2026-09-28',
is_locked: true,
department_rectification_items: [{ item_id: 'item-1', status: '已整改' }],
field_not_in_archive_subject: '不会写入',
},
};
const snapshot = buildProblemRectificationArchiveSnapshot(source, ARCHIVE_SUBJECT);
assert.equal(snapshot.title, '问题 A');
assert.deepEqual(snapshot.metadata, {
project_name: '检查项目',
responsible_department: '法律合规部,审计部',
fill_in_the_people: ['user-1', 'user-2'],
the_time_of_rectification: '2026-09-28T00:00:00.000Z',
is_locked: true,
department_rectification_items: [{ item_id: 'item-1', status: '已整改' }],
});
});
test('deep-copies nested archive objects so later source edits cannot change the snapshot', () => {
const source = {
metadata: {
project_name: '检查项目',
department_rectification_items: [
{ item_id: 'item-1', proof: { name: '归档前材料.pdf' } },
],
},
};
const snapshot = buildProblemRectificationArchiveSnapshot(source, ARCHIVE_SUBJECT);
source.metadata.department_rectification_items[0].proof.name = '原数据后续修改.pdf';
assert.equal(
snapshot.metadata.department_rectification_items[0].proof.name,
'归档前材料.pdf',
);
});
test('normalizes legacy JSON strings into independent object snapshots', () => {
const snapshot = buildProblemRectificationArchiveSnapshot(
{
metadata: {
department_rectification_items: '[{"item_id":"legacy-item"}]',
},
},
ARCHIVE_SUBJECT,
);
assert.deepEqual(snapshot.metadata.department_rectification_items, [
{ item_id: 'legacy-item' },
]);
});
test('rejects invalid configured date values instead of writing a non-ISO archive value', () => {
assert.throws(
() => buildProblemRectificationArchiveSnapshot(
{ metadata: { the_time_of_rectification: '不是日期' } },
ARCHIVE_SUBJECT,
),
/预计整改完成时间.*不是有效日期/,
);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment