Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
M
model_analysis
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
model_analysis
Commits
963b3a6e
Commit
963b3a6e
authored
Aug 06, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 完善模型同步与转发审批流程
parent
a46c3e8d
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
249 additions
and
127 deletions
+249
-127
ForwardApprovals.jsx
src/pages/project_manager_xy/ForwardApprovals.jsx
+48
-21
ModelBatchData.jsx
src/pages/project_manager_xy/ModelBatchData.jsx
+3
-3
ModelBatches.jsx
src/pages/project_manager_xy/ModelBatches.jsx
+8
-6
ModelMonitorMockService.js
src/pages/project_manager_xy/ModelMonitorMockService.js
+55
-26
ModelOverview.jsx
src/pages/project_manager_xy/ModelOverview.jsx
+1
-1
Models.jsx
src/pages/project_manager_xy/Models.jsx
+0
-0
model-monitor.business-flow.md
src/pages/project_manager_xy/model-monitor.business-flow.md
+45
-29
modelMonitorService.js
src/pages/project_manager_xy/modelMonitorService.js
+0
-0
record-api.md
src/services/record-api.md
+54
-23
subjectsDefinition.md
src/services/subjectsDefinition.md
+35
-18
No files found.
src/pages/project_manager_xy/ForwardApprovals.jsx
View file @
963b3a6e
...
@@ -40,6 +40,27 @@ const FILTER_OPTIONS = [
...
@@ -40,6 +40,27 @@ const FILTER_OPTIONS = [
{
value
:
'involved'
,
label
:
'我参与的'
},
{
value
:
'involved'
,
label
:
'我参与的'
},
];
];
function
isReturnedForEditing
(
record
)
{
return
record
?.
approvalStatus
===
'rejected'
||
record
?.
finalApprovalStatus
===
'rejected'
;
}
function
canRecipientEdit
(
record
,
currentUserId
)
{
return
Boolean
(
record
?.
recipientIds
.
includes
(
currentUserId
)
&&
(
record
.
editorStatus
!==
'completed'
||
isReturnedForEditing
(
record
)),
);
}
function
getForwardEditorStatusLabel
(
record
)
{
return
isReturnedForEditing
(
record
)
?
'待重新填写'
:
getEditorStatusLabel
(
record
?.
editorStatus
);
}
function
getForwardEditorStatusTone
(
record
)
{
return
!
isReturnedForEditing
(
record
)
&&
record
?.
editorStatus
===
'completed'
?
'emerald'
:
'amber'
;
}
export
default
function
ForwardApprovals
()
{
export
default
function
ForwardApprovals
()
{
const
userInfo
=
useContext
(
UserContext
);
const
userInfo
=
useContext
(
UserContext
);
const
currentUserId
=
userInfo
?.
_id
||
userInfo
?.
id
||
userInfo
?.
username
||
''
;
const
currentUserId
=
userInfo
?.
_id
||
userInfo
?.
id
||
userInfo
?.
username
||
''
;
...
@@ -69,9 +90,7 @@ export default function ForwardApprovals() {
...
@@ -69,9 +90,7 @@ export default function ForwardApprovals() {
if
(
!
cancelled
)
{
if
(
!
cancelled
)
{
setRecords
(
result
);
setRecords
(
result
);
const
hasEditableInvolvedRecord
=
result
.
some
(
const
hasEditableInvolvedRecord
=
result
.
some
(
(
record
)
=>
(
record
)
=>
canRecipientEdit
(
record
,
currentUserId
),
record
.
recipientIds
.
includes
(
currentUserId
)
&&
record
.
editorStatus
!==
'completed'
,
);
);
if
(
hasEditableInvolvedRecord
)
{
if
(
hasEditableInvolvedRecord
)
{
setActiveFilter
(
'involved'
);
setActiveFilter
(
'involved'
);
...
@@ -97,9 +116,7 @@ export default function ForwardApprovals() {
...
@@ -97,9 +116,7 @@ export default function ForwardApprovals() {
const
editableInvolvedCount
=
useMemo
(
const
editableInvolvedCount
=
useMemo
(
()
=>
()
=>
records
.
filter
(
records
.
filter
(
(
record
)
=>
(
record
)
=>
canRecipientEdit
(
record
,
currentUserId
),
record
.
recipientIds
.
includes
(
currentUserId
)
&&
record
.
editorStatus
!==
'completed'
,
).
length
,
).
length
,
[
currentUserId
,
records
],
[
currentUserId
,
records
],
);
);
...
@@ -262,6 +279,8 @@ export default function ForwardApprovals() {
...
@@ -262,6 +279,8 @@ export default function ForwardApprovals() {
completed_by_name
:
completed_by_name
:
userInfo
?.
display_name
||
userInfo
?.
displayName
||
userInfo
?.
username
||
''
,
userInfo
?.
display_name
||
userInfo
?.
displayName
||
userInfo
?.
username
||
''
,
completed_at
:
new
Date
().
toISOString
(),
completed_at
:
new
Date
().
toISOString
(),
approval_status
:
'pending'
,
final_approval_status
:
'pending'
,
});
});
setEditingRecord
(
updatedForwardRecord
);
setEditingRecord
(
updatedForwardRecord
);
...
@@ -301,6 +320,10 @@ export default function ForwardApprovals() {
...
@@ -301,6 +320,10 @@ export default function ForwardApprovals() {
record
.
approvalStatus
===
'pending'
record
.
approvalStatus
===
'pending'
)
{
)
{
await
handleUpdateRecord
(
record
,
{
await
handleUpdateRecord
(
record
,
{
editor_status
:
'editing'
,
completed_by_id
:
''
,
completed_by_name
:
''
,
completed_at
:
''
,
approval_status
:
'rejected'
,
approval_status
:
'rejected'
,
final_approval_status
:
'pending'
,
final_approval_status
:
'pending'
,
});
});
...
@@ -312,14 +335,18 @@ export default function ForwardApprovals() {
...
@@ -312,14 +335,18 @@ export default function ForwardApprovals() {
record
.
approvalStatus
===
'approved'
&&
record
.
approvalStatus
===
'approved'
&&
record
.
finalApprovalStatus
===
'pending'
record
.
finalApprovalStatus
===
'pending'
)
{
)
{
await
handleUpdateRecord
(
record
,
{
final_approval_status
:
'rejected'
});
await
handleUpdateRecord
(
record
,
{
editor_status
:
'editing'
,
completed_by_id
:
''
,
completed_by_name
:
''
,
completed_at
:
''
,
final_approval_status
:
'rejected'
,
});
}
}
}
}
const
canCurrentUserEdit
=
Boolean
(
const
canCurrentUserEdit
=
Boolean
(
activeRecord
&&
canRecipientEdit
(
activeRecord
,
currentUserId
),
activeRecord
.
recipientIds
.
includes
(
currentUserId
)
&&
activeRecord
.
editorStatus
!==
'completed'
,
);
);
const
canCurrentUserViewData
=
Boolean
(
const
canCurrentUserViewData
=
Boolean
(
...
@@ -332,9 +359,7 @@ export default function ForwardApprovals() {
...
@@ -332,9 +359,7 @@ export default function ForwardApprovals() {
);
);
const
canEditingRecordEdit
=
Boolean
(
const
canEditingRecordEdit
=
Boolean
(
editingRecord
&&
canRecipientEdit
(
editingRecord
,
currentUserId
),
editingRecord
.
recipientIds
.
includes
(
currentUserId
)
&&
editingRecord
.
editorStatus
!==
'completed'
,
);
);
const
activeEditableRecord
=
useMemo
(
const
activeEditableRecord
=
useMemo
(
...
@@ -459,8 +484,8 @@ export default function ForwardApprovals() {
...
@@ -459,8 +484,8 @@ export default function ForwardApprovals() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
<
div
className=
"flex flex-wrap gap-2"
>
<
Tag
tone=
{
record
.
editorStatus
===
'completed'
?
'emerald'
:
'amber'
}
>
<
Tag
tone=
{
getForwardEditorStatusTone
(
record
)
}
>
{
get
EditorStatusLabel
(
record
.
editorStatus
)
}
{
get
ForwardEditorStatusLabel
(
record
)
}
</
Tag
>
</
Tag
>
<
ApprovalStatusTag
status=
{
record
.
approvalStatus
}
/>
<
ApprovalStatusTag
status=
{
record
.
approvalStatus
}
/>
<
ApprovalStatusTag
status=
{
record
.
finalApprovalStatus
}
/>
<
ApprovalStatusTag
status=
{
record
.
finalApprovalStatus
}
/>
...
@@ -495,8 +520,8 @@ export default function ForwardApprovals() {
...
@@ -495,8 +520,8 @@ export default function ForwardApprovals() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
<
div
className=
"flex flex-wrap gap-2"
>
<
Tag
tone=
{
activeRecord
.
editorStatus
===
'completed'
?
'emerald'
:
'amber'
}
>
<
Tag
tone=
{
getForwardEditorStatusTone
(
activeRecord
)
}
>
{
get
EditorStatusLabel
(
activeRecord
.
editorStatus
)
}
{
get
ForwardEditorStatusLabel
(
activeRecord
)
}
</
Tag
>
</
Tag
>
<
ApprovalStatusTag
status=
{
activeRecord
.
approvalStatus
}
/>
<
ApprovalStatusTag
status=
{
activeRecord
.
approvalStatus
}
/>
<
ApprovalStatusTag
status=
{
activeRecord
.
finalApprovalStatus
}
/>
<
ApprovalStatusTag
status=
{
activeRecord
.
finalApprovalStatus
}
/>
...
@@ -529,10 +554,12 @@ export default function ForwardApprovals() {
...
@@ -529,10 +554,12 @@ export default function ForwardApprovals() {
<
div
className=
"rounded-lg border border-zinc-200 bg-white p-4"
>
<
div
className=
"rounded-lg border border-zinc-200 bg-white p-4"
>
<
div
className=
"text-sm font-medium text-zinc-500"
>
填写状态
</
div
>
<
div
className=
"text-sm font-medium text-zinc-500"
>
填写状态
</
div
>
<
div
className=
"mt-2 text-base font-semibold text-zinc-950"
>
<
div
className=
"mt-2 text-base font-semibold text-zinc-950"
>
{
get
EditorStatusLabel
(
activeRecord
.
editorStatus
)
}
{
get
ForwardEditorStatusLabel
(
activeRecord
)
}
</
div
>
</
div
>
<
div
className=
"mt-2 text-sm text-zinc-500"
>
<
div
className=
"mt-2 text-sm text-zinc-500"
>
{
activeRecord
.
completedByName
{
isReturnedForEditing
(
activeRecord
)
?
'审批已驳回,接收人可修改后重新提交'
:
activeRecord
.
completedByName
?
`已由 ${activeRecord.completedByName} 于 ${formatDateTime(activeRecord.completedAt)} 完成填写`
?
`已由 ${activeRecord.completedByName} 于 ${formatDateTime(activeRecord.completedAt)} 完成填写`
:
'接收人仍可进入填写并保存修改'
}
:
'接收人仍可进入填写并保存修改'
}
</
div
>
</
div
>
...
@@ -569,7 +596,7 @@ export default function ForwardApprovals() {
...
@@ -569,7 +596,7 @@ export default function ForwardApprovals() {
</
Button
>
</
Button
>
</
div
>
</
div
>
<
div
className=
"mt-3 text-xs leading-5 text-zinc-500"
>
<
div
className=
"mt-3 text-xs leading-5 text-zinc-500"
>
规则:填写人、一级审批人和终审人都可以查看这批次数据;
只有接收人且批次仍处于填写中时可以修改。任意一位接收人点击“完成填写”后,这批数据即锁定为不可修改,并进入待审批状态
。
规则:填写人、一级审批人和终审人都可以查看这批次数据;
接收人可在填写中或审批驳回后修改。重新完成填写后,从一级审批重新开始
。
</
div
>
</
div
>
</
div
>
</
div
>
...
@@ -614,7 +641,7 @@ export default function ForwardApprovals() {
...
@@ -614,7 +641,7 @@ export default function ForwardApprovals() {
</
Button
>
</
Button
>
</
div
>
</
div
>
<
div
className=
"mt-3 text-xs leading-5 text-zinc-500"
>
<
div
className=
"mt-3 text-xs leading-5 text-zinc-500"
>
规则:终审只能在一级审批通过后推进;
如果一级审批驳回,最终审批状态保持待处理
。
规则:终审只能在一级审批通过后推进;
任一环节驳回都会退回接收人修改,重新提交后从一级审批开始
。
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/pages/project_manager_xy/ModelBatchData.jsx
View file @
963b3a6e
...
@@ -231,18 +231,18 @@ export default function ModelBatchData() {
...
@@ -231,18 +231,18 @@ export default function ModelBatchData() {
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
loading
?
<
NoticeBanner
>
正在加载批次数据和转发状态...
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
error
?
<
NoticeBanner
tone=
"error"
>
{
error
}
</
NoticeBanner
>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
!
error
&&
notice
?
<
NoticeBanner
tone=
"success"
>
{
notice
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'
请返回批次页重新同步
。'
}
</
NoticeBanner
>
:
null
}
{
batch
?.
syncStatus
===
'partial'
||
batch
?.
syncStatus
===
'failed'
?
<
NoticeBanner
tone=
"error"
>
最近一次同步未完整成功:
{
batch
.
errorMessage
||
'
该批次不可重试,请联系管理员排查
。'
}
</
NoticeBanner
>
:
null
}
{
!
loading
&&
batch
?
(
{
!
loading
&&
batch
?
(
<
Panel
className=
"min-w-0 p-4 sm:p-5"
>
<
Panel
className=
"min-w-0 p-4 sm:p-5"
>
<
SectionHeader
title=
"批次数据"
description=
{
`当前展示 ${visibleRecords.length} 条,手动隐藏 ${hiddenRecords.length} 条,流程临时隐藏 ${forwardStatusIndex.flowHiddenRecordIds.size} 条。`
}
action=
{
<
Button
outline
onClick=
{
()
=>
setIsHiddenDialogOpen
(
true
)
}
disabled=
{
!
hiddenRecords
.
length
}
><
EyeIcon
className=
"size-4"
/>
管理已隐藏
</
Button
>
}
/>
<
SectionHeader
title=
"批次数据"
description=
{
`当前展示 ${visibleRecords.length} 条,手动隐藏 ${hiddenRecords.length} 条,流程临时隐藏 ${forwardStatusIndex.flowHiddenRecordIds.size} 条。`
}
action=
{
<
Button
outline
onClick=
{
()
=>
setIsHiddenDialogOpen
(
true
)
}
disabled=
{
!
hiddenRecords
.
length
}
><
EyeIcon
className=
"size-4"
/>
管理已隐藏
</
Button
>
}
/>
<
SelectionToolbar
count=
{
selectedRecordIds
.
length
}
onForward=
{
openForwardDialog
}
onHide=
{
handleHideSelected
}
onClear=
{
()
=>
setSelectedRecordIds
([])
}
/>
<
SelectionToolbar
count=
{
selectedRecordIds
.
length
}
onForward=
{
openForwardDialog
}
onHide=
{
handleHideSelected
}
onClear=
{
()
=>
setSelectedRecordIds
([])
}
/>
{
pageRecords
.
length
?
<
RecordsResultTable
records=
{
pageRecords
}
columns=
{
batch
.
columnSchema
}
selectedIds=
{
selectedRecordIds
}
rowStatusByRecordId=
{
rowStatusByRecordId
}
forwardItemByRecordId=
{
forwardItemByRecordId
}
onOpenForwardItem=
{
(
record
,
item
)
=>
setForwardItemDetail
({
record
,
item
})
}
onToggleRow=
{
handleToggleRow
}
onToggleAll=
{
handleToggleAll
}
/>
:
<
EmptyCollection
title=
"当前批次没有可展示数据"
description=
{
allRecords
.
length
?
'数据可能已被手动隐藏,或因未结束转发中的“不下发”设置暂时隐藏。'
:
'请
返回批次页重新同步
,或确认动态数据对象中是否已有记录。'
}
/>
}
{
pageRecords
.
length
?
<
RecordsResultTable
records=
{
pageRecords
}
columns=
{
batch
.
columnSchema
}
selectedIds=
{
selectedRecordIds
}
rowStatusByRecordId=
{
rowStatusByRecordId
}
forwardItemByRecordId=
{
forwardItemByRecordId
}
onOpenForwardItem=
{
(
record
,
item
)
=>
setForwardItemDetail
({
record
,
item
})
}
onToggleRow=
{
handleToggleRow
}
onToggleAll=
{
handleToggleAll
}
/>
:
<
EmptyCollection
title=
"当前批次没有可展示数据"
description=
{
allRecords
.
length
?
'数据可能已被手动隐藏,或因未结束转发中的“不下发”设置暂时隐藏。'
:
'请
查看批次错误摘要
,或确认动态数据对象中是否已有记录。'
}
/>
}
{
visibleRecords
.
length
>
PAGE_SIZE
?
<
div
className=
"mt-4 flex items-center justify-between border-t border-zinc-200 pt-4"
><
Button
outline
disabled=
{
page
===
0
}
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
max
(
0
,
value
-
1
))
}
>
上一页
</
Button
><
span
className=
"text-sm tabular-nums text-zinc-500"
>
第
{
page
+
1
}
/
{
pageCount
}
页
</
span
><
Button
outline
disabled=
{
page
+
1
>=
pageCount
}
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
min
(
pageCount
-
1
,
value
+
1
))
}
>
下一页
</
Button
></
div
>
:
null
}
{
visibleRecords
.
length
>
PAGE_SIZE
?
<
div
className=
"mt-4 flex items-center justify-between border-t border-zinc-200 pt-4"
><
Button
outline
disabled=
{
page
===
0
}
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
max
(
0
,
value
-
1
))
}
>
上一页
</
Button
><
span
className=
"text-sm tabular-nums text-zinc-500"
>
第
{
page
+
1
}
/
{
pageCount
}
页
</
span
><
Button
outline
disabled=
{
page
+
1
>=
pageCount
}
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
min
(
pageCount
-
1
,
value
+
1
))
}
>
下一页
</
Button
></
div
>
:
null
}
</
Panel
>
</
Panel
>
)
:
null
}
)
:
null
}
<
Dialog
open=
{
isHiddenDialogOpen
}
onClose=
{
setIsHiddenDialogOpen
}
size=
"xl"
><
DialogTitle
>
管理已隐藏记录
</
DialogTitle
><
DialogDescription
>
隐藏范围属于当前模型批次,对所有用户生效;恢复后记录会重新出现在数据页。
</
DialogDescription
><
DialogBody
>
{
hiddenRecords
.
length
?
<
div
className=
"divide-y divide-zinc-100"
>
{
hiddenRecords
.
map
((
record
)
=>
<
div
key=
{
record
.
recordName
||
record
.
id
}
className=
"flex items-center justify-between gap-4 py-3"
><
div
className=
"min-w-0"
><
div
className=
"truncate text-sm font-semibold text-zinc-900"
>
{
record
.
title
}
</
div
><
div
className=
"mt-1 truncate text-xs text-zinc-500"
>
{
record
.
sourceR
ecordKey
}
</
div
></
div
><
Button
outline
disabled=
{
saving
}
onClick=
{
()
=>
handleRestoreRecord
(
record
.
recordName
||
record
.
id
)
}
><
EyeIcon
className=
"size-4"
/>
恢复
</
Button
></
div
>)
}
</
div
>
:
<
EmptyCollection
title=
"没有手动隐藏记录"
description=
"当前批次的全部记录都处于可展示状态。"
/>
}
</
DialogBody
><
DialogActions
><
Button
plain
onClick=
{
()
=>
setIsHiddenDialogOpen
(
false
)
}
>
关闭
</
Button
></
DialogActions
></
Dialog
>
<
Dialog
open=
{
isHiddenDialogOpen
}
onClose=
{
setIsHiddenDialogOpen
}
size=
"xl"
><
DialogTitle
>
管理已隐藏记录
</
DialogTitle
><
DialogDescription
>
隐藏范围属于当前模型批次,对所有用户生效;恢复后记录会重新出现在数据页。
</
DialogDescription
><
DialogBody
>
{
hiddenRecords
.
length
?
<
div
className=
"divide-y divide-zinc-100"
>
{
hiddenRecords
.
map
((
record
)
=>
<
div
key=
{
record
.
recordName
||
record
.
id
}
className=
"flex items-center justify-between gap-4 py-3"
><
div
className=
"min-w-0"
><
div
className=
"truncate text-sm font-semibold text-zinc-900"
>
{
record
.
title
}
</
div
><
div
className=
"mt-1 truncate text-xs text-zinc-500"
>
{
record
.
sourceR
owNumber
?
`来源第 ${record.sourceRowNumber} 条`
:
record
.
recordName
}
</
div
></
div
><
Button
outline
disabled=
{
saving
}
onClick=
{
()
=>
handleRestoreRecord
(
record
.
recordName
||
record
.
id
)
}
><
EyeIcon
className=
"size-4"
/>
恢复
</
Button
></
div
>)
}
</
div
>
:
<
EmptyCollection
title=
"没有手动隐藏记录"
description=
"当前批次的全部记录都处于可展示状态。"
/>
}
</
DialogBody
><
DialogActions
><
Button
plain
onClick=
{
()
=>
setIsHiddenDialogOpen
(
false
)
}
>
关闭
</
Button
></
DialogActions
></
Dialog
>
<
Dialog
open=
{
isForwardDialogOpen
}
onClose=
{
saving
?
()
=>
{}
:
setIsForwardDialogOpen
}
size=
"2xl"
><
DialogTitle
>
提交转发审批
</
DialogTitle
><
DialogDescription
>
勾选本次实际下发的数据;取消勾选的记录会作为“不下发”记录,在本流程结束前暂时从批次页隐藏。
</
DialogDescription
><
DialogBody
className=
"space-y-5"
><
div
><
FieldLabel
required
>
本次下发记录
</
FieldLabel
><
div
className=
"max-h-56 divide-y divide-zinc-100 overflow-y-auto rounded-lg border border-zinc-200"
>
{
selectedRecords
.
map
((
record
)
=>
{
const
key
=
record
.
recordName
||
record
.
id
;
return
<
label
key=
{
key
}
className=
"flex min-h-11 cursor-pointer items-center gap-3 px-4 py-2 text-sm hover:bg-zinc-50"
><
Checkbox
color=
"blue"
checked=
{
forwardForm
.
deliverRecordIds
.
includes
(
key
)
}
onChange=
{
(
checked
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
deliverRecordIds
:
checked
?
[...
current
.
deliverRecordIds
,
key
]
:
current
.
deliverRecordIds
.
filter
((
id
)
=>
id
!==
key
)
}))
}
/><
span
className=
"truncate"
>
{
record
.
title
}
</
span
></
label
>;
})
}
</
div
></
div
><
UserMultiSelectField
values=
{
forwardForm
.
recipients
}
onChange=
{
(
recipients
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
recipients
}))
}
label=
"转发接收人"
required
/><
div
className=
"grid gap-4 sm:grid-cols-2"
><
UserSelectField
value=
{
forwardForm
.
firstApprover
}
onChange=
{
(
firstApprover
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
firstApprover
}))
}
label=
"一级审批人"
required
/><
UserSelectField
value=
{
forwardForm
.
finalApprover
}
onChange=
{
(
finalApprover
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
finalApprover
}))
}
label=
"终审人"
required
/></
div
><
div
><
FieldLabel
required
>
转发说明
</
FieldLabel
><
Textarea
rows=
{
4
}
value=
{
forwardForm
.
note
}
onChange=
{
(
event
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
note
:
event
.
target
.
value
}))
}
/></
div
></
DialogBody
><
DialogActions
><
Button
plain
disabled=
{
saving
}
onClick=
{
()
=>
setIsForwardDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
color=
"blue"
disabled=
{
saving
}
onClick=
{
handleSubmitForward
}
>
提交审批
</
Button
></
DialogActions
></
Dialog
>
<
Dialog
open=
{
isForwardDialogOpen
}
onClose=
{
saving
?
()
=>
{}
:
setIsForwardDialogOpen
}
size=
"2xl"
><
DialogTitle
>
提交转发审批
</
DialogTitle
><
DialogDescription
>
勾选本次实际下发的数据;取消勾选的记录会作为“不下发”记录,在本流程结束前暂时从批次页隐藏。
</
DialogDescription
><
DialogBody
className=
"space-y-5"
><
div
><
FieldLabel
required
>
本次下发记录
</
FieldLabel
><
div
className=
"max-h-56 divide-y divide-zinc-100 overflow-y-auto rounded-lg border border-zinc-200"
>
{
selectedRecords
.
map
((
record
)
=>
{
const
key
=
record
.
recordName
||
record
.
id
;
return
<
label
key=
{
key
}
className=
"flex min-h-11 cursor-pointer items-center gap-3 px-4 py-2 text-sm hover:bg-zinc-50"
><
Checkbox
color=
"blue"
checked=
{
forwardForm
.
deliverRecordIds
.
includes
(
key
)
}
onChange=
{
(
checked
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
deliverRecordIds
:
checked
?
[...
current
.
deliverRecordIds
,
key
]
:
current
.
deliverRecordIds
.
filter
((
id
)
=>
id
!==
key
)
}))
}
/><
span
className=
"truncate"
>
{
record
.
title
}
</
span
></
label
>;
})
}
</
div
></
div
><
UserMultiSelectField
values=
{
forwardForm
.
recipients
}
onChange=
{
(
recipients
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
recipients
}))
}
label=
"转发接收人"
required
/><
div
className=
"grid gap-4 sm:grid-cols-2"
><
UserSelectField
value=
{
forwardForm
.
firstApprover
}
onChange=
{
(
firstApprover
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
firstApprover
}))
}
label=
"一级审批人"
required
/><
UserSelectField
value=
{
forwardForm
.
finalApprover
}
onChange=
{
(
finalApprover
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
finalApprover
}))
}
label=
"终审人"
required
/></
div
><
div
><
FieldLabel
required
>
转发说明
</
FieldLabel
><
Textarea
rows=
{
4
}
value=
{
forwardForm
.
note
}
onChange=
{
(
event
)
=>
setForwardForm
((
current
)
=>
({
...
current
,
note
:
event
.
target
.
value
}))
}
/></
div
></
DialogBody
><
DialogActions
><
Button
plain
disabled=
{
saving
}
onClick=
{
()
=>
setIsForwardDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
color=
"blue"
disabled=
{
saving
}
onClick=
{
handleSubmitForward
}
>
提交审批
</
Button
></
DialogActions
></
Dialog
>
...
...
src/pages/project_manager_xy/ModelBatches.jsx
View file @
963b3a6e
...
@@ -81,7 +81,7 @@ export default function ModelBatches() {
...
@@ -81,7 +81,7 @@ export default function ModelBatches() {
return
batches
.
filter
((
batch
)
=>
!
search
||
`
${
batch
.
batchName
}
${
batch
.
batchCode
}
${
batch
.
syncStatus
}
`
.
toLowerCase
().
includes
(
search
));
return
batches
.
filter
((
batch
)
=>
!
search
||
`
${
batch
.
batchName
}
${
batch
.
batchCode
}
${
batch
.
syncStatus
}
`
.
toLowerCase
().
includes
(
search
));
},
[
batches
,
keyword
]);
},
[
batches
,
keyword
]);
const
syncedBatchCodes
=
useMemo
(
const
syncedBatchCodes
=
useMemo
(
()
=>
new
Set
(
batches
.
filter
((
batch
)
=>
batch
.
syncStatus
===
'success'
).
map
((
batch
)
=>
batch
.
batchCode
)),
()
=>
new
Set
(
batches
.
map
((
batch
)
=>
batch
.
batchCode
)),
[
batches
],
[
batches
],
);
);
...
@@ -92,10 +92,12 @@ export default function ModelBatches() {
...
@@ -92,10 +92,12 @@ export default function ModelBatches() {
setProgress
({
completed
:
0
,
total
:
0
});
setProgress
({
completed
:
0
,
total
:
0
});
try
{
try
{
const
sourceModels
=
await
listSourceModels
();
const
sourceModels
=
await
listSourceModels
();
const
matchedModel
=
sourceModels
.
find
((
item
)
=>
item
.
model_code
===
model
.
modelCode
);
const
matchedModel
=
sourceModels
.
find
(
(
item
)
=>
String
(
item
.
source_model_id
)
===
model
.
sourceModelId
&&
item
.
model_code
===
model
.
modelCode
,
);
if
(
!
matchedModel
)
throw
new
Error
(
'外部模型列表中已找不到当前模型。'
);
if
(
!
matchedModel
)
throw
new
Error
(
'外部模型列表中已找不到当前模型。'
);
setSourceModel
(
matchedModel
);
setSourceModel
(
matchedModel
);
setSourceBatches
(
await
listSourceBatches
(
matchedModel
.
model_name
));
setSourceBatches
(
await
listSourceBatches
(
matchedModel
));
}
catch
(
loadError
)
{
}
catch
(
loadError
)
{
setError
(
loadError
?.
message
||
'外部批次列表加载失败。'
);
setError
(
loadError
?.
message
||
'外部批次列表加载失败。'
);
setIsSyncDialogOpen
(
false
);
setIsSyncDialogOpen
(
false
);
...
@@ -108,14 +110,14 @@ export default function ModelBatches() {
...
@@ -108,14 +110,14 @@ export default function ModelBatches() {
const
sourceBatch
=
sourceBatches
.
find
((
item
)
=>
item
.
batch_code
===
selectedBatchCode
);
const
sourceBatch
=
sourceBatches
.
find
((
item
)
=>
item
.
batch_code
===
selectedBatchCode
);
if
(
!
sourceModel
||
!
sourceBatch
)
return
;
if
(
!
sourceModel
||
!
sourceBatch
)
return
;
if
(
syncedBatchCodes
.
has
(
sourceBatch
.
batch_code
))
{
if
(
syncedBatchCodes
.
has
(
sourceBatch
.
batch_code
))
{
setError
(
'该批次已经
同步成功
,不能再次同步。'
);
setError
(
'该批次已经
执行过同步
,不能再次同步。'
);
return
;
return
;
}
}
setSyncing
(
true
);
setSyncing
(
true
);
try
{
try
{
const
result
=
await
synchronizeModelBatch
(
sourceModel
,
sourceBatch
,
userInfo
,
setProgress
);
const
result
=
await
synchronizeModelBatch
(
sourceModel
,
sourceBatch
,
userInfo
,
setProgress
);
await
refresh
(
result
.
model
);
await
refresh
(
result
.
model
);
setNotice
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条
,更新
${
result
.
updatedCount
}
条
。`
);
setNotice
(
`批次“
${
sourceBatch
.
batch_name
}
”同步完成:新增
${
result
.
insertedCount
}
条。`
);
setError
(
''
);
setError
(
''
);
setIsSyncDialogOpen
(
false
);
setIsSyncDialogOpen
(
false
);
}
catch
(
syncError
)
{
}
catch
(
syncError
)
{
...
@@ -155,7 +157,7 @@ export default function ModelBatches() {
...
@@ -155,7 +157,7 @@ export default function ModelBatches() {
</
Panel
>
</
Panel
>
)
:
null
}
)
:
null
}
<
Dialog
open=
{
isSyncDialogOpen
}
onClose=
{
syncing
?
()
=>
{}
:
setIsSyncDialogOpen
}
size=
"xl"
><
DialogTitle
>
同步
{
model
?.
modelName
}
</
DialogTitle
><
DialogDescription
>
选择一个外部批次。
已同步成功的批次不可再次同步,失败或部分成功的批次仍可重试
。
</
DialogDescription
><
DialogBody
className=
"space-y-4"
><
div
><
FieldLabel
required
>
外部批次
</
FieldLabel
><
Select
value=
{
selectedBatchCode
}
disabled=
{
syncing
}
onChange=
{
(
event
)
=>
setSelectedBatchCode
(
event
.
target
.
value
)
}
><
option
value=
""
>
请选择批次
</
option
>
{
sourceBatches
.
map
((
batch
)
=>
{
const
synced
=
syncedBatchCodes
.
has
(
batch
.
batch_code
);
return
<
option
key=
{
batch
.
batch_code
}
value=
{
batch
.
batch_code
}
disabled=
{
synced
}
>
{
batch
.
batch_name
}
(
{
batch
.
batch_code
}
)
{
synced
?
' - 已同步'
:
''
}
</
option
>;
})
}
</
Select
></
div
>
{
syncing
&&
progress
.
total
?
<
div
className=
"rounded-lg border border-sky-200 bg-sky-50 p-4"
><
div
className=
"flex justify-between text-sm text-sky-900"
><
span
>
正在写入模型数据...
</
span
><
span
className=
"tabular-nums"
>
{
progress
.
completed
}
/
{
progress
.
total
}
</
span
></
div
><
div
className=
"mt-3 h-2 overflow-hidden rounded-full bg-sky-100"
><
div
className=
"h-full bg-sky-600 transition-all"
style=
{
{
width
:
`${Math.round(progress.completed / progress.total * 100)}%`
}
}
/></
div
></
div
>
:
null
}
</
DialogBody
><
DialogActions
><
Button
plain
disabled=
{
syncing
}
onClick=
{
()
=>
setIsSyncDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
color=
"blue"
disabled=
{
!
selectedBatchCode
||
syncedBatchCodes
.
has
(
selectedBatchCode
)
||
syncing
}
onClick=
{
handleSync
}
>
开始同步
</
Button
></
DialogActions
></
Dialog
>
<
Dialog
open=
{
isSyncDialogOpen
}
onClose=
{
syncing
?
()
=>
{}
:
setIsSyncDialogOpen
}
size=
"xl"
><
DialogTitle
>
同步
{
model
?.
modelName
}
</
DialogTitle
><
DialogDescription
>
选择一个外部批次。
每个来源批次只能同步一次,执行后不可再次同步
。
</
DialogDescription
><
DialogBody
className=
"space-y-4"
><
div
><
FieldLabel
required
>
外部批次
</
FieldLabel
><
Select
value=
{
selectedBatchCode
}
disabled=
{
syncing
}
onChange=
{
(
event
)
=>
setSelectedBatchCode
(
event
.
target
.
value
)
}
><
option
value=
""
>
请选择批次
</
option
>
{
sourceBatches
.
map
((
batch
)
=>
{
const
synced
=
syncedBatchCodes
.
has
(
batch
.
batch_code
);
return
<
option
key=
{
batch
.
batch_code
}
value=
{
batch
.
batch_code
}
disabled=
{
synced
}
>
{
batch
.
batch_name
}
(
{
batch
.
batch_code
}
)
{
synced
?
' - 已同步'
:
''
}
</
option
>;
})
}
</
Select
></
div
>
{
syncing
&&
progress
.
total
?
<
div
className=
"rounded-lg border border-sky-200 bg-sky-50 p-4"
><
div
className=
"flex justify-between text-sm text-sky-900"
><
span
>
正在写入模型数据...
</
span
><
span
className=
"tabular-nums"
>
{
progress
.
completed
}
/
{
progress
.
total
}
</
span
></
div
><
div
className=
"mt-3 h-2 overflow-hidden rounded-full bg-sky-100"
><
div
className=
"h-full bg-sky-600 transition-all"
style=
{
{
width
:
`${Math.round(progress.completed / progress.total * 100)}%`
}
}
/></
div
></
div
>
:
null
}
</
DialogBody
><
DialogActions
><
Button
plain
disabled=
{
syncing
}
onClick=
{
()
=>
setIsSyncDialogOpen
(
false
)
}
>
取消
</
Button
><
Button
color=
"blue"
disabled=
{
!
selectedBatchCode
||
syncedBatchCodes
.
has
(
selectedBatchCode
)
||
syncing
}
onClick=
{
handleSync
}
>
开始同步
</
Button
></
DialogActions
></
Dialog
>
</
div
>
</
div
>
);
);
}
}
src/pages/project_manager_xy/ModelMonitorMockService.js
View file @
963b3a6e
const
MOCK_MODELS
=
[
const
MOCK_MODELS
=
[
{
{
source_model_id
:
'430'
,
source_model_name
:
'AML_CUSTOMER_RISK'
,
model_code
:
'AML_CUSTOMER_RISK'
,
model_code
:
'AML_CUSTOMER_RISK'
,
model_name
:
'客户洗钱风险模型'
,
model_name
:
'客户洗钱风险模型'
,
primary_key_fields
:
[
'customer_id'
,
'risk_date'
],
model_state
:
'enabled'
,
model_apply_state
:
'applied'
,
model_risk_level
:
'high'
,
model_business_type
:
'12'
,
model_business_type_name
:
'风险分析'
,
source_model_updated_at
:
'2026-06-30 08:00:00'
,
},
},
{
{
source_model_id
:
'412'
,
source_model_name
:
'TRANSACTION_ALERT'
,
model_code
:
'TRANSACTION_ALERT'
,
model_code
:
'TRANSACTION_ALERT'
,
model_name
:
'异常交易预警模型'
,
model_name
:
'异常交易预警模型'
,
primary_key_fields
:
[
'alert_id'
],
model_state
:
'enabled'
,
model_apply_state
:
'applied'
,
model_risk_level
:
'medium'
,
model_business_type
:
'12'
,
model_business_type_name
:
'风险分析'
,
source_model_updated_at
:
'2026-07-20 08:00:00'
,
},
},
{
{
source_model_id
:
'411'
,
source_model_name
:
'CREDIT_EXPOSURE'
,
model_code
:
'CREDIT_EXPOSURE'
,
model_code
:
'CREDIT_EXPOSURE'
,
model_name
:
'授信敞口监测模型'
,
model_name
:
'授信敞口监测模型'
,
primary_key_fields
:
[
'institution_code'
,
'reporting_period'
],
model_state
:
'enabled'
,
model_apply_state
:
'applied'
,
model_risk_level
:
'medium'
,
model_business_type
:
'13'
,
model_business_type_name
:
'合规分析'
,
source_model_updated_at
:
'2026-06-30 08:00:00'
,
},
},
];
];
const
MOCK_BATCHES
=
{
const
MOCK_BATCHES
=
{
AML_CUSTOMER_RISK
:
[
430
:
[
{
batch_code
:
'2026-06'
,
batch_name
:
'2026 年 6 月批次'
},
{
source_model_id
:
'430'
,
batch_code
:
'690'
,
batch_name
:
'2026 年 6 月批次'
,
source_batch_date
:
'2026-06-30 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_430'
,
source_model_data_count
:
5
,
source_unchecked_count
:
5
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
batch_code
:
'2026-05'
,
batch_name
:
'2026 年 5 月批次'
},
{
source_model_id
:
'430'
,
batch_code
:
'652'
,
batch_name
:
'2026 年 5 月批次'
,
source_batch_date
:
'2026-05-31 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_430'
,
source_model_data_count
:
5
,
source_unchecked_count
:
5
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
source_model_id
:
'430'
,
batch_code
:
'590'
,
batch_name
:
'2025 年 12 月批次'
,
source_batch_date
:
'2025-12-31 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_430'
,
source_model_data_count
:
5
,
source_unchecked_count
:
5
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
],
],
TRANSACTION_ALERT
:
[
412
:
[
{
batch_code
:
'2026-W29'
,
batch_name
:
'2026 年第 29 周'
},
{
source_model_id
:
'412'
,
batch_code
:
'670'
,
batch_name
:
'2026 年第 29 周'
,
source_batch_date
:
'2026-07-20 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_412'
,
source_model_data_count
:
7
,
source_unchecked_count
:
7
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
batch_code
:
'2026-W28'
,
batch_name
:
'2026 年第 28 周'
},
{
source_model_id
:
'412'
,
batch_code
:
'669'
,
batch_name
:
'2026 年第 28 周'
,
source_batch_date
:
'2026-07-13 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_412'
,
source_model_data_count
:
7
,
source_unchecked_count
:
7
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
source_model_id
:
'412'
,
batch_code
:
'570'
,
batch_name
:
'2025 年第 52 周'
,
source_batch_date
:
'2025-12-29 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_412'
,
source_model_data_count
:
7
,
source_unchecked_count
:
7
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
],
],
CREDIT_EXPOSURE
:
[
411
:
[
{
batch_code
:
'2026-Q2'
,
batch_name
:
'2026 年第二季度'
},
{
source_model_id
:
'411'
,
batch_code
:
'630'
,
batch_name
:
'2026 年第二季度'
,
source_batch_date
:
'2026-06-30 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_411'
,
source_model_data_count
:
4
,
source_unchecked_count
:
4
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
batch_code
:
'2026-Q1'
,
batch_name
:
'2026 年第一季度'
},
{
source_model_id
:
'411'
,
batch_code
:
'629'
,
batch_name
:
'2026 年第一季度'
,
source_batch_date
:
'2026-03-31 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_411'
,
source_model_data_count
:
4
,
source_unchecked_count
:
4
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
{
source_model_id
:
'411'
,
batch_code
:
'530'
,
batch_name
:
'2025 年第四季度'
,
source_batch_date
:
'2025-12-31 08:00:00'
,
data_table_name
:
'ANALYZE_MODEL_DATA_MX_411'
,
source_model_data_count
:
4
,
source_unchecked_count
:
4
,
source_unsubmitted_count
:
0
,
source_reviewed_count
:
0
,
remark
:
''
},
],
],
};
};
...
@@ -41,16 +65,16 @@ const DATA_BUILDERS = {
...
@@ -41,16 +65,16 @@ const DATA_BUILDERS = {
{
name
:
'risk_score'
,
label
:
'风险分值'
,
type
:
'number'
},
{
name
:
'risk_score'
,
label
:
'风险分值'
,
type
:
'number'
},
{
name
:
'is_focus_customer'
,
label
:
'是否重点客户'
,
type
:
'boolean'
},
{
name
:
'is_focus_customer'
,
label
:
'是否重点客户'
,
type
:
'boolean'
},
];
];
const
month
=
batchCode
===
'2026-05'
?
'05'
:
'06'
;
const
period
=
batchCode
===
'590'
?
{
year
:
'2025'
,
month
:
'12'
}
:
batchCode
===
'652'
?
{
year
:
'2026'
,
month
:
'05'
}
:
{
year
:
'2026'
,
month
:
'06'
}
;
const
names
=
[
'华远实业'
,
'海川商贸'
,
'恒信科技'
,
'嘉禾供应链'
,
'远景投资'
];
const
names
=
[
'华远实业'
,
'海川商贸'
,
'恒信科技'
,
'嘉禾供应链'
,
'远景投资'
];
return
{
return
{
columns
,
columns
,
rows
:
names
.
map
((
name
,
index
)
=>
({
rows
:
names
.
map
((
name
,
index
)
=>
({
customer_id
:
`C
${
String
(
index
+
1
).
padStart
(
4
,
'0'
)}
`
,
customer_id
:
`C
${
String
(
index
+
1
).
padStart
(
4
,
'0'
)}
`
,
risk_date
:
`
2026-
${
month
}
-
${
String
(
index
+
3
).
padStart
(
2
,
'0'
)}
T00:00:00.000Z`
,
risk_date
:
`
${
period
.
year
}
-
${
period
.
month
}
-
${
String
(
index
+
3
).
padStart
(
2
,
'0'
)}
T00:00:00.000Z`
,
customer_name
:
name
,
customer_name
:
name
,
risk_level
:
index
<
2
?
'高'
:
index
<
4
?
'中'
:
'低'
,
risk_level
:
index
<
2
?
'高'
:
index
<
4
?
'中'
:
'低'
,
risk_score
:
91
-
index
*
9
+
(
month
===
'06'
?
2
:
0
),
risk_score
:
91
-
index
*
9
+
(
period
.
month
===
'06'
?
2
:
0
),
is_focus_customer
:
index
<
2
,
is_focus_customer
:
index
<
2
,
})),
})),
};
};
...
@@ -63,14 +87,18 @@ const DATA_BUILDERS = {
...
@@ -63,14 +87,18 @@ const DATA_BUILDERS = {
{
name
:
'transaction_time'
,
label
:
'交易时间'
,
type
:
'date'
},
{
name
:
'transaction_time'
,
label
:
'交易时间'
,
type
:
'date'
},
{
name
:
'alert_reason'
,
label
:
'预警原因'
,
type
:
'text'
},
{
name
:
'alert_reason'
,
label
:
'预警原因'
,
type
:
'text'
},
];
];
const
week
=
batchCode
.
endsWith
(
'28'
)
?
28
:
29
;
const
period
=
batchCode
===
'570'
?
{
year
:
'2025'
,
week
:
52
,
startDay
:
22
}
:
batchCode
===
'669'
?
{
year
:
'2026'
,
week
:
28
,
startDay
:
6
}
:
{
year
:
'2026'
,
week
:
29
,
startDay
:
13
};
return
{
return
{
columns
,
columns
,
rows
:
Array
.
from
({
length
:
7
},
(
_
,
index
)
=>
({
rows
:
Array
.
from
({
length
:
7
},
(
_
,
index
)
=>
({
alert_id
:
`AL
${
week
}${
String
(
index
+
1
).
padStart
(
3
,
'0'
)}
`
,
alert_id
:
`AL
${
period
.
week
}${
String
(
index
+
1
).
padStart
(
3
,
'0'
)}
`
,
account_name
:
`监测账户
${
index
+
1
}
`
,
account_name
:
`监测账户
${
index
+
1
}
`
,
transaction_amount
:
180000
+
index
*
73500
,
transaction_amount
:
180000
+
index
*
73500
,
transaction_time
:
`
2026-07-
${
String
((
week
===
29
?
13
:
6
)
+
index
).
padStart
(
2
,
'0'
)}
T0
${
index
%
9
}
:30:00.000Z`
,
transaction_time
:
`
${
period
.
year
}
-
${
period
.
year
===
'2025'
?
'12'
:
'07'
}
-
${
String
(
period
.
startDay
+
index
).
padStart
(
2
,
'0'
)}
T0
${
index
%
9
}
:30:00.000Z`
,
alert_reason
:
index
%
2
===
0
?
'短期高频大额交易'
:
'交易对手集中度异常'
,
alert_reason
:
index
%
2
===
0
?
'短期高频大额交易'
:
'交易对手集中度异常'
,
})),
})),
};
};
...
@@ -84,7 +112,7 @@ const DATA_BUILDERS = {
...
@@ -84,7 +112,7 @@ const DATA_BUILDERS = {
{
name
:
'used_amount'
,
label
:
'已用额度'
,
type
:
'number'
},
{
name
:
'used_amount'
,
label
:
'已用额度'
,
type
:
'number'
},
{
name
:
'over_limit'
,
label
:
'是否超限'
,
type
:
'boolean'
},
{
name
:
'over_limit'
,
label
:
'是否超限'
,
type
:
'boolean'
},
];
];
const
multiplier
=
batchCode
===
'
2026-Q1
'
?
0.94
:
1
;
const
multiplier
=
batchCode
===
'
629
'
?
0.94
:
1
;
return
{
return
{
columns
,
columns
,
rows
:
[
'总行营业部'
,
'华东分行'
,
'华南分行'
,
'西部中心'
].
map
((
name
,
index
)
=>
{
rows
:
[
'总行营业部'
,
'华东分行'
,
'华南分行'
,
'西部中心'
].
map
((
name
,
index
)
=>
{
...
@@ -112,19 +140,20 @@ export async function listSourceModels() {
...
@@ -112,19 +140,20 @@ export async function listSourceModels() {
return
MOCK_MODELS
.
map
((
item
)
=>
({
...
item
}));
return
MOCK_MODELS
.
map
((
item
)
=>
({
...
item
}));
}
}
export
async
function
listSourceBatches
(
modelName
)
{
export
async
function
listSourceBatches
(
sourceModel
)
{
await
wait
();
await
wait
();
const
model
=
MOCK_MODELS
.
find
((
item
)
=>
item
.
model_name
===
modelName
);
return
(
MOCK_BATCHES
[
sourceModel
?.
source_model_id
]
||
[]).
map
((
item
)
=>
({
return
(
MOCK_BATCHES
[
model
?.
model_code
]
||
[]).
map
((
item
)
=>
({
...
item
}));
...
item
,
source_model_name
:
sourceModel
.
source_model_name
||
sourceModel
.
model_code
,
}));
}
}
export
async
function
loadSourceBatchData
(
modelName
,
batchCode
)
{
export
async
function
loadSourceBatchData
(
sourceModel
,
sourceBatch
)
{
await
wait
(
420
);
await
wait
(
420
);
const
model
=
MOCK_MODELS
.
find
((
item
)
=>
item
.
model_name
===
modelName
);
const
builder
=
DATA_BUILDERS
[
sourceModel
?.
model_code
];
const
builder
=
DATA_BUILDERS
[
model
?.
model_code
];
if
(
!
builder
)
{
if
(
!
builder
)
{
throw
new
Error
(
'未找到所选模型的 Mock 数据。'
);
throw
new
Error
(
'未找到所选模型的 Mock 数据。'
);
}
}
return
builder
(
batchC
ode
);
return
builder
(
sourceBatch
?.
batch_c
ode
);
}
}
src/pages/project_manager_xy/ModelOverview.jsx
View file @
963b3a6e
...
@@ -98,7 +98,7 @@ export default function ModelOverview() {
...
@@ -98,7 +98,7 @@ export default function ModelOverview() {
{
failedBatches
.
length
?
(
{
failedBatches
.
length
?
(
<
NoticeBanner
tone=
"error"
>
<
NoticeBanner
tone=
"error"
>
有
{
failedBatches
.
length
}
个批次
需要处理。进入对应模型批次后重新同步,系统会按组合主键继续增量写入
。
有
{
failedBatches
.
length
}
个批次
同步异常。请进入对应模型批次查看错误摘要;一次性同步批次不允许重试
。
</
NoticeBanner
>
</
NoticeBanner
>
)
:
null
}
)
:
null
}
...
...
src/pages/project_manager_xy/Models.jsx
View file @
963b3a6e
This diff is collapsed.
Click to expand it.
src/pages/project_manager_xy/model-monitor.business-flow.md
View file @
963b3a6e
...
@@ -22,27 +22,37 @@
...
@@ -22,27 +22,37 @@
## 3. 外部数据源契约
## 3. 外部数据源契约
本期外部来源使用 Mock 适配器,UI 仅依赖以下方法
:
当前外部来源仍使用 Mock 适配器,但返回结构已映射到
`ANALYZE_MXVIEW_INFO`
和
`ANALYZE_MODEL_RUN_BATCH_INFO`
。UI 仅依赖以下方法,真实 Oracle 适配器接入时不得修改页面调用
:
```
ts
```
ts
listSourceModels
():
Promise
<
Array
<
{
listSourceModels
():
Promise
<
Array
<
{
source_model_id
:
string
;
source_model_name
:
string
;
model_code
:
string
;
model_code
:
string
;
model_name
:
string
;
model_name
:
string
;
primary_key_fields
:
string
[];
}
>>
;
}
>>
;
listSourceBatches
(
modelName
:
string
):
Promise
<
Array
<
{
listSourceBatches
(
sourceModel
:
SourceModel
):
Promise
<
Array
<
{
batch_code
:
string
;
source_model_id
:
string
;
source_model_name
:
string
;
batch_code
:
string
;
// MODEL_RUN_BATCH_ID
batch_name
:
string
;
batch_name
:
string
;
source_batch_date
:
string
;
data_table_name
:
string
;
source_model_data_count
:
number
;
}
>>
;
}
>>
;
loadSourceBatchData
(
modelName
:
string
,
batchCode
:
string
):
Promise
<
{
loadSourceBatchData
(
sourceModel
:
SourceModel
,
sourceBatch
:
SourceBatch
):
Promise
<
{
columns
:
Array
<
{
name
:
string
;
label
:
string
;
type
:
'text'
|
'number'
|
'date'
|
'boolean'
|
'object'
}
>
;
columns
:
Array
<
{
name
:
string
;
label
:
string
;
type
:
'text'
|
'number'
|
'date'
|
'boolean'
|
'object'
}
>
;
rows
:
Array
<
Record
<
string
,
unknown
>>
;
rows
:
Array
<
Record
<
string
,
unknown
>>
;
}
>
;
}
>
;
```
```
未来真实后台接口到位时只替换适配器,不修改页面和本地持久化服务。
来源映射规则:模型稳定键为
`ANALYZE_MXVIEW_INFO.MODEL_NAME + ID`
;批次稳定键为
`ANALYZE_MODEL_RUN_BATCH_INFO.MODEL_RUN_BATCH_ID`
,直接映射到本地
`batch_code`
;结果表来自
`MODEL_DATA_TABLE_NAME`
。结果表查询必须先对白名单表名做校验,再使用绑定参数按
`MODEL_RUN_BATCH_ID`
过滤,不能整表读取。
`ANALYZE_MODEL_DATA_MX_*`
结果行不要求业务主键。每个批次只允许同步一次,结果行按来源读取顺序记录
`source_row_number`
并纯新增,不进行去重或更新。
未来真实后台接口到位时只替换适配器,不修改页面和本地持久化服务。仓库目前没有 Oracle Connector 的调用协议,接入前必须补充接口 URL、请求体、分页和字段类型响应定义。
## 4. 核心流程
## 4. 核心流程
...
@@ -53,31 +63,34 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
...
@@ -53,31 +63,34 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
3.
缺失 Subject 时调用 Subject 创建接口。
3.
缺失 Subject 时调用 Subject 创建接口。
4.
初始化失败时显示明确错误,页面不得使用 localStorage 或演示 Record 冒充真实数据。
4.
初始化失败时显示明确错误,页面不得使用 localStorage 或演示 Record 冒充真实数据。
### 4.2 同步单个模型批次
### 4.2 批量同步模型批次
1.
用户在“全部模型”点击“同步模型数据”,或在某模型批次页点击“同步批次”。
1.
用户在“全部模型”点击“同步模型数据”;某模型批次页仍保留当前模型的单批次同步入口。
2.
选择一个外部模型;模型接口同时提供组合主键字段。
2.
系统加载全部外部模型及其批次,同时读取本地所有批次;模型继续使用
`MODEL_NAME + ID`
识别。
3.
系统按模型名称加载批次列表,并与本地批次比对:
`success`
批次显示“已同步”且不可选择,
`partial/failed`
批次仍可重试。
3.
来源批次按
`source_batch_date`
的年份归类,默认仅展示当前年份;用户必须主动切换年份才能查看历史批次,系统不自动勾选任何批次。
4.
系统获取
`columns + rows`
,在写入前完成:
4.
系统按
`MODEL_RUN_BATCH_ID`
与本地
`batch_code`
比对;本地已经存在的批次,无论
`syncing/success/partial/failed`
状态,均显示“已同步”、禁用复选框且不计入可选数量。
5.
用户可以跨模型逐项勾选批次,也可以“一键全选”当前年份内所有未同步批次;切换年份时保留已经勾选的批次,但全选不跨年份生效。
6.
提交后系统按选择顺序逐批同步;单个批次异常不阻断后续批次,结束后汇总成功批次数、异常批次数、新增数据量和失败数据量。
7.
每个批次获取
`columns + rows`
后,在写入前完成:
-
来源模型 ID、跑批 ID、模型归属和动态表名检查
-
来源声明数量与实际读取数量检查
-
字段别名、重复字段和类型检查
-
字段别名、重复字段和类型检查
-
组合主键字段存在性检查
-
所有数值、日期、布尔和对象字段的规范化检查
-
主键空值检查
8.
本地没有模型时按
`MODEL_NAME + ID`
创建稳定
`model_monitor_model`
Record,并生成稳定动态数据 Subject 名称。
-
批次内重复主键检查
9.
自动创建或扩展模型数据 Subject。只允许增加字段;类型冲突立即停止。
5.
本地没有模型时创建
`model_monitor_model`
,并生成稳定动态数据 Subject 名称。
10.
按
`MODEL_RUN_BATCH_ID`
查找批次;任何状态的本地批次一旦存在都拒绝重复同步,不再读取来源结果表。
6.
自动创建或扩展模型数据 Subject。只允许增加字段;类型冲突立即停止。
11.
为来源行生成从 1 开始的
`source_row_number`
。
7.
按“模型 ID + 批次编码”查找批次;不存在则创建,
`partial/failed`
批次更新为
`syncing`
,
`success`
批次直接拒绝重复同步。
12.
每 20 条分块纯新增写入,不查询已有结果行,也不执行更新;数据 Record name 由
`MODEL_RUN_BATCH_ID + source_row_number`
生成。
8.
读取当前批次已有记录,以
`source_record_key`
建立索引。
13.
同步完成后更新批次数量和状态,并更新模型最近同步状态;界面随即重新读取本地批次,把本轮已创建的批次锁定为不可选择。
9.
每 20 条分块写入:已有主键更新,新主键新增,本次未返回的旧记录保留。
10.
同步完成后更新批次数量和状态,并更新模型最近同步状态。
### 4.3 同步状态与失败恢复
### 4.3 同步状态与失败恢复
-
`syncing`
:同步执行中;
开始时间 30 分钟内
阻止再次发起。
-
`syncing`
:同步执行中;
批次 Record 创建后永久
阻止再次发起。
-
`success`
:全部行写入成功
;批次视为不可变,不允许再次同步
。
-
`success`
:全部行写入成功。
-
`partial`
:部分行写入失败,成功数据保留。
-
`partial`
:部分行写入失败,成功数据保留。
-
`failed`
:校验、对象创建或同步过程失败。
-
`failed`
:校验、对象创建或同步过程失败。
-
仅
`partial/failed`
批次允许重试;重试继续按组合主键新增或更新,不清空已有数据
。
-
`partial/failed`
批次保留现场供排查,同样不允许重试,避免重复写入
。
-
批次保留错误摘要、新增数、
更新数、失败数、来源行数和本地总数
。
-
批次保留错误摘要、新增数、
失败数、来源行数和本地总数;更新数固定为 0
。
### 4.4 模型分组
### 4.4 模型分组
...
@@ -107,23 +120,26 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
...
@@ -107,23 +120,26 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
1.
用户在批次数据页勾选 1-N 条记录。
1.
用户在批次数据页勾选 1-N 条记录。
2.
弹窗中再次确认本次实际下发记录;取消勾选的 ID 写入
`excluded_record_ids`
。
2.
弹窗中再次确认本次实际下发记录;取消勾选的 ID 写入
`excluded_record_ids`
。
3.
填写接收人、转发说明、一级审批人和终审人。
3.
填写接收人、转发说明、一级审批人和终审人。
4.
创建
`model_monitor_forward_record`
,并为实际下发记录创建
`model_monitor_forward_item`
。
4.
创建
`model_monitor_forward_record`
,并为实际下发记录创建
`model_monitor_forward_item`
;发起时不写入尚未填写的是否属实、整改方案和预计整改完成时间字段
。
5.
初始化
`editor_status = editing`
、
`approval_status = pending`
、
`final_approval_status = pending`
。
5.
初始化
`editor_status = editing`
、
`approval_status = pending`
、
`final_approval_status = pending`
。
6.
未结束流程中的
`excluded_record_ids`
在来源批次页暂时隐藏,
流程结束
后恢复展示。
6.
未结束流程中的
`excluded_record_ids`
在来源批次页暂时隐藏,
终审通过
后恢复展示。
### 4.8 填写与两级审批
### 4.8 填写与两级审批
1.
接收人在“我参与的”打开批次数据。
1.
接收人在“我参与的”打开批次数据。
2.
对每条下发记录填写是否属实、整改方案、预计整改完成时间。
2.
对每条下发记录填写是否属实、整改方案、预计整改完成时间。
3.
可多次保存;
点击“完成填写”前必须
校验全部三项。
3.
可多次保存;
三个回填字段在 Subject 层为可选,前端仍显示必填标记,并在点击“完成填写”前
校验全部三项。
4.
完成后写入完成人和时间,行级数据锁定。
4.
完成后写入完成人和时间,行级数据锁定。
5.
一级审批人通过或驳回;只有一级通过后终审人才可操作。
5.
一级审批人通过或驳回;只有一级通过后终审人才可操作。
6.
任一审批驳回后流程结束;终审通过后状态为已最终审批。
6.
任一审批驳回后,填写状态恢复为
`editing`
,清空上次完成人和完成时间,接收人可修改原回填内容。
7.
接收人重新完成填写时,
`approval_status`
和
`final_approval_status`
均重置为
`pending`
,流程从一级审批重新开始。
8.
终审通过后状态为已最终审批,流程结束。
## 5. 页面状态与异常处理
## 5. 页面状态与异常处理
-
所有接口必须展示 loading、success、error 三种状态。
-
所有接口必须展示 loading、success、error 三种状态。
-
同步、保存、审批期间禁用重复提交按钮。
-
同步、保存、审批期间禁用重复提交按钮。
-
批量同步期间禁止关闭弹窗;单批错误在结果区逐项展示,其余已选批次继续执行。
-
写入失败时保留当前选择和可恢复信息。
-
写入失败时保留当前选择和可恢复信息。
-
模型/批次/数据不存在时显示空状态或加载错误,不得白屏。
-
模型/批次/数据不存在时显示空状态或加载错误,不得白屏。
-
本期不提供模型或批次删除,也不限制同步和分组维护权限。
-
本期不提供模型或批次删除,也不限制同步和分组维护权限。
...
...
src/pages/project_manager_xy/modelMonitorService.js
View file @
963b3a6e
This diff is collapsed.
Click to expand it.
src/services/record-api.md
View file @
963b3a6e
# 模型监控接口文档
# 模型监控接口文档
**最后更新**
:2026 年
7
月
**最后更新**
:2026 年
8
月
## 1. 基础约定
## 1. 基础约定
...
@@ -107,15 +107,17 @@
...
@@ -107,15 +107,17 @@
```
json
```
json
{
{
"name"
:
"model_data_<stable_hash>"
,
"name"
:
"model_data_<stable_hash>"
,
"title"
:
"
组合主键展示值
"
,
"title"
:
"
第 1 条
"
,
"subject"
:
"model_monitor_data_example"
,
"subject"
:
"model_monitor_data_example"
,
"content_type"
:
"text"
,
"content_type"
:
"text"
,
"metadata"
:
{
"metadata"
:
{
"customer_id"
:
"C0001"
,
"customer_id"
:
"C0001"
,
"source_model_id"
:
"模型记录 ID"
,
"source_model_id"
:
"本地模型 Record ID"
,
"source_batch_id"
:
"批次记录 ID"
,
"source_external_model_id"
:
"ANALYZE_MXVIEW_INFO.ID"
,
"source_batch_code"
:
"2026-06"
,
"source_batch_id"
:
"本地批次 Record ID"
,
"source_record_key"
:
"sha256"
,
"source_batch_code"
:
"MODEL_RUN_BATCH_ID 的字符串形式"
,
"source_data_table_name"
:
"ANALYZE_MODEL_DATA_MX_430"
,
"source_row_number"
:
1
,
"source_synced_at"
:
"2026-07-23T06:00:00.000Z"
"source_synced_at"
:
"2026-07-23T06:00:00.000Z"
}
}
}
}
...
@@ -125,7 +127,7 @@
...
@@ -125,7 +127,7 @@
`PUT /record/:name`
`PUT /record/:name`
请求体使用完整 Record payload。
同步命中已有
`source_record_key`
时调用该接口更新,不删除本次来源未返回的旧记录
。
请求体使用完整 Record payload。
当前用于更新模型、批次、隐藏状态和审批数据;一次性同步的模型结果行不调用更新接口
。
### 3.6 删除 Record
### 3.6 删除 Record
...
@@ -154,23 +156,39 @@
...
@@ -154,23 +156,39 @@
转发接收人、一级审批人和终审人必须远程查询,不使用静态用户选项。
转发接收人、一级审批人和终审人必须远程查询,不使用静态用户选项。
## 5. 外部来源
Mock
契约
## 5. 外部来源
适配
契约
本期不调用数据库或真实外部 HTTP 接口。
`ModelMonitorMockService`
提供以下异步方法
:
当前仓库尚未提供 Oracle Connector 的请求体和响应体协议,因此仍由
`ModelMonitorMockService`
提供数据,但 Mock 已按
`ANALYZE_MXVIEW_INFO`
和
`ANALYZE_MODEL_RUN_BATCH_INFO`
的字段语义返回。真实适配器接入后必须保持以下签名,页面和 Record 同步逻辑不直接拼 SQL
:
```
ts
```
ts
listSourceModels
():
Promise
<
Array
<
{
listSourceModels
():
Promise
<
Array
<
{
source_model_id
:
string
;
// ANALYZE_MXVIEW_INFO.ID
source_model_name
:
string
;
// MODEL_NAME,来源技术名称
model_code
:
string
;
model_code
:
string
;
model_name
:
string
;
model_name
:
string
;
// MODEL_ZH_NAME,空时回退 MODEL_NAME
primary_key_fields
:
string
[];
source_model_updated_at
?:
string
;
model_state
?:
string
;
model_apply_state
?:
string
;
model_risk_level
?:
string
;
model_business_type
?:
string
;
model_business_type_name
?:
string
;
}
>>
;
}
>>
;
listSourceBatches
(
modelName
:
string
):
Promise
<
Array
<
{
listSourceBatches
(
sourceModel
:
SourceModel
):
Promise
<
Array
<
{
batch_code
:
string
;
source_model_id
:
string
;
// MODEL_ID
source_model_name
:
string
;
// MODEL_NAME
batch_code
:
string
;
// MODEL_RUN_BATCH_ID
batch_name
:
string
;
batch_name
:
string
;
source_batch_date
:
string
;
// MODEL_RUN_BATCH_DATE
data_table_name
:
string
;
// MODEL_DATA_TABLE_NAME
remark
:
string
;
source_model_data_count
:
number
;
source_unchecked_count
:
number
;
source_unsubmitted_count
:
number
;
source_reviewed_count
:
number
;
}
>>
;
}
>>
;
loadSourceBatchData
(
modelName
:
string
,
batchCode
:
string
):
Promise
<
{
loadSourceBatchData
(
sourceModel
:
SourceModel
,
sourceBatch
:
SourceBatch
):
Promise
<
{
columns
:
Array
<
{
columns
:
Array
<
{
name
:
string
;
name
:
string
;
label
:
string
;
label
:
string
;
...
@@ -182,21 +200,34 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
...
@@ -182,21 +200,34 @@ loadSourceBatchData(modelName: string, batchCode: string): Promise<{
约束:
约束:
-
`model_code`
为外部模型稳定唯一键。
-
模型稳定唯一键为
`ANALYZE_MXVIEW_INFO.MODEL_NAME + ID`
;本地分别保存为
`model_code + source_model_id`
。
-
`model_name`
用于加载批次。
-
`batch_code`
直接保存
`ANALYZE_MODEL_RUN_BATCH_INFO.MODEL_RUN_BATCH_ID`
,不再增加重复的来源跑批 ID 字段。
-
`primary_key_fields`
至少包含一个字段,并且字段必须存在于批次
`columns`
。
-
批次的
`MODEL_NAME + MODEL_ID`
必须等于所选模型主键。
-
`batch_code`
与本地模型 ID 组成批次唯一键。
-
`MODEL_DATA_TABLE_NAME`
只能来自批次表,并必须匹配 Oracle 非引号标识符白名单;不得接受页面输入的任意表名。
-
本地批次状态为
`success`
时,数据源适配层返回的对应批次必须在界面禁用,service 层同时拒绝重复同步。
-
动态表名不能作为绑定参数,后端必须先校验白名单再拼接;批次 ID 必须使用绑定参数查询。
-
`partial/failed`
批次允许重试,并继续按组合主键处理已有记录。
-
结果表必须按
`MODEL_RUN_BATCH_ID`
过滤,禁止只按表名读取整张模型结果表。
-
`source_model_data_count`
存在时必须与实际读取行数一致,否则停止写入。
-
本地一旦存在相同
`batch_code`
的批次 Record,无论状态为何,界面和 service 层都必须拒绝再次同步。
-
`columns[].name`
必须匹配
`^[a-z][a-z0-9_]*$`
且不可重复。
-
`columns[].name`
必须匹配
`^[a-z][a-z0-9_]*$`
且不可重复。
-
日期在写入 Record 前转换为 ISO 8601。
-
日期在写入 Record 前转换为 ISO 8601。
-
空组合主键、重复组合主键或字段类型冲突必须在写入前报错。
-
非法字段值或字段类型冲突必须在写入前报错。
-
结果行只执行新增,不读取已有结果行,也不执行更新;
`source_row_number`
保存本次来源读取顺序。
-
模型、批次和数据行 Record name 分别由
`MODEL_NAME + ID`
、
`MODEL_RUN_BATCH_ID`
、
`MODEL_RUN_BATCH_ID + source_row_number`
生成稳定哈希。
真实后台接口就绪后,应在该适配层内完成请求/响应转换,保持上述方法签名不变。
### 5.1 Oracle 字段建议
现有字段已经能够满足一次性同步的唯一性要求:模型使用
`MODEL_NAME + ID`
,批次使用
`MODEL_RUN_BATCH_ID`
。
`ANALYZE_MODEL_DATA_MX_*`
每个批次只同步一次且只新增,因此不要求增加结果行主键或主键配置字段。
建议为
`ANALYZE_MODEL_RUN_BATCH_INFO`
增加:
-
`RUN_BATCH_STATE VARCHAR2(30)`
:只允许同步已成功完成的来源批次。
-
`ERROR_MESSAGE VARCHAR2(4000)`
:保留来源跑批失败原因。
同时应把
`MODEL_RUN_BATCH_ID`
设为主键,把
`MODEL_ID`
、
`MODEL_DATA_TABLE_NAME`
设为非空,并为
`(MODEL_ID, MODEL_RUN_BATCH_DATE)`
建索引。现有日期字段均为
`VARCHAR2`
,新字段优先使用
`TIMESTAMP(3)`
;存量字段改型需单独迁移,不应由前端执行。
## 6. 错误处理
## 6. 错误处理
-
Subject 或 Record 写入失败不得回退 localStorage。
-
Subject 或 Record 写入失败不得回退 localStorage。
-
同步部分失败时保留已成功记录,并把批次标记为
`partial`
。
-
同步部分失败时保留已成功记录,并把批次标记为
`partial`
。
-
同步校验失败时不得开始写入动态数据 Record。
-
同步校验失败时不得开始写入动态数据 Record。
-
页面必须分别呈现 loading、success、error 状态
,并保留可
重试入口。
-
页面必须分别呈现 loading、success、error 状态
;已创建批次不提供
重试入口。
src/services/subjectsDefinition.md
View file @
963b3a6e
...
@@ -49,14 +49,20 @@ type Record = {
...
@@ -49,14 +49,20 @@ type Record = {
| 标签 | 别名 | 类型 | 多值 | 必填 | 说明 |
| 标签 | 别名 | 类型 | 多值 | 必填 | 说明 |
| --- | --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| 模型编码 |
`model_code`
| text | 否 | 是 | 外部模型唯一编码,本地不可编辑 |
| 来源模型 ID |
`source_model_id`
| text | 否 | 是 |
`ANALYZE_MXVIEW_INFO.ID`
,与
`model_code`
组成同步唯一键 |
| 模型编码 |
`model_code`
| text | 否 | 是 |
`MODEL_NAME`
,与来源模型 ID 组成同步唯一键 |
| 模型名称 |
`model_name`
| text | 否 | 是 | 外部模型显示名称,本地不可编辑 |
| 模型名称 |
`model_name`
| text | 否 | 是 | 外部模型显示名称,本地不可编辑 |
| 来源模型数据更新时间 |
`source_model_updated_at`
| date | 否 | 否 |
`MODEL_DATA_UPDATE_TIME`
转 ISO 8601 |
| 模型状态 |
`model_state`
| text | 否 | 否 |
`MODEL_STATE`
|
| 模型应用状态 |
`model_apply_state`
| text | 否 | 否 |
`MODEL_APPLY_STATE`
|
| 模型风险等级 |
`model_risk_level`
| text | 否 | 否 |
`MODEL_RISK_LEVEL`
|
| 模型业务类型 |
`model_business_type`
| text | 否 | 否 |
`MODEL_BUSINESS_TYPE`
|
| 模型业务类型名称 |
`model_business_type_name`
| text | 否 | 否 |
`MODEL_BUSINESS_TYPE_NAME`
|
| 模型说明 |
`description`
| long_text | 否 | 否 | 本地归档说明 |
| 模型说明 |
`description`
| long_text | 否 | 否 | 本地归档说明 |
| 所属分组 ID |
`group_id`
| text | 否 | 否 | 关联模型分组 ID |
| 所属分组 ID |
`group_id`
| text | 否 | 否 | 关联模型分组 ID |
| 所属分组名称 |
`group_name`
| text | 否 | 否 | 冗余展示字段 |
| 所属分组名称 |
`group_name`
| text | 否 | 否 | 冗余展示字段 |
| 模型标签 |
`tags`
| text | 是 | 否 | 本地检索标签 |
| 模型标签 |
`tags`
| text | 是 | 否 | 本地检索标签 |
| 数据对象 |
`data_subject_name`
| text | 否 | 是 | 自动创建的动态 Subject 名称 |
| 数据对象 |
`data_subject_name`
| text | 否 | 是 | 自动创建的动态 Subject 名称 |
| 组合主键字段 |
`primary_key_fields`
| text | 是 | 是 | 来源模型接口提供,首次同步后锁定 |
| 最近同步状态 |
`last_sync_status`
| select | 否 | 否 |
`syncing/success/partial/failed`
|
| 最近同步状态 |
`last_sync_status`
| select | 否 | 否 |
`syncing/success/partial/failed`
|
| 最近同步时间 |
`last_synced_at`
| date | 否 | 否 | ISO 8601 |
| 最近同步时间 |
`last_synced_at`
| date | 否 | 否 | ISO 8601 |
| 创建人 ID |
`created_by_user_id`
| text | 否 | 否 | 首次同步用户 ID |
| 创建人 ID |
`created_by_user_id`
| text | 否 | 否 | 首次同步用户 ID |
...
@@ -69,16 +75,22 @@ type Record = {
...
@@ -69,16 +75,22 @@ type Record = {
| 模型 ID |
`model_id`
| text | 否 | 是 | 仅保存模型 Record ID |
| 模型 ID |
`model_id`
| text | 否 | 是 | 仅保存模型 Record ID |
| 模型编码 |
`model_code`
| text | 否 | 是 | 冗余展示字段 |
| 模型编码 |
`model_code`
| text | 否 | 是 | 冗余展示字段 |
| 模型名称 |
`model_name`
| text | 否 | 是 | 冗余展示字段 |
| 模型名称 |
`model_name`
| text | 否 | 是 | 冗余展示字段 |
| 批次编码 |
`batch_code`
| text | 否 | 是 |
与模型 ID 组成
批次唯一键 |
| 批次编码 |
`batch_code`
| text | 否 | 是 |
直接保存
`MODEL_RUN_BATCH_ID`
,来源
批次唯一键 |
| 批次名称 |
`batch_name`
| text | 否 | 是 | 外部批次显示名称 |
| 批次名称 |
`batch_name`
| text | 否 | 是 | 外部批次显示名称 |
| 来源跑批时间 |
`source_batch_date`
| date | 否 | 否 |
`MODEL_RUN_BATCH_DATE`
转 ISO 8601 |
| 来源数据表 |
`source_data_table_name`
| text | 否 | 是 |
`MODEL_DATA_TABLE_NAME`
,只允许白名单标识符 |
| 来源备注 |
`source_remark`
| long_text | 否 | 否 |
`REMARK`
|
| 来源未检查数量 |
`source_unchecked_count`
| number | 否 | 否 |
`UNCHECKED_CNT`
|
| 来源未提交数量 |
`source_unsubmitted_count`
| number | 否 | 否 |
`UNSUBMITTED_CNT`
|
| 来源已复核数量 |
`source_reviewed_count`
| number | 否 | 否 |
`REVIEWED_CNT`
|
| 数据对象 |
`data_subject_name`
| text | 否 | 是 | 当前模型动态 Subject |
| 数据对象 |
`data_subject_name`
| text | 否 | 是 | 当前模型动态 Subject |
| 字段快照 |
`column_schema`
| object | 是 | 否 | 当前批次最近同步的
`{name,label,type}`
列表 |
| 字段快照 |
`column_schema`
| object | 是 | 否 | 当前批次最近同步的
`{name,label,type}`
列表 |
| 隐藏记录 ID |
`hidden_record_ids`
| text | 是 | 否 | 批次级手动隐藏的稳定 Record name |
| 隐藏记录 ID |
`hidden_record_ids`
| text | 是 | 否 | 批次级手动隐藏的稳定 Record name |
| 同步状态 |
`sync_status`
| select | 否 | 是 |
`syncing/success/partial/failed`
|
| 同步状态 |
`sync_status`
| select | 否 | 是 |
`syncing/success/partial/failed`
|
| 源数据数量 |
`source_row_count`
| number | 否 | 否 |
最近一次来源响应
行数 |
| 源数据数量 |
`source_row_count`
| number | 否 | 否 |
`MODEL_DATA_CNT`
与实际读取数量校验后的
行数 |
| 本地数据数量 |
`total_record_count`
| number | 否 | 否 |
增量同步后批次保留
总数 |
| 本地数据数量 |
`total_record_count`
| number | 否 | 否 |
本次导入后保存成功的
总数 |
| 新增数量 |
`inserted_count`
| number | 否 | 否 |
最近
同步新增数 |
| 新增数量 |
`inserted_count`
| number | 否 | 否 |
本次
同步新增数 |
| 更新数量 |
`updated_count`
| number | 否 | 否 |
最近同步更新数
|
| 更新数量 |
`updated_count`
| number | 否 | 否 |
兼容字段,一次性同步固定为 0
|
| 失败数量 |
`failed_count`
| number | 否 | 否 | 最近同步失败数 |
| 失败数量 |
`failed_count`
| number | 否 | 否 | 最近同步失败数 |
| 同步开始时间 |
`sync_started_at`
| date | 否 | 否 | 用于前端防重和超时判断 |
| 同步开始时间 |
`sync_started_at`
| date | 否 | 否 | 用于前端防重和超时判断 |
| 同步完成时间 |
`synced_at`
| date | 否 | 否 | ISO 8601 |
| 同步完成时间 |
`synced_at`
| date | 否 | 否 | ISO 8601 |
...
@@ -110,7 +122,7 @@ type Record = {
...
@@ -110,7 +122,7 @@ type Record = {
| 终审人 ID/名称 |
`final_approver`
/
`final_approver_name`
| text | 否 | 是/否 |
| 终审人 ID/名称 |
`final_approver`
/
`final_approver_name`
| text | 否 | 是/否 |
| 发起人 ID/名称 |
`initiator_id`
/
`initiator_name`
| text | 否 | 否 |
| 发起人 ID/名称 |
`initiator_id`
/
`initiator_name`
| text | 否 | 否 |
状态选项由字段
`settings.options`
提供:填写状态为
`editing/completed`
,审批状态为
`pending/approved/rejected`
。
状态选项由字段
`settings.options`
提供:填写状态为
`editing/completed`
,审批状态为
`pending/approved/rejected`
。
任一审批环节驳回后将
`editor_status`
重置为
`editing`
;接收人重新完成填写时,两级审批状态均重置为
`pending`
。
### 2.5 `model_monitor_forward_item`
### 2.5 `model_monitor_forward_item`
...
@@ -122,37 +134,42 @@ type Record = {
...
@@ -122,37 +134,42 @@ type Record = {
| 批次 ID/名称 |
`batch_id`
/
`batch_name`
| text | 否 | 是/否 |
| 批次 ID/名称 |
`batch_id`
/
`batch_name`
| text | 否 | 是/否 |
| 数据对象 |
`data_subject_name`
| text | 否 | 是 |
| 数据对象 |
`data_subject_name`
| text | 否 | 是 |
| 源记录 ID/标题 |
`source_record_id`
/
`source_record_title`
| text | 否 | 是/否 |
| 源记录 ID/标题 |
`source_record_id`
/
`source_record_title`
| text | 否 | 是/否 |
| 是否属实 |
`truth_status`
| select | 否 |
是
|
| 是否属实 |
`truth_status`
| select | 否 |
否
|
| 整改方案 |
`rectification_plan`
| long_text | 否 |
是
|
| 整改方案 |
`rectification_plan`
| long_text | 否 |
否
|
| 预计整改完成时间 |
`expected_rectification_completed_at`
| date | 否 |
是
|
| 预计整改完成时间 |
`expected_rectification_completed_at`
| date | 否 |
否
|
| 填写人 ID/名称 |
`filled_by_id`
/
`filled_by_name`
| text | 否 | 否 |
| 填写人 ID/名称 |
`filled_by_id`
/
`filled_by_name`
| text | 否 | 否 |
| 填写时间 |
`filled_at`
| date | 否 | 否 |
| 填写时间 |
`filled_at`
| date | 否 | 否 |
| 备注 |
`remark`
| long_text | 否 | 否 |
| 备注 |
`remark`
| long_text | 否 | 否 |
`truth_status`
选项必须来自
`settings.options`
,当前值为
`truthful/untruthful`
。
`truth_status`
选项必须来自
`settings.options`
,当前值为
`truthful/untruthful`
。
上述三个回填字段在 Subject 层保持可选,以允许转发发起时先创建空白明细并分次保存。未填写时不向
`metadata`
写入这三个字段,不使用空字符串占位;前端界面仍标记为必填,并在“完成填写”时校验三项均已填写。
## 3. 动态模型数据 Subject
## 3. 动态模型数据 Subject
每个模型首次同步时
自动创建一个稳定的
`model_monitor_data_<model_code>_<hash>`
Subject。模型改名或后续批次同步不得改变该 Subject 名称
。
每个模型首次同步时
按
`ANALYZE_MXVIEW_INFO.MODEL_NAME + ID`
自动创建稳定的动态数据 Subject。后续批次继续使用同一 Subject
。
固定系统字段:
固定系统字段:
| 标签 | 别名 | 类型 | 必填 | 说明 |
| 标签 | 别名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- |
| 来源模型 ID |
`source_model_id`
| text | 是 | 模型 Record ID |
| 本地模型 Record ID |
`source_model_id`
| text | 是 | 模型 Record ID,兼容既有查询字段 |
| 来源批次 ID |
`source_batch_id`
| text | 是 | 批次 Record ID |
| 来源模型 ID |
`source_external_model_id`
| text | 是 |
`ANALYZE_MXVIEW_INFO.ID`
|
| 来源批次编码 |
`source_batch_code`
| text | 是 | 外部批次编码 |
| 本地批次 Record ID |
`source_batch_id`
| text | 是 | 批次 Record ID,批次数据查询条件 |
| 来源记录键 |
`source_record_key`
| text | 是 | 组合主键规范化后 SHA-256 |
| 来源批次编码 |
`source_batch_code`
| text | 是 |
`MODEL_RUN_BATCH_ID`
|
| 来源数据表 |
`source_data_table_name`
| text | 是 |
`MODEL_DATA_TABLE_NAME`
|
| 来源行序号 |
`source_row_number`
| number | 是 | 本次来源读取的 1 基序号,不作为业务主键 |
| 同步时间 |
`source_synced_at`
| date | 是 | 最近写入时间 |
| 同步时间 |
`source_synced_at`
| date | 是 | 最近写入时间 |
业务字段来自数据接口的
`columns`
,仅支持
`text/number/date/boolean/object`
。新字段允许自动添加;已有字段不得自动删除,同名字段类型变化必须阻止同步。
业务字段来自数据接口的
`columns`
,仅支持
`text/number/date/boolean/object`
。新字段允许自动添加;已有字段不得自动删除,同名字段类型变化必须阻止同步。
数据 Record
`name`
由模型、批次和
`source_record_key`
生成稳定哈希。增量同步命中主键时更新,未命中时新增,本次未返回的历史记录继续保留
。
结果行不配置业务主键。数据 Record
`name`
由
`MODEL_RUN_BATCH_ID + source_row_number`
生成稳定哈希,只执行一次性新增,不查询或更新已有结果行
。
## 4. 当前约束
## 4. 当前约束
-
模型和批次不提供删除。
-
模型和批次不提供删除。
-
模型仅由同步自动创建,
模型编码、名称和组合主键
不可本地修改。
-
模型仅由同步自动创建,
来源模型 ID、模型编码和名称
不可本地修改。
-
`hidden_record_ids`
为批次级共享状态,与转发的
`excluded_record_ids`
分开维护。
-
`hidden_record_ids`
为批次级共享状态,与转发的
`excluded_record_ids`
分开维护。
-
固定 Subject 在开发阶段由前端检查并创建;正式环境应由部署流程预建。
-
固定 Subject 在开发阶段由前端检查并创建;正式环境应由部署流程预建。
-
已存在的 Subject 自动补充新字段时先按可选字段添加,以兼容历史 Record;新同步数据仍由 service 层执行必填校验。
-
Subject/Record 写入失败必须明确报错,不得回退 localStorage。
-
Subject/Record 写入失败必须明确报错,不得回退 localStorage。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment