Commit a2e249fe authored by David Yang's avatar David Yang

feat: integrate audit material center in problem detail

parent ceba014b
...@@ -53,10 +53,12 @@ export default function SubjectViewContent({ ...@@ -53,10 +53,12 @@ export default function SubjectViewContent({
editSessionId, editSessionId,
mode = 'view', mode = 'view',
onFieldChange, onFieldChange,
onPendingLocalMaterialChange,
onFieldValidation, onFieldValidation,
readonly, readonly,
isFieldDisabled, isFieldDisabled,
minimal = false, minimal = false,
useAuditMaterialCenter = false,
}) { }) {
const displayFields = buildViewColumns(view, subject); const displayFields = buildViewColumns(view, subject);
const contentFields = minimal const contentFields = minimal
...@@ -119,8 +121,10 @@ export default function SubjectViewContent({ ...@@ -119,8 +121,10 @@ export default function SubjectViewContent({
) )
} }
onValidationChange={onFieldValidation} onValidationChange={onFieldValidation}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
readonly={readonly} readonly={readonly}
disabled={fieldDisabled} disabled={fieldDisabled}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
</div> </div>
</div> </div>
......
...@@ -15,6 +15,7 @@ import FileUploadField from '@/components/FileUploadField'; ...@@ -15,6 +15,7 @@ import FileUploadField from '@/components/FileUploadField';
import { queryUsers, uploadFile } from '@/services/DataService'; import { queryUsers, uploadFile } from '@/services/DataService';
import ReferenceSearchSelect from '@/components/ReferenceSearchSelect'; import ReferenceSearchSelect from '@/components/ReferenceSearchSelect';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete'; import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import ProblemRectificationMaterialField from '@/pages/problem_rectification/components/ProblemRectificationMaterialField';
import ReferenceRecordsTable from './ReferenceRecordsTable'; import ReferenceRecordsTable from './ReferenceRecordsTable';
import { import {
dateInputValue, dateInputValue,
...@@ -303,7 +304,9 @@ function ControlledFieldEditor({ ...@@ -303,7 +304,9 @@ function ControlledFieldEditor({
fieldVariant, fieldVariant,
placeholder, placeholder,
onChange, onChange,
onPendingLocalMaterialChange,
onValidationChange, onValidationChange,
useAuditMaterialCenter,
}) { }) {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
...@@ -388,6 +391,22 @@ function ControlledFieldEditor({ ...@@ -388,6 +391,22 @@ function ControlledFieldEditor({
} }
if (fieldType === 'file') { if (fieldType === 'file') {
if (
useAuditMaterialCenter &&
fieldPath === 'metadata.rectification_proof_materials'
) {
return (
<ProblemRectificationMaterialField
editSessionId={editSessionId}
fieldPath={fieldPath}
onChange={onChange}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
onValidationChange={onValidationChange}
value={value}
/>
);
}
return ( return (
<Card variant="secondary"> <Card variant="secondary">
<Card.Content className="space-y-3"> <Card.Content className="space-y-3">
...@@ -518,7 +537,9 @@ export default function TailwindEditableField({ ...@@ -518,7 +537,9 @@ export default function TailwindEditableField({
placeholder, placeholder,
onSave, onSave,
onChange, onChange,
onPendingLocalMaterialChange,
onValidationChange, onValidationChange,
useAuditMaterialCenter = false,
mode = 'inline', mode = 'inline',
readonly = false, readonly = false,
disabled = false, disabled = false,
...@@ -559,7 +580,9 @@ export default function TailwindEditableField({ ...@@ -559,7 +580,9 @@ export default function TailwindEditableField({
fieldVariant={fieldVariant} fieldVariant={fieldVariant}
placeholder={textPlaceholder} placeholder={textPlaceholder}
onChange={onChange} onChange={onChange}
onPendingLocalMaterialChange={onPendingLocalMaterialChange}
onValidationChange={onValidationChange} onValidationChange={onValidationChange}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
); );
} }
......
...@@ -4,6 +4,7 @@ import { Close } from '@/components/AppIcons'; ...@@ -4,6 +4,7 @@ import { Close } from '@/components/AppIcons';
import { Alert, Button, Modal, Spinner, Tabs, Tooltip } from '@heroui/react'; import { Alert, Button, Modal, Spinner, Tabs, Tooltip } from '@heroui/react';
import React, { import React, {
useCallback, useCallback,
useContext,
forwardRef, forwardRef,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
...@@ -13,6 +14,8 @@ import React, { ...@@ -13,6 +14,8 @@ import React, {
} from 'react'; } from 'react';
import { useSubjectRecord } from './hooks/useSubjectRecord'; import { useSubjectRecord } from './hooks/useSubjectRecord';
import SubjectViewContent from './components/SubjectViewContent'; import SubjectViewContent from './components/SubjectViewContent';
import { UserContext } from '@/wrapper/Auth';
import { createProblemRectificationMaterial } from '@/pages/problem_rectification/problemRectificationMaterialService';
import { import {
buildViewColumns, buildViewColumns,
checkViewCondition, checkViewCondition,
...@@ -58,8 +61,11 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -58,8 +61,11 @@ const RecordDetailDrawer = (props, ref) => {
disabledFields = EMPTY_ARRAY, disabledFields = EMPTY_ARRAY,
authorizationView = null, authorizationView = null,
deriveFieldValues, deriveFieldValues,
useAuditMaterialCenter = false,
} = props; } = props;
const currentUser = useContext(UserContext);
const [refreshKey, setRefreshKey] = useState(() => Date.now()); const [refreshKey, setRefreshKey] = useState(() => Date.now());
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [activeTabKey, setActiveTabKey] = useState(); const [activeTabKey, setActiveTabKey] = useState();
...@@ -70,6 +76,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -70,6 +76,7 @@ const RecordDetailDrawer = (props, ref) => {
const [validationErrors, setValidationErrors] = useState({}); const [validationErrors, setValidationErrors] = useState({});
const [saveError, setSaveError] = useState(''); const [saveError, setSaveError] = useState('');
const editSessionRef = useRef(0); const editSessionRef = useRef(0);
const pendingLocalMaterialsRef = useRef({});
const { const {
editableRecord, editableRecord,
...@@ -100,6 +107,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -100,6 +107,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
editSessionRef.current += 1; editSessionRef.current += 1;
}, [open, recordName]); }, [open, recordName]);
...@@ -200,6 +208,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -200,6 +208,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
}; };
const handleRequestClose = () => { const handleRequestClose = () => {
...@@ -215,6 +224,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -215,6 +224,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
setSaveError(''); setSaveError('');
pendingLocalMaterialsRef.current = {};
setIsEditing(true); setIsEditing(true);
}; };
...@@ -257,6 +267,21 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -257,6 +267,21 @@ const RecordDetailDrawer = (props, ref) => {
}); });
}, []); }, []);
const handlePendingLocalMaterialChange = useCallback(
(fieldPath, pendingMaterial, editSessionId) => {
if (editSessionId !== editSessionRef.current) return;
const nextPendingMaterials = { ...pendingLocalMaterialsRef.current };
if (pendingMaterial) {
nextPendingMaterials[fieldPath] = pendingMaterial;
} else {
delete nextPendingMaterials[fieldPath];
}
pendingLocalMaterialsRef.current = nextPendingMaterials;
},
[],
);
const handleSave = async () => { const handleSave = async () => {
if (!hasDirtyFields || hasValidationErrors || isSaving) return; if (!hasDirtyFields || hasValidationErrors || isSaving) return;
...@@ -264,10 +289,30 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -264,10 +289,30 @@ const RecordDetailDrawer = (props, ref) => {
setSaveError(''); setSaveError('');
try { try {
const pendingMaterials = Object.entries(pendingLocalMaterialsRef.current)
.filter(([fieldPath]) => dirtyFields[fieldPath]);
for (const [fieldPath, pendingMaterial] of pendingMaterials) {
if (pendingMaterial.materialRecordId) continue;
const createdMaterial = await createProblemRectificationMaterial({
fileAsset: pendingMaterial.fileAsset,
currentUser,
});
pendingLocalMaterialsRef.current = {
...pendingLocalMaterialsRef.current,
[fieldPath]: {
...pendingMaterial,
materialRecordId: createdMaterial.id,
},
};
}
await handleFieldsUpdate(Object.values(dirtyFields)); await handleFieldsUpdate(Object.values(dirtyFields));
setIsEditing(false); setIsEditing(false);
setDirtyFields({}); setDirtyFields({});
setValidationErrors({}); setValidationErrors({});
pendingLocalMaterialsRef.current = {};
onDataChange?.(true); onDataChange?.(true);
} catch (saveFailure) { } catch (saveFailure) {
setSaveError(saveFailure?.message || '保存失败,请稍后重试。'); setSaveError(saveFailure?.message || '保存失败,请稍后重试。');
...@@ -389,9 +434,11 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -389,9 +434,11 @@ const RecordDetailDrawer = (props, ref) => {
mode={isEditing ? 'edit' : 'view'} mode={isEditing ? 'edit' : 'view'}
onFieldChange={handleDraftFieldChange} onFieldChange={handleDraftFieldChange}
onFieldValidation={handleFieldValidation} onFieldValidation={handleFieldValidation}
onPendingLocalMaterialChange={handlePendingLocalMaterialChange}
readonly={effectiveReadonly} readonly={effectiveReadonly}
isFieldDisabled={isDrawerFieldDisabled} isFieldDisabled={isDrawerFieldDisabled}
minimal={minimalContent} minimal={minimalContent}
useAuditMaterialCenter={useAuditMaterialCenter}
/> />
</div> </div>
)} )}
......
...@@ -1872,6 +1872,7 @@ export default function ProblemRectificationListPage() { ...@@ -1872,6 +1872,7 @@ export default function ProblemRectificationListPage() {
}} }}
preferredViewName={activeGridView?.title || activeGridView?.name} preferredViewName={activeGridView?.title || activeGridView?.name}
authorizationView={activeGridView} authorizationView={activeGridView}
useAuditMaterialCenter
/> />
), ),
}} }}
......
...@@ -142,7 +142,13 @@ ...@@ -142,7 +142,13 @@
- `problem_rectification_archive_records`:归档后的问题快照明细 - `problem_rectification_archive_records`:归档后的问题快照明细
- `issue_rectification_archive`:一次归档批次主单 - `issue_rectification_archive`:一次归档批次主单
### 4.3 流程辅助对象 ### 4.3 审计材料共享对象
- `am_material`:审计材料中心共享主档。问题清单详情可查询已有主档;从本地上传证明材料并保存详情时同步创建主档
问题记录仍在 `metadata.rectification_proof_materials` 保存 `{name,type,size,url}` 文件快照,不保存材料主档 `_id`,不建立自动联动关系。
### 4.4 流程辅助对象
- `message_reference_id`(信息关联id):旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单 - `message_reference_id`(信息关联id):旧应用中用于承载“推送填写、复核、最终审核”状态的消息流程主单
...@@ -745,9 +751,18 @@ ...@@ -745,9 +751,18 @@
- 使用抽屉或对话框,而不是表格内直接改单元格 - 使用抽屉或对话框,而不是表格内直接改单元格
- 基础信息与整改信息分区展示 - 基础信息与整改信息分区展示
- 附件上传单独区域展示 - 附件在单独区域展示,编辑时默认进入“材料中心”,也可切换到“本地上传”
- 状态、责任人、计划时间等字段用结构化控件维护 - 状态、责任人、计划时间等字段用结构化控件维护
整改证明材料交互限定在问题清单详情弹框,不改变整改处理页、整改流程表格和归档详情:
1. “材料中心”首次读取当前用户可访问的最近 20 条 `am_material`,支持按材料标题或文件名远程搜索;选中后仅把 `metadata.file_asset` 快照写入详情草稿。
2. “本地上传”选择文件后立即调用 `/upload`,上传期间禁止保存;成功后将文件对象写入详情草稿,并提示保存后同步到材料中心。
3. 用户点击详情“保存”时,先创建 `am_material`,写入 `source_app = issue_rectification`、当前用户 `_id` 和 ISO 上传时间,再更新 `problem_rectification.metadata.rectification_proof_materials`。
4. 用户取消编辑时不创建材料主档;选择已有材料不重复上传、不创建新主档。
5. 材料创建失败时保留编辑草稿且不更新问题;材料创建成功但问题更新失败时保留已建档标记,再次保存只重试问题更新,避免重复材料。
6. 清空或替换问题附件不删除材料中心主档或底层文件;已有材料后续修改或软删除不联动历史问题快照。
建议字段分组: 建议字段分组:
- 基础信息 - 基础信息
......
import dayjs from 'dayjs';
import { createRecord, queryRecords, uploadFile } from '@/services/DataService';
export const AUDIT_MATERIAL_SUBJECT = 'am_material';
export const ISSUE_RECTIFICATION_SOURCE_APP = 'issue_rectification';
const MATERIAL_PAGE_SIZE = 20;
function extractRecords(response) {
const candidates = [
response,
response?.data,
response?.data?.list,
response?.data?.records,
response?.list,
response?.records,
response?.items,
response?.result,
response?.result?.records,
];
return candidates.find((candidate) => Array.isArray(candidate)) || [];
}
function escapeRegExp(value = '') {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function inferMaterialTitle(fileName = '') {
return String(fileName || '').replace(/\.[^.]+$/, '').trim() || '未命名材料';
}
function normalizeFileAsset(uploadResult, file) {
const candidates = [
uploadResult?.file,
uploadResult?.data?.file,
uploadResult?.data,
uploadResult?.result?.file,
uploadResult?.result,
uploadResult,
];
for (const candidate of candidates) {
if (!candidate || typeof candidate !== 'object') continue;
const url =
candidate.url ||
candidate.path ||
candidate.location ||
candidate?.data?.url ||
'';
if (!url) continue;
return {
name: candidate.name || candidate.filename || file?.name || '未命名文件',
type: candidate.type || candidate.mimetype || file?.type || 'application/octet-stream',
size: Number(candidate.size || file?.size || 0),
url,
};
}
throw new Error('文件上传成功,但未获取到可用的文件地址。');
}
function normalizeMaterial(record = {}) {
const metadata = record?.metadata || {};
const fileAsset = metadata.file_asset || {};
return {
id: record._id || record.id || record.name || '',
materialTitle: metadata.material_title || record.title || fileAsset.name || '未命名材料',
fileAsset,
archiveCategory: metadata.archive_category || '',
archiveYear: metadata.archive_year || '',
sourceApp: metadata.source_app || '',
uploadedAt: metadata.uploaded_at || record.created_at || '',
};
}
export async function queryAuditMaterials(keyword = '') {
const normalizedKeyword = String(keyword || '').trim();
const escapedKeyword = escapeRegExp(normalizedKeyword);
const filter = escapedKeyword
? {
$or: [
{
'metadata.material_title': {
$regex: escapedKeyword,
$options: 'i',
},
},
{
'metadata.file_asset.name': {
$regex: escapedKeyword,
$options: 'i',
},
},
],
}
: {};
const response = await queryRecords({
subject: AUDIT_MATERIAL_SUBJECT,
page: 0,
page_size: MATERIAL_PAGE_SIZE,
filter,
sort: '-updated_at -created_at',
mode: 'full',
});
return extractRecords(response)
.map(normalizeMaterial)
.filter((material) => material.id && material.fileAsset?.url);
}
export async function uploadProblemRectificationFile(file) {
if (!file) {
throw new Error('请选择要上传的本地文件。');
}
const uploadResult = await uploadFile(file);
return normalizeFileAsset(uploadResult, file);
}
export async function createProblemRectificationMaterial({ fileAsset, currentUser }) {
if (!fileAsset?.url) {
throw new Error('缺少有效的文件地址,无法写入审计材料中心。');
}
const uploaderId = currentUser?._id || currentUser?.id || '';
if (!uploaderId) {
throw new Error('未获取到当前登录用户,无法写入审计材料中心。');
}
const materialTitle = inferMaterialTitle(fileAsset.name);
const response = await createRecord({
subject: AUDIT_MATERIAL_SUBJECT,
title: materialTitle,
content_type: 'text',
metadata: {
material_title: materialTitle,
file_asset: {
name: fileAsset.name,
type: fileAsset.type || 'application/octet-stream',
size: Number(fileAsset.size || 0),
url: fileAsset.url,
},
source_app: ISSUE_RECTIFICATION_SOURCE_APP,
uploader: uploaderId,
uploaded_at: dayjs().toISOString(),
},
});
const createdRecord = response?.data || response?.result || response;
return {
id: createdRecord?._id || createdRecord?.id || createdRecord?.name || true,
record: createdRecord,
};
}
# Record 接口文档 # Record 接口文档
**最后更新**:2026 年 3 月 **最后更新**:2026 年 8 月
**基于代码实现整理** **基于代码实现整理**
参考文件: 参考文件:
- `src/routers/record.js` - `src/routers/record.js`
...@@ -267,5 +267,67 @@ Query:`calculated=true|false` ...@@ -267,5 +267,67 @@ Query:`calculated=true|false`
6. **软删除 vs 物理删除**:默认软删除(status=removed),需显式 `force` 才物理删除。 6. **软删除 vs 物理删除**:默认软删除(status=removed),需显式 `force` 才物理删除。
7. **导入接口**:支持 name 自动转 id,但仅限同一 subject 内查找。 7. **导入接口**:支持 name 自动转 id,但仅限同一 subject 内查找。
如需补充特定 subject 的 metadata 字段规范、文件上传流程、权限点详细对应表等,请继续提供信息,我可以进一步扩展对应章节。 ## 5. 问题详情与审计材料中心接入
问题清单详情弹框中的 `metadata.rectification_proof_materials` 支持从统一材料池选择已有文件,或上传本地文件后同步建档。整改处理页、整改流程表格和归档详情不使用本节流程。
### 5.1 查询已有材料
调用 `POST /record/query`,按当前用户的 Record 访问权限返回有效材料:
```json
{
"subject": "am_material",
"page": 0,
"page_size": 20,
"filter": {
"$or": [
{
"metadata.material_title": {
"$regex": "整改说明",
"$options": "i"
}
},
{
"metadata.file_asset.name": {
"$regex": "整改说明",
"$options": "i"
}
}
]
},
"sort": "-updated_at -created_at",
"mode": "full"
}
``` ```
搜索关键词必须先转义正则特殊字符。选中材料后,只将 `metadata.file_asset` 的 `{name,type,size,url}` 快照写入问题详情草稿,不保存 `am_material` `_id`。
### 5.2 本地上传并写入材料中心
1. 选择文件后立即调用 `POST /upload`,将返回结果规范化为 `{name,type,size,url}` 并保留在详情草稿。
2. 用户点击详情“保存”时调用 `POST /record` 创建材料主档:
```json
{
"subject": "am_material",
"title": "整改说明",
"content_type": "text",
"metadata": {
"material_title": "整改说明",
"file_asset": {
"name": "整改说明.pdf",
"type": "application/pdf",
"size": 204800,
"url": "https://example.com/files/proof.pdf"
},
"source_app": "issue_rectification",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-08-25T08:30:00.000Z"
}
}
```
3. 材料主档创建成功后,再调用 `PUT /record/:name` 更新问题记录的 `metadata.rectification_proof_materials`。
4. 材料创建失败时不更新问题记录;材料创建成功但问题更新失败时,当前编辑会话记住已创建的材料,重试保存不得重复创建。
5. 取消详情编辑不创建 `am_material`;已经上传但未建档的二进制文件遵循上传服务自身的清理策略。
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
本文件定义当前项目使用的 Subject(业务对象)字段协议,供前端页面、服务层、动态表单与 AI 代码生成使用。 本文件定义当前项目使用的 Subject(业务对象)字段协议,供前端页面、服务层、动态表单与 AI 代码生成使用。
当前项目当前涉及两个业务模块: 当前项目涉及以下业务对象:
- `pm_project_task`:项目管理
- `problem_rectification`:问题整改 - `problem_rectification`:问题整改
- `am_material`:审计材料中心共享主档,仅供问题详情查询和本地附件同步建档
--- ---
...@@ -187,6 +187,7 @@ type Record = { ...@@ -187,6 +187,7 @@ type Record = {
- 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析 - 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
- 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码 - 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码
- `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖 - `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖
- 问题清单详情弹框中的 `rectification_proof_materials` 支持从 `am_material.metadata.file_asset` 复制文件快照,或上传本地文件后创建 `am_material`。问题字段仍只保存 `{name,type,size,url}`,不保存材料主档 `_id`,材料后续修改或软删除不反向联动问题记录
- `department_rectification_items` 用于新转发流程的部门级整改明细。无论单条还是多条推送,每个问题都按责任部门生成明细;单条问题即使只有 1 个责任部门,也生成 1 条明细。已推送过的问题再次推送时,必须保留原部门明细记录和 `item_id`,只在本次参与推送的原明细行上更新填写人、审批人、流程状态和待办 ID;未参与本次推送的原明细行保持不变,不因弹窗删除部门配置而从父记录移除。转发后 `rectification_progress`、`rectification_situation`、`the_time_of_rectification`、`rectification_proof_materials`、`audit_status`、`final_audit_status` 以明细为事实来源,父字段仅做兼容汇总展示;整改进度、审批状态和最终审批状态按部门明细中最落后的状态汇总,例如“整改中”优先于“已整改”,“未审批”优先于“已审批” - `department_rectification_items` 用于新转发流程的部门级整改明细。无论单条还是多条推送,每个问题都按责任部门生成明细;单条问题即使只有 1 个责任部门,也生成 1 条明细。已推送过的问题再次推送时,必须保留原部门明细记录和 `item_id`,只在本次参与推送的原明细行上更新填写人、审批人、流程状态和待办 ID;未参与本次推送的原明细行保持不变,不因弹窗删除部门配置而从父记录移除。转发后 `rectification_progress`、`rectification_situation`、`the_time_of_rectification`、`rectification_proof_materials`、`audit_status`、`final_audit_status` 以明细为事实来源,父字段仅做兼容汇总展示;整改进度、审批状态和最终审批状态按部门明细中最落后的状态汇总,例如“整改中”优先于“已整改”,“未审批”优先于“已审批”
- `problem_rectification_delay_request` 为历史问题级单次延期申请嵌套对象,与 `department_rectification_items` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items` - `problem_rectification_delay_request` 为历史问题级单次延期申请嵌套对象,与 `department_rectification_items` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items`
- `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID - `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID
...@@ -290,7 +291,26 @@ type Record = { ...@@ -290,7 +291,26 @@ type Record = {
} }
``` ```
## 3.2 问题整改归档明细 ## 3.2 审计材料中心共享主档
- **subject**: `am_material`
本项目只使用该对象完成问题详情的材料查询和本地附件同步建档,不修改线上 Subject 配置。
| 字段标签 | 字段别名 | 字段类型 | 是否必填 | 问题详情写入规则 |
| --- | --- | --- | --- | --- |
| 材料标题 | `material_title` | text | 是 | 默认取文件名去后缀 |
| 文件对象 | `file_asset` | file | 是 | `{name,type,size,url}` |
| 来源应用 | `source_app` | text | 否 | 固定写入 `issue_rectification` |
| 上传人 | `uploader` | user | 是 | 当前登录用户 `_id` |
| 上传时间 | `uploaded_at` | date | 是 | ISO 8601 字符串 |
| 来源系统 | `source_system` | select | 否 | 问题详情不写入 |
| 材料分类 | `archive_category` | select | 否 | 问题详情不写入 |
| 所属周期/年份 | `archive_year` | text | 否 | 问题详情不写入 |
问题详情选择已有材料时读取 `material_title` 与 `file_asset`;搜索范围为 `metadata.material_title` 和 `metadata.file_asset.name`。列表只展示当前登录用户通过 Record 权限可读取且 `file_asset.url` 有效的记录。
## 3.3 问题整改归档明细
- **subject**: `problem_rectification_archive_records` - **subject**: `problem_rectification_archive_records`
...@@ -319,14 +339,14 @@ type Record = { ...@@ -319,14 +339,14 @@ type Record = {
| 检查项目名称 | `project_name` | text | 否 | 否 | 否 | | 检查项目名称 | `project_name` | text | 否 | 否 | 否 |
| 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 | | 整改证明材料 | `rectification_proof_materials` | file | 否 | 否 | 否 |
### 3.2.1 归档写入约定 ### 3.3.1 归档写入约定
- 归档对象存的是快照,不再要求保留原来的 `select` 选项结构 - 归档对象存的是快照,不再要求保留原来的 `select` 选项结构
- 旧应用归档时会把 `responsible_department` 转为逗号拼接文本 - 旧应用归档时会把 `responsible_department` 转为逗号拼接文本
- 归档记录仍需保留 `fill_in_the_people`、`review_people` 的用户 `_id[]` - 归档记录仍需保留 `fill_in_the_people`、`review_people` 的用户 `_id[]`
- `the_time_of_rectification` 继续按 ISO 字符串写入 - `the_time_of_rectification` 继续按 ISO 字符串写入
## 3.3 问题整改归档批次 ## 3.4 问题整改归档批次
- **subject**: `issue_rectification_archive` - **subject**: `issue_rectification_archive`
...@@ -336,7 +356,7 @@ type Record = { ...@@ -336,7 +356,7 @@ type Record = {
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 归档明细记录 | `reference_records` | reference | 否 | 是 | 是 | | 归档明细记录 | `reference_records` | reference | 否 | 是 | 是 |
### 3.3.1 metadata 示例 ### 3.4.1 metadata 示例
```json ```json
{ {
...@@ -347,7 +367,7 @@ type Record = { ...@@ -347,7 +367,7 @@ type Record = {
} }
``` ```
## 3.4 信息关联id(问题整改流程消息主单) ## 3.5 信息关联id(问题整改流程消息主单)
- **subject**: `message_reference_id` - **subject**: `message_reference_id`
- **对象名称**: 信息关联id - **对象名称**: 信息关联id
...@@ -373,7 +393,7 @@ type Record = { ...@@ -373,7 +393,7 @@ type Record = {
| 审核状态 | `auditor_status` | select | 否 | 否 | 否 | | 审核状态 | `auditor_status` | select | 否 | 否 | 否 |
| 发送人状态 | `sender_status` | select | 否 | 否 | 否 | | 发送人状态 | `sender_status` | select | 否 | 否 | 否 |
### 3.4.1 当前流程写入约定 ### 3.5.1 当前流程写入约定
- `restriction_key` 默认写 `_id` - `restriction_key` 默认写 `_id`
- `restriction_value` 保存被推送整改记录的 `_id[]` - `restriction_value` 保存被推送整改记录的 `_id[]`
...@@ -388,7 +408,7 @@ type Record = { ...@@ -388,7 +408,7 @@ type Record = {
- `sender_status`:当前项目统一使用 `未完成`、`已完成` - `sender_status`:当前项目统一使用 `未完成`、`已完成`
- 当批量流程复核或最终审核驳回时,流程主单 `editor_status` 重置为 `未完成` 并回写新的 `progress_id_editor`,同一批次的全部问题共同重新进入填写阶段 - 当批量流程复核或最终审核驳回时,流程主单 `editor_status` 重置为 `未完成` 并回写新的 `progress_id_editor`,同一批次的全部问题共同重新进入填写阶段
## 3.5 项目管理 ## 3.6 项目管理
- **subject**: `pm_project_task` - **subject**: `pm_project_task`
...@@ -414,7 +434,7 @@ type Record = { ...@@ -414,7 +434,7 @@ type Record = {
| 延期原因 | `delay_reason` | text | 否 | 否 | 否 | | 延期原因 | `delay_reason` | text | 否 | 否 | 否 |
| 周进度记录 | `weekly_updates` | object | 否 | 是 | 否 | | 周进度记录 | `weekly_updates` | object | 否 | 是 | 否 |
### 3.5.1 `project_status` 选项 ### 3.6.1 `project_status` 选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -425,7 +445,7 @@ type Record = { ...@@ -425,7 +445,7 @@ type Record = {
| 受阻 | `blocked` | | 受阻 | `blocked` |
| 已取消 | `cancelled` | | 已取消 | `cancelled` |
### 3.5.2 `priority` 建议选项 ### 3.6.2 `priority` 建议选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -433,7 +453,7 @@ type Record = { ...@@ -433,7 +453,7 @@ type Record = {
| 中 | `medium` | | 中 | `medium` |
| 低 | `low` | | 低 | `low` |
### 3.5.3 `result_status` 建议选项 ### 3.6.3 `result_status` 建议选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -444,7 +464,7 @@ type Record = { ...@@ -444,7 +464,7 @@ type Record = {
| 准时完成 | `completed_on_time` | | 准时完成 | `completed_on_time` |
| 超时完成 | `completed_overdue` | | 超时完成 | `completed_overdue` |
### 3.5.4 `weekly_updates` 子结构建议 ### 3.6.4 `weekly_updates` 子结构建议
`weekly_updates` 为对象数组,建议每项至少包含以下字段: `weekly_updates` 为对象数组,建议每项至少包含以下字段:
...@@ -465,7 +485,7 @@ type Record = { ...@@ -465,7 +485,7 @@ type Record = {
- 周进度记录通过“新增本周进度 / 编辑周进度”弹窗录入 - 周进度记录通过“新增本周进度 / 编辑周进度”弹窗录入
- 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON - 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON
### 3.5.5 metadata 示例 ### 3.6.5 metadata 示例
```json ```json
{ {
......
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