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
5b9d1940
Commit
5b9d1940
authored
Aug 27, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 接入模型监控数据源并加强审批校验
parent
41e4f24d
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
606 additions
and
62 deletions
+606
-62
.umirc.ts
.umirc.ts
+1
-0
model-monitor-source-fixtures.sql
scripts/mysql/model-monitor-source-fixtures.sql
+213
-0
ForwardApprovals.jsx
src/pages/project_manager_xy/ForwardApprovals.jsx
+38
-49
ModelMonitorConnectorService.js
src/pages/project_manager_xy/ModelMonitorConnectorService.js
+314
-0
model-monitor.business-flow.md
src/pages/project_manager_xy/model-monitor.business-flow.md
+9
-6
modelMonitorService.js
src/pages/project_manager_xy/modelMonitorService.js
+8
-2
DataService.js
src/services/DataService.js
+15
-0
record-api.md
src/services/record-api.md
+4
-3
subjectsDefinition.md
src/services/subjectsDefinition.md
+4
-2
No files found.
.umirc.ts
View file @
5b9d1940
...
...
@@ -133,5 +133,6 @@ export default defineConfig({
define
:
{
STREAMS_API_URL
:
'/api'
,
STREAMS_PARSER_URL
:
'/parser'
,
MODEL_MONITOR_CONNECTOR_NAME
:
process
.
env
.
MODEL_MONITOR_CONNECTOR_NAME
||
'MODEL_MONITOR_ORACLE'
,
},
});
scripts/mysql/model-monitor-source-fixtures.sql
0 → 100644
View file @
5b9d1940
-- MySQL 5.7+ fixture data for the model-monitor source connector.
-- This script is repeatable: existing fixture rows are updated by their stable keys.
CREATE
TABLE
IF
NOT
EXISTS
ANALYZE_MXVIEW_INFO
(
ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源模型 ID'
,
MODEL_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型技术名称'
,
MODEL_ZH_NAME
VARCHAR
(
128
)
NULL
COMMENT
'模型中文名称'
,
MODEL_DATA_UPDATE_TIME
DATETIME
(
3
)
NULL
COMMENT
'模型数据更新时间'
,
MODEL_STATE
VARCHAR
(
30
)
NULL
COMMENT
'模型状态'
,
MODEL_APPLY_STATE
VARCHAR
(
30
)
NULL
COMMENT
'模型应用状态'
,
MODEL_RISK_LEVEL
VARCHAR
(
30
)
NULL
COMMENT
'模型风险等级'
,
MODEL_BUSINESS_TYPE
VARCHAR
(
30
)
NULL
COMMENT
'模型业务类型'
,
MODEL_BUSINESS_TYPE_NAME
VARCHAR
(
128
)
NULL
COMMENT
'模型业务类型名称'
,
PRIMARY
KEY
(
ID
),
UNIQUE
KEY
UK_ANALYZE_MXVIEW_INFO_MODEL
(
MODEL_NAME
,
ID
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'模型主档来源表'
;
CREATE
TABLE
IF
NOT
EXISTS
ANALYZE_MODEL_RUN_BATCH_INFO
(
MODEL_RUN_BATCH_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源跑批 ID'
,
MODEL_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源模型 ID'
,
MODEL_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型技术名称'
,
MODEL_RUN_BATCH_DATE
DATETIME
(
3
)
NOT
NULL
COMMENT
'来源跑批时间'
,
MODEL_DATA_TABLE_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型结果表名'
,
REMARK
VARCHAR
(
1000
)
NULL
COMMENT
'来源备注'
,
MODEL_DATA_CNT
INT
UNSIGNED
NOT
NULL
DEFAULT
0
COMMENT
'模型结果行数'
,
UNCHECKED_CNT
INT
UNSIGNED
NOT
NULL
DEFAULT
0
COMMENT
'未检查数量'
,
UNSUBMITTED_CNT
INT
UNSIGNED
NOT
NULL
DEFAULT
0
COMMENT
'未提交数量'
,
REVIEWED_CNT
INT
UNSIGNED
NOT
NULL
DEFAULT
0
COMMENT
'已复核数量'
,
RUN_BATCH_STATE
VARCHAR
(
30
)
NOT
NULL
DEFAULT
'success'
COMMENT
'跑批状态'
,
ERROR_MESSAGE
VARCHAR
(
4000
)
NULL
COMMENT
'失败原因'
,
PRIMARY
KEY
(
MODEL_RUN_BATCH_ID
),
KEY
IDX_ANALYZE_BATCH_MODEL_DATE
(
MODEL_ID
,
MODEL_RUN_BATCH_DATE
),
CONSTRAINT
FK_ANALYZE_BATCH_MODEL
FOREIGN
KEY
(
MODEL_ID
)
REFERENCES
ANALYZE_MXVIEW_INFO
(
ID
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'模型运行批次来源表'
;
CREATE
TABLE
IF
NOT
EXISTS
ANALYZE_MODEL_DATA_MX_430
(
MODEL_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源模型 ID'
,
MODEL_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型技术名称'
,
MODEL_RUN_BATCH_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源跑批 ID'
,
MODEL_RUN_BATCH_DATE
DATETIME
(
3
)
NOT
NULL
COMMENT
'来源跑批时间'
,
REMARK
VARCHAR
(
1000
)
NULL
COMMENT
'来源备注'
,
CUSTOMER_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'客户编号'
,
RISK_DATE
DATETIME
(
3
)
NOT
NULL
COMMENT
'风险日期'
,
CUSTOMER_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'客户名称'
,
RISK_LEVEL
VARCHAR
(
30
)
NOT
NULL
COMMENT
'风险等级'
,
RISK_SCORE
DECIMAL
(
10
,
2
)
NOT
NULL
COMMENT
'风险分值'
,
IS_FOCUS_CUSTOMER
TINYINT
(
1
)
NOT
NULL
COMMENT
'是否重点客户'
,
UNIQUE
KEY
UK_ANALYZE_DATA_430_ROW
(
MODEL_RUN_BATCH_ID
,
CUSTOMER_ID
),
KEY
IDX_ANALYZE_DATA_430_BATCH
(
MODEL_RUN_BATCH_ID
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'客户洗钱风险模型结果'
;
CREATE
TABLE
IF
NOT
EXISTS
ANALYZE_MODEL_DATA_MX_412
(
MODEL_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源模型 ID'
,
MODEL_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型技术名称'
,
MODEL_RUN_BATCH_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源跑批 ID'
,
MODEL_RUN_BATCH_DATE
DATETIME
(
3
)
NOT
NULL
COMMENT
'来源跑批时间'
,
REMARK
VARCHAR
(
1000
)
NULL
COMMENT
'来源备注'
,
ALERT_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'预警编号'
,
ACCOUNT_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'账户名称'
,
TRANSACTION_AMOUNT
DECIMAL
(
18
,
2
)
NOT
NULL
COMMENT
'交易金额'
,
TRANSACTION_TIME
DATETIME
(
3
)
NOT
NULL
COMMENT
'交易时间'
,
ALERT_REASON
VARCHAR
(
500
)
NOT
NULL
COMMENT
'预警原因'
,
UNIQUE
KEY
UK_ANALYZE_DATA_412_ROW
(
MODEL_RUN_BATCH_ID
,
ALERT_ID
),
KEY
IDX_ANALYZE_DATA_412_BATCH
(
MODEL_RUN_BATCH_ID
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'异常交易预警模型结果'
;
CREATE
TABLE
IF
NOT
EXISTS
ANALYZE_MODEL_DATA_MX_411
(
MODEL_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源模型 ID'
,
MODEL_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'模型技术名称'
,
MODEL_RUN_BATCH_ID
VARCHAR
(
64
)
NOT
NULL
COMMENT
'来源跑批 ID'
,
MODEL_RUN_BATCH_DATE
DATETIME
(
3
)
NOT
NULL
COMMENT
'来源跑批时间'
,
REMARK
VARCHAR
(
1000
)
NULL
COMMENT
'来源备注'
,
INSTITUTION_CODE
VARCHAR
(
64
)
NOT
NULL
COMMENT
'机构编码'
,
REPORTING_PERIOD
VARCHAR
(
30
)
NOT
NULL
COMMENT
'报告期间'
,
INSTITUTION_NAME
VARCHAR
(
128
)
NOT
NULL
COMMENT
'机构名称'
,
CREDIT_LIMIT
DECIMAL
(
18
,
2
)
NOT
NULL
COMMENT
'授信额度'
,
USED_AMOUNT
DECIMAL
(
18
,
2
)
NOT
NULL
COMMENT
'已用额度'
,
OVER_LIMIT
TINYINT
(
1
)
NOT
NULL
COMMENT
'是否超限'
,
UNIQUE
KEY
UK_ANALYZE_DATA_411_ROW
(
MODEL_RUN_BATCH_ID
,
INSTITUTION_CODE
),
KEY
IDX_ANALYZE_DATA_411_BATCH
(
MODEL_RUN_BATCH_ID
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8mb4
COMMENT
=
'授信敞口监测模型结果'
;
START
TRANSACTION
;
INSERT
INTO
ANALYZE_MXVIEW_INFO
(
ID
,
MODEL_NAME
,
MODEL_ZH_NAME
,
MODEL_DATA_UPDATE_TIME
,
MODEL_STATE
,
MODEL_APPLY_STATE
,
MODEL_RISK_LEVEL
,
MODEL_BUSINESS_TYPE
,
MODEL_BUSINESS_TYPE_NAME
)
VALUES
(
'430'
,
'AML_CUSTOMER_RISK'
,
'客户洗钱风险模型'
,
'2026-08-26 08:00:00.000'
,
'enabled'
,
'applied'
,
'high'
,
'12'
,
'风险分析'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'异常交易预警模型'
,
'2026-08-25 08:00:00.000'
,
'enabled'
,
'applied'
,
'medium'
,
'12'
,
'风险分析'
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'授信敞口监测模型'
,
'2026-08-24 08:00:00.000'
,
'enabled'
,
'applied'
,
'medium'
,
'13'
,
'合规分析'
)
ON
DUPLICATE
KEY
UPDATE
MODEL_NAME
=
VALUES
(
MODEL_NAME
),
MODEL_ZH_NAME
=
VALUES
(
MODEL_ZH_NAME
),
MODEL_DATA_UPDATE_TIME
=
VALUES
(
MODEL_DATA_UPDATE_TIME
),
MODEL_STATE
=
VALUES
(
MODEL_STATE
),
MODEL_APPLY_STATE
=
VALUES
(
MODEL_APPLY_STATE
),
MODEL_RISK_LEVEL
=
VALUES
(
MODEL_RISK_LEVEL
),
MODEL_BUSINESS_TYPE
=
VALUES
(
MODEL_BUSINESS_TYPE
),
MODEL_BUSINESS_TYPE_NAME
=
VALUES
(
MODEL_BUSINESS_TYPE_NAME
);
INSERT
INTO
ANALYZE_MODEL_RUN_BATCH_INFO
(
MODEL_RUN_BATCH_ID
,
MODEL_ID
,
MODEL_NAME
,
MODEL_RUN_BATCH_DATE
,
MODEL_DATA_TABLE_NAME
,
REMARK
,
MODEL_DATA_CNT
,
UNCHECKED_CNT
,
UNSUBMITTED_CNT
,
REVIEWED_CNT
,
RUN_BATCH_STATE
,
ERROR_MESSAGE
)
VALUES
(
'690'
,
'430'
,
'AML_CUSTOMER_RISK'
,
'2026-08-26 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_430'
,
'2026 年 8 月批次'
,
5
,
5
,
0
,
0
,
'success'
,
NULL
),
(
'652'
,
'430'
,
'AML_CUSTOMER_RISK'
,
'2026-07-31 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_430'
,
'2026 年 7 月批次'
,
5
,
3
,
1
,
1
,
'success'
,
NULL
),
(
'590'
,
'430'
,
'AML_CUSTOMER_RISK'
,
'2025-12-31 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_430'
,
'2025 年 12 月批次'
,
5
,
0
,
0
,
5
,
'success'
,
NULL
),
(
'670'
,
'412'
,
'TRANSACTION_ALERT'
,
'2026-08-25 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_412'
,
'2026 年第 35 周'
,
7
,
7
,
0
,
0
,
'success'
,
NULL
),
(
'669'
,
'412'
,
'TRANSACTION_ALERT'
,
'2026-08-18 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_412'
,
'2026 年第 34 周'
,
7
,
4
,
2
,
1
,
'success'
,
NULL
),
(
'570'
,
'412'
,
'TRANSACTION_ALERT'
,
'2025-12-29 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_412'
,
'2025 年第 52 周'
,
7
,
0
,
0
,
7
,
'success'
,
NULL
),
(
'630'
,
'411'
,
'CREDIT_EXPOSURE'
,
'2026-08-24 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_411'
,
'2026 年 8 月批次'
,
4
,
4
,
0
,
0
,
'success'
,
NULL
),
(
'629'
,
'411'
,
'CREDIT_EXPOSURE'
,
'2026-06-30 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_411'
,
'2026 年第二季度'
,
4
,
2
,
1
,
1
,
'success'
,
NULL
),
(
'530'
,
'411'
,
'CREDIT_EXPOSURE'
,
'2025-12-31 08:00:00.000'
,
'ANALYZE_MODEL_DATA_MX_411'
,
'2025 年第四季度'
,
4
,
0
,
0
,
4
,
'success'
,
NULL
)
ON
DUPLICATE
KEY
UPDATE
MODEL_ID
=
VALUES
(
MODEL_ID
),
MODEL_NAME
=
VALUES
(
MODEL_NAME
),
MODEL_RUN_BATCH_DATE
=
VALUES
(
MODEL_RUN_BATCH_DATE
),
MODEL_DATA_TABLE_NAME
=
VALUES
(
MODEL_DATA_TABLE_NAME
),
REMARK
=
VALUES
(
REMARK
),
MODEL_DATA_CNT
=
VALUES
(
MODEL_DATA_CNT
),
UNCHECKED_CNT
=
VALUES
(
UNCHECKED_CNT
),
UNSUBMITTED_CNT
=
VALUES
(
UNSUBMITTED_CNT
),
REVIEWED_CNT
=
VALUES
(
REVIEWED_CNT
),
RUN_BATCH_STATE
=
VALUES
(
RUN_BATCH_STATE
),
ERROR_MESSAGE
=
VALUES
(
ERROR_MESSAGE
);
INSERT
INTO
ANALYZE_MODEL_DATA_MX_430
(
MODEL_ID
,
MODEL_NAME
,
MODEL_RUN_BATCH_ID
,
MODEL_RUN_BATCH_DATE
,
REMARK
,
CUSTOMER_ID
,
RISK_DATE
,
CUSTOMER_NAME
,
RISK_LEVEL
,
RISK_SCORE
,
IS_FOCUS_CUSTOMER
)
VALUES
(
'430'
,
'AML_CUSTOMER_RISK'
,
'690'
,
'2026-08-26 08:00:00.000'
,
NULL
,
'C0001'
,
'2026-08-03 00:00:00.000'
,
'华远实业'
,
'高'
,
93
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'690'
,
'2026-08-26 08:00:00.000'
,
NULL
,
'C0002'
,
'2026-08-04 00:00:00.000'
,
'海川商贸'
,
'高'
,
84
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'690'
,
'2026-08-26 08:00:00.000'
,
NULL
,
'C0003'
,
'2026-08-05 00:00:00.000'
,
'恒信科技'
,
'中'
,
75
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'690'
,
'2026-08-26 08:00:00.000'
,
NULL
,
'C0004'
,
'2026-08-06 00:00:00.000'
,
'嘉禾供应链'
,
'中'
,
66
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'690'
,
'2026-08-26 08:00:00.000'
,
NULL
,
'C0005'
,
'2026-08-07 00:00:00.000'
,
'远景投资'
,
'低'
,
57
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'652'
,
'2026-07-31 08:00:00.000'
,
NULL
,
'C0001'
,
'2026-07-03 00:00:00.000'
,
'华远实业'
,
'高'
,
91
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'652'
,
'2026-07-31 08:00:00.000'
,
NULL
,
'C0002'
,
'2026-07-04 00:00:00.000'
,
'海川商贸'
,
'高'
,
82
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'652'
,
'2026-07-31 08:00:00.000'
,
NULL
,
'C0003'
,
'2026-07-05 00:00:00.000'
,
'恒信科技'
,
'中'
,
73
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'652'
,
'2026-07-31 08:00:00.000'
,
NULL
,
'C0004'
,
'2026-07-06 00:00:00.000'
,
'嘉禾供应链'
,
'中'
,
64
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'652'
,
'2026-07-31 08:00:00.000'
,
NULL
,
'C0005'
,
'2026-07-07 00:00:00.000'
,
'远景投资'
,
'低'
,
55
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'590'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'C0001'
,
'2025-12-03 00:00:00.000'
,
'华远实业'
,
'高'
,
89
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'590'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'C0002'
,
'2025-12-04 00:00:00.000'
,
'海川商贸'
,
'高'
,
80
.
00
,
1
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'590'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'C0003'
,
'2025-12-05 00:00:00.000'
,
'恒信科技'
,
'中'
,
71
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'590'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'C0004'
,
'2025-12-06 00:00:00.000'
,
'嘉禾供应链'
,
'中'
,
62
.
00
,
0
),
(
'430'
,
'AML_CUSTOMER_RISK'
,
'590'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'C0005'
,
'2025-12-07 00:00:00.000'
,
'远景投资'
,
'低'
,
53
.
00
,
0
)
ON
DUPLICATE
KEY
UPDATE
MODEL_ID
=
VALUES
(
MODEL_ID
),
MODEL_NAME
=
VALUES
(
MODEL_NAME
),
MODEL_RUN_BATCH_DATE
=
VALUES
(
MODEL_RUN_BATCH_DATE
),
REMARK
=
VALUES
(
REMARK
),
RISK_DATE
=
VALUES
(
RISK_DATE
),
CUSTOMER_NAME
=
VALUES
(
CUSTOMER_NAME
),
RISK_LEVEL
=
VALUES
(
RISK_LEVEL
),
RISK_SCORE
=
VALUES
(
RISK_SCORE
),
IS_FOCUS_CUSTOMER
=
VALUES
(
IS_FOCUS_CUSTOMER
);
INSERT
INTO
ANALYZE_MODEL_DATA_MX_412
(
MODEL_ID
,
MODEL_NAME
,
MODEL_RUN_BATCH_ID
,
MODEL_RUN_BATCH_DATE
,
REMARK
,
ALERT_ID
,
ACCOUNT_NAME
,
TRANSACTION_AMOUNT
,
TRANSACTION_TIME
,
ALERT_REASON
)
VALUES
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35001'
,
'监测账户 1'
,
180000
.
00
,
'2026-08-18 00:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35002'
,
'监测账户 2'
,
253500
.
00
,
'2026-08-19 01:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35003'
,
'监测账户 3'
,
327000
.
00
,
'2026-08-20 02:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35004'
,
'监测账户 4'
,
400500
.
00
,
'2026-08-21 03:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35005'
,
'监测账户 5'
,
474000
.
00
,
'2026-08-22 04:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35006'
,
'监测账户 6'
,
547500
.
00
,
'2026-08-23 05:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'670'
,
'2026-08-25 08:00:00.000'
,
NULL
,
'AL35007'
,
'监测账户 7'
,
621000
.
00
,
'2026-08-24 06:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34001'
,
'监测账户 1'
,
172000
.
00
,
'2026-08-11 00:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34002'
,
'监测账户 2'
,
245500
.
00
,
'2026-08-12 01:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34003'
,
'监测账户 3'
,
319000
.
00
,
'2026-08-13 02:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34004'
,
'监测账户 4'
,
392500
.
00
,
'2026-08-14 03:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34005'
,
'监测账户 5'
,
466000
.
00
,
'2026-08-15 04:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34006'
,
'监测账户 6'
,
539500
.
00
,
'2026-08-16 05:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'669'
,
'2026-08-18 08:00:00.000'
,
NULL
,
'AL34007'
,
'监测账户 7'
,
613000
.
00
,
'2026-08-17 06:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52001'
,
'监测账户 1'
,
165000
.
00
,
'2025-12-22 00:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52002'
,
'监测账户 2'
,
238500
.
00
,
'2025-12-23 01:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52003'
,
'监测账户 3'
,
312000
.
00
,
'2025-12-24 02:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52004'
,
'监测账户 4'
,
385500
.
00
,
'2025-12-25 03:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52005'
,
'监测账户 5'
,
459000
.
00
,
'2025-12-26 04:30:00.000'
,
'短期高频大额交易'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52006'
,
'监测账户 6'
,
532500
.
00
,
'2025-12-27 05:30:00.000'
,
'交易对手集中度异常'
),
(
'412'
,
'TRANSACTION_ALERT'
,
'570'
,
'2025-12-29 08:00:00.000'
,
NULL
,
'AL52007'
,
'监测账户 7'
,
606000
.
00
,
'2025-12-28 06:30:00.000'
,
'短期高频大额交易'
)
ON
DUPLICATE
KEY
UPDATE
MODEL_ID
=
VALUES
(
MODEL_ID
),
MODEL_NAME
=
VALUES
(
MODEL_NAME
),
MODEL_RUN_BATCH_DATE
=
VALUES
(
MODEL_RUN_BATCH_DATE
),
REMARK
=
VALUES
(
REMARK
),
ACCOUNT_NAME
=
VALUES
(
ACCOUNT_NAME
),
TRANSACTION_AMOUNT
=
VALUES
(
TRANSACTION_AMOUNT
),
TRANSACTION_TIME
=
VALUES
(
TRANSACTION_TIME
),
ALERT_REASON
=
VALUES
(
ALERT_REASON
);
INSERT
INTO
ANALYZE_MODEL_DATA_MX_411
(
MODEL_ID
,
MODEL_NAME
,
MODEL_RUN_BATCH_ID
,
MODEL_RUN_BATCH_DATE
,
REMARK
,
INSTITUTION_CODE
,
REPORTING_PERIOD
,
INSTITUTION_NAME
,
CREDIT_LIMIT
,
USED_AMOUNT
,
OVER_LIMIT
)
VALUES
(
'411'
,
'CREDIT_EXPOSURE'
,
'630'
,
'2026-08-24 08:00:00.000'
,
NULL
,
'ORG1'
,
'2026-08'
,
'总行营业部'
,
12000000
.
00
,
8640000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'630'
,
'2026-08-24 08:00:00.000'
,
NULL
,
'ORG2'
,
'2026-08'
,
'华东分行'
,
15500000
.
00
,
12710000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'630'
,
'2026-08-24 08:00:00.000'
,
NULL
,
'ORG3'
,
'2026-08'
,
'华南分行'
,
19000000
.
00
,
17480000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'630'
,
'2026-08-24 08:00:00.000'
,
NULL
,
'ORG4'
,
'2026-08'
,
'西部中心'
,
22500000
.
00
,
22950000
.
00
,
1
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'629'
,
'2026-06-30 08:00:00.000'
,
NULL
,
'ORG1'
,
'2026-Q2'
,
'总行营业部'
,
11280000
.
00
,
8121600
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'629'
,
'2026-06-30 08:00:00.000'
,
NULL
,
'ORG2'
,
'2026-Q2'
,
'华东分行'
,
14570000
.
00
,
11947400
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'629'
,
'2026-06-30 08:00:00.000'
,
NULL
,
'ORG3'
,
'2026-Q2'
,
'华南分行'
,
17860000
.
00
,
16431200
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'629'
,
'2026-06-30 08:00:00.000'
,
NULL
,
'ORG4'
,
'2026-Q2'
,
'西部中心'
,
21150000
.
00
,
21573000
.
00
,
1
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'530'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'ORG1'
,
'2025-Q4'
,
'总行营业部'
,
10800000
.
00
,
7560000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'530'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'ORG2'
,
'2025-Q4'
,
'华东分行'
,
13950000
.
00
,
11160000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'530'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'ORG3'
,
'2025-Q4'
,
'华南分行'
,
17100000
.
00
,
15390000
.
00
,
0
),
(
'411'
,
'CREDIT_EXPOSURE'
,
'530'
,
'2025-12-31 08:00:00.000'
,
NULL
,
'ORG4'
,
'2025-Q4'
,
'西部中心'
,
20250000
.
00
,
20452500
.
00
,
1
)
ON
DUPLICATE
KEY
UPDATE
MODEL_ID
=
VALUES
(
MODEL_ID
),
MODEL_NAME
=
VALUES
(
MODEL_NAME
),
MODEL_RUN_BATCH_DATE
=
VALUES
(
MODEL_RUN_BATCH_DATE
),
REMARK
=
VALUES
(
REMARK
),
REPORTING_PERIOD
=
VALUES
(
REPORTING_PERIOD
),
INSTITUTION_NAME
=
VALUES
(
INSTITUTION_NAME
),
CREDIT_LIMIT
=
VALUES
(
CREDIT_LIMIT
),
USED_AMOUNT
=
VALUES
(
USED_AMOUNT
),
OVER_LIMIT
=
VALUES
(
OVER_LIMIT
);
COMMIT
;
src/pages/project_manager_xy/ForwardApprovals.jsx
View file @
5b9d1940
...
...
@@ -47,6 +47,27 @@ function canRecipientEdit(record, currentUserId) {
);
}
function
canFirstApprove
(
record
,
currentUserId
)
{
return
Boolean
(
record
?.
firstApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'pending'
,
);
}
function
canFinalApprove
(
record
,
currentUserId
)
{
return
Boolean
(
record
?.
finalApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'approved'
&&
record
.
finalApprovalStatus
===
'pending'
,
);
}
function
canApproveCurrentStep
(
record
,
currentUserId
)
{
return
canFirstApprove
(
record
,
currentUserId
)
||
canFinalApprove
(
record
,
currentUserId
);
}
function
getForwardEditorStatusLabel
(
record
)
{
return
isReturnedForEditing
(
record
)
?
'待重新填写'
:
getEditorStatusLabel
(
record
?.
editorStatus
);
}
...
...
@@ -125,15 +146,7 @@ export default function ForwardApprovals() {
}
if
(
activeFilter
===
'pending'
)
{
return
(
(
record
.
firstApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'pending'
)
||
(
record
.
finalApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'approved'
&&
record
.
finalApprovalStatus
===
'pending'
)
);
return
canApproveCurrentStep
(
record
,
currentUserId
);
}
return
(
...
...
@@ -226,7 +239,8 @@ export default function ForwardApprovals() {
}
async
function
handleSaveEditableRecords
()
{
if
(
!
editingRecord
)
{
if
(
!
canRecipientEdit
(
editingRecord
,
currentUserId
))
{
setError
(
'当前用户不是可填写接收人,或该转发记录已完成填写。'
);
return
;
}
...
...
@@ -248,7 +262,8 @@ export default function ForwardApprovals() {
}
async
function
handleCompleteEditing
()
{
if
(
!
editingRecord
)
{
if
(
!
canRecipientEdit
(
editingRecord
,
currentUserId
))
{
setError
(
'当前用户不是可填写接收人,或该转发记录已完成填写。'
);
return
;
}
...
...
@@ -292,30 +307,21 @@ export default function ForwardApprovals() {
}
async
function
handleApprove
(
record
)
{
if
(
record
.
firstApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'pending'
)
{
if
(
canFirstApprove
(
record
,
currentUserId
))
{
await
handleUpdateRecord
(
record
,
{
approval_status
:
'approved'
});
return
;
}
if
(
record
.
finalApprover
===
currentUserId
&&
record
.
approvalStatus
===
'approved'
&&
record
.
finalApprovalStatus
===
'pending'
)
{
if
(
canFinalApprove
(
record
,
currentUserId
))
{
await
handleUpdateRecord
(
record
,
{
final_approval_status
:
'approved'
});
return
;
}
setError
(
'当前用户无权审批该环节,或审批状态已经变化。'
);
}
async
function
handleReject
(
record
)
{
if
(
record
.
firstApprover
===
currentUserId
&&
record
.
editorStatus
===
'completed'
&&
record
.
approvalStatus
===
'pending'
)
{
if
(
canFirstApprove
(
record
,
currentUserId
))
{
await
handleUpdateRecord
(
record
,
{
editor_status
:
'editing'
,
completed_by_id
:
''
,
...
...
@@ -327,11 +333,7 @@ export default function ForwardApprovals() {
return
;
}
if
(
record
.
finalApprover
===
currentUserId
&&
record
.
approvalStatus
===
'approved'
&&
record
.
finalApprovalStatus
===
'pending'
)
{
if
(
canFinalApprove
(
record
,
currentUserId
))
{
await
handleUpdateRecord
(
record
,
{
editor_status
:
'editing'
,
completed_by_id
:
''
,
...
...
@@ -339,7 +341,10 @@ export default function ForwardApprovals() {
completed_at
:
''
,
final_approval_status
:
'rejected'
,
});
return
;
}
setError
(
'当前用户无权审批该环节,或审批状态已经变化。'
);
}
const
canCurrentUserEdit
=
Boolean
(
...
...
@@ -481,15 +486,7 @@ export default function ForwardApprovals() {
<
Button
isDisabled=
{
saving
||
!
(
(
activeRecord
.
firstApprover
===
currentUserId
&&
activeRecord
.
editorStatus
===
'completed'
&&
activeRecord
.
approvalStatus
===
'pending'
)
||
(
activeRecord
.
finalApprover
===
currentUserId
&&
activeRecord
.
editorStatus
===
'completed'
&&
activeRecord
.
approvalStatus
===
'approved'
&&
activeRecord
.
finalApprovalStatus
===
'pending'
)
)
!
canApproveCurrentStep
(
activeRecord
,
currentUserId
)
}
variant=
"danger-soft"
onPress=
{
()
=>
handleReject
(
activeRecord
)
}
...
...
@@ -499,15 +496,7 @@ export default function ForwardApprovals() {
<
Button
isDisabled=
{
saving
||
!
(
(
activeRecord
.
firstApprover
===
currentUserId
&&
activeRecord
.
editorStatus
===
'completed'
&&
activeRecord
.
approvalStatus
===
'pending'
)
||
(
activeRecord
.
finalApprover
===
currentUserId
&&
activeRecord
.
editorStatus
===
'completed'
&&
activeRecord
.
approvalStatus
===
'approved'
&&
activeRecord
.
finalApprovalStatus
===
'pending'
)
)
!
canApproveCurrentStep
(
activeRecord
,
currentUserId
)
}
variant=
"primary"
onPress=
{
()
=>
handleApprove
(
activeRecord
)
}
...
...
src/pages/project_manager_xy/ModelMonitorConnectorService.js
0 → 100644
View file @
5b9d1940
import
{
connectorExecute
,
connectorInspect
,
getSource
,
}
from
'@/services/DataService'
;
const
DEFAULT_CONNECTOR_NAME
=
typeof
MODEL_MONITOR_CONNECTOR_NAME
!==
'undefined'
&&
MODEL_MONITOR_CONNECTOR_NAME
?
MODEL_MONITOR_CONNECTOR_NAME
:
'MODEL_MONITOR_ORACLE'
;
const
RESERVED_SOURCE_COLUMNS
=
new
Set
([
'MODEL_ID'
,
'MODEL_NAME'
,
'MODEL_RUN_BATCH_ID'
,
'MODEL_RUN_BATCH_DATE'
,
'REMARK'
,
]);
let
connectorConfigCache
=
null
;
function
extractArray
(
response
)
{
const
candidates
=
[
response
,
response
?.
rows
,
response
?.
data
,
response
?.
result
,
response
?.
records
,
response
?.
data
?.
rows
,
response
?.
data
?.
records
,
response
?.
result
?.
rows
,
response
?.
result
?.
records
,
];
for
(
const
candidate
of
candidates
)
{
if
(
Array
.
isArray
(
candidate
))
return
candidate
;
}
return
[];
}
function
extractObject
(
response
)
{
return
response
?.
data
||
response
?.
result
||
response
;
}
function
escapeSql
(
value
)
{
return
String
(
value
??
''
).
replace
(
/'/g
,
"''"
);
}
function
isMysqlClient
(
settings
)
{
return
[
'mysql'
,
'mysql2'
].
includes
(
String
(
settings
?.
client
??
''
).
toLowerCase
());
}
function
quoteIdentifier
(
identifier
,
settings
)
{
const
value
=
String
(
identifier
??
''
);
if
(
isMysqlClient
(
settings
))
{
return
`\`
${
value
.
replace
(
/`/g
,
'``'
)}
\``
;
}
return
`"
${
value
.
replace
(
/"/g
,
'""'
)}
"`
;
}
function
quoteQualifiedIdentifier
(
identifier
,
settings
)
{
return
String
(
identifier
??
''
)
.
split
(
'.'
)
.
map
((
part
)
=>
quoteIdentifier
(
part
,
settings
))
.
join
(
'.'
);
}
function
normalizeName
(
value
,
index
,
usedNames
)
{
let
name
=
String
(
value
??
''
)
.
trim
()
.
toLowerCase
()
.
replace
(
/
[^
a-z0-9_
]
+/g
,
'_'
)
.
replace
(
/^_+|_+$/g
,
''
);
if
(
!
name
)
name
=
`field_
${
index
+
1
}
`
;
if
(
!
/^
[
a-z
]
/
.
test
(
name
))
name
=
`f_
${
name
}
`
;
let
suffix
=
1
;
while
(
usedNames
.
has
(
name
))
{
suffix
+=
1
;
name
=
`
${
name
}
_
${
suffix
}
`
;
}
usedNames
.
add
(
name
);
return
name
;
}
function
mapColumnType
(
rawType
)
{
const
value
=
String
(
rawType
??
''
).
toLowerCase
();
if
(
/
(
date|timestamp|time
)
/
.
test
(
value
))
return
'date'
;
if
(
/
(
number|decimal|numeric|int|integer|float|double|real|binary_float|binary_double
)
/
.
test
(
value
))
{
return
'number'
;
}
if
(
/
(
bool|boolean
)
/
.
test
(
value
))
return
'boolean'
;
if
(
/
(
json|object|struct|udt|collection
)
/
.
test
(
value
))
return
'object'
;
return
'text'
;
}
function
normalizeColumns
(
columns
=
[])
{
const
usedNames
=
new
Set
();
return
columns
.
map
((
column
,
index
)
=>
{
const
rawName
=
String
(
column
.
name
||
column
.
column_name
||
column
.
COLUMN_NAME
||
column
.
alias
||
column
.
ALIAS
||
''
,
).
trim
();
const
name
=
normalizeName
(
rawName
,
index
,
usedNames
);
const
label
=
String
(
column
.
label
||
column
.
comment
||
column
.
column_comment
||
column
.
COLUMN_COMMENT
||
rawName
||
name
,
).
trim
();
return
{
sourceName
:
rawName
||
name
,
name
,
label
,
type
:
mapColumnType
(
column
.
data_type
||
column
.
DATA_TYPE
||
column
.
type
||
column
.
TYPE
),
};
});
}
async
function
resolveConnectorConfig
()
{
if
(
connectorConfigCache
)
return
connectorConfigCache
;
const
source
=
await
getSource
(
DEFAULT_CONNECTOR_NAME
);
if
(
!
source
)
{
throw
new
Error
(
`未找到固定数据源“
${
DEFAULT_CONNECTOR_NAME
}
”。`
);
}
if
(
source
.
status
&&
source
.
status
!==
'active'
)
{
throw
new
Error
(
`固定数据源“
${
DEFAULT_CONNECTOR_NAME
}
”未启用。`
);
}
if
(
!
source
.
settings
)
{
throw
new
Error
(
`固定数据源“
${
DEFAULT_CONNECTOR_NAME
}
”缺少连接配置。`
);
}
connectorConfigCache
=
{
name
:
source
.
name
||
DEFAULT_CONNECTOR_NAME
,
settings
:
source
.
settings
,
};
return
connectorConfigCache
;
}
function
normalizeModelRow
(
row
=
{})
{
return
{
source_model_id
:
String
(
row
.
source_model_id
??
row
.
SOURCE_MODEL_ID
??
row
.
id
??
row
.
ID
??
''
).
trim
(),
source_model_name
:
String
(
row
.
source_model_name
??
row
.
SOURCE_MODEL_NAME
??
row
.
model_name
??
row
.
MODEL_NAME
??
''
).
trim
(),
model_code
:
String
(
row
.
model_code
??
row
.
MODEL_CODE
??
row
.
source_model_name
??
row
.
SOURCE_MODEL_NAME
??
row
.
MODEL_NAME
??
''
).
trim
(),
model_name
:
String
(
row
.
model_name
??
row
.
MODEL_NAME
??
row
.
source_model_name
??
row
.
SOURCE_MODEL_NAME
??
''
).
trim
(),
source_model_updated_at
:
row
.
source_model_updated_at
??
row
.
SOURCE_MODEL_UPDATED_AT
??
''
,
model_state
:
row
.
model_state
??
row
.
MODEL_STATE
??
''
,
model_apply_state
:
row
.
model_apply_state
??
row
.
MODEL_APPLY_STATE
??
''
,
model_risk_level
:
row
.
model_risk_level
??
row
.
MODEL_RISK_LEVEL
??
''
,
model_business_type
:
row
.
model_business_type
??
row
.
MODEL_BUSINESS_TYPE
??
''
,
model_business_type_name
:
row
.
model_business_type_name
??
row
.
MODEL_BUSINESS_TYPE_NAME
??
''
,
};
}
function
normalizeBatchRow
(
row
=
{})
{
const
sourceBatchDate
=
row
.
source_batch_date
??
row
.
SOURCE_BATCH_DATE
??
''
;
const
batchCode
=
String
(
row
.
batch_code
??
row
.
BATCH_CODE
??
row
.
model_run_batch_id
??
row
.
MODEL_RUN_BATCH_ID
??
''
).
trim
();
return
{
source_model_id
:
String
(
row
.
source_model_id
??
row
.
SOURCE_MODEL_ID
??
row
.
model_id
??
row
.
MODEL_ID
??
''
).
trim
(),
source_model_name
:
String
(
row
.
source_model_name
??
row
.
SOURCE_MODEL_NAME
??
row
.
model_name
??
row
.
MODEL_NAME
??
''
).
trim
(),
batch_code
:
batchCode
,
batch_name
:
String
(
sourceBatchDate
||
batchCode
||
''
).
trim
()
||
batchCode
,
source_batch_date
:
sourceBatchDate
,
data_table_name
:
String
(
row
.
data_table_name
??
row
.
DATA_TABLE_NAME
??
row
.
model_data_table_name
??
row
.
MODEL_DATA_TABLE_NAME
??
''
).
trim
().
toUpperCase
(),
remark
:
String
(
row
.
remark
??
row
.
REMARK
??
''
).
trim
(),
source_model_data_count
:
Number
(
row
.
source_model_data_count
??
row
.
SOURCE_MODEL_DATA_COUNT
??
row
.
model_data_cnt
??
row
.
MODEL_DATA_CNT
??
0
),
source_unchecked_count
:
Number
(
row
.
source_unchecked_count
??
row
.
SOURCE_UNCHECKED_COUNT
??
row
.
unchecked_cnt
??
row
.
UNCHECKED_CNT
??
0
),
source_unsubmitted_count
:
Number
(
row
.
source_unsubmitted_count
??
row
.
SOURCE_UNSUBMITTED_COUNT
??
row
.
unsubmitted_cnt
??
row
.
UNSUBMITTED_CNT
??
0
),
source_reviewed_count
:
Number
(
row
.
source_reviewed_count
??
row
.
SOURCE_REVIEWED_COUNT
??
row
.
reviewed_cnt
??
row
.
REVIEWED_CNT
??
0
),
};
}
function
normalizeInspectColumns
(
response
)
{
const
data
=
extractObject
(
response
);
const
columns
=
Array
.
isArray
(
data
?.
columns
)
?
data
.
columns
:
Array
.
isArray
(
data
)
?
data
:
[];
return
normalizeColumns
(
columns
)
.
filter
((
column
)
=>
!
RESERVED_SOURCE_COLUMNS
.
has
(
String
(
column
.
sourceName
||
''
).
toUpperCase
()));
}
function
buildSelectList
(
columns
,
settings
)
{
return
columns
.
map
((
column
)
=>
`
${
quoteIdentifier
(
column
.
sourceName
,
settings
)}
as
${
quoteIdentifier
(
column
.
name
,
settings
)}
`
)
.
join
(
', '
);
}
function
extractRows
(
response
)
{
const
rows
=
extractArray
(
response
);
if
(
rows
.
length
>
0
)
return
rows
;
if
(
Array
.
isArray
(
response
?.[
0
]))
return
response
[
0
];
return
[];
}
function
readRowValue
(
row
,
column
)
{
const
candidates
=
[
column
.
name
,
column
.
name
.
toUpperCase
(),
column
.
sourceName
,
column
.
sourceName
.
toUpperCase
(),
];
for
(
const
key
of
candidates
)
{
if
(
key
&&
row
&&
Object
.
prototype
.
hasOwnProperty
.
call
(
row
,
key
))
{
return
row
[
key
];
}
}
return
null
;
}
export
async
function
listSourceModels
()
{
const
{
settings
}
=
await
resolveConnectorConfig
();
const
response
=
await
connectorExecute
({
config
:
settings
,
query
:
`
select
ID as "source_model_id",
MODEL_NAME as "source_model_name",
MODEL_NAME as "model_code",
COALESCE(MODEL_ZH_NAME, MODEL_NAME) as "model_name",
MODEL_DATA_UPDATE_TIME as "source_model_updated_at",
MODEL_STATE as "model_state",
MODEL_APPLY_STATE as "model_apply_state",
MODEL_RISK_LEVEL as "model_risk_level",
MODEL_BUSINESS_TYPE as "model_business_type",
MODEL_BUSINESS_TYPE_NAME as "model_business_type_name"
from ANALYZE_MXVIEW_INFO
order by ID desc
`
,
});
return
extractRows
(
response
).
map
(
normalizeModelRow
);
}
export
async
function
listSourceBatches
(
sourceModel
)
{
const
{
settings
}
=
await
resolveConnectorConfig
();
const
sourceModelId
=
String
(
sourceModel
?.
source_model_id
??
sourceModel
?.
id
??
''
).
trim
();
const
sourceModelName
=
String
(
sourceModel
?.
source_model_name
??
sourceModel
?.
model_code
??
''
).
trim
();
if
(
!
sourceModelId
)
throw
new
Error
(
'来源模型缺少 ANALYZE_MXVIEW_INFO.ID。'
);
if
(
!
sourceModelName
)
throw
new
Error
(
'来源模型缺少 ANALYZE_MXVIEW_INFO.MODEL_NAME。'
);
const
response
=
await
connectorExecute
({
config
:
settings
,
query
:
`
select
MODEL_ID as "source_model_id",
MODEL_NAME as "source_model_name",
MODEL_RUN_BATCH_ID as "batch_code",
MODEL_RUN_BATCH_DATE as "source_batch_date",
MODEL_DATA_TABLE_NAME as "data_table_name",
REMARK as "remark",
MODEL_DATA_CNT as "source_model_data_count",
UNCHECKED_CNT as "source_unchecked_count",
UNSUBMITTED_CNT as "source_unsubmitted_count",
REVIEWED_CNT as "source_reviewed_count"
from ANALYZE_MODEL_RUN_BATCH_INFO
where MODEL_ID = '
${
escapeSql
(
sourceModelId
)}
'
and MODEL_NAME = '
${
escapeSql
(
sourceModelName
)}
'
order by MODEL_RUN_BATCH_DATE desc, MODEL_RUN_BATCH_ID desc
`
,
});
return
extractRows
(
response
).
map
(
normalizeBatchRow
);
}
export
async
function
loadSourceBatchData
(
sourceModel
,
sourceBatch
)
{
const
{
settings
}
=
await
resolveConnectorConfig
();
const
sourceModelId
=
String
(
sourceModel
?.
source_model_id
??
sourceModel
?.
id
??
''
).
trim
();
const
sourceModelName
=
String
(
sourceModel
?.
source_model_name
??
sourceModel
?.
model_code
??
''
).
trim
();
const
batchCode
=
String
(
sourceBatch
?.
batch_code
??
''
).
trim
();
const
tableName
=
String
(
sourceBatch
?.
data_table_name
??
''
).
trim
().
toUpperCase
();
if
(
!
sourceModelId
)
throw
new
Error
(
'来源模型缺少 ANALYZE_MXVIEW_INFO.ID。'
);
if
(
!
sourceModelName
)
throw
new
Error
(
'来源模型缺少 ANALYZE_MXVIEW_INFO.MODEL_NAME。'
);
if
(
!
batchCode
)
throw
new
Error
(
'来源批次缺少 MODEL_RUN_BATCH_ID。'
);
if
(
!
tableName
)
throw
new
Error
(
'来源批次缺少 MODEL_DATA_TABLE_NAME。'
);
if
(
!
/^
[
A-Z
][
A-Z0-9_$#
]
*
(\.[
A-Z
][
A-Z0-9_$#
]
*
)?
$/
.
test
(
tableName
))
{
throw
new
Error
(
'来源批次的数据表名为空或不符合来源数据库标识符规范。'
);
}
const
inspectResponse
=
await
connectorInspect
({
config
:
settings
,
table
:
tableName
,
});
const
columns
=
normalizeInspectColumns
(
inspectResponse
);
if
(
!
columns
.
length
)
{
throw
new
Error
(
`数据表“
${
tableName
}
”没有可用字段定义。`
);
}
const
selectColumns
=
buildSelectList
(
columns
,
settings
);
const
response
=
await
connectorExecute
({
config
:
settings
,
query
:
`
select
${
selectColumns
}
from
${
quoteQualifiedIdentifier
(
tableName
,
settings
)}
where MODEL_RUN_BATCH_ID = '
${
escapeSql
(
batchCode
)}
'
`
,
});
const
rows
=
extractRows
(
response
).
map
((
row
)
=>
Object
.
fromEntries
(
columns
.
map
((
column
)
=>
{
return
[
column
.
name
,
readRowValue
(
row
,
column
)];
}),
));
return
{
columns
:
columns
.
map
(({
sourceName
,
...
column
})
=>
column
),
rows
,
};
}
export
{
DEFAULT_CONNECTOR_NAME
as
MODEL_MONITOR_CONNECTOR_NAME
};
src/pages/project_manager_xy/model-monitor.business-flow.md
View file @
5b9d1940
...
...
@@ -30,7 +30,7 @@
## 3. 外部数据源契约
当前外部来源仍使用 Mock 适配器,但返回结构已映射到
`ANALYZE_MXVIEW_INFO`
和
`ANALYZE_MODEL_RUN_BATCH_INFO`
。UI 仅依赖以下方法,真实 Oracle 适配器接入时不得修改页面调用
:
生产环境的外部来源通过通用 Connector 接口读取固定 Oracle 数据源,开发联调允许使用遵循同一表结构的 MySQL 5.7+ 测试源;适配器根据 Connector 的
`settings.client`
使用 Oracle 双引号或 MySQL 反引号引用动态表与字段。同步后的模型监控数据写入平台普通 Subject。模型监控页面不提供数据源选择器,来源 connector 名称由构建常量
`MODEL_MONITOR_CONNECTOR_NAME`
提供。UI 仅依赖以下方法,页面调用保持不变
:
```
ts
listSourceModels
():
Promise
<
Array
<
{
...
...
@@ -56,11 +56,13 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
}
>
;
```
来源映射规则:模型稳定键为
`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_MXVIEW_INFO.MODEL_NAME + ID`
;批次稳定键为
`ANALYZE_MODEL_RUN_BATCH_INFO.MODEL_RUN_BATCH_ID`
,直接映射到本地
`batch_code`
;结果表来自
`MODEL_DATA_TABLE_NAME`
。结果表查询必须先对白名单表名做校验,再
对批次 ID 做字符串转义并
按
`MODEL_RUN_BATCH_ID`
过滤,不能整表读取。
`ANALYZE_MODEL_DATA_MX_*`
结果行不要求业务主键。每个批次只允许同步一次,结果行按来源读取顺序记录
`source_row_number`
并纯新增,不进行去重或更新。
未来真实后台接口到位时只替换适配器,不修改页面和本地持久化服务。仓库目前没有 Oracle Connector 的调用协议,接入前必须补充接口 URL、请求体、分页和字段类型响应定义。
MySQL 联调数据可通过
`scripts/mysql/model-monitor-source-fixtures.sql`
初始化。脚本只创建不存在的来源表,并按稳定键更新 3 个模型、9 个批次和 48 条结果样例;使用测试源启动前端时,必须把
`MODEL_MONITOR_CONNECTOR_NAME`
配置为对应的 MySQL Connector。该环境只验证通用同步流程,不替代 Oracle 驱动和方言的集成验收。
未来若固定数据源配置变化,只替换构建常量和适配器实现,不修改页面和本地持久化服务。
## 4. 核心流程
...
...
@@ -84,8 +86,8 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
-
来源声明数量与实际读取数量检查
-
字段别名、重复字段和类型检查
-
所有数值、日期、布尔和对象字段的规范化检查
8.
本地没有模型时按
`MODEL_NAME + ID`
创建稳定
`model_monitor_model`
Record,并生成稳定动态数据 Subject 名称。
9.
自动创建或扩展
模型数据 Subject。只允许增加字段;
类型冲突立即停止。
8.
本地没有模型时按
`MODEL_NAME + ID`
创建稳定
`model_monitor_model`
Record,并生成
以
`model_monitor_data_`
开头的
稳定动态数据 Subject 名称。
9.
自动创建或扩展
普通模型数据 Subject。只允许增加字段,
类型冲突立即停止。
10.
按
`MODEL_RUN_BATCH_ID`
查找批次;任何状态的本地批次一旦存在都拒绝重复同步,不再读取来源结果表。
11.
为来源行生成从 1 开始的
`source_row_number`
。
12.
每 20 条分块纯新增写入,不查询已有结果行,也不执行更新;数据 Record name 由
`MODEL_RUN_BATCH_ID + source_row_number`
生成。
...
...
@@ -98,7 +100,7 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
-
`partial`
:部分行写入失败,成功数据保留。
-
`failed`
:校验、对象创建或同步过程失败。
-
`partial/failed`
批次保留现场供排查,同样不允许重试,避免重复写入。
-
批次保留错误摘要、新增数、失败数、来源行数和本地总数;更新数固定为 0。
-
批次保留错误摘要、新增数、失败数、来源行数和本地总数;更新数固定为 0。
模型监控 Record 写入失败时保留已成功写入的数据和批次状态。
### 4.4 模型分组
...
...
@@ -142,6 +144,7 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
6.
任一审批驳回后,填写状态恢复为
`editing`
,清空上次完成人和完成时间,接收人可修改原回填内容。
7.
接收人重新完成填写时,
`approval_status`
和
`final_approval_status`
均重置为
`pending`
,流程从一级审批重新开始。
8.
终审通过后状态为已最终审批,流程结束。
9.
接收人填写、完成填写、一级审批和终审在前端同时执行按钮禁用与提交事件校验;用户身份或流程状态不满足时不发送更新请求,并提示状态已经变化或当前用户无权操作。
## 5. 页面状态与异常处理
...
...
src/pages/project_manager_xy/modelMonitorService.js
View file @
5b9d1940
...
...
@@ -16,7 +16,7 @@ import {
listSourceBatches
,
listSourceModels
,
loadSourceBatchData
,
}
from
'./ModelMonitor
Mock
Service'
;
}
from
'./ModelMonitor
Connector
Service'
;
export
const
MODEL_GROUP_SUBJECT
=
'model_monitor_group'
;
export
const
MODEL_SUBJECT
=
'model_monitor_model'
;
...
...
@@ -302,6 +302,7 @@ function normalizeSubject(subject = {}) {
id
:
candidate
?.
_id
||
candidate
?.
id
||
candidate
?.
name
||
''
,
name
:
candidate
?.
name
||
candidate
?.
title
||
''
,
title
:
candidate
?.
title
||
candidate
?.
name
||
''
,
type
:
candidate
?.
type
||
'normal'
,
fields
:
(
Array
.
isArray
(
fields
)
?
fields
:
[]).
map
(
normalizeField
),
};
}
...
...
@@ -495,7 +496,7 @@ function validateSourceSelection(sourceModel = {}, sourceBatch = {}) {
throw
new
Error
(
'来源批次的 MODEL_NAME 与所选模型名称不一致。'
);
}
if
(
!
/^
[
A-Z
][
A-Z0-9_$#
]
*
(\.[
A-Z
][
A-Z0-9_$#
]
*
)?
$/
.
test
(
sourceDataTableName
))
{
throw
new
Error
(
'来源批次的数据表名为空或不符合
Oracle 非引号
标识符规范。'
);
throw
new
Error
(
'来源批次的数据表名为空或不符合
来源数据库
标识符规范。'
);
}
return
{
...
...
@@ -570,6 +571,11 @@ async function ensureSubject(definition) {
return
normalizeSubject
(
await
createSubject
(
toSubjectPayload
(
definition
)));
}
const
expectedType
=
String
(
definition
.
type
||
'normal'
).
toLowerCase
();
if
(
String
(
subject
.
type
||
'normal'
).
toLowerCase
()
!==
expectedType
)
{
throw
new
Error
(
`对象“
${
definition
.
name
}
”当前类型为
${
subject
.
type
}
,期望类型为
${
expectedType
}
,已停止初始化。`
);
}
const
existingFields
=
new
Map
(
subject
.
fields
.
map
((
field
)
=>
[
field
.
alias
,
field
]));
for
(
const
field
of
definition
.
fields
)
{
const
existing
=
existingFields
.
get
(
field
.
name
);
...
...
src/services/DataService.js
View file @
5b9d1940
...
...
@@ -1339,6 +1339,19 @@ function connectorTest(data) {
});
}
function
querySources
(
query
)
{
return
request
(
'/connector/manage/query'
,
{
method
:
'post'
,
data
:
query
,
});
}
function
getSource
(
name
)
{
return
request
(
`/connector/manage/
${
name
}
`
,
{
method
:
'get'
,
});
}
function
connectorList
(
data
)
{
return
request
(
`/connector/list`
,
{
method
:
'post'
,
...
...
@@ -2080,6 +2093,7 @@ export {
connectorExecute
,
connectorInspect
,
connectorList
,
getSource
,
connectorQuery
,
connectorQueryExecute
,
connectorSelect
,
...
...
@@ -2243,6 +2257,7 @@ export {
// Notice
queryNotice
,
queryPlaywrightSession
,
querySources
,
queryRecords
,
queryRemovedRecord
,
//rule
...
...
src/services/record-api.md
View file @
5b9d1940
...
...
@@ -44,7 +44,7 @@
}
```
开发阶段用于自动创建固定
`model_monitor_*`
Subject 和每模型动态数据 Subject。正式环境建议由部署流程预建固定 Subject。
开发阶段用于自动创建固定
`model_monitor_*`
Subject 和每模型动态数据 Subject。
当前均使用后端默认的
`normal`
类型,不传
`type`
和
`external`
;对象名不附加数据库类型前缀,
正式环境建议由部署流程预建固定 Subject。
### 2.3 增加 Subject 字段
...
...
@@ -158,7 +158,7 @@
## 5. 外部来源适配契约
当前仓库
尚未提供 Oracle Connector 的请求体和响应体协议,因此仍由
`ModelMonitorMockService`
提供数据,但 Mock 已按
`ANALYZE_MXVIEW_INFO`
和
`ANALYZE_MODEL_RUN_BATCH_INFO`
的字段语义返回。真实适配器接入后必须保持以下签名,页面和 Record 同步逻辑不直接拼 SQL
:
当前仓库
通过通用 Connector 接口读取生产 Oracle 数据源;开发联调允许接入遵循相同来源表结构的 MySQL 5.7+ 测试数据源。同步后的模型、批次和动态数据写入平台普通 Subject。模型监控页面不直接拼 SQL,也不让用户在页面上选择数据源;来源 connector 名称由构建常量
`MODEL_MONITOR_CONNECTOR_NAME`
提供。前端通过
`/connector/manage/:name`
读取来源连接配置,再用
`/connector/execute`
与
`/connector/inspect`
读取模型、批次和批次数据;动态标识符必须根据 Connector 客户端使用对应方言引用。接口适配层必须保持以下签名
:
```
ts
listSourceModels
():
Promise
<
Array
<
{
...
...
@@ -204,13 +204,14 @@ loadSourceBatchData(sourceModel: SourceModel, sourceBatch: SourceBatch): Promise
-
`batch_code`
直接保存
`ANALYZE_MODEL_RUN_BATCH_INFO.MODEL_RUN_BATCH_ID`
,不再增加重复的来源跑批 ID 字段。
-
批次的
`MODEL_NAME + MODEL_ID`
必须等于所选模型主键。
-
`MODEL_DATA_TABLE_NAME`
只能来自批次表,并必须匹配 Oracle 非引号标识符白名单;不得接受页面输入的任意表名。
-
动态表名不能作为绑定参数,
后端必须先校验白名单再拼接;批次 ID 必须使用绑定参数
查询。
-
动态表名不能作为绑定参数,
必须先校验白名单再拼接;批次 ID 需要在适配层做字符串转义后再参与
查询。
-
结果表必须按
`MODEL_RUN_BATCH_ID`
过滤,禁止只按表名读取整张模型结果表。
-
`source_model_data_count`
存在时必须与实际读取行数一致,否则停止写入。
-
本地一旦存在相同
`batch_code`
的批次 Record,无论状态为何,界面和 service 层都必须拒绝再次同步。
-
`columns[].name`
必须匹配
`^[a-z][a-z0-9_]*$`
且不可重复。
-
日期在写入 Record 前转换为 ISO 8601。
-
非法字段值或字段类型冲突必须在写入前报错。
-
所有模型监控 Subject 当前均使用默认
`normal`
类型,数据写入平台内部存储;对象名统一使用
`model_monitor_`
前缀,不附加数据库类型前缀。
-
结果行只执行新增,不读取已有结果行,也不执行更新;
`source_row_number`
保存本次来源读取顺序。
-
模型、批次和数据行 Record name 分别由
`MODEL_NAME + ID`
、
`MODEL_RUN_BATCH_ID`
、
`MODEL_RUN_BATCH_ID + source_row_number`
生成稳定哈希。
...
...
src/services/subjectsDefinition.md
View file @
5b9d1940
...
...
@@ -15,6 +15,8 @@
旧
`analysis_view_config`
、
`analysis_view_group`
、
`analysis_forward_record`
、
`analysis_forward_record_item`
、
`analysis_dashboard_board`
仅作为历史数据保留,新界面不得读取或写入。
当前阶段所有模型监控 Subject 均使用默认
`normal`
类型,Record 保存在平台内部存储中;MySQL 仅作为模型来源数据源。对象名统一使用
`model_monitor_`
前缀,不附加数据库类型前缀。
## 1. 统一 Record 协议
```
ts
...
...
@@ -147,7 +149,7 @@ type Record = {
## 3. 动态模型数据 Subject
每个模型首次同步时按
`ANALYZE_MXVIEW_INFO.MODEL_NAME + ID`
自动创建稳定的
动态数据 Subject。
后续批次继续使用同一 Subject。
每个模型首次同步时按
`ANALYZE_MXVIEW_INFO.MODEL_NAME + ID`
自动创建稳定的
`model_monitor_data_<stable_id>`
普通动态数据 Subject,
后续批次继续使用同一 Subject。
固定系统字段:
...
...
@@ -170,6 +172,6 @@ type Record = {
-
模型和批次不提供删除。
-
模型仅由同步自动创建,来源模型 ID、模型编码和名称不可本地修改。
-
`hidden_record_ids`
为批次级共享状态,与转发的
`excluded_record_ids`
分开维护。
-
固定 Subject 在开发阶段由前端检查并创建;正式环境应由部署流程预建。
-
固定 Subject 在开发阶段由前端检查并创建
为普通对象
;正式环境应由部署流程预建。
-
已存在的 Subject 自动补充新字段时先按可选字段添加,以兼容历史 Record;新同步数据仍由 service 层执行必填校验。
-
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