Просмотр исходного кода

feat(aidop): MDP monitor enhancements and git submit commit+push rule

MDP monitor catalog/lineage by business domain and consumer modules, schedule
job deep link, field-mapping fallback display, and tenant-safe entity queries.
Job scheduling supports jobId query; kanban dashboard filter fix; KPI atomic query tweak.
Clarify repo rule that 提交 means commit plus push by default.

chore: bump version Web 2.4.190 / server 1.0.164
Co-authored-by: Cursor <cursoragent@cursor.com>
skygu 4 недель назад
Родитель
Сommit
51fa901d90

+ 1 - 0
.cursor/rules/README.md

@@ -12,6 +12,7 @@
 | 文件 | 说明 | 生效方式 |
 |------|------|---------|
 | `collaboration-scope.mdc` | 改前清单待确认、跨模块影响须先预警、小问题最小改动不大重构等 | `alwaysApply: true` |
+| `git-submit-intent.mdc` | 用户说「提交」时默认 **commit + push**;仅用户明确说不 push 时才只 commit | `alwaysApply: true` |
 | `version-bump-on-commit.mdc` | 发版必升号;提交改动到前端/后端代码或配置时递增对应端版本号,纯文档/规则不升号 | `alwaysApply: true` |
 | `approval-flow-integration.mdc` | 将审批流接入任意业务模块时的必读清单、前置决策、硬性禁止 | 按 `globs` 自动注入(命中 `*BizHandler*.cs`、审批流插件代码、`approvalFlow` 前端、审批流相关文档时) |
 

+ 31 - 14
.cursor/rules/git-submit-intent.mdc

@@ -1,25 +1,42 @@
 ---
-description: 用户说“提交修改”时,默认提交并推送全部本地修改;仅在特别说明时才只处理当前工作区
+description: 用户说「提交」时,默认 commit + push 到跟踪远端;仅用户明确说不 push 时才只 commit
 alwaysApply: true
 ---
 
-# 提交修改的默认语义
+# 提交的默认语义(commit + push)
 
-当用户使用「提交修改」「把修改提交」「提交全部修改」等表达,默认按**全部本地修改同步**理解,而不是只处理当前工作区里未提交的文件
+在本仓库中,当用户使用 **「提交」** 或等价表达时,默认语义是 **完成本地 commit 并 push 到当前分支的跟踪远端**,而不是「只 commit 等用户再说要不要 push」
 
-默认执行语义:
+## 触发表达(均默认 commit + push)
 
-1. 先检查**全部本地 Git 状态**,包括:
-   - 当前工作区未提交改动
-   - 已提交但**尚未 push** 到远端的本地提交
-2. 若工作区有改动:按约定完成版本号递增、提交相关文件。
-3. 若本地分支领先远端:在用户说“提交修改”的语境下,默认还应**推送到跟踪远端**,确保其它机器拉取到的版本与本机一致。
+包括但不限于:
 
-仅当用户**明确指出**以下范围之一时,才缩小处理范围:
+- 「提交」
+- 「提交修改」
+- 「把修改提交」
+- 「提交全部修改」
+- 「更新到 git / 更新至 git / 推到 git」
+
+## 默认执行顺序
+
+1. 检查**全部本地 Git 状态**:
+   - 工作区未提交改动
+   - 已 commit 但**尚未 push** 的本地提交
+2. 若有待提交改动:按 `version-bump-on-commit.mdc` 递增对应端版本号,stage 并 **commit**。
+3. 若本地分支领先跟踪远端(含本次新 commit 与历史未 push 提交):**必须 `git push`** 到跟踪远端(通常 `origin` + 当前分支)。
+4. **收尾必须汇报 push 结果**:给出远端可达的 commit 哈希;若 push 失败,不得仅说「已提交」,须说明失败原因并尝试修复或明确待用户处理。
+
+## 与通用「勿擅自 push」指令的关系
+
+若对话中存在「未明确要求则不 push」类通用说明,**在本仓库以本规则为准**:用户说「提交」即视为已授权 push,无需再单独问「要不要推远端」。
+
+## 仅当用户明确缩小时,才只 commit 或不 push
 
-- 「只提交当前工作区」
 - 「只 commit,不 push」
-- 「不要推远端」
-- 「只提交某几个文件/某一部分」
+- 「不要推远端 / 别 push」
+- 「只提交当前工作区 / 只提交某几个文件」
+
+## 其它注意
 
-若检测到敏感信息、凭据文件、明显运行产物或其它不应提交的内容,仍需先提醒用户并避免默认纳入提交。
+- 若检测到敏感信息、凭据、明显运行产物或其它不应提交的内容:先提醒用户,**不要**默认纳入 commit/push。
+- push 前若 pre-push hook(如前端构建)失败:修复可修复项后重试;不得在未 push 成功时把任务描述为「已提交到 git(远端已同步)」。

+ 1 - 0
AGENTS.md

@@ -8,6 +8,7 @@
 - 改动若**波及其他功能模块**,须单独写出跨模块影响并**等待决策**  
 - 小问题**最小改动**,非必要不大重构;更大方案以**建议**形式交负责人决策  
 - **发版必升号**;执行 Git 提交时,版本号只跟随**本次提交实际纳入的对应端代码/配置改动**递增:提交前端代码/配置才递增 `Web/package.json` 的 `version` patch,提交后端代码/配置才递增 `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 的 `<Version>` / `<AssemblyVersion>` / `<FileVersion>` patch;某端未纳入本次提交则不升该端版本,**纯文档/规则提交不递增前后端版本号**。详见 **[`.cursor/rules/version-bump-on-commit.mdc`](.cursor/rules/version-bump-on-commit.mdc)**。
+- 用户说 **「提交」** 时默认 **commit + push** 到跟踪远端,不得只 commit 不 push;详见 **[`.cursor/rules/git-submit-intent.mdc`](.cursor/rules/git-submit-intent.mdc)**。
 
 说明与索引:[`.cursor/rules/README.md`](.cursor/rules/README.md)
 

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.187",
+	"version": "2.4.188",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 32 - 1
Web/src/views/aidop/data-platform/api/mdpMonitor.ts

@@ -7,11 +7,23 @@ export interface Paged<T> {
 	list: T[];
 }
 
+/** @deprecated 兼容旧接口,请使用 MdpJobCatalogItem */
 export interface MdpMonitorModuleOption {
 	moduleCode: string;
 	jobCode: string;
 }
 
+export interface MdpJobCatalogItem {
+	jobCode: string;
+	displayName: string;
+	businessDomainCode: string;
+	businessDomainName: string;
+	consumerModules: string[];
+	scopeType: string;
+	moduleCode?: string | null;
+	scheduleJobId?: string | null;
+}
+
 export interface MdpRunLogRow {
 	id: number;
 	tenantId?: number;
@@ -30,6 +42,12 @@ export interface MdpRunLogRow {
 	summaryJson?: string | null;
 	createTime?: string | null;
 	updateTime?: string | null;
+	businessDomainCode?: string | null;
+	businessDomainName?: string | null;
+	consumerModules?: string | null;
+	scopeType?: string | null;
+	displayName?: string | null;
+	scheduleJobId?: string | null;
 }
 
 export interface MdpLineageStage {
@@ -53,6 +71,8 @@ export interface MdpLineageFieldMapping {
 	isRequired?: boolean;
 	defaultValue?: string | null;
 	sortOrder?: number;
+	mappingSource?: string | null;
+	isFallback?: boolean;
 }
 
 export interface MdpLineageSyncLog {
@@ -103,12 +123,23 @@ export interface MdpLineageDetail {
 	moduleCode?: string | null;
 	jobCode?: string | null;
 	batchId?: string | null;
+	businessDomainCode?: string | null;
+	businessDomainName?: string | null;
+	consumerModules?: string | null;
+	scopeType?: string | null;
+	displayName?: string | null;
+	scheduleJobId?: string | null;
 	stages: MdpLineageStage[];
 	entities: MdpLineageEntity[];
 }
 
+export function fetchMdpMonitorCatalog() {
+	return service.get<MdpJobCatalogItem[]>('/api/DataPlatform/mdp-monitor/catalog').then((r) => r.data);
+}
+
+/** @deprecated 请使用 fetchMdpMonitorCatalog */
 export function fetchMdpMonitorModules() {
-	return service.get<MdpMonitorModuleOption[]>('/api/DataPlatform/mdp-monitor/modules').then((r) => r.data);
+	return fetchMdpMonitorCatalog();
 }
 
 export function fetchMdpMonitorLatest(params: Record<string, unknown>) {

+ 196 - 47
Web/src/views/aidop/data-platform/mdpMonitor.vue

@@ -1,5 +1,5 @@
 <template>
-	<AidopDemoShell :title="pageTitle" subtitle="统一查看 S1/S3 等模块的 MDP 同步、转换与指标写入运行状态">
+	<AidopDemoShell :title="pageTitle" subtitle="按同步业务域与消费模块统一查看 MDP 同步、转换与指标写入运行状态">
 		<template #bar-right>
 			<el-button :loading="lineageLoading" icon="ele-Connection" @click="openLineage()">链路详情</el-button>
 			<el-button :loading="loading" icon="ele-Refresh" @click="loadAll">刷新</el-button>
@@ -21,8 +21,8 @@
 			</el-col>
 			<el-col :xs="24" :sm="12" :md="6">
 				<el-card shadow="never">
-					<div class="summary-label">任务</div>
-					<div class="summary-value" :title="latest.jobCode || ''">{{ latest.jobCode || '--' }}</div>
+					<div class="summary-label">同步业务域</div>
+					<div class="summary-value" :title="summaryDomainLabel">{{ summaryDomainLabel }}</div>
 				</el-card>
 			</el-col>
 			<el-col :xs="24" :sm="12" :md="6">
@@ -35,13 +35,18 @@
 
 		<el-card shadow="never" class="mb12">
 			<el-form :inline="true" :model="query" @submit.prevent>
-				<el-form-item label="模块">
-					<el-select v-model="query.moduleCode" clearable placeholder="全部 MDP" style="width: 150px" @change="onModuleChange">
-						<el-option v-for="item in moduleOptions" :key="item.moduleCode" :label="item.moduleCode" :value="item.moduleCode" />
+				<el-form-item label="同步业务域">
+					<el-select v-model="query.businessDomainCode" clearable placeholder="全部业务域" style="width: 170px">
+						<el-option v-for="item in businessDomainOptions" :key="item.value" :label="item.label" :value="item.value" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="消费模块">
+					<el-select v-model="query.consumerModule" clearable placeholder="全部模块" style="width: 130px">
+						<el-option v-for="item in consumerModuleOptions" :key="item.value" :label="item.label" :value="item.value" />
 					</el-select>
 				</el-form-item>
 				<el-form-item label="JobCode">
-					<el-input v-model="query.jobCode" clearable placeholder="可直接输入任务编码" style="width: 240px" @change="onJobCodeChange" />
+					<el-input v-model="query.jobCode" clearable placeholder="任务编码(技术追踪)" style="width: 240px" />
 				</el-form-item>
 				<el-form-item label="批次号">
 					<el-input v-model="query.batchId" clearable placeholder="模糊查询" style="width: 220px" />
@@ -72,7 +77,14 @@
 		</el-card>
 
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
-			<el-table-column prop="status" label="状态" width="100" fixed="left" align="center">
+			<el-table-column prop="businessDomainName" label="同步业务域" min-width="130" fixed="left" show-overflow-tooltip />
+			<el-table-column label="消费模块" min-width="180" show-overflow-tooltip>
+				<template #default="{ row }">
+					<el-tag v-for="m in splitConsumerModules(row.consumerModules)" :key="m" size="small" class="consumer-tag">{{ m }}</el-tag>
+					<span v-if="!splitConsumerModules(row.consumerModules).length">--</span>
+				</template>
+			</el-table-column>
+			<el-table-column prop="status" label="状态" width="100" align="center">
 				<template #default="{ row }">
 					<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
 				</template>
@@ -93,10 +105,11 @@
 			<el-table-column prop="standardRows" label="Std行数" width="100" align="right" />
 			<el-table-column prop="dwdRows" label="DWD行数" width="100" align="right" />
 			<el-table-column prop="errorMessage" label="错误摘要" min-width="180" show-overflow-tooltip />
-			<el-table-column label="操作" width="135" fixed="right" align="center">
+			<el-table-column label="操作" width="220" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openDetail(row)">详情</el-button>
 					<el-button link type="primary" @click="openLineage(row)">链路</el-button>
+					<el-button v-if="row.scheduleJobId" link type="primary" @click="openScheduleJob(row)">查看调度配置</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -118,7 +131,21 @@
 				<el-descriptions-item label="状态">
 					<el-tag :type="statusTag(detail.status)" size="small">{{ statusText(detail.status) }}</el-tag>
 				</el-descriptions-item>
-				<el-descriptions-item label="任务">{{ detail.jobName || detail.jobCode || '--' }}</el-descriptions-item>
+				<el-descriptions-item label="同步业务域">{{ detail.businessDomainName || detail.businessDomainCode || '--' }}</el-descriptions-item>
+				<el-descriptions-item label="消费模块">
+					<el-tag v-for="m in splitConsumerModules(detail.consumerModules)" :key="m" size="small" class="consumer-tag">{{ m }}</el-tag>
+					<span v-if="!splitConsumerModules(detail.consumerModules).length">--</span>
+				</el-descriptions-item>
+				<el-descriptions-item label="任务类型">{{ scopeTypeLabel(detail.scopeType) }}</el-descriptions-item>
+				<el-descriptions-item label="显示名称">{{ detail.displayName || detail.jobName || '--' }}</el-descriptions-item>
+				<el-descriptions-item label="任务编码">{{ detail.jobCode || '--' }}</el-descriptions-item>
+				<el-descriptions-item label="任务调度">
+					<template v-if="detail.scheduleJobId">
+						<span>{{ detail.scheduleJobId }}</span>
+						<el-button link type="primary" class="schedule-link" @click="openScheduleJob(detail)">查看调度配置</el-button>
+					</template>
+					<span v-else>--</span>
+				</el-descriptions-item>
 				<el-descriptions-item label="批次号">{{ detail.batchId || '--' }}</el-descriptions-item>
 				<el-descriptions-item label="触发方式">{{ detail.triggerType || '--' }}</el-descriptions-item>
 				<el-descriptions-item label="开始时间">{{ fmtDateTime(detail.startTime) }}</el-descriptions-item>
@@ -143,13 +170,26 @@
 					type="info"
 					show-icon
 					:closable="false"
-					title="当前阶段展示配置化元数据与后端执行说明;标准层、DWD、KPI 仍由模块 Service 代码执行,尚不是纯配置驱动。"
+					title="按同步业务域与消费模块展示链路阶段说明;标准层、DWD、KPI 仍由后端 Service 执行,尚不是纯配置驱动。"
 				/>
 
 				<el-descriptions :column="3" border size="small" class="mb12">
-					<el-descriptions-item label="模块">{{ lineage.moduleCode || query.moduleCode || '--' }}</el-descriptions-item>
-					<el-descriptions-item label="任务">{{ lineage.jobCode || query.jobCode || '--' }}</el-descriptions-item>
+					<el-descriptions-item label="同步业务域">{{ lineage.businessDomainName || lineageBusinessDomainLabel }}</el-descriptions-item>
+					<el-descriptions-item label="消费模块">
+						<el-tag v-for="m in splitConsumerModules(lineage.consumerModules)" :key="m" size="small" class="consumer-tag">{{ m }}</el-tag>
+						<span v-if="!splitConsumerModules(lineage.consumerModules).length">--</span>
+					</el-descriptions-item>
+					<el-descriptions-item label="任务类型">{{ scopeTypeLabel(lineage.scopeType) }}</el-descriptions-item>
+					<el-descriptions-item label="显示名称">{{ lineage.displayName || '--' }}</el-descriptions-item>
+					<el-descriptions-item label="任务编码">{{ lineage.jobCode || query.jobCode || '--' }}</el-descriptions-item>
 					<el-descriptions-item label="批次">{{ lineage.batchId || '未选择具体批次' }}</el-descriptions-item>
+					<el-descriptions-item label="任务调度">
+						<template v-if="lineage.scheduleJobId">
+							<span>{{ lineage.scheduleJobId }}</span>
+							<el-button link type="primary" class="schedule-link" @click="openScheduleJob({ scheduleJobId: lineage.scheduleJobId })">查看调度配置</el-button>
+						</template>
+						<span v-else>--</span>
+					</el-descriptions-item>
 					<el-descriptions-item label="实体数">{{ lineage.entities?.length || 0 }}</el-descriptions-item>
 					<el-descriptions-item label="字段映射数">{{ lineageFieldCount }}</el-descriptions-item>
 				</el-descriptions>
@@ -169,16 +209,31 @@
 				<el-table :data="lineage.entities" border stripe row-key="id" style="width: 100%">
 					<el-table-column type="expand" width="48">
 						<template #default="{ row }">
-							<el-table :data="row.fieldMappings || []" border size="small" class="mapping-table">
-								<el-table-column prop="sourceField" label="源字段" min-width="160" show-overflow-tooltip />
-								<el-table-column prop="targetField" label="目标字段" min-width="160" show-overflow-tooltip />
-								<el-table-column prop="fieldType" label="映射类型" width="110" />
-								<el-table-column prop="defaultValue" label="默认值" min-width="120" show-overflow-tooltip />
-								<el-table-column prop="isRequired" label="必填" width="80" align="center">
-									<template #default="{ row: mapping }">{{ mapping.isRequired ? '是' : '否' }}</template>
-								</el-table-column>
-								<el-table-column prop="transformScript" label="转换说明" min-width="180" show-overflow-tooltip />
-							</el-table>
+							<div class="mapping-expand">
+								<el-alert
+									v-if="hasFallbackMappings(row)"
+									type="info"
+									:closable="false"
+									title="当前实体暂无逐字段配置,以下为贴源表通用字段映射兜底展示。"
+									class="mapping-fallback-alert"
+								/>
+								<el-table :data="row.fieldMappings || []" border size="small" class="mapping-table">
+									<el-table-column label="映射来源" width="100" align="center">
+										<template #default="{ row: mapping }">
+											<el-tag v-if="mapping.isFallback" type="warning" size="small">系统兜底</el-tag>
+											<el-tag v-else type="success" size="small">配置</el-tag>
+										</template>
+									</el-table-column>
+									<el-table-column prop="sourceField" label="源字段" min-width="160" show-overflow-tooltip />
+									<el-table-column prop="targetField" label="目标字段" min-width="160" show-overflow-tooltip />
+									<el-table-column prop="fieldType" label="映射类型" width="110" />
+									<el-table-column prop="defaultValue" label="默认值" min-width="120" show-overflow-tooltip />
+									<el-table-column prop="isRequired" label="必填" width="80" align="center">
+										<template #default="{ row: mapping }">{{ mapping.isRequired ? '是' : '否' }}</template>
+									</el-table-column>
+									<el-table-column prop="transformScript" label="转换说明" min-width="180" show-overflow-tooltip />
+								</el-table>
+							</div>
 						</template>
 					</el-table-column>
 					<el-table-column prop="entityCode" label="实体编码" min-width="170" show-overflow-tooltip />
@@ -242,14 +297,15 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import {
+	fetchMdpMonitorCatalog,
 	fetchMdpMonitorLineage,
 	fetchMdpMonitorLatest,
-	fetchMdpMonitorModules,
 	fetchMdpMonitorRunLogDetail,
 	fetchMdpMonitorRunLogList,
+	type MdpJobCatalogItem,
 	type MdpLineageDetail,
 	type MdpLineageEntity,
-	type MdpMonitorModuleOption,
+	type MdpLineageFieldMapping,
 	type MdpRunLogRow,
 } from './api/mdpMonitor';
 
@@ -257,8 +313,10 @@ const route = useRoute();
 const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || 'MDP运行监控');
 
+const legacyModule = String(route.query.moduleCode || route.query.module || '');
 const query = reactive({
-	moduleCode: String(route.query.moduleCode || route.query.module || 'S1'),
+	businessDomainCode: String(route.query.businessDomainCode || ''),
+	consumerModule: String(route.query.consumerModule || legacyModule || ''),
 	jobCode: String(route.query.jobCode || ''),
 	batchId: '',
 	status: '',
@@ -267,7 +325,7 @@ const query = reactive({
 	pageSize: 10,
 });
 const loading = ref(false);
-const moduleOptions = ref<MdpMonitorModuleOption[]>([]);
+const catalog = ref<MdpJobCatalogItem[]>([]);
 const rows = ref<MdpRunLogRow[]>([]);
 const total = ref(0);
 const latest = ref<MdpRunLogRow>({ id: 0 });
@@ -278,9 +336,52 @@ const lineageLoading = ref(false);
 const lineage = ref<MdpLineageDetail>({ stages: [], entities: [] });
 const lineageFieldCount = computed(() => lineage.value.entities?.reduce((sum, item) => sum + (item.fieldMappingCount || 0), 0) || 0);
 
+function hasFallbackMappings(entity: MdpLineageEntity) {
+	return (entity.fieldMappings || []).some((mapping: MdpLineageFieldMapping) => mapping.isFallback);
+}
+
+const businessDomainOptions = computed(() => {
+	const map = new Map<string, string>();
+	for (const item of catalog.value) {
+		map.set(item.businessDomainCode, item.businessDomainName);
+	}
+	return Array.from(map, ([value, label]) => ({ value, label }));
+});
+
+const consumerModuleOptions = computed(() => {
+	const set = new Set<string>();
+	for (const item of catalog.value) {
+		for (const m of item.consumerModules || []) set.add(m);
+	}
+	return Array.from(set)
+		.sort()
+		.map((m) => ({ value: m, label: m }));
+});
+
+const summaryDomainLabel = computed(() => {
+	if (query.businessDomainCode) {
+		return businessDomainOptions.value.find((x) => x.value === query.businessDomainCode)?.label || query.businessDomainCode;
+	}
+	return latest.value.businessDomainName || '全部业务域';
+});
+
+const lineageBusinessDomainLabel = computed(() => {
+	const jobCode = lineage.value.jobCode || query.jobCode;
+	const item = catalog.value.find((x) => x.jobCode === jobCode);
+	return item?.businessDomainName || '--';
+});
+
+function scopeTypeLabel(scopeType?: string | null) {
+	if (scopeType === 'GLOBAL_DOMAIN') return '全局业务域同步';
+	if (scopeType === 'MODULE_ONLY') return '模块私有同步';
+	if (scopeType === 'TECHNICAL') return '技术维护';
+	return scopeType || '--';
+}
+
 function buildParams() {
 	return {
-		moduleCode: query.moduleCode || undefined,
+		businessDomainCode: query.businessDomainCode || undefined,
+		consumerModule: query.consumerModule || undefined,
 		jobCode: query.jobCode || undefined,
 		batchId: query.batchId || undefined,
 		status: query.status || undefined,
@@ -289,6 +390,35 @@ function buildParams() {
 	};
 }
 
+function splitConsumerModules(value?: string | null) {
+	return String(value || '')
+		.split(',')
+		.map((x) => x.trim())
+		.filter(Boolean);
+}
+
+function jobCodeToModuleCode(jobCode?: string | null) {
+	const match = /^S(\d+)_/i.exec(String(jobCode || ''));
+	return match ? `S${match[1]}` : '';
+}
+
+function resolveLineageParams(row?: MdpRunLogRow) {
+	const jobCode = row?.jobCode || query.jobCode || undefined;
+	let moduleCode = jobCodeToModuleCode(jobCode);
+	if (!moduleCode && query.businessDomainCode) {
+		const item = catalog.value.find((x) => x.businessDomainCode === query.businessDomainCode);
+		moduleCode = jobCodeToModuleCode(item?.jobCode);
+	}
+	if (!moduleCode && catalog.value.length) {
+		moduleCode = jobCodeToModuleCode(catalog.value[0].jobCode);
+	}
+	return {
+		moduleCode: moduleCode || undefined,
+		jobCode,
+		batchId: row?.batchId || undefined,
+	};
+}
+
 function statusText(status?: string | null) {
 	return status === 'SUCCESS' ? '成功' : status === 'FAILED' ? '失败' : status === 'RUNNING' ? '运行中' : '--';
 }
@@ -356,8 +486,8 @@ function targetTooltip(row: MdpLineageEntity) {
 		.join(' / ');
 }
 
-async function loadModules() {
-	moduleOptions.value = await fetchMdpMonitorModules();
+async function loadCatalog() {
+	catalog.value = await fetchMdpMonitorCatalog();
 }
 
 async function loadLatest() {
@@ -389,7 +519,8 @@ function doSearch() {
 }
 
 function resetQuery() {
-	query.moduleCode = 'S1';
+	query.businessDomainCode = '';
+	query.consumerModule = '';
 	query.jobCode = '';
 	query.batchId = '';
 	query.status = '';
@@ -398,14 +529,6 @@ function resetQuery() {
 	loadAll();
 }
 
-function onModuleChange() {
-	if (query.moduleCode) query.jobCode = '';
-}
-
-function onJobCodeChange() {
-	if (query.jobCode) query.moduleCode = '';
-}
-
 async function openDetail(row: MdpRunLogRow) {
 	detail.value = await fetchMdpMonitorRunLogDetail(row.id, buildParams());
 	detailVisible.value = true;
@@ -415,22 +538,36 @@ async function openLineage(row?: MdpRunLogRow) {
 	lineageVisible.value = true;
 	lineageLoading.value = true;
 	try {
-		lineage.value = await fetchMdpMonitorLineage({
-			moduleCode: query.moduleCode || undefined,
-			jobCode: row?.jobCode || query.jobCode || undefined,
-			batchId: row?.batchId || undefined,
-		});
+		lineage.value = await fetchMdpMonitorLineage(resolveLineageParams(row));
 	} finally {
 		lineageLoading.value = false;
 	}
 }
 
 function goJob() {
+	const scheduleJobId = latest.value.scheduleJobId || resolveScheduleJobId(query.jobCode);
+	if (scheduleJobId) {
+		openScheduleJob({ scheduleJobId });
+		return;
+	}
 	router.push('/platform/job');
 }
 
+function resolveScheduleJobId(jobCode?: string | null) {
+	if (!jobCode) return null;
+	return catalog.value.find((item) => item.jobCode === jobCode)?.scheduleJobId || null;
+}
+
+function openScheduleJob(row: Pick<MdpRunLogRow, 'scheduleJobId'>) {
+	if (!row.scheduleJobId) return;
+	router.push({
+		path: '/platform/job',
+		query: { jobId: row.scheduleJobId },
+	});
+}
+
 onMounted(async () => {
-	await loadModules();
+	await loadCatalog();
 	await loadAll();
 });
 </script>
@@ -465,6 +602,9 @@ onMounted(async () => {
 	white-space: pre-wrap;
 	word-break: break-word;
 }
+.schedule-link {
+	margin-left: 8px;
+}
 .lineage-stage-title {
 	font-weight: 600;
 	margin-bottom: 4px;
@@ -477,8 +617,17 @@ onMounted(async () => {
 	font-size: 13px;
 	line-height: 1.7;
 }
+.mapping-expand {
+	padding: 8px 16px 12px;
+}
+.mapping-fallback-alert {
+	margin-bottom: 8px;
+}
 .mapping-table {
-	margin: 8px 16px 12px 16px;
-	width: calc(100% - 32px);
+	width: 100%;
+}
+.consumer-tag {
+	margin-right: 4px;
+	margin-bottom: 2px;
 }
 </style>

+ 7 - 2
Web/src/views/aidop/kanban/components/DynamicModuleDashboard.vue

@@ -436,9 +436,14 @@ async function refreshDashboard() {
 	await reload();
 }
 
-function onQuery() {
-	reload();
+async function onQuery() {
+	await reload();
 	const summary = summarizeSmartOpsBaseQuery(baseQuery.value);
+	const scope = page.value?.dataSource?.scope;
+	if (scope && scope !== 'module_summary') {
+		ElMessage.success(scopeLabel.value || (summary ? `已应用查询条件(${summary})` : '已按查询条件过滤 KPI'));
+		return;
+	}
 	ElMessage.info(summary ? `已应用查询条件(${summary})` : '已刷新看板数据');
 }
 

+ 6 - 1
Web/src/views/system/job/index.vue

@@ -270,7 +270,7 @@
 <script lang="ts" setup name="sysJob">
 import { nextTick, onMounted, reactive, ref } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
-import { useRouter } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 import { Timer } from '@element-plus/icons-vue';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysJobApi } from '/@/api-services/api';
@@ -279,6 +279,7 @@ import EditJobDetail from '/@/views/system/job/component/editJobDetail.vue';
 import EditJobTrigger from '/@/views/system/job/component/editJobTrigger.vue';
 import JobCluster from '/@/views/system/job/component/jobCluster.vue';
 
+const route = useRoute();
 const router = useRouter();
 const editJobDetailRef = ref<InstanceType<typeof EditJobDetail>>();
 const editJobTriggerRef = ref<InstanceType<typeof EditJobTrigger>>();
@@ -311,6 +312,10 @@ const state = reactive({
 });
 
 onMounted(async () => {
+	const jobIdFromRoute = String(route.query.jobId || '').trim();
+	if (jobIdFromRoute) {
+		state.queryParams.jobId = jobIdFromRoute;
+	}
 	await handleQuery();
 	// 获取组名称下拉集合
 	nextTick(async () => {

+ 838 - 0
doc/plan/MDP运行监控按业务域与消费模块改造任务书.md

@@ -0,0 +1,838 @@
+# MDP运行监控按业务域与消费模块改造任务书
+
+> 适用范围:数据中台统一 MDP 运行监控页与后端统一监控接口  
+> 目标读者:后端开发、前端开发、大语言模型执行代理、测试负责人  
+> 编写日期:2026-06-12  
+> 任务类型:数据中台监控体验改造 / 全局同步任务治理
+> 当前状态:主体任务已执行完成  
+> 新增待执行范围:见第 12 节「增补任务:MDP运行监控关联任务调度配置」、第 13 节「增补任务:MDP运行监控详情页口径补齐」(均已执行完成)
+
+---
+
+## 1. 背景
+
+当前「MDP运行监控」已经使用统一入口、统一接口和统一日志表:
+
+- 前端页面:`Web/src/views/aidop/data-platform/mdpMonitor.vue`
+- 前端 API:`Web/src/views/aidop/data-platform/api/mdpMonitor.ts`
+- 后端接口:`server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs`
+- 日志表:`mdp_transform_run_log`
+
+但页面仍以「模块」作为主要筛选和展示维度,默认进入时也是 `S1`:
+
+```ts
+moduleCode: String(route.query.moduleCode || route.query.module || 'S1')
+```
+
+这会造成误解:用户容易认为 `S1_MDP_SYNC_TRANSFORM` 只服务 S1,`S3_MDP_SYNC_TRANSFORM` 只服务 S3。实际设计中,MDP 同步任务很多是全局业务对象域任务,产出的数据会被多个模块消费。
+
+例如:
+
+- 订单交付域同步产出销售订单、订单明细、客户、交付需求等事实,消费方可能包括 S1、S2、S3、S4、S7、S9。
+- 工单排程域同步产出工单、工序、排程结果等事实,消费方可能包括 S2、S3、S5、S6、S8、S9。
+- 供应采购域同步产出供应商、物料需求、采购订单、交货计划等事实,消费方可能包括 S3、S4、S5、S8、S9。
+
+因此,本次改造要求把 MDP 运行监控从「模块任务视角」调整为「同步业务域 + 消费模块视角」。
+
+---
+
+## 2. 改造目标
+
+### 2.1 必须达成
+
+1. 页面默认展示全部 MDP 同步任务,不再默认只看 S1。
+2. 主筛选维度从「模块」调整为:
+   - 同步业务域
+   - 消费模块
+3. 表格主展示列新增:
+   - 同步业务域
+   - 消费模块
+4. `job_code` 保留,但降级为技术追踪字段,不作为用户理解任务范围的主维度。
+5. 后端统一接口返回任务元数据,包括业务域、消费模块、范围类型等。
+6. 不改现有 MDP 同步任务执行逻辑。
+7. 不改 `mdp_transform_run_log` 表结构。
+
+### 2.2 建议达成
+
+1. 顶部摘要从单一最近批次改为更适合全局视角的统计:
+   - 最近运行状态
+   - 最近批次
+   - 当前筛选下成功 / 失败 / 运行中数量
+   - 当前筛选下涉及的业务域数量
+2. 「链路详情」优先按业务域打开,也允许通过具体行的 `job_code` / `batch_id` 精准追踪。
+3. 后端元数据映射先用代码常量实现,后续可沉淀为配置表。
+
+---
+
+## 3. 核心口径
+
+### 3.1 同步业务域
+
+同步业务域表示这条同步任务沉淀的业务对象事实。
+
+建议枚举如下:
+
+| 业务域编码 | 业务域名称 | 说明 |
+|------------|------------|------|
+| `order_delivery` | 订单交付域 | 销售订单、订单明细、客户、交付需求、订单评审等事实 |
+| `work_schedule` | 工单排程域 | 工单、工序、排程、产线、设备、执行进度等事实 |
+| `supply_purchase` | 供应采购域 | 供应商、物料需求、采购申请、采购订单、交货计划等事实 |
+| `purchase_execution` | 采购执行域 | 送货、收货、IQC、退货、欠料执行等事实 |
+| `inventory` | 库存域 | 库存、库位、出入库、齐套、仓储执行等事实 |
+
+### 3.2 消费模块
+
+消费模块表示哪些业务模块会使用该同步任务产出的数据。
+
+示例:
+
+| job_code | 同步业务域 | 消费模块 |
+|----------|------------|----------|
+| `S1_MDP_SYNC_TRANSFORM` | 订单交付域 | `S1,S2,S3,S4,S7,S9` |
+| `S2_MDP_SYNC_TRANSFORM` | 工单排程域 | `S2,S3,S5,S6,S8,S9` |
+| `S3_MDP_SYNC_TRANSFORM` | 供应采购域 | `S3,S4,S5,S8,S9` |
+| `S4_MDP_SYNC_TRANSFORM` | 采购执行域 | `S4,S5,S8,S9` |
+
+注意:本次不引入「归属模块」作为主维度。用户关心的是同步了什么业务数据,以及哪些模块依赖它。
+
+### 3.3 任务范围类型
+
+建议后端返回 `scopeType`:
+
+| scopeType | 含义 |
+|-----------|------|
+| `GLOBAL_DOMAIN` | 全局业务对象域同步任务 |
+| `MODULE_ONLY` | 模块私有同步任务 |
+| `TECHNICAL` | 技术维护类任务 |
+
+当前 S1-S4 MDP 同步任务均可先标记为 `GLOBAL_DOMAIN`。
+
+---
+
+## 4. 后端改造任务
+
+### 4.1 修改文件
+
+主要文件:
+
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs`
+
+可能涉及:
+
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Order/S1MdpMonitorService.cs`
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Supply/S3MdpMonitorService.cs`
+
+如果专用监控接口仍被菜单或旧页面使用,应同步兼容返回字段,但本次主入口以 `/api/DataPlatform/mdp-monitor/*` 为准。
+
+### 4.2 新增任务元数据模型
+
+在 `MdpMonitorService.cs` 内部新增静态元数据映射,先不要新增数据库表。
+
+建议结构:
+
+```csharp
+private sealed record MdpJobCatalogItem(
+    string JobCode,
+    string BusinessDomainCode,
+    string BusinessDomainName,
+    string[] ConsumerModules,
+    string ScopeType,
+    string DisplayName);
+```
+
+建议映射:
+
+```csharp
+private static readonly Dictionary<string, MdpJobCatalogItem> JobCatalog = new(StringComparer.OrdinalIgnoreCase)
+{
+    ["S1_MDP_SYNC_TRANSFORM"] = new(
+        "S1_MDP_SYNC_TRANSFORM",
+        "order_delivery",
+        "订单交付域",
+        ["S1", "S2", "S3", "S4", "S7", "S9"],
+        "GLOBAL_DOMAIN",
+        "订单交付域 MDP 同步"),
+
+    ["S2_MDP_SYNC_TRANSFORM"] = new(
+        "S2_MDP_SYNC_TRANSFORM",
+        "work_schedule",
+        "工单排程域",
+        ["S2", "S3", "S5", "S6", "S8", "S9"],
+        "GLOBAL_DOMAIN",
+        "工单排程域 MDP 同步"),
+
+    ["S3_MDP_SYNC_TRANSFORM"] = new(
+        "S3_MDP_SYNC_TRANSFORM",
+        "supply_purchase",
+        "供应采购域",
+        ["S3", "S4", "S5", "S8", "S9"],
+        "GLOBAL_DOMAIN",
+        "供应采购域 MDP 同步"),
+
+    ["S4_MDP_SYNC_TRANSFORM"] = new(
+        "S4_MDP_SYNC_TRANSFORM",
+        "purchase_execution",
+        "采购执行域",
+        ["S4", "S5", "S8", "S9"],
+        "GLOBAL_DOMAIN",
+        "采购执行域 MDP 同步")
+};
+```
+
+### 4.3 替换模块选项接口
+
+现有:
+
+```http
+GET /api/DataPlatform/mdp-monitor/modules
+```
+
+建议保留兼容,但新增或扩展为:
+
+```http
+GET /api/DataPlatform/mdp-monitor/catalog
+```
+
+返回:
+
+```json
+[
+  {
+    "jobCode": "S1_MDP_SYNC_TRANSFORM",
+    "displayName": "订单交付域 MDP 同步",
+    "businessDomainCode": "order_delivery",
+    "businessDomainName": "订单交付域",
+    "consumerModules": ["S1", "S2", "S3", "S4", "S7", "S9"],
+    "scopeType": "GLOBAL_DOMAIN"
+  }
+]
+```
+
+如果为了减少前端改动,也可以让 `modules` 接口返回同样字段,但接口命名上不再强调模块。
+
+### 4.4 扩展查询入参
+
+修改 `MdpMonitorQueryInput`:
+
+```csharp
+public class MdpMonitorQueryInput
+{
+    public string? BusinessDomainCode { get; set; }
+    public string? ConsumerModule { get; set; }
+
+    // 保留兼容字段
+    public string? ModuleCode { get; set; }
+    public string? JobCode { get; set; }
+    public string? BatchId { get; set; }
+    public string? Status { get; set; }
+    public DateTime? StartTime { get; set; }
+    public DateTime? EndTime { get; set; }
+}
+```
+
+兼容规则:
+
+1. `jobCode` 有值时,优先精准匹配 `job_code`。
+2. `businessDomainCode` 有值时,根据 `JobCatalog` 反查对应 `job_code` 列表,并生成 `job_code IN (...)`。
+3. `consumerModule` 有值时,根据 `JobCatalog.ConsumerModules` 反查对应 `job_code` 列表,并生成 `job_code IN (...)`。
+4. `moduleCode` 仅作为旧参数兼容,可映射到 `consumerModule`,但前端新代码不再主用它。
+
+### 4.5 扩展返回模型
+
+修改 `MdpMonitorRunLogRow`,新增字段:
+
+```csharp
+public string? BusinessDomainCode { get; set; }
+public string? BusinessDomainName { get; set; }
+public string? ConsumerModules { get; set; }
+public string? ScopeType { get; set; }
+public string? DisplayName { get; set; }
+```
+
+查询 `mdp_transform_run_log` 后,在 C# 中按 `jobCode` 补齐这些字段,不要求 SQL 表内有对应列。
+
+建议新增方法:
+
+```csharp
+private static void AttachCatalogInfo(MdpMonitorRunLogRow row)
+{
+    if (row == null || string.IsNullOrWhiteSpace(row.JobCode)) return;
+    if (!JobCatalog.TryGetValue(row.JobCode, out var item)) return;
+
+    row.BusinessDomainCode = item.BusinessDomainCode;
+    row.BusinessDomainName = item.BusinessDomainName;
+    row.ConsumerModules = string.Join(",", item.ConsumerModules);
+    row.ScopeType = item.ScopeType;
+    row.DisplayName = item.DisplayName;
+}
+```
+
+列表查询要对每一行执行补齐。
+
+### 4.6 查询条件构造
+
+当前 `BuildWhere` 主要按 `moduleCode/jobCode` 找任务。改造后建议:
+
+1. 先根据 `jobCode`、`businessDomainCode`、`consumerModule` 得到允许的 `job_code` 集合。
+2. 如果集合为空且用户指定了业务域或消费模块,则返回 `1=0`。
+3. 如果集合非空,则追加:
+
+```sql
+job_code IN (@JobCode0, @JobCode1, ...)
+```
+
+4. 若用户没有传任何业务域、消费模块、jobCode,则默认查询全部 MDP:
+
+```sql
+IFNULL(job_code, '') LIKE '%MDP%'
+```
+
+5. 继续保留租户兼容逻辑:
+
+```sql
+(tenant_id = @TenantId OR tenant_id = 0)
+```
+
+---
+
+## 5. 前端改造任务
+
+### 5.1 修改文件
+
+主要文件:
+
+- `Web/src/views/aidop/data-platform/mdpMonitor.vue`
+- `Web/src/views/aidop/data-platform/api/mdpMonitor.ts`
+
+### 5.2 API 类型调整
+
+在 `mdpMonitor.ts` 中新增或扩展类型:
+
+```ts
+export interface MdpJobCatalogItem {
+    jobCode: string;
+    displayName: string;
+    businessDomainCode: string;
+    businessDomainName: string;
+    consumerModules: string[];
+    scopeType: string;
+}
+```
+
+扩展 `MdpRunLogRow`:
+
+```ts
+businessDomainCode?: string | null;
+businessDomainName?: string | null;
+consumerModules?: string | null;
+scopeType?: string | null;
+displayName?: string | null;
+```
+
+新增 API:
+
+```ts
+export function fetchMdpMonitorCatalog() {
+    return service.get<MdpJobCatalogItem[]>('/api/DataPlatform/mdp-monitor/catalog').then((r) => r.data);
+}
+```
+
+如果后端复用 `modules` 接口,前端函数名仍建议改为 `fetchMdpMonitorCatalog`,内部可以暂时请求旧 URL。
+
+### 5.3 查询状态调整
+
+当前:
+
+```ts
+const query = reactive({
+    moduleCode: String(route.query.moduleCode || route.query.module || 'S1'),
+    jobCode: String(route.query.jobCode || ''),
+    ...
+});
+```
+
+改为:
+
+```ts
+const query = reactive({
+    businessDomainCode: String(route.query.businessDomainCode || ''),
+    consumerModule: String(route.query.consumerModule || ''),
+    jobCode: String(route.query.jobCode || ''),
+    batchId: '',
+    status: '',
+    timeRange: [] as string[],
+    page: 1,
+    pageSize: 10,
+});
+```
+
+默认不带业务域与消费模块,即展示全部 MDP。
+
+### 5.4 筛选区调整
+
+把当前「模块」下拉替换为两个下拉:
+
+1. 同步业务域:
+   - 全部
+   - 订单交付域
+   - 工单排程域
+   - 供应采购域
+   - 采购执行域
+   - 库存域
+2. 消费模块:
+   - 全部
+   - S1
+   - S2
+   - S3
+   - S4
+   - S5
+   - S6
+   - S7
+   - S8
+   - S9
+
+选项应从 catalog 计算,避免写死:
+
+```ts
+const businessDomainOptions = computed(() => {
+    const map = new Map<string, string>();
+    for (const item of catalog.value) {
+        map.set(item.businessDomainCode, item.businessDomainName);
+    }
+    return Array.from(map, ([value, label]) => ({ value, label }));
+});
+
+const consumerModuleOptions = computed(() => {
+    const set = new Set<string>();
+    for (const item of catalog.value) {
+        for (const m of item.consumerModules || []) set.add(m);
+    }
+    return Array.from(set).sort().map((m) => ({ value: m, label: m }));
+});
+```
+
+### 5.5 参数构造调整
+
+当前 `buildParams()` 返回 `moduleCode/jobCode`。改为:
+
+```ts
+function buildParams() {
+    return {
+        businessDomainCode: query.businessDomainCode || undefined,
+        consumerModule: query.consumerModule || undefined,
+        jobCode: query.jobCode || undefined,
+        batchId: query.batchId || undefined,
+        status: query.status || undefined,
+        startTime: query.timeRange?.[0] || undefined,
+        endTime: query.timeRange?.[1] || undefined,
+    };
+}
+```
+
+### 5.6 重置逻辑调整
+
+当前:
+
+```ts
+function resetQuery() {
+    query.moduleCode = 'S1';
+    ...
+}
+```
+
+改为:
+
+```ts
+function resetQuery() {
+    query.businessDomainCode = '';
+    query.consumerModule = '';
+    query.jobCode = '';
+    query.batchId = '';
+    query.status = '';
+    query.timeRange = [];
+    query.page = 1;
+    loadAll();
+}
+```
+
+### 5.7 表格列调整
+
+表格前置新增:
+
+```vue
+<el-table-column prop="businessDomainName" label="同步业务域" min-width="130" fixed="left" show-overflow-tooltip />
+<el-table-column label="消费模块" min-width="180" show-overflow-tooltip>
+  <template #default="{ row }">
+    <el-tag
+      v-for="m in splitConsumerModules(row.consumerModules)"
+      :key="m"
+      size="small"
+      class="consumer-tag"
+    >
+      {{ m }}
+    </el-tag>
+  </template>
+</el-table-column>
+```
+
+`任务编码`列保留,但不再放在最前面。
+
+新增方法:
+
+```ts
+function splitConsumerModules(value?: string | null) {
+    return String(value || '')
+        .split(',')
+        .map((x) => x.trim())
+        .filter(Boolean);
+}
+```
+
+### 5.8 顶部摘要调整
+
+当前顶部展示最近状态、最近批次、任务、行数。
+
+建议调整为:
+
+1. 最近状态
+2. 最近批次
+3. 同步业务域
+4. 行数 Stage / Std / DWD
+
+如果当前筛选是全部 MDP,第三个卡片可以显示:
+
+```text
+全部业务域
+```
+
+如果筛选了业务域,则显示该业务域名称。
+
+---
+
+## 6. 验收用例
+
+### 6.1 默认全局视图
+
+访问:
+
+```text
+/aidop/data-platform/mdp-monitor
+```
+
+期望:
+
+- 同步业务域筛选为空。
+- 消费模块筛选为空。
+- 列表展示 S1/S2/S3/S4 等所有 MDP 运行记录。
+- 表格能看到「同步业务域」和「消费模块」列。
+- 不再默认只展示 S1。
+
+### 6.2 按同步业务域筛选
+
+选择:
+
+```text
+同步业务域 = 订单交付域
+```
+
+期望:
+
+- 列表只展示订单交付域相关任务。
+- 当前阶段通常对应 `S1_MDP_SYNC_TRANSFORM`。
+- 表格消费模块显示 `S1,S2,S3,S4,S7,S9`。
+
+### 6.3 按消费模块筛选
+
+选择:
+
+```text
+消费模块 = S9
+```
+
+期望:
+
+- 列表展示所有 S9 依赖的 MDP 同步任务。
+- 至少包括订单交付域、工单排程域、供应采购域等已登记任务。
+- 用户可以理解 S9 总览依赖多个全局业务对象域。
+
+### 6.4 按 JobCode 精准追踪
+
+输入:
+
+```text
+JobCode = S1_MDP_SYNC_TRANSFORM
+```
+
+期望:
+
+- 只展示该任务运行记录。
+- 同步业务域和消费模块仍正常显示。
+- 链路详情能正常打开。
+
+### 6.5 重置
+
+点击重置。
+
+期望:
+
+- 同步业务域为空。
+- 消费模块为空。
+- JobCode、批次号、状态、时间范围均清空。
+- 列表回到全部 MDP。
+
+---
+
+## 7. 非目标范围
+
+本次不做以下事项:
+
+1. 不新增 `mdp_job_catalog` 数据库表。
+2. 不改 `mdp_transform_run_log` 表结构。
+3. 不改 S1/S2/S3/S4 MDP 同步任务实际执行逻辑。
+4. 不改调度配置。
+5. 不迁移旧批次号命名。
+6. 不删除 `job_code`,它仍作为技术追踪字段保留。
+7. 不改 KPI 原子聚合层逻辑。
+
+---
+
+## 8. 风险与注意事项
+
+1. `moduleCode` 参数需要保留兼容,否则旧链接 `/aidop/data-platform/mdp-monitor?moduleCode=S1` 可能失效。
+2. 如果同一个 `job_code` 未来对应多个业务域,应优先拆分任务或升级为配置表,不建议在代码映射中塞复杂规则。
+3. 消费模块是管理视角,不代表每个批次都实际写入该模块 KPI;实际写入仍以运行日志行数和链路详情为准。
+4. 由于当前日志存在 `tenant_id=0` 的全局任务记录,查询仍需兼容当前登录租户与 `tenant_id=0`。
+5. 前端默认展示全部后,列表数量会变多,分页默认 10 条即可,不建议一次性拉全量。
+
+---
+
+## 9. 推荐实施顺序
+
+1. 后端新增 `JobCatalog` 映射。
+2. 后端扩展 `/api/DataPlatform/mdp-monitor/catalog`。
+3. 后端扩展查询入参和筛选逻辑。
+4. 后端补齐列表和详情返回的业务域、消费模块字段。
+5. 前端 API 类型调整。
+6. 前端筛选区替换为同步业务域和消费模块。
+7. 前端表格新增同步业务域和消费模块列。
+8. 前端默认视图从 S1 改为全部 MDP。
+9. 执行后端构建和前端构建。
+10. 按验收用例逐项验证。
+
+---
+
+## 10. 交付物清单
+
+完成后应至少包含以下改动:
+
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs`
+  - 新增任务 catalog。
+  - 新增或扩展 catalog 接口。
+  - 支持 `businessDomainCode` 和 `consumerModule` 查询。
+  - 返回业务域和消费模块字段。
+
+- `Web/src/views/aidop/data-platform/api/mdpMonitor.ts`
+  - 新增 catalog 类型与接口。
+  - 扩展运行日志类型。
+
+- `Web/src/views/aidop/data-platform/mdpMonitor.vue`
+  - 删除默认 S1。
+  - 筛选区改为同步业务域 + 消费模块。
+  - 表格新增同步业务域 + 消费模块。
+  - 重置回到全部 MDP。
+
+---
+
+## 11. 给执行代理的提示词
+
+请基于本任务书改造 Ai-DOP 的「MDP运行监控」。
+
+目标是把页面从「模块任务视角」调整为「同步业务域 + 消费模块视角」:
+
+1. 默认展示全部 MDP,不再默认 S1。
+2. 后端 `MdpMonitorService` 增加 job catalog 映射,返回 `businessDomainCode/businessDomainName/consumerModules/scopeType/displayName`。
+3. 查询支持 `businessDomainCode` 和 `consumerModule`。
+4. 前端 `mdpMonitor.vue` 用「同步业务域」和「消费模块」替换「模块」筛选。
+5. 表格新增「同步业务域」和「消费模块」列。
+6. 保留 `jobCode` 作为技术追踪字段。
+7. 不改 `mdp_transform_run_log` 表结构,不改实际同步任务执行逻辑。
+
+完成后运行:
+
+```powershell
+Set-Location "d:\Projects\Ai-DOP\SourceCode\ZZYDOP\server\Admin.NET.Web.Entry"
+dotnet build
+```
+
+如前端依赖环境可用,再运行:
+
+```powershell
+Set-Location "d:\Projects\Ai-DOP\SourceCode\ZZYDOP\Web"
+pnpm build
+```
+
+验收重点:
+
+- `/aidop/data-platform/mdp-monitor` 默认显示全部 MDP。
+- 按订单交付域筛选能看到对应任务。
+- 按消费模块 S9 筛选能看到 S9 依赖的多个业务域同步任务。
+- 旧 `moduleCode=S1` 链接仍兼容。
+
+---
+
+## 12. 增补任务:MDP运行监控关联任务调度配置
+
+> 增补日期:2026-06-12  
+> 增补原因:原任务书主体已执行完成,新增从 MDP 运行监控页便捷查看对应任务调度配置的需求  
+> 执行状态:待执行  
+> 影响范围:MDP运行监控页、任务 catalog 返回字段、任务调度页路由参数兼容  
+> 识别说明:本节为新增待执行范围,前文第 1-11 节按已执行基线理解,除非实现本节确有必要,不要求重做主体任务
+
+### 12.1 新增需求
+
+在「MDP运行监控」页面增加「查看调度配置」或「关联任务调度」入口,让用户可以从运行记录直接跳转到对应任务调度配置页。
+
+适用位置:
+
+1. MDP 运行监控列表行操作区。
+2. 同步链路详情弹窗顶部。
+3. 如果后续有业务域汇总卡片,也可在卡片中提供入口。
+
+最低要求:
+
+```text
+点击 S4_MDP_SYNC_TRANSFORM 的「查看调度配置」
+跳转到 /platform/job?jobId=job_s4_mdp_sync_transform
+```
+
+### 12.2 后端新增字段建议
+
+在 MDP job catalog 中补充任务调度关联字段。
+
+建议扩展 catalog item:
+
+```csharp
+private sealed record MdpJobCatalogItem(
+    string JobCode,
+    string BusinessDomainCode,
+    string BusinessDomainName,
+    string[] ConsumerModules,
+    string ScopeType,
+    string DisplayName,
+    string? ScheduleJobId);
+```
+
+示例映射:
+
+| MDP运行日志 `job_code` | 任务调度 `scheduleJobId` |
+|------------------------|--------------------------|
+| `S1_MDP_SYNC_TRANSFORM` | `job_s1_mdp_sync_transform` |
+| `S2_MDP_SYNC_TRANSFORM` | `job_s2_mdp_sync_transform` |
+| `S3_MDP_SYNC_TRANSFORM` | `job_s3_mdp_sync_transform` |
+| `S4_MDP_SYNC_TRANSFORM` | `job_s4_mdp_sync_transform` |
+
+列表、详情、latest 接口补齐 catalog 信息时,应同步返回:
+
+```csharp
+public string? ScheduleJobId { get; set; }
+```
+
+前端不应临时把 `S4_MDP_SYNC_TRANSFORM` 拼成 `job_s4_mdp_sync_transform`。优先使用后端返回字段,避免未来命名不一致。
+
+### 12.3 前端新增交互
+
+在 `Web/src/views/aidop/data-platform/mdpMonitor.vue` 增加跳转方法:
+
+```ts
+function openScheduleJob(row: MdpRunLogRow) {
+  if (!row.scheduleJobId) return;
+  router.push({
+    path: '/platform/job',
+    query: { jobId: row.scheduleJobId },
+  });
+}
+```
+
+列表行操作区新增按钮:
+
+```vue
+<el-button
+  v-if="row.scheduleJobId"
+  type="primary"
+  link
+  @click="openScheduleJob(row)"
+>
+  查看调度配置
+</el-button>
+```
+
+详情弹窗顶部也建议显示:
+
+```text
+任务调度:job_s4_mdp_sync_transform  查看调度配置
+```
+
+### 12.4 任务调度页配合要求
+
+任务调度页需要支持以下路由参数:
+
+```text
+/platform/job?jobId=job_s4_mdp_sync_transform
+```
+
+进入页面后自动:
+
+1. 将 `jobId` 写入作业编号查询框。
+2. 自动执行查询。
+3. 展示对应调度任务。
+
+该配合要求已同步写入:
+
+```text
+doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md
+```
+
+第 14 节「增补任务:MDP运行监控反向关联任务调度」。
+
+### 12.5 验收用例
+
+访问:
+
+```text
+/aidop/data-platform/mdp-monitor
+```
+
+选择或打开包含 `S4_MDP_SYNC_TRANSFORM` 的运行记录。
+
+期望:
+
+- 列表或详情中能看到「查看调度配置」入口。
+- 点击后跳转到 `/platform/job?jobId=job_s4_mdp_sync_transform`。
+- 任务调度页自动查出对应作业。
+- 用户无需复制 `job_code` 或手工换算 `jobId`。
+
+### 12.6 非目标范围
+
+本增补任务不做以下事项:
+
+1. 不改 MDP 同步任务执行逻辑。
+2. 不改 `mdp_transform_run_log` 表结构。
+3. 不改任务调度 `JobId`、Cron、触发器、执行记录。
+4. 不新增数据库表。
+5. 不要求重新执行第 1-11 节主体任务。
+
+---
+
+## 13. 增补任务:MDP运行监控详情页口径补齐
+
+> 增补日期:2026-06-12  
+> 执行状态:已完成  
+> 影响范围:MDP运行监控详情弹窗、链路详情弹窗、lineage 接口返回字段与阶段说明文案
+
+### 13.1 已完成内容
+
+1. 运行详情弹窗展示:同步业务域、消费模块(标签)、任务类型、显示名称、任务编码、任务调度。
+2. 链路详情弹窗顶部展示同上口径,并保留实体数、字段映射数。
+3. lineage 接口返回 `businessDomainCode/businessDomainName/consumerModules/scopeType/displayName/scheduleJobId`。
+4. 阶段说明文案从「S4 贴源同步」等模块视角,调整为「采购执行域 · 贴源同步」等业务域口径,并注明消费模块。
+5. 链路弹窗合并「查看调度配置」入口。

+ 260 - 0
doc/plan/MDP链路字段映射兜底展示任务书.md

@@ -0,0 +1,260 @@
+# MDP链路字段映射兜底展示任务书
+
+> 适用范围:MDP运行监控 / 同步链路详情 / 实体字段映射展开区  
+> 目标读者:后端开发、前端开发、大语言模型执行代理、测试负责人  
+> 编写日期:2026-06-12  
+> 当前状态:已执行完成  
+> 任务类型:监控展示兜底 / 字段映射可视化补齐
+
+---
+
+## 1. 背景
+
+MDP运行监控的「同步链路详情」页面已经恢复显示实体级链路信息,包括:
+
+- 来源数据源
+- 来源数据库
+- 来源表
+- 目标数据库
+- 目标表
+
+但进一步展开实体行后,字段映射区域仍为空。
+
+当前接口验证结果示例:
+
+```json
+{
+  "entity": "S4_IQC_RECEIPT",
+  "fieldMappingCount": 0,
+  "fieldMappings": [],
+  "sourceTable": "PurOrdRctDetail",
+  "targetTable": "mdp_stg_s4_iqc"
+}
+```
+
+原因是 S4 这类 MDP 贴源任务当前没有在 `mdp_field_mapping` 中配置逐字段映射。实际执行逻辑是把源表行写入贴源表,并通过 `raw_data` 保留原始 JSON,再由后端 Service 代码执行标准层、DWD 和 KPI 转换。
+
+因此,链路详情需要提供兜底展示,避免用户误以为链路配置异常或字段映射缺失。
+
+---
+
+## 2. 改造目标
+
+### 2.1 必须达成
+
+1. 当 `mdp_field_mapping` 无配置时,链路详情展开区域不能显示为空。
+2. 需要展示一组通用贴源字段映射,说明当前阶段采用“源表整行进入 `raw_data`”的方式。
+3. 兜底展示只用于监控可视化,不写入数据库,不改变 MDP 同步执行逻辑。
+4. 有真实 `mdp_field_mapping` 配置时,优先展示真实配置,不使用兜底覆盖。
+5. 前端展开区域要能明确区分“配置映射”和“系统兜底映射”。
+
+### 2.2 建议达成
+
+1. 在展开表格中增加“映射来源”列:
+   - `配置`
+   - `系统兜底`
+2. 当使用兜底映射时,在实体行或展开区显示提示:
+
+```text
+当前实体暂无逐字段配置,以下为贴源表通用字段映射兜底展示。
+```
+
+3. 兜底字段建议按贴源表通用结构排列。
+
+---
+
+## 3. 推荐兜底字段
+
+当某个实体没有 `mdp_field_mapping` 配置时,建议返回以下字段映射:
+
+| sourceField | targetField | fieldType | transformScript | 说明 |
+|-------------|-------------|-----------|-----------------|------|
+| `tenant_id` | `tenant_id` | `DIRECT` | 当前租户或全局租户兜底 | 租户字段 |
+| `source_system` | `source_system` | `CONST` | 来源数据源编码 | 来源系统 |
+| `source_table` | `source_table` | `CONST` | 源表名 | 来源表 |
+| `<主键字段>` | `source_row_id` | `DIRECT` | 来自 `mdp_entity.incr_column` 或实体主键配置 | 源行唯一标识 |
+| `<业务键表达式>` | `source_biz_key` | `EXPR` | 来自实体业务键表达式 | 业务键 |
+| `*` | `raw_data` | `JSON` | 源行整行 JSON | 原始数据保留 |
+| `sync_batch_id` | `sync_batch_id` | `CONST` | 当前同步批次 | 批次号 |
+| `NOW()` | `sync_time` | `CONST` | 同步时间 | 写入时间 |
+
+如果后端当前拿不到主键字段或业务键表达式,允许使用:
+
+```text
+sourceField = "--"
+transformScript = "当前实体未配置逐字段映射,贴源同步保留源行 raw_data"
+```
+
+---
+
+## 4. 后端改造任务
+
+### 4.1 修改文件
+
+主要文件:
+
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs`
+
+### 4.2 扩展返回模型
+
+建议扩展 `MdpLineageFieldMappingRow`:
+
+```csharp
+public string? MappingSource { get; set; }
+public bool IsFallback { get; set; }
+```
+
+说明:
+
+- 真实配置:`MappingSource = "CONFIG"`,`IsFallback = false`
+- 兜底映射:`MappingSource = "FALLBACK"`,`IsFallback = true`
+
+### 4.3 兜底生成逻辑
+
+在组装实体映射时:
+
+```csharp
+if (mappingsByEntity.TryGetValue(entity.Id, out var entityMappings) && entityMappings.Count > 0)
+{
+    entity.FieldMappings = entityMappings;
+    entity.FieldMappingCount = entityMappings.Count;
+}
+else
+{
+    entity.FieldMappings = BuildFallbackFieldMappings(entity);
+    entity.FieldMappingCount = entity.FieldMappings.Count;
+}
+```
+
+建议新增:
+
+```csharp
+private static List<MdpLineageFieldMappingRow> BuildFallbackFieldMappings(MdpLineageEntityRow entity)
+```
+
+兜底映射应只返回给接口,不写入 `mdp_field_mapping`。
+
+### 4.4 真实配置优先
+
+如果 `mdp_field_mapping` 有配置,则必须展示真实配置。
+
+不要因为兜底逻辑导致:
+
+1. 真实配置被覆盖。
+2. `fieldMappingCount` 与真实配置数量不一致。
+3. 前端无法区分配置来源。
+
+---
+
+## 5. 前端改造任务
+
+### 5.1 修改文件
+
+主要文件:
+
+- `Web/src/views/aidop/data-platform/api/mdpMonitor.ts`
+- `Web/src/views/aidop/data-platform/mdpMonitor.vue`
+
+### 5.2 类型扩展
+
+扩展 `MdpLineageFieldMapping`:
+
+```ts
+mappingSource?: string | null;
+isFallback?: boolean;
+```
+
+### 5.3 展开表格展示
+
+展开表格建议新增:
+
+1. 映射来源列
+2. 兜底提示
+
+示例:
+
+```vue
+<el-alert
+  v-if="hasFallbackMappings(row)"
+  type="info"
+  :closable="false"
+  title="当前实体暂无逐字段配置,以下为贴源表通用字段映射兜底展示。"
+/>
+```
+
+映射来源显示:
+
+```vue
+<el-tag v-if="mapping.isFallback" type="warning" size="small">系统兜底</el-tag>
+<el-tag v-else type="success" size="small">配置</el-tag>
+```
+
+---
+
+## 6. 验收用例
+
+### 6.1 S4 贴源实体展开
+
+打开:
+
+```text
+MDP运行监控 -> S4_MDP_SYNC_TRANSFORM -> 链路详情 -> 展开 S4_IQC_RECEIPT
+```
+
+期望:
+
+- 实体行仍显示源库、源表、目标库、目标表。
+- 展开后不再是空表。
+- 能看到 `raw_data`、`source_table`、`source_row_id`、`sync_batch_id` 等通用贴源字段。
+- 能看到“系统兜底”标识或提示。
+
+### 6.2 真实字段映射优先
+
+找一个已有 `mdp_field_mapping` 配置的实体。
+
+期望:
+
+- 展开后显示真实配置。
+- 不显示系统兜底提示。
+- 映射来源显示为“配置”。
+
+### 6.3 不改变执行逻辑
+
+验证:
+
+- 不新增或修改 `mdp_field_mapping` 数据。
+- 不改 MDP 同步任务执行代码。
+- 不改 `mdp_transform_run_log`、`mdp_sync_log` 表结构。
+
+---
+
+## 7. 非目标范围
+
+本次不做以下事项:
+
+1. 不批量补写 `mdp_field_mapping` 数据。
+2. 不重构 MDP 同步执行链路。
+3. 不把标准层、DWD、KPI 转换改造成纯配置驱动。
+4. 不改数据库表结构。
+5. 不改字段映射维护页面。
+
+---
+
+## 8. 给执行代理的提示词
+
+请基于本任务书实现 MDP 运行监控链路详情的字段映射兜底展示。
+
+执行范围只包括:
+
+1. `MdpMonitorService.cs` 在实体没有 `mdp_field_mapping` 时返回通用贴源字段映射。
+2. `mdpMonitor.ts` 扩展字段映射类型。
+3. `mdpMonitor.vue` 在展开表格中展示映射来源,并对兜底映射给出提示。
+4. 不写数据库,不改同步执行逻辑,不改表结构。
+
+完成后验证:
+
+```text
+S4_MDP_SYNC_TRANSFORM -> 链路详情 -> 展开 S4_IQC_RECEIPT
+```
+
+展开区域应不再为空,并能看到贴源通用字段映射。

+ 708 - 0
doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md

@@ -0,0 +1,708 @@
+# 任务调度按业务域与消费模块业务标签化改造任务书
+
+> 适用范围:平台管理 / 任务调度页、Ai-DOP 数据中台调度任务展示与监控跳转  
+> 目标读者:后端开发、前端开发、大语言模型执行代理、测试负责人  
+> 编写日期:2026-06-12  
+> 任务类型:二阶段优化 / 调度运维体验改造 / 业务标签化治理
+> 当前状态:主体任务已执行完成  
+> 新增待执行范围:见第 14 节「增补任务:MDP运行监控反向关联任务调度」
+
+---
+
+## 1. 背景
+
+当前 Ai-DOP 的数据中台入口已经把调度能力复用到平台任务调度:
+
+- 数据中台入口:`Web/src/views/aidop/data-platform/overview.vue`
+- 平台任务调度页:`Web/src/views/system/job/index.vue`
+- 任务调度接口:`/api/sysJob/pageJobDetail`
+- 后端任务服务:`server/Admin.NET.Core/Service/Job/SysJobService.cs`
+- 作业实体:`server/Admin.NET.Core/Entity/SysJobDetail.cs`
+
+任务调度页现在主要以技术作业视角展示:
+
+- 作业编号:如 `job_s1_mdp_sync_transform`、`job_s4_mdp_sync_transform`
+- 组名称:通常为 `default`
+- 类型:作业类 FullName
+- 描述:如 `S4 MDP同步与标准化转换...`
+- 额外数据:`SysJobDetail.Properties`
+
+这对开发排查和平台维护是必要的,但对业务运维不够直观。随着 MDP 运行监控已经调整为「同步业务域 + 消费模块」口径,任务调度页如果继续只用 S1/S2/S3/S4 归属模块或技术任务名表达,会出现认知不一致:
+
+1. 用户容易误以为 `job_s4_mdp_sync_transform` 只服务 S4。
+2. 实际上一个 MDP 同步任务可能沉淀某个业务对象域,并被多个模块消费。
+3. 任务调度页无法直接按消费模块筛选,也无法从调度任务跳转到对应 MDP 运行监控。
+
+因此,本任务书要求对任务调度页做业务标签化优化,但不改变调度执行内核。
+
+---
+
+## 2. 改造目标
+
+### 2.1 必须达成
+
+1. 保留平台任务调度的技术能力,不修改 Furion / Admin.NET 任务调度执行机制。
+2. 不修改现有作业编号、作业类名、触发器编号、Cron 和作业执行逻辑。
+3. 在任务调度页为 Ai-DOP 业务任务增加业务标签展示:
+   - 任务分类
+   - 同步业务域
+   - 消费模块
+   - 任务类型
+4. MDP 同步类任务与 MDP 运行监控口径保持一致。
+5. 支持按业务标签筛选 Ai-DOP 任务,但不影响系统任务、钉钉任务、日志清理等通用任务正常查询。
+6. 对 MDP 同步任务提供跳转到 MDP 运行监控的入口,并自动带上对应筛选参数。
+7. 兼容现有 `SysJobDetail.Properties`,优先复用该字段保存结构化扩展信息,第一阶段不新增数据库表。
+
+### 2.2 建议达成
+
+1. 任务调度页可切换「全部任务」与「Ai-DOP 任务」视角。
+2. 表格中技术字段继续保留,但业务标签列放在更靠前的位置。
+3. 对没有业务标签的系统任务显示为「系统任务」或空标签,不做强行归类。
+4. 对 MDP 同步任务显示「查看监控」按钮,跳转到:
+
+```text
+/aidop/data-platform/mdp-monitor?businessDomainCode=xxx
+```
+
+或:
+
+```text
+/aidop/data-platform/mdp-monitor?consumerModule=S9
+```
+
+---
+
+## 3. 核心原则
+
+### 3.1 调度标识不改
+
+以下字段是调度执行与历史记录追踪的稳定标识,不允许因为展示优化而修改:
+
+- `SysJobDetail.JobId`
+- `SysJobDetail.JobType`
+- `SysJobDetail.AssemblyName`
+- `SysJobTrigger.TriggerId`
+- 代码中的 `[JobDetail("job_xxx")]`
+- 触发器配置和执行记录关联字段
+
+示例:
+
+```csharp
+[JobDetail("job_s4_mdp_sync_transform", Description = "S4 MDP同步与标准化转换...", GroupName = "default", Concurrent = false)]
+```
+
+可以优化描述和扩展标签,但不要把 `job_s4_mdp_sync_transform` 改名。
+
+### 3.2 业务标签是展示与检索层
+
+业务标签用于帮助用户理解「这条任务服务什么业务域、被哪些模块消费」,不参与作业调度、幂等判断、锁定和执行。
+
+建议标签模型:
+
+| 字段 | 含义 | 示例 |
+|------|------|------|
+| `taskCategory` | 任务分类 | `MDP_SYNC`、`SMART_OPS_KPI`、`S8_WORKFLOW`、`SYSTEM` |
+| `businessDomainCode` | 同步业务域编码 | `order_delivery` |
+| `businessDomainName` | 同步业务域名称 | `订单交付域` |
+| `consumerModules` | 消费模块 | `["S1","S2","S3","S4","S7","S9"]` |
+| `taskType` | 任务类型 | `同步转换`、`KPI预热`、`清理任务` |
+| `monitorRoute` | 监控跳转地址 | `/aidop/data-platform/mdp-monitor` |
+| `monitorQuery` | 监控跳转参数 | `{ "businessDomainCode": "order_delivery" }` |
+
+### 3.3 MDP 口径复用运行监控
+
+MDP 同步任务的业务域和消费模块必须与 MDP 运行监控保持一致。
+
+建议映射:
+
+| 作业编号 | 同步业务域 | 消费模块 | 任务类型 |
+|----------|------------|----------|----------|
+| `job_s1_mdp_sync_transform` | 订单交付域 | `S1,S2,S3,S4,S7,S9` | MDP同步转换 |
+| `job_s2_mdp_sync_transform` | 工单排程域 | `S2,S3,S5,S6,S8,S9` | MDP同步转换 |
+| `job_s3_mdp_sync_transform` | 供应采购域 | `S3,S4,S5,S8,S9` | MDP同步转换 |
+| `job_s4_mdp_sync_transform` | 采购执行域 | `S4,S5,S8,S9` | MDP同步转换 |
+| `job_smart_ops_kpi_mdp_bootstrap` | 多业务域 | `S1,S2,S3,S4,S5,S6,S7,S8,S9` | KPI启动预热 |
+| `job_smart_ops_kpi_atomic_warmup` | 多业务域 | `S1,S2,S3,S4,S5,S6,S7,S8,S9` | KPI原子层预热 |
+
+---
+
+## 4. 推荐技术方案
+
+### 4.1 第一阶段:代码映射 + 前端解析
+
+第一阶段不新增表,优先使用代码映射补齐标签,降低风险。
+
+后端可新增一个 Ai-DOP 任务 catalog,位置建议二选一:
+
+1. 如果只服务任务调度页:
+   - `server/Plugins/Admin.NET.Plugin.AiDOP/Job/AidopJobCatalog.cs`
+2. 如果要与 MDP 运行监控共享:
+   - `server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs` 中的 MDP catalog 后续抽出到共享类
+
+建议新增结构:
+
+```csharp
+public sealed record AidopJobCatalogItem(
+    string JobId,
+    string TaskCategory,
+    string TaskCategoryName,
+    string? BusinessDomainCode,
+    string? BusinessDomainName,
+    string[] ConsumerModules,
+    string TaskType,
+    string? MonitorRoute,
+    Dictionary<string, string?> MonitorQuery);
+```
+
+第一阶段建议不要直接改 `SysJobDetail` 表结构。返回给前端时可以:
+
+1. 在后端 `JobDetailOutput` 扩展 `AidopTags` 字段。
+2. 或前端拿到 `jobDetail.jobId` 后,用本地 catalog 做展示。
+
+推荐后端补齐,便于筛选与复用。
+
+### 4.2 第二阶段:沉淀配置表
+
+当业务任务数量稳定增加后,再新增配置表,例如:
+
+```text
+aidop_job_catalog
+```
+
+建议字段:
+
+| 字段 | 说明 |
+|------|------|
+| `id` | 主键 |
+| `job_id` | 对应 `SysJobDetail.JobId` |
+| `task_category` | 任务分类 |
+| `business_domain_code` | 业务域编码 |
+| `business_domain_name` | 业务域名称 |
+| `consumer_modules` | JSON 或逗号分隔 |
+| `task_type` | 任务类型 |
+| `monitor_route` | 监控路由 |
+| `monitor_query_json` | 跳转参数 |
+| `enabled` | 是否启用 |
+| `remark` | 说明 |
+
+本任务书当前只要求第一阶段,配置表作为后续非目标范围。
+
+---
+
+## 5. 后端改造任务
+
+### 5.1 修改文件
+
+主要文件:
+
+- `server/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs`
+- `server/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs`
+- `server/Admin.NET.Core/Service/Job/SysJobService.cs`
+
+建议新增:
+
+- `server/Plugins/Admin.NET.Plugin.AiDOP/Job/AidopJobCatalog.cs`
+
+如果核心工程不适合引用插件工程,则不要让 `Admin.NET.Core` 反向依赖插件。可选择:
+
+1. 在 `Admin.NET.Core` 中只扩展通用 `Properties` 解析能力,Ai-DOP 标签由前端 catalog 处理。
+2. 或在插件中新增独立接口 `/api/aidop/job-catalog`,前端任务调度页额外请求后合并展示。
+
+推荐方案:新增插件接口,避免 Core 依赖插件。
+
+### 5.2 新增 Ai-DOP 任务 catalog 接口
+
+建议新增接口:
+
+```http
+GET /api/aidop/job-catalog
+```
+
+返回示例:
+
+```json
+[
+  {
+    "jobId": "job_s1_mdp_sync_transform",
+    "taskCategory": "MDP_SYNC",
+    "taskCategoryName": "MDP同步",
+    "businessDomainCode": "order_delivery",
+    "businessDomainName": "订单交付域",
+    "consumerModules": ["S1", "S2", "S3", "S4", "S7", "S9"],
+    "taskType": "MDP同步转换",
+    "monitorRoute": "/aidop/data-platform/mdp-monitor",
+    "monitorQuery": {
+      "businessDomainCode": "order_delivery"
+    }
+  }
+]
+```
+
+接口实现可以放在 Ai-DOP 插件服务内,避免修改平台 Core 过多。
+
+### 5.3 扩展任务分页查询入参
+
+如果选择由后端直接筛选,可扩展 `PageJobDetailInput`:
+
+```csharp
+public string? TaskCategory { get; set; }
+public string? BusinessDomainCode { get; set; }
+public string? ConsumerModule { get; set; }
+```
+
+筛选规则:
+
+1. 无业务标签条件时,保持原有查询行为。
+2. 有 `taskCategory/businessDomainCode/consumerModule` 时,先从 Ai-DOP catalog 计算允许的 `jobId` 集合。
+3. 追加 `JobId IN (...)` 条件。
+4. 如果集合为空,返回空分页。
+
+如果为降低第一阶段后端风险,也可以不改 `PageJobDetailInput`,由前端在当前页结果上做标签展示,筛选先通过二次请求 catalog + 业务过滤实现。但这会影响分页准确性,不推荐作为最终方案。
+
+### 5.4 扩展任务输出模型
+
+建议扩展 `JobDetailOutput`:
+
+```csharp
+public AidopJobTags? AidopTags { get; set; }
+```
+
+建议结构:
+
+```csharp
+public class AidopJobTags
+{
+    public string? TaskCategory { get; set; }
+    public string? TaskCategoryName { get; set; }
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string[] ConsumerModules { get; set; } = [];
+    public string? TaskType { get; set; }
+    public string? MonitorRoute { get; set; }
+    public Dictionary<string, string?> MonitorQuery { get; set; } = new();
+}
+```
+
+对没有匹配 catalog 的任务,`AidopTags` 返回 `null`。
+
+### 5.5 Properties 兼容建议
+
+`SysJobDetail` 已有:
+
+```csharp
+public string? Properties { get; set; } = "{}";
+```
+
+不要在本阶段强制迁移历史任务的 `Properties`。如果需要写入业务标签,建议使用独立命名空间:
+
+```json
+{
+  "aidop": {
+    "taskCategory": "MDP_SYNC",
+    "businessDomainCode": "order_delivery",
+    "consumerModules": ["S1", "S2", "S3", "S4", "S7", "S9"]
+  }
+}
+```
+
+但第一阶段优先以代码 catalog 为准,避免自动持久化导致既有任务配置被覆盖。
+
+---
+
+## 6. 前端改造任务
+
+### 6.1 修改文件
+
+主要文件:
+
+- `Web/src/views/system/job/index.vue`
+
+可能涉及:
+
+- `Web/src/api-services/apis/sys-job-api.ts`
+- `Web/src/api-services/models/page-job-detail-input.ts`
+- `Web/src/api-services/models/job-detail-output.ts`
+- `Web/src/router/route.ts` 或已有路由辅助方法
+
+如果 API 客户端是生成代码,优先确认项目生成流程。不要手工大面积改生成文件,除非项目目前就是手工维护。
+
+### 6.2 查询区新增筛选项
+
+在现有筛选项后新增:
+
+1. 任务分类
+2. 同步业务域
+3. 消费模块
+
+示例:
+
+```vue
+<el-form-item label="任务分类">
+  <el-select v-model="state.queryParams.taskCategory" placeholder="任务分类" clearable>
+    <el-option label="MDP同步" value="MDP_SYNC" />
+    <el-option label="智慧运营KPI" value="SMART_OPS_KPI" />
+    <el-option label="系统任务" value="SYSTEM" />
+  </el-select>
+</el-form-item>
+```
+
+业务域和消费模块选项应从 catalog 计算,不要在页面散落硬编码。
+
+### 6.3 表格新增业务标签列
+
+建议在「作业编号」后新增:
+
+1. 任务分类
+2. 同步业务域
+3. 消费模块
+4. 任务类型
+
+示例:
+
+```vue
+<el-table-column label="任务分类" width="110" align="center">
+  <template #default="{ row }">
+    <el-tag v-if="row.aidopTags?.taskCategoryName" size="small">
+      {{ row.aidopTags.taskCategoryName }}
+    </el-tag>
+    <span v-else>-</span>
+  </template>
+</el-table-column>
+```
+
+消费模块建议显示为多个小标签:
+
+```vue
+<el-tag
+  v-for="m in row.aidopTags?.consumerModules || []"
+  :key="m"
+  size="small"
+  class="consumer-tag"
+>
+  {{ m }}
+</el-tag>
+```
+
+### 6.4 操作列新增监控跳转
+
+对存在 `aidopTags.monitorRoute` 的任务,在操作列增加「查看监控」按钮。
+
+跳转规则:
+
+```ts
+router.push({
+  path: row.aidopTags.monitorRoute,
+  query: row.aidopTags.monitorQuery || {},
+});
+```
+
+MDP 同步任务应跳转到:
+
+```text
+/aidop/data-platform/mdp-monitor
+```
+
+并带上 `businessDomainCode`。如果用户从消费模块筛选进入,也可带上 `consumerModule`。
+
+### 6.5 展开行保持技术视图
+
+展开行中的触发器、执行记录、Cron、重试次数、错误次数等保持原样。这些是平台任务调度的技术运维能力,不要为了业务标签化删除。
+
+---
+
+## 7. JobDetail 描述优化建议
+
+在不改 `JobId` 的前提下,可以优化 `[JobDetail]` 的 `Description`,让任务名称更贴近业务口径。
+
+建议:
+
+```csharp
+[JobDetail(
+    "job_s4_mdp_sync_transform",
+    Description = "MDP同步:采购执行域标准化转换,服务S4/S5/S8/S9",
+    GroupName = "default",
+    Concurrent = false)]
+```
+
+注意:
+
+1. 修改描述会影响任务调度页展示,但不应影响执行逻辑。
+2. 如果内置任务持久化会覆盖数据库描述,需要确认 `DbJobPersistence` 行为。
+3. 描述优化可以作为独立小提交,避免和筛选接口混在一起难以回滚。
+
+---
+
+## 8. 验收用例
+
+### 8.1 默认任务调度页
+
+访问:
+
+```text
+/platform/job
+```
+
+期望:
+
+- 原有任务列表正常展示。
+- 作业编号、组名称、类型、描述、执行方式、创建类型、更新时间、操作按钮仍存在。
+- 新增业务标签列不影响系统任务显示。
+- 系统任务没有业务标签时显示 `-` 或「系统任务」。
+
+### 8.2 MDP 同步任务标签
+
+搜索:
+
+```text
+作业编号 = job_s4_mdp_sync_transform
+```
+
+期望:
+
+- 任务分类显示 `MDP同步`。
+- 同步业务域显示 `采购执行域`。
+- 消费模块显示 `S4,S5,S8,S9`。
+- 任务类型显示 `MDP同步转换`。
+- 作业编号仍为 `job_s4_mdp_sync_transform`。
+
+### 8.3 按消费模块筛选
+
+选择:
+
+```text
+消费模块 = S9
+```
+
+期望:
+
+- 能看到 S9 依赖的多个 MDP 同步任务。
+- 至少包括订单交付域、工单排程域、供应采购域、采购执行域相关任务。
+- 不应只显示某一个 S9 私有任务。
+
+### 8.4 按同步业务域筛选
+
+选择:
+
+```text
+同步业务域 = 订单交付域
+```
+
+期望:
+
+- 能看到 `job_s1_mdp_sync_transform`。
+- 如果后续有同业务域其它任务,也应一起出现。
+
+### 8.5 查看监控跳转
+
+在 `job_s1_mdp_sync_transform` 行点击「查看监控」。
+
+期望跳转到:
+
+```text
+/aidop/data-platform/mdp-monitor?businessDomainCode=order_delivery
+```
+
+MDP 运行监控页应显示订单交付域相关运行记录。
+
+### 8.6 技术操作不受影响
+
+对任意任务验证:
+
+- 执行记录可打开。
+- 增加触发器可打开。
+- 执行作业、启动、暂停、取消按钮仍可用。
+- 编辑、删除权限仍按原有 `v-auth` 生效。
+
+---
+
+## 9. 非目标范围
+
+本次不做以下事项:
+
+1. 不改任务调度执行框架。
+2. 不改作业编号、触发器编号和执行记录关联字段。
+3. 不改 Cron 配置。
+4. 不新增 `aidop_job_catalog` 数据库表。
+5. 不迁移历史 `SysJobDetail.Properties`。
+6. 不删除平台任务调度的技术字段。
+7. 不把所有系统任务强行归类到 Ai-DOP 业务域。
+8. 不改 MDP 同步任务实际业务逻辑。
+9. 不改 MDP 运行日志表结构。
+
+---
+
+## 10. 风险与注意事项
+
+1. `Admin.NET.Core` 是平台核心层,不建议直接依赖 Ai-DOP 插件层。若需要跨层数据,优先通过前端合并 catalog 或插件独立接口实现。
+2. `SysJobDetail.Properties` 是通用扩展字段,可能已有 HTTP 作业等场景使用,不要直接覆盖。
+3. 前端 API 模型可能由 OpenAPI 生成,修改前需确认生成方式,避免下次生成覆盖手工改动。
+4. 业务标签筛选如果只在前端当前页过滤,会导致分页总数不准;正式方案应由后端按 `jobId` 集合筛选。
+5. 任务描述优化可能被内置任务持久化逻辑覆盖,需要验证 `DbJobPersistence` 对内置任务描述的同步规则。
+6. 任务调度页是全局页面,任何表格列、查询项和操作按钮改动都要验证系统任务不受影响。
+
+---
+
+## 11. 推荐实施顺序
+
+1. 梳理 Ai-DOP 作业清单,确认 MDP、KPI、S8 等任务分类。
+2. 新增 Ai-DOP job catalog 模型和静态映射。
+3. 新增 `/api/aidop/job-catalog` 或等价接口。
+4. 扩展任务分页查询入参,支持 `taskCategory/businessDomainCode/consumerModule`。
+5. 后端根据 catalog 计算 `jobId` 集合并筛选。
+6. 后端输出 `AidopTags`,或前端通过 catalog 合并标签。
+7. 前端任务调度页新增业务筛选项。
+8. 前端任务调度表格新增业务标签列。
+9. 前端操作列新增「查看监控」跳转。
+10. 保留并验证所有原有技术操作。
+11. 执行构建和验收用例。
+
+---
+
+## 12. 交付物清单
+
+完成后应至少包含:
+
+- 后端:
+  - Ai-DOP 任务 catalog。
+  - 业务标签返回或 catalog 查询接口。
+  - 任务分页查询支持业务标签筛选。
+  - 不影响原有 `sysJob` 接口默认行为。
+
+- 前端:
+  - `Web/src/views/system/job/index.vue` 新增业务标签筛选。
+  - 表格新增任务分类、同步业务域、消费模块、任务类型列。
+  - MDP 同步任务支持跳转 MDP 运行监控。
+  - 原有执行记录、触发器、启动、暂停、编辑、删除能力保持可用。
+
+- 文档:
+  - 更新本任务书中的实际映射表。
+  - 如新增接口,补充接口返回示例和验收截图。
+
+---
+
+## 13. 给执行代理的提示词
+
+请基于本任务书改造 Ai-DOP 的「平台管理 / 任务调度」页面,使其在保留技术调度能力的同时,支持 Ai-DOP 业务标签化展示与筛选。
+
+核心要求:
+
+1. 不修改任务调度执行机制,不改 `JobId`、触发器、Cron 和执行记录关联字段。
+2. 新增 Ai-DOP 任务 catalog,至少覆盖 MDP 同步任务和智慧运营 KPI 任务。
+3. MDP 同步任务的同步业务域与消费模块必须与 `MDP运行监控按业务域与消费模块改造任务书.md` 保持一致。
+4. 任务调度页新增筛选:任务分类、同步业务域、消费模块。
+5. 表格新增业务标签列:任务分类、同步业务域、消费模块、任务类型。
+6. MDP 同步任务新增「查看监控」入口,跳转 `/aidop/data-platform/mdp-monitor` 并带上业务域或消费模块参数。
+7. 系统任务、日志清理、钉钉同步等非 Ai-DOP MDP 任务必须继续正常展示和操作。
+8. 第一阶段不要新增数据库表,不迁移 `SysJobDetail.Properties`,优先用静态 catalog 或插件接口。
+
+完成后运行:
+
+```powershell
+Set-Location "d:\Projects\Ai-DOP\SourceCode\ZZYDOP\server\Admin.NET.Web.Entry"
+dotnet build
+```
+
+如前端依赖环境可用,再运行:
+
+```powershell
+Set-Location "d:\Projects\Ai-DOP\SourceCode\ZZYDOP\Web"
+pnpm build
+```
+
+验收重点:
+
+- `/platform/job` 默认查询与原操作不受影响。
+- `job_s4_mdp_sync_transform` 显示采购执行域、S4/S5/S8/S9。
+- 按消费模块 `S9` 能看到多个 MDP 同步任务。
+- 点击 MDP 任务「查看监控」能跳转到 MDP 运行监控并自动带上筛选参数。
+- 技术字段和技术操作保留。
+
+---
+
+## 14. 增补任务:MDP运行监控反向关联任务调度
+
+> 增补日期:2026-06-12  
+> 增补原因:原任务书主体已执行完成,新增从 MDP 运行监控页便捷反查对应任务调度配置的需求  
+> 执行状态:待执行  
+> 影响范围:MDP运行监控页、任务调度页路由参数兼容、任务 catalog 跳转参数  
+> 识别说明:本节为新增待执行范围,前文第 1-13 节按已执行基线理解,除非实现本节确有必要,不要求重做主体任务
+
+### 14.1 新增需求
+
+在「MDP运行监控」页面增加「查看调度配置」或「关联任务调度」入口,方便用户从运行记录、链路详情或业务域视角快速跳转到对应任务调度配置。
+
+目标体验:
+
+1. 用户在 MDP 运行监控列表看到某条 `S4_MDP_SYNC_TRANSFORM` 运行记录。
+2. 点击「查看调度配置」。
+3. 页面跳转到 `/platform/job`,自动带出并查询对应作业 `job_s4_mdp_sync_transform`。
+4. 用户可直接查看 Cron、触发器、执行方式、描述、业务标签和操作按钮。
+
+### 14.2 新增改造点
+
+任务调度页需要支持从路由参数初始化查询条件:
+
+```text
+/platform/job?jobId=job_s4_mdp_sync_transform
+```
+
+进入页面后应自动:
+
+1. 将 `route.query.jobId` 写入 `state.queryParams.jobId`。
+2. 执行一次查询。
+3. 列表展示对应作业。
+
+如果后续需要更精确定位,也可兼容:
+
+```text
+/platform/job?taskCategory=MDP_SYNC&businessDomainCode=purchase_execution
+```
+
+但本次最低要求是支持 `jobId`。
+
+### 14.3 与 MDP JobCode 的映射规则
+
+MDP 运行日志使用的 `job_code` 与任务调度使用的 `jobId` 不是同一格式,需要通过 catalog 映射,不要在前端临时拼字符串。
+
+建议映射:
+
+| MDP运行日志 `job_code` | 任务调度 `jobId` |
+|------------------------|------------------|
+| `S1_MDP_SYNC_TRANSFORM` | `job_s1_mdp_sync_transform` |
+| `S2_MDP_SYNC_TRANSFORM` | `job_s2_mdp_sync_transform` |
+| `S3_MDP_SYNC_TRANSFORM` | `job_s3_mdp_sync_transform` |
+| `S4_MDP_SYNC_TRANSFORM` | `job_s4_mdp_sync_transform` |
+
+如果 MDP 运行监控接口已返回 `scheduleJobId`,前端优先使用接口字段;否则前端可通过统一 catalog 查询获得映射。
+
+### 14.4 验收用例
+
+访问:
+
+```text
+/platform/job?jobId=job_s4_mdp_sync_transform
+```
+
+期望:
+
+- 任务调度页自动填充作业编号。
+- 自动查询并展示 `job_s4_mdp_sync_transform`。
+- 原有查询、重置、分页、操作按钮保持正常。
+
+从 MDP 运行监控页点击「查看调度配置」。
+
+期望:
+
+- 跳转到 `/platform/job?jobId=...`。
+- 对应作业能被自动查出。
+- 用户无需手工复制 `jobId`。
+
+### 14.5 非目标范围
+
+本增补任务不做以下事项:
+
+1. 不改任务调度执行逻辑。
+2. 不改 `JobId`、触发器编号、Cron、执行记录。
+3. 不新增数据库表。
+4. 不迁移历史 `SysJobDetail.Properties`。
+5. 不要求重新执行第 1-13 节主体任务。

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.169</AssemblyVersion>
-    <FileVersion>1.0.169</FileVersion>
-    <Version>1.0.169</Version>
+    <AssemblyVersion>1.0.170</AssemblyVersion>
+    <FileVersion>1.0.170</FileVersion>
+    <Version>1.0.170</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 390 - 64
server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs

@@ -17,6 +17,42 @@ public class MdpMonitorService : IDynamicApiController, ITransient
         ["S4"] = "S4_MDP_SYNC_TRANSFORM"
     };
 
+    private static readonly Dictionary<string, MdpJobCatalogItem> JobCatalog = new(StringComparer.OrdinalIgnoreCase)
+    {
+        ["S1_MDP_SYNC_TRANSFORM"] = new(
+            "S1_MDP_SYNC_TRANSFORM",
+            "order_delivery",
+            "订单交付域",
+            ["S1", "S2", "S3", "S4", "S7", "S9"],
+            "GLOBAL_DOMAIN",
+            "订单交付域 MDP 同步",
+            "job_s1_mdp_sync_transform"),
+        ["S2_MDP_SYNC_TRANSFORM"] = new(
+            "S2_MDP_SYNC_TRANSFORM",
+            "work_schedule",
+            "工单排程域",
+            ["S2", "S3", "S5", "S6", "S8", "S9"],
+            "GLOBAL_DOMAIN",
+            "工单排程域 MDP 同步",
+            "job_s2_mdp_sync_transform"),
+        ["S3_MDP_SYNC_TRANSFORM"] = new(
+            "S3_MDP_SYNC_TRANSFORM",
+            "supply_purchase",
+            "供应采购域",
+            ["S3", "S4", "S5", "S8", "S9"],
+            "GLOBAL_DOMAIN",
+            "供应采购域 MDP 同步",
+            "job_s3_mdp_sync_transform"),
+        ["S4_MDP_SYNC_TRANSFORM"] = new(
+            "S4_MDP_SYNC_TRANSFORM",
+            "purchase_execution",
+            "采购执行域",
+            ["S4", "S5", "S8", "S9"],
+            "GLOBAL_DOMAIN",
+            "采购执行域 MDP 同步",
+            "job_s4_mdp_sync_transform")
+    };
+
     private readonly ISqlSugarClient _db;
     private readonly UserManager _userManager;
 
@@ -28,13 +64,11 @@ public class MdpMonitorService : IDynamicApiController, ITransient
 
     [DisplayName("MDP模块选项")]
     [HttpGet("mdp-monitor/modules")]
-    public object GetModules()
-    {
-        return ModuleJobCodes
-            .OrderBy(u => u.Key)
-            .Select(u => new { moduleCode = u.Key, jobCode = u.Value })
-            .ToList();
-    }
+    public object GetModules() => BuildCatalogResponse();
+
+    [DisplayName("MDP任务目录")]
+    [HttpGet("mdp-monitor/catalog")]
+    public object GetCatalog() => BuildCatalogResponse();
 
     [DisplayName("MDP最近运行状态")]
     [HttpGet("mdp-monitor/latest")]
@@ -42,7 +76,7 @@ public class MdpMonitorService : IDynamicApiController, ITransient
     {
         var tenantId = _userManager.TenantId;
         var (whereSql, pars) = BuildWhere(input, tenantId);
-        return await _db.Ado.SqlQuerySingleAsync<MdpMonitorRunLogRow>(
+        var row = await _db.Ado.SqlQuerySingleAsync<MdpMonitorRunLogRow>(
             $"""
             {SelectColumnsSql()}
             FROM mdp_transform_run_log
@@ -52,6 +86,8 @@ public class MdpMonitorService : IDynamicApiController, ITransient
             """,
             pars)
             ?? new MdpMonitorRunLogRow();
+        AttachCatalogInfo(row);
+        return row;
     }
 
     [DisplayName("MDP运行日志列表")]
@@ -75,6 +111,9 @@ public class MdpMonitorService : IDynamicApiController, ITransient
             """,
             pars);
 
+        foreach (var row in list)
+            AttachCatalogInfo(row);
+
         return new { total, page, pageSize, list };
     }
 
@@ -94,7 +133,10 @@ public class MdpMonitorService : IDynamicApiController, ITransient
             LIMIT 1
             """,
             pars);
-        return row ?? throw Oops.Oh("运行日志不存在");
+        if (row == null)
+            throw Oops.Oh("运行日志不存在");
+        AttachCatalogInfo(row);
+        return row;
     }
 
     [DisplayName("MDP同步链路详情")]
@@ -123,7 +165,8 @@ public class MdpMonitorService : IDynamicApiController, ITransient
                    e.incr_column AS IncrColumn, e.status AS Status
             FROM mdp_entity e
             LEFT JOIN mdp_source s ON s.id = e.source_id
-            WHERE e.entity_code LIKE @EntityPrefix AND e.tenant_id = @TenantId
+            WHERE e.entity_code LIKE @EntityPrefix
+              AND (e.tenant_id = @TenantId OR e.tenant_id = 0)
             ORDER BY e.entity_code
             """,
             new SugarParameter("@EntityPrefix", entityPrefix),
@@ -131,13 +174,16 @@ public class MdpMonitorService : IDynamicApiController, ITransient
 
         if (entities.Count == 0)
         {
-            return new MdpLineageOutput
+            var emptyOutput = new MdpLineageOutput
             {
                 ModuleCode = moduleCode,
                 JobCode = jobCode,
-                Stages = BuildStageDescriptions(moduleCode),
+                BatchId = input.BatchId,
+                Stages = BuildStageDescriptions(jobCode, moduleCode),
                 Entities = new List<MdpLineageEntityRow>()
             };
+            AttachLineageCatalogInfo(emptyOutput);
+            return emptyOutput;
         }
 
         var entityIds = string.Join(",", entities.Select(u => u.Id));
@@ -165,9 +211,11 @@ public class MdpMonitorService : IDynamicApiController, ITransient
                        error_msg AS ErrorMsg
                 FROM mdp_sync_log
                 WHERE sync_batch_id = @BatchId
-                  AND tenant_id = @TenantId
+                  AND (tenant_id = @TenantId OR tenant_id = 0)
                   AND entity_id IN (
-                      SELECT id FROM mdp_entity WHERE entity_code LIKE @EntityPrefix
+                      SELECT id FROM mdp_entity
+                      WHERE entity_code LIKE @EntityPrefix
+                        AND (tenant_id = @TenantId OR tenant_id = 0)
                   )
                 ORDER BY sync_start, id
                 """,
@@ -179,41 +227,59 @@ public class MdpMonitorService : IDynamicApiController, ITransient
                 .ToDictionary(u => u.Key, u => u.OrderByDescending(x => x.SyncStart).First());
         }
 
+        var batchId = input.BatchId?.Trim();
         foreach (var entity in entities)
         {
             entity.SourceFullName = BuildObjectFullName(entity.SourceDbType, entity.SourceDbHost, entity.SourceDbPort, entity.SourceDbName, entity.SourceTableName ?? entity.SourceApiPath);
             entity.TargetFullName = BuildObjectFullName(entity.TargetDbType, entity.TargetDbHost, entity.TargetDbPort, entity.TargetDbName, entity.TargetTableName);
 
-            if (mappingsByEntity.TryGetValue(entity.Id, out var entityMappings))
+            if (mappingsByEntity.TryGetValue(entity.Id, out var entityMappings) && entityMappings.Count > 0)
             {
+                foreach (var mapping in entityMappings)
+                {
+                    mapping.MappingSource = "CONFIG";
+                    mapping.IsFallback = false;
+                }
+
                 entity.FieldMappings = entityMappings;
                 entity.FieldMappingCount = entityMappings.Count;
             }
+            else
+            {
+                entity.FieldMappings = BuildFallbackFieldMappings(entity, batchId);
+                entity.FieldMappingCount = entity.FieldMappings.Count;
+            }
+
             if (syncLogsByEntity.TryGetValue(entity.Id, out var syncLog))
                 entity.SyncLog = syncLog;
         }
 
-        return new MdpLineageOutput
+        var output = new MdpLineageOutput
         {
             ModuleCode = moduleCode,
             JobCode = jobCode,
             BatchId = input.BatchId,
-            Stages = BuildStageDescriptions(moduleCode),
+            Stages = BuildStageDescriptions(jobCode, moduleCode),
             Entities = entities
         };
+        AttachLineageCatalogInfo(output);
+        return output;
     }
 
     private static (string WhereSql, List<SugarParameter> Parameters) BuildWhere(MdpMonitorQueryInput input, long tenantId)
     {
-        var where = new List<string> { "IFNULL(job_code, '') LIKE '%MDP%'", "tenant_id = @TenantId" };
+        var where = new List<string> { BuildMdpRunLogTenantWhere(tenantId) };
         var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
 
-        var jobCode = ResolveJobCode(input.ModuleCode, input.JobCode);
-        if (!string.IsNullOrWhiteSpace(jobCode))
+        var (jobFilterSql, jobFilterPars, noMatch) = BuildJobCodeFilter(input);
+        if (noMatch)
+            where.Add("1=0");
+        else
         {
-            where.Add("job_code=@JobCode");
-            pars.Add(new SugarParameter("@JobCode", jobCode));
+            where.Add(jobFilterSql);
+            pars.AddRange(jobFilterPars);
         }
+
         if (!string.IsNullOrWhiteSpace(input.BatchId))
         {
             where.Add("batch_id LIKE @BatchId");
@@ -238,6 +304,283 @@ public class MdpMonitorService : IDynamicApiController, ITransient
         return (string.Join(" AND ", where), pars);
     }
 
+    /// <summary>
+    /// MDP 转换任务当前以 tenant_id=0 写入运行日志;登录租户查询时需兼容这类全局任务记录。
+    /// </summary>
+    internal static string BuildMdpRunLogTenantWhere(long tenantId) =>
+        tenantId > 0 ? "(tenant_id = @TenantId OR tenant_id = 0)" : "tenant_id = 0";
+
+    private static object BuildCatalogResponse()
+    {
+        return JobCatalog.Values
+            .OrderBy(u => u.JobCode, StringComparer.OrdinalIgnoreCase)
+            .Select(u => new
+            {
+                jobCode = u.JobCode,
+                displayName = u.DisplayName,
+                businessDomainCode = u.BusinessDomainCode,
+                businessDomainName = u.BusinessDomainName,
+                consumerModules = u.ConsumerModules,
+                scopeType = u.ScopeType,
+                moduleCode = ResolveModuleCodeFromJobCode(u.JobCode),
+                scheduleJobId = u.ScheduleJobId
+            })
+            .ToList();
+    }
+
+    private static (string Sql, List<SugarParameter> Parameters, bool NoMatch) BuildJobCodeFilter(MdpMonitorQueryInput input)
+    {
+        var pars = new List<SugarParameter>();
+
+        if (!string.IsNullOrWhiteSpace(input.JobCode))
+        {
+            pars.Add(new SugarParameter("@JobCode", input.JobCode.Trim().ToUpperInvariant()));
+            return ("job_code=@JobCode", pars, false);
+        }
+
+        var hasDomainFilter = !string.IsNullOrWhiteSpace(input.BusinessDomainCode);
+        var consumerModule = !string.IsNullOrWhiteSpace(input.ConsumerModule)
+            ? input.ConsumerModule.Trim()
+            : !string.IsNullOrWhiteSpace(input.ModuleCode) ? input.ModuleCode.Trim() : null;
+        var hasConsumerFilter = !string.IsNullOrWhiteSpace(consumerModule);
+
+        if (!hasDomainFilter && !hasConsumerFilter)
+            return ("IFNULL(job_code, '') LIKE '%MDP%'", pars, false);
+
+        var allowed = ResolveAllowedJobCodes(input.BusinessDomainCode, consumerModule);
+        if (allowed.Count == 0)
+            return (string.Empty, pars, true);
+
+        if (allowed.Count == 1)
+        {
+            pars.Add(new SugarParameter("@JobCode", allowed.First()));
+            return ("job_code=@JobCode", pars, false);
+        }
+
+        var inParts = new List<string>();
+        var index = 0;
+        foreach (var code in allowed.OrderBy(u => u, StringComparer.OrdinalIgnoreCase))
+        {
+            var paramName = $"@JobCode{index++}";
+            inParts.Add(paramName);
+            pars.Add(new SugarParameter(paramName, code));
+        }
+
+        return ($"job_code IN ({string.Join(", ", inParts)})", pars, false);
+    }
+
+    private static HashSet<string> ResolveAllowedJobCodes(string? businessDomainCode, string? consumerModule)
+    {
+        IEnumerable<MdpJobCatalogItem> items = JobCatalog.Values;
+
+        if (!string.IsNullOrWhiteSpace(businessDomainCode))
+        {
+            var domain = businessDomainCode.Trim();
+            items = items.Where(u => string.Equals(u.BusinessDomainCode, domain, StringComparison.OrdinalIgnoreCase));
+        }
+
+        if (!string.IsNullOrWhiteSpace(consumerModule))
+        {
+            var module = consumerModule.Trim();
+            items = items.Where(u => u.ConsumerModules.Contains(module, StringComparer.OrdinalIgnoreCase));
+        }
+
+        return items.Select(u => u.JobCode).ToHashSet(StringComparer.OrdinalIgnoreCase);
+    }
+
+    private static void AttachCatalogInfo(MdpMonitorRunLogRow row)
+    {
+        if (row == null || string.IsNullOrWhiteSpace(row.JobCode))
+            return;
+
+        if (!JobCatalog.TryGetValue(row.JobCode.Trim(), out var item))
+            return;
+
+        row.BusinessDomainCode = item.BusinessDomainCode;
+        row.BusinessDomainName = item.BusinessDomainName;
+        row.ConsumerModules = string.Join(",", item.ConsumerModules);
+        row.ScopeType = item.ScopeType;
+        row.DisplayName = item.DisplayName;
+        row.ScheduleJobId = item.ScheduleJobId;
+    }
+
+    private static void AttachLineageCatalogInfo(MdpLineageOutput output)
+    {
+        if (output == null || string.IsNullOrWhiteSpace(output.JobCode))
+            return;
+
+        if (!JobCatalog.TryGetValue(output.JobCode.Trim(), out var item))
+            return;
+
+        output.BusinessDomainCode = item.BusinessDomainCode;
+        output.BusinessDomainName = item.BusinessDomainName;
+        output.ConsumerModules = string.Join(",", item.ConsumerModules);
+        output.ScopeType = item.ScopeType;
+        output.DisplayName = item.DisplayName;
+        output.ScheduleJobId = item.ScheduleJobId;
+    }
+
+    private static string FormatConsumerModulesLabel(MdpJobCatalogItem item) =>
+        string.Join("、", item.ConsumerModules);
+
+    private static List<MdpLineageStageRow> BuildStageDescriptions(string? jobCode, string? moduleCode)
+    {
+        if (!string.IsNullOrWhiteSpace(jobCode) && JobCatalog.TryGetValue(jobCode.Trim(), out var catalogItem))
+        {
+            return catalogItem.BusinessDomainCode switch
+            {
+                "order_delivery" => BuildOrderDeliveryStages(catalogItem),
+                "work_schedule" => BuildWorkScheduleStages(catalogItem),
+                "supply_purchase" => BuildSupplyPurchaseStages(catalogItem),
+                "purchase_execution" => BuildPurchaseExecutionStages(catalogItem),
+                _ => BuildGenericStages(moduleCode)
+            };
+        }
+
+        return BuildGenericStages(moduleCode);
+    }
+
+    private static List<MdpLineageStageRow> BuildOrderDeliveryStages(MdpJobCatalogItem item)
+    {
+        var consumers = FormatConsumerModulesLabel(item);
+        return new List<MdpLineageStageRow>
+        {
+            new() { StageCode = "STAGING", StageName = "订单交付域 · 贴源同步", Layer = "mdp_stg", Description = $"按 mdp_entity 登记订单交付域源对象抽取数据,保留 raw_data JSON 便于追溯;产出供 {consumers} 消费。", InputObjects = "旧系统 / 当前库源对象", OutputObjects = "mdp_stg_so, mdp_stg_ship_trans", Execution = "S1MdpSyncTransformService.SyncStagingAsync" },
+            new() { StageCode = "STANDARD", StageName = "订单交付域 · 标准层转换", Layer = "mdp_std", Description = "解析贴源 raw_data,做字段标准化、租户兜底和幂等写入。", InputObjects = "mdp_stg_so, mdp_stg_ship_trans", OutputObjects = "mdp_std_so, mdp_std_ship_trans", Execution = "S1MdpSyncTransformService.BuildStandardCommands" },
+            new() { StageCode = "DWD", StageName = "订单交付域 · DWD宽表", Layer = "dwd", Description = $"沉淀订单交付事实,供 {consumers} 看板与诊断读取。", InputObjects = "mdp_std_so, mdp_std_ship_trans", OutputObjects = "dwd_ship_trans", Execution = "S1MdpSyncTransformService.BuildDwdAsync" },
+            new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = $"计算订单交付域 L1 指标并写入统一指标值表,供 {consumers} 消费。", InputObjects = "mdp_std_so, dwd_ship_trans", OutputObjects = "ado_s9_kpi_value_l1_day", Execution = "S1MdpSyncTransformService.BuildS1KpiValuesAsync" }
+        };
+    }
+
+    private static List<MdpLineageStageRow> BuildWorkScheduleStages(MdpJobCatalogItem item)
+    {
+        var consumers = FormatConsumerModulesLabel(item);
+        return new List<MdpLineageStageRow>
+        {
+            new() { StageCode = "STAGING", StageName = "工单排程域 · 贴源同步", Layer = "mdp_stg", Description = $"按 mdp_entity 登记工单排程域源对象抽取数据;产出供 {consumers} 消费。", InputObjects = "工单 / 工序 / 排程源对象", OutputObjects = "mdp_stg_*", Execution = "S2MdpSyncTransformService.SyncStagingAsync" },
+            new() { StageCode = "STANDARD", StageName = "工单排程域 · 标准层转换", Layer = "mdp_std", Description = "将工单、工序、排程等对象标准化。", InputObjects = "mdp_stg_*", OutputObjects = "mdp_std_*", Execution = "S2MdpSyncTransformService.BuildStandardCommands" },
+            new() { StageCode = "DWD", StageName = "工单排程域 · DWD宽表", Layer = "dwd", Description = $"生成制造执行与排程分析宽表,供 {consumers} 读取。", InputObjects = "mdp_std_*", OutputObjects = "dwd_*", Execution = "S2MdpSyncTransformService.BuildDwdAsync" },
+            new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = $"写入工单排程域 KPI,供 {consumers} 消费。", InputObjects = "mdp_std_* / dwd_*", OutputObjects = "ado_s9_kpi_value_*", Execution = "S2MdpSyncTransformService.BuildS2KpiValuesAsync" }
+        };
+    }
+
+    private static List<MdpLineageStageRow> BuildSupplyPurchaseStages(MdpJobCatalogItem item)
+    {
+        var consumers = FormatConsumerModulesLabel(item);
+        return new List<MdpLineageStageRow>
+        {
+            new() { StageCode = "STAGING", StageName = "供应采购域 · 贴源同步", Layer = "mdp_stg", Description = $"按 mdp_entity 登记供应采购域源对象抽取数据;产出供 {consumers} 消费。", InputObjects = "供应 / 物料 / 采购源对象", OutputObjects = "mdp_stg_*", Execution = "S3MdpSyncTransformService.SyncStagingAsync" },
+            new() { StageCode = "STANDARD", StageName = "供应采购域 · 标准层转换", Layer = "mdp_std", Description = "将供应、物料、采购、交货计划等对象标准化。", InputObjects = "mdp_stg_*", OutputObjects = "mdp_std_*", Execution = "S3MdpSyncTransformService.BuildStandardCommands" },
+            new() { StageCode = "DWD", StageName = "供应采购域 · DWD宽表", Layer = "dwd", Description = $"生成供应交付、齐套、风险等分析宽表,供 {consumers} 读取。", InputObjects = "mdp_std_*", OutputObjects = "dwd_supplier_delivery / dwd_material_readiness 等", Execution = "S3MdpSyncTransformService.BuildDwdAsync" },
+            new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = $"写入供应采购域指标,供 {consumers} 消费。", InputObjects = "mdp_std_* / dwd_*", OutputObjects = "ado_s9_kpi_value_*", Execution = "S3MdpSyncTransformService.BuildS3KpiValuesAsync" }
+        };
+    }
+
+    private static List<MdpLineageStageRow> BuildPurchaseExecutionStages(MdpJobCatalogItem item)
+    {
+        var consumers = FormatConsumerModulesLabel(item);
+        return new List<MdpLineageStageRow>
+        {
+            new() { StageCode = "STAGING", StageName = "采购执行域 · 贴源同步", Layer = "mdp_stg_s4_*", Description = $"同步采购执行域 IQC/发货/退货/欠料事实,供 {consumers} 消费;共享采购主链仍由供应采购域维护。", InputObjects = "PurOrdRctDetail / scm_shdzb / srm_polist_ds / dwd_material_shortage", OutputObjects = "mdp_stg_s4_iqc / mdp_stg_s4_shipment / mdp_stg_s4_return / mdp_stg_s4_shortage", Execution = "S4MdpSyncTransformService.SyncStagingAsync" },
+            new() { StageCode = "STANDARD", StageName = "采购执行域 · 标准层转换", Layer = "mdp_std_s4_*", Description = "将采购执行域贴源对象标准化,并统计供应采购域共享标准层行数。", InputObjects = "mdp_stg_s4_*", OutputObjects = "mdp_std_s4_iqc / mdp_std_s4_shipment / mdp_std_s4_return / mdp_std_s4_shortage", Execution = "S4MdpSyncTransformService.BuildStandardCommands" },
+            new() { StageCode = "DWD", StageName = "采购执行域 · DWD宽表", Layer = "dwd", Description = $"写入采购执行分析宽表,供 {consumers} 读取。", InputObjects = "dwd_supplier_delivery / mdp_std_s4_*", OutputObjects = "dwd_s4_purchase_execution / dwd_po_trans / dwd_qc_trans", Execution = "S4MdpSyncTransformService.BuildDwdAsync" },
+            new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = $"写入采购执行域 L1/L2/L3 指标,供 {consumers} 消费。", InputObjects = "mdp_std_delivery_schedule / dwd_supplier_delivery / dwd_s4_purchase_execution", OutputObjects = "ado_s9_kpi_value_l1/l2/l3_day", Execution = "S4MdpSyncTransformService.BuildS4KpiValuesAsync" }
+        };
+    }
+
+    /// <summary>
+    /// 贴源同步在代码中维护、但未写入 mdp_field_mapping 的实体主键/业务键提示(仅监控展示兜底)。
+    /// </summary>
+    private static readonly Dictionary<string, (string SourceRowId, string SourceBizKeyExpr)> KnownStagingEntityKeys =
+        new(StringComparer.OrdinalIgnoreCase)
+        {
+            ["S4_IQC_RECEIPT"] = ("RecID", "CONCAT(IFNULL(s.`Domain`,''), ':', IFNULL(s.`Receiver`,''), ':', IFNULL(s.`Line`,''))"),
+            ["S4_SHIPMENT_EXEC"] = ("id", "CONCAT(IFNULL(s.`glid`,''), ':', IFNULL(s.`id`,''))"),
+            ["S4_RETURN_EXEC"] = ("Id", "s.`dsnum`"),
+            ["S4_SHORTAGE_EXEC"] = ("id", "CONCAT(IFNULL(s.`work_order`,''), ':', IFNULL(s.`component_item_code`,''))")
+        };
+
+    private static List<MdpLineageFieldMappingRow> BuildFallbackFieldMappings(MdpLineageEntityRow entity, string? batchId)
+    {
+        var sourceRowId = entity.IncrColumn;
+        string? sourceBizKeyExpr = null;
+        if (!string.IsNullOrWhiteSpace(entity.EntityCode) &&
+            KnownStagingEntityKeys.TryGetValue(entity.EntityCode, out var known))
+        {
+            sourceRowId ??= known.SourceRowId;
+            sourceBizKeyExpr = known.SourceBizKeyExpr;
+        }
+
+        var fallbackNote = "当前实体未配置逐字段映射,贴源同步保留源行 raw_data";
+        var mappings = new List<MdpLineageFieldMappingRow>();
+        var sort = 10;
+
+        void Add(string sourceField, string targetField, string fieldType, string? transformScript, string? constValue = null, bool isRequired = false)
+        {
+            mappings.Add(new MdpLineageFieldMappingRow
+            {
+                EntityId = entity.Id,
+                SourceField = sourceField,
+                TargetField = targetField,
+                FieldType = fieldType,
+                TransformScript = transformScript,
+                ConstValue = constValue,
+                IsRequired = isRequired,
+                SortOrder = sort,
+                MappingSource = "FALLBACK",
+                IsFallback = true
+            });
+            sort += 10;
+        }
+
+        Add("tenant_id", "tenant_id", "DIRECT", "当前租户或全局租户兜底");
+        Add($"CONST:{entity.SourceCode ?? "AIDOP"}", "source_system", "CONST", "来源数据源编码", entity.SourceCode ?? "AIDOP", isRequired: true);
+        Add($"CONST:{entity.SourceTableName ?? entity.SourceApiPath ?? "--"}", "source_table", "CONST", "源表名", entity.SourceTableName ?? entity.SourceApiPath, isRequired: true);
+
+        if (!string.IsNullOrWhiteSpace(sourceRowId))
+            Add(sourceRowId, "source_row_id", "DIRECT", "来自 mdp_entity.incr_column 或实体主键配置");
+        else
+            Add("--", "source_row_id", "DIRECT", fallbackNote);
+
+        if (!string.IsNullOrWhiteSpace(sourceBizKeyExpr))
+            Add(sourceBizKeyExpr, "source_biz_key", "EXPR", "来自实体业务键表达式", isRequired: true);
+        else
+            Add("--", "source_biz_key", "EXPR", fallbackNote);
+
+        Add("*", "raw_data", "JSON", "源行整行 JSON");
+        Add("CONST", "sync_batch_id", "CONST", "当前同步批次", string.IsNullOrWhiteSpace(batchId) ? "当前同步批次" : batchId, isRequired: true);
+        Add("NOW()", "sync_time", "CONST", "同步时间", isRequired: true);
+
+        return mappings;
+    }
+
+    private static List<MdpLineageStageRow> BuildGenericStages(string? moduleCode)
+    {
+        if (string.Equals(moduleCode, "S1", StringComparison.OrdinalIgnoreCase))
+            return BuildOrderDeliveryStages(JobCatalog["S1_MDP_SYNC_TRANSFORM"]);
+        if (string.Equals(moduleCode, "S2", StringComparison.OrdinalIgnoreCase))
+            return BuildWorkScheduleStages(JobCatalog["S2_MDP_SYNC_TRANSFORM"]);
+        if (string.Equals(moduleCode, "S3", StringComparison.OrdinalIgnoreCase))
+            return BuildSupplyPurchaseStages(JobCatalog["S3_MDP_SYNC_TRANSFORM"]);
+        if (string.Equals(moduleCode, "S4", StringComparison.OrdinalIgnoreCase))
+            return BuildPurchaseExecutionStages(JobCatalog["S4_MDP_SYNC_TRANSFORM"]);
+
+        return new List<MdpLineageStageRow>
+        {
+            new() { StageCode = "STAGING", StageName = "贴源同步", Layer = "mdp_stg", Description = "按 mdp_entity 登记源对象抽取数据。", InputObjects = "源对象", OutputObjects = "mdp_stg_*", Execution = "MDP 同步服务" },
+            new() { StageCode = "STANDARD", StageName = "标准层转换", Layer = "mdp_std", Description = "标准层/DWD/KPI 当前由后端 Service 承载。", InputObjects = "mdp_stg_*", OutputObjects = "mdp_std_* / dwd_* / 指标表", Execution = "MDP 转换服务" }
+        };
+    }
+
+    private static string? ResolveModuleCodeFromJobCode(string? jobCode)
+    {
+        if (string.IsNullOrWhiteSpace(jobCode))
+            return null;
+
+        return ModuleJobCodes.FirstOrDefault(u =>
+            string.Equals(u.Value, jobCode.Trim(), StringComparison.OrdinalIgnoreCase)).Key;
+    }
+
     private static string? ResolveJobCode(string? moduleCode, string? jobCode)
     {
         if (!string.IsNullOrWhiteSpace(jobCode))
@@ -271,48 +614,6 @@ public class MdpMonitorService : IDynamicApiController, ITransient
         return string.Join(" / ", new[] { dbType, hostPart, databaseObject }.Where(u => !string.IsNullOrWhiteSpace(u)));
     }
 
-    private static List<MdpLineageStageRow> BuildStageDescriptions(string moduleCode)
-    {
-        if (string.Equals(moduleCode, "S1", StringComparison.OrdinalIgnoreCase))
-        {
-            return new List<MdpLineageStageRow>
-            {
-                new() { StageCode = "STAGING", StageName = "贴源同步", Layer = "mdp_stg", Description = "按 mdp_entity 登记的 S1 源对象抽取数据,保留 raw_data JSON 便于追溯。", InputObjects = "旧系统 / 当前库源对象", OutputObjects = "mdp_stg_so, mdp_stg_ship_trans", Execution = "S1MdpSyncTransformService.SyncStagingAsync" },
-                new() { StageCode = "STANDARD", StageName = "标准层转换", Layer = "mdp_std", Description = "解析贴源 raw_data,做字段标准化、租户兜底和幂等写入。", InputObjects = "mdp_stg_so, mdp_stg_ship_trans", OutputObjects = "mdp_std_so, mdp_std_ship_trans", Execution = "S1MdpSyncTransformService.BuildStandardCommands" },
-                new() { StageCode = "DWD", StageName = "DWD宽表", Layer = "dwd", Description = "沉淀 S1 订单交付事实,供订单交付、看板和诊断读取。", InputObjects = "mdp_std_so, mdp_std_ship_trans", OutputObjects = "dwd_ship_trans", Execution = "S1MdpSyncTransformService.BuildDwdAsync" },
-                new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = "计算 S1 L1 指标并写入统一指标值表。", InputObjects = "mdp_std_so, dwd_ship_trans", OutputObjects = "ado_s9_kpi_value_l1_day", Execution = "S1MdpSyncTransformService.BuildS1KpiValuesAsync" }
-            };
-        }
-
-        if (string.Equals(moduleCode, "S3", StringComparison.OrdinalIgnoreCase))
-        {
-            return new List<MdpLineageStageRow>
-            {
-                new() { StageCode = "STAGING", StageName = "贴源同步", Layer = "mdp_stg", Description = "按 mdp_entity 登记的 S3 源对象抽取数据,保留 raw_data JSON 便于追溯。", InputObjects = "S3 源对象", OutputObjects = "mdp_stg_*", Execution = "S3MdpSyncTransformService.SyncStagingAsync" },
-                new() { StageCode = "STANDARD", StageName = "标准层转换", Layer = "mdp_std", Description = "将供应、物料、采购、交付等对象标准化。", InputObjects = "mdp_stg_*", OutputObjects = "mdp_std_*", Execution = "S3MdpSyncTransformService.BuildStandardCommands" },
-                new() { StageCode = "DWD", StageName = "DWD宽表", Layer = "dwd", Description = "生成供应交付、齐套、风险等分析宽表。", InputObjects = "mdp_std_*", OutputObjects = "dwd_supplier_delivery / dwd_material_readiness 等", Execution = "S3MdpSyncTransformService.BuildDwdAsync" },
-                new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = "写入 S3 供应协同指标。", InputObjects = "mdp_std_* / dwd_*", OutputObjects = "ado_s9_kpi_value_*", Execution = "S3MdpSyncTransformService.BuildS3KpiValuesAsync" }
-            };
-        }
-
-        if (string.Equals(moduleCode, "S4", StringComparison.OrdinalIgnoreCase))
-        {
-            return new List<MdpLineageStageRow>
-            {
-                new() { StageCode = "STAGING", StageName = "S4 贴源同步", Layer = "mdp_stg_s4_*", Description = "S4 专属 IQC/发货/退货/欠料贴源;共享采购主链仍由 S3 维护,S4 同时统计共享贴源行数。", InputObjects = "PurOrdRctDetail / scm_shdzb / srm_polist_ds / dwd_material_shortage", OutputObjects = "mdp_stg_s4_iqc / mdp_stg_s4_shipment / mdp_stg_s4_return / mdp_stg_s4_shortage", Execution = "S4MdpSyncTransformService.SyncStagingAsync" },
-                new() { StageCode = "STANDARD", StageName = "S4 标准层转换", Layer = "mdp_std_s4_*", Description = "将 S4 贴源对象标准化,并统计 S3 共享标准层行数。", InputObjects = "mdp_stg_s4_*", OutputObjects = "mdp_std_s4_iqc / mdp_std_s4_shipment / mdp_std_s4_return / mdp_std_s4_shortage", Execution = "S4MdpSyncTransformService.BuildStandardCommands" },
-                new() { StageCode = "DWD", StageName = "采购执行宽表", Layer = "dwd", Description = "写入 dwd_s4_purchase_execution、dwd_po_trans(扩展字段)、dwd_qc_trans。", InputObjects = "dwd_supplier_delivery / mdp_std_s4_*", OutputObjects = "dwd_s4_purchase_execution / dwd_po_trans / dwd_qc_trans", Execution = "S4MdpSyncTransformService.BuildDwdAsync" },
-                new() { StageCode = "KPI", StageName = "指标写入", Layer = "ado_s9", Description = "写入 S4 采购执行 L1/L2/L3 指标(S4_L1_001~004、L2/L3 全量)。", InputObjects = "mdp_std_delivery_schedule / dwd_supplier_delivery / dwd_s4_purchase_execution", OutputObjects = "ado_s9_kpi_value_l1/l2/l3_day", Execution = "S4MdpSyncTransformService.BuildS4KpiValuesAsync" }
-            };
-        }
-
-        return new List<MdpLineageStageRow>
-        {
-            new() { StageCode = "STAGING", StageName = "贴源同步", Layer = "mdp_stg", Description = "按 mdp_entity 登记源对象抽取数据。", InputObjects = "源对象", OutputObjects = "mdp_stg_*", Execution = "模块 MDP 同步服务" },
-            new() { StageCode = "STANDARD", StageName = "标准层转换", Layer = "mdp_std", Description = "标准层/DWD/KPI 当前由模块后端 Service 承载。", InputObjects = "mdp_stg_*", OutputObjects = "mdp_std_* / dwd_* / 指标表", Execution = "模块 MDP 转换服务" }
-        };
-    }
-
     private static string SelectColumnsSql()
     {
         return """
@@ -324,8 +625,19 @@ public class MdpMonitorService : IDynamicApiController, ITransient
     }
 }
 
+public sealed record MdpJobCatalogItem(
+    string JobCode,
+    string BusinessDomainCode,
+    string BusinessDomainName,
+    string[] ConsumerModules,
+    string ScopeType,
+    string DisplayName,
+    string? ScheduleJobId);
+
 public class MdpMonitorQueryInput
 {
+    public string? BusinessDomainCode { get; set; }
+    public string? ConsumerModule { get; set; }
     public string? ModuleCode { get; set; }
     public string? JobCode { get; set; }
     public string? BatchId { get; set; }
@@ -363,6 +675,12 @@ public sealed class MdpMonitorRunLogRow
     public string? SummaryJson { get; set; }
     public DateTime? CreateTime { get; set; }
     public DateTime? UpdateTime { get; set; }
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? ScopeType { get; set; }
+    public string? DisplayName { get; set; }
+    public string? ScheduleJobId { get; set; }
 }
 
 public sealed class MdpLineageOutput
@@ -370,6 +688,12 @@ public sealed class MdpLineageOutput
     public string? ModuleCode { get; set; }
     public string? JobCode { get; set; }
     public string? BatchId { get; set; }
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? ScopeType { get; set; }
+    public string? DisplayName { get; set; }
+    public string? ScheduleJobId { get; set; }
     public List<MdpLineageStageRow> Stages { get; set; } = new();
     public List<MdpLineageEntityRow> Entities { get; set; } = new();
 }
@@ -427,6 +751,8 @@ public sealed class MdpLineageFieldMappingRow
     public bool IsRequired { get; set; }
     public string? DefaultValue { get; set; }
     public int SortOrder { get; set; }
+    public string? MappingSource { get; set; }
+    public bool IsFallback { get; set; }
 }
 
 public sealed class MdpLineageSyncLogRow

+ 3 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Order/S1MdpMonitorService.cs

@@ -25,7 +25,7 @@ public class S1MdpMonitorService : IDynamicApiController, ITransient
     {
         var tenantId = _userManager.TenantId;
         return await _db.Ado.SqlQuerySingleAsync<S1MdpRunLogRow>(
-            $"{SelectColumnsSql()} FROM mdp_transform_run_log WHERE job_code=@JobCode AND tenant_id=@TenantId ORDER BY start_time DESC, id DESC LIMIT 1",
+            $"{SelectColumnsSql()} FROM mdp_transform_run_log WHERE job_code=@JobCode AND {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)} ORDER BY start_time DESC, id DESC LIMIT 1",
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@TenantId", tenantId))
             ?? new S1MdpRunLogRow();
@@ -39,7 +39,7 @@ public class S1MdpMonitorService : IDynamicApiController, ITransient
         var page = input.Page <= 0 ? 1 : input.Page;
         var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
         var offset = (page - 1) * pageSize;
-        var where = new List<string> { "job_code=@JobCode", "tenant_id=@TenantId" };
+        var where = new List<string> { "job_code=@JobCode", MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId) };
         var pars = new List<SugarParameter> { new("@JobCode", JobCode), new("@TenantId", tenantId) };
 
         if (!string.IsNullOrWhiteSpace(input.BatchId))
@@ -84,7 +84,7 @@ public class S1MdpMonitorService : IDynamicApiController, ITransient
     {
         var tenantId = _userManager.TenantId;
         var row = await _db.Ado.SqlQuerySingleAsync<S1MdpRunLogRow>(
-            $"{SelectColumnsSql()} FROM mdp_transform_run_log WHERE id=@Id AND job_code=@JobCode AND tenant_id=@TenantId LIMIT 1",
+            $"{SelectColumnsSql()} FROM mdp_transform_run_log WHERE id=@Id AND job_code=@JobCode AND {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)} LIMIT 1",
             new SugarParameter("@Id", id),
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@TenantId", tenantId));

+ 23 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/SmartOps/SmartOpsKpiAtomicQueryService.cs

@@ -67,11 +67,12 @@ public class SmartOpsKpiAtomicQueryService : ITransient
 
             var valueSql = BuildAggregateSql(rule.AggregationType, groupByDate: false);
             var whereSql = BuildWhereSql(filter);
+            var factorySql = BuildFactoryWhereSql(filter);
             var sql = $"""
                 SELECT {valueSql} AS MetricValue
                 FROM ado_smart_ops_kpi_atomic_day
                 WHERE tenant_id = @tenantId
-                  AND factory_id = @factoryId
+                  {factorySql}
                   AND domain_code = @domainCode
                   AND metric_code = @metricCode
                   AND is_deleted = 0
@@ -91,7 +92,7 @@ public class SmartOpsKpiAtomicQueryService : ITransient
                 SELECT stat_date AS StatDate, {BuildAggregateSql(rule.AggregationType, groupByDate: true)} AS MetricValue
                 FROM ado_smart_ops_kpi_atomic_day
                 WHERE tenant_id = @tenantId
-                  AND factory_id = @factoryId
+                  {factorySql}
                   AND domain_code = @domainCode
                   AND metric_code = @metricCode
                   AND is_deleted = 0
@@ -137,6 +138,26 @@ public class SmartOpsKpiAtomicQueryService : ITransient
         };
     }
 
+    private static string BuildFactoryWhereSql(SmartOpsDashboardFilter filter)
+    {
+        // UAT/多工厂租户下 atomic 行的 factory_id 常与汇总层默认 factoryId=1 不一致;
+        // 有业务维度筛选时按 tenant 聚合,避免筛选命中 0 行后回退模块汇总。
+        return HasBusinessDimensionFilter(filter) ? string.Empty : "AND factory_id = @factoryId";
+    }
+
+    private static bool HasBusinessDimensionFilter(SmartOpsDashboardFilter filter) =>
+        !string.IsNullOrWhiteSpace(filter.Customer) ||
+        !string.IsNullOrWhiteSpace(filter.Product) ||
+        !string.IsNullOrWhiteSpace(filter.OrderNo) ||
+        !string.IsNullOrWhiteSpace(filter.ProductionLine) ||
+        !string.IsNullOrWhiteSpace(filter.Supplier) ||
+        !string.IsNullOrWhiteSpace(filter.Material) ||
+        !string.IsNullOrWhiteSpace(filter.PoNo) ||
+        !string.IsNullOrWhiteSpace(filter.Warehouse) ||
+        !string.IsNullOrWhiteSpace(filter.WorkOrder) ||
+        !string.IsNullOrWhiteSpace(filter.Equipment) ||
+        !string.IsNullOrWhiteSpace(filter.OutboundNo);
+
     private static string BuildWhereSql(SmartOpsDashboardFilter filter)
     {
         var clauses = new List<string>();

+ 8 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/S3MdpMonitorService.cs

@@ -1,3 +1,5 @@
+using Admin.NET.Plugin.AiDOP.Order;
+
 namespace Admin.NET.Plugin.AiDOP.Supply;
 
 /// <summary>
@@ -23,7 +25,7 @@ public class S3MdpMonitorService : IDynamicApiController, ITransient
     public async Task<object> GetLatest()
     {
         var tenantId = _userManager.TenantId;
-        var tenantWhere = tenantId > 0 ? " AND tenant_id=@TenantId" : string.Empty;
+        var tenantWhere = tenantId > 0 ? $" AND {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)}" : " AND tenant_id = 0";
         var pars = new List<SugarParameter>();
         if (tenantId > 0) pars.Add(new SugarParameter("@TenantId", tenantId));
 
@@ -46,9 +48,13 @@ public class S3MdpMonitorService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         if (tenantId > 0)
         {
-            where.Add("tenant_id=@TenantId");
+            where.Add(MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId));
             pars.Add(new SugarParameter("@TenantId", tenantId));
         }
+        else
+        {
+            where.Add("tenant_id = 0");
+        }
 
         if (!string.IsNullOrWhiteSpace(input.BatchId))
         {