Переглянути джерело

feat(data-platform): sync config center with entity and field mapping CRUD

Build mdp_sync_task config tables, S1-S4 seeds, config APIs, sync-tasks page,
entity/field-mapping editors; menu renamed to 同步配置中心.
chore: bump version Web 2.4.189 / server 1.0.163
skygu 3 місяців тому
батько
коміт
aa1c49080d

+ 1 - 1
Web/package.json

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

+ 223 - 0
Web/src/views/aidop/data-platform/api/syncTasks.ts

@@ -0,0 +1,223 @@
+import service from '/@/utils/request';
+import type { Paged } from './mdpMonitor';
+
+export interface MdpSyncTaskRow {
+	id: number;
+	tenantId?: number;
+	taskCode: string;
+	taskName: string;
+	taskType: string;
+	businessDomainCode?: string | null;
+	businessDomainName?: string | null;
+	consumerModules?: string | null;
+	sourceSystemCode?: string | null;
+	serviceKey?: string | null;
+	jobCode?: string | null;
+	scheduleJobId?: string | null;
+	scheduleMode?: string | null;
+	status: number;
+	configVersion: number;
+	description?: string | null;
+	autoEnabled?: boolean;
+	manualEnabled?: boolean;
+	lastRunStatus?: string | null;
+	lastRunTime?: string | null;
+}
+
+export interface MdpSyncTaskListQuery {
+	keyword?: string;
+	businessDomainCode?: string;
+	status?: number;
+	page?: number;
+	pageSize?: number;
+}
+
+export interface MdpSyncTaskUpsertInput {
+	taskCode: string;
+	taskName: string;
+	taskType?: string;
+	businessDomainCode?: string | null;
+	businessDomainName?: string | null;
+	consumerModules?: string | null;
+	sourceSystemCode?: string | null;
+	serviceKey?: string | null;
+	jobCode?: string | null;
+	scheduleJobId?: string | null;
+	status?: number;
+	ownerRole?: string | null;
+	description?: string | null;
+}
+
+export interface MdpSyncTaskStepRow {
+	id?: number | null;
+	stepCode: string;
+	stepName: string;
+	stageType: string;
+	serviceMethodKey?: string | null;
+	enabled: boolean;
+	sortOrder: number;
+	description?: string | null;
+}
+
+export interface MdpSyncTaskScheduleRow {
+	taskCode: string;
+	scheduleJobId?: string | null;
+	scheduleMode?: string;
+	cronExpr?: string | null;
+	cronDesc?: string | null;
+	timezone?: string;
+	autoEnabled?: boolean;
+	manualEnabled?: boolean;
+	retryEnabled?: boolean;
+	maxRetryCount?: number;
+	retryIntervalSeconds?: number;
+	timeoutSeconds?: number;
+	misfirePolicy?: string | null;
+	syncWindowType?: string;
+	syncWindowValue?: string | null;
+	lastScheduleTime?: string | null;
+	nextScheduleTime?: string | null;
+	adminJobConfigJson?: string | null;
+	description?: string | null;
+}
+
+export function fetchSyncTaskList(params: MdpSyncTaskListQuery) {
+	return service.get<Paged<MdpSyncTaskRow>>('/api/DataPlatform/sync-tasks', { params }).then((r) => r.data);
+}
+
+export function fetchSyncTaskDetail(id: number) {
+	return service.get<MdpSyncTaskRow>(`/api/DataPlatform/sync-tasks/${id}`).then((r) => r.data);
+}
+
+export function createSyncTask(payload: MdpSyncTaskUpsertInput) {
+	return service.post<{ id: number }>('/api/DataPlatform/sync-tasks', payload).then((r) => r.data);
+}
+
+export function updateSyncTask(id: number, payload: MdpSyncTaskUpsertInput) {
+	return service.put<{ id: number }>(`/api/DataPlatform/sync-tasks/${id}`, payload).then((r) => r.data);
+}
+
+export function deleteSyncTask(id: number) {
+	return service.delete<{ id: number }>(`/api/DataPlatform/sync-tasks/${id}`).then((r) => r.data);
+}
+
+export function fetchSyncTaskSteps(taskCode: string) {
+	return service.get<MdpSyncTaskStepRow[]>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/steps`).then((r) => r.data);
+}
+
+export function saveSyncTaskSteps(taskCode: string, steps: MdpSyncTaskStepRow[]) {
+	return service.put<{ count: number }>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/steps`, steps).then((r) => r.data);
+}
+
+export function fetchSyncTaskSchedule(taskCode: string) {
+	return service.get<MdpSyncTaskScheduleRow>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/schedule`).then((r) => r.data);
+}
+
+export function saveSyncTaskSchedule(taskCode: string, payload: MdpSyncTaskScheduleRow) {
+	return service.put<MdpSyncTaskScheduleRow>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/schedule`, payload).then((r) => r.data);
+}
+
+export function syncAdminJobSnapshot(taskCode: string) {
+	return service
+		.post<{ taskCode: string; scheduleJobId?: string | null; adminJobConfigJson?: string | null }>(
+			`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/schedule/sync-admin-job`
+		)
+		.then((r) => r.data);
+}
+
+export interface MdpEntityRow {
+	id: number;
+	tenantId?: number;
+	sourceId: number;
+	sourceCode?: string | null;
+	sourceName?: string | null;
+	sourceType?: string | null;
+	entityCode: string;
+	entityName: string;
+	entityType: string;
+	sourceTableName?: string | null;
+	sourceApiPath?: string | null;
+	targetTableName?: string | null;
+	syncMode: string;
+	incrColumn?: string | null;
+	batchSize?: number;
+	jobId?: string | null;
+	status: number;
+	remark?: string | null;
+	fieldMappingCount?: number;
+}
+
+export interface MdpEntityUpsertInput {
+	entityCode: string;
+	entityName: string;
+	entityType?: string;
+	sourceId?: number;
+	sourceCode?: string | null;
+	sourceTableName?: string | null;
+	sourceApiPath?: string | null;
+	targetTableName?: string | null;
+	syncMode?: string;
+	incrColumn?: string | null;
+	batchSize?: number;
+	jobId?: string | null;
+	status?: number;
+	remark?: string | null;
+}
+
+export interface MdpFieldMappingRow {
+	id: number;
+	entityId: number;
+	sourceField: string;
+	targetField: string;
+	fieldType: string;
+	transformScript?: string | null;
+	constValue?: string | null;
+	lookupTable?: string | null;
+	isRequired: boolean;
+	defaultValue?: string | null;
+	sortOrder: number;
+}
+
+export interface MdpFieldMappingUpsertInput {
+	sourceField: string;
+	targetField: string;
+	fieldType?: string;
+	transformScript?: string | null;
+	constValue?: string | null;
+	lookupTable?: string | null;
+	isRequired?: boolean;
+	defaultValue?: string | null;
+	sortOrder?: number;
+}
+
+export function fetchSyncTaskEntities(taskCode: string) {
+	return service.get<MdpEntityRow[]>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/entities`).then((r) => r.data);
+}
+
+export function createSyncTaskEntity(taskCode: string, payload: MdpEntityUpsertInput) {
+	return service.post<{ id: number }>(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/entities`, payload).then((r) => r.data);
+}
+
+export function updateSyncEntity(id: number, payload: MdpEntityUpsertInput) {
+	return service.put<{ id: number }>(`/api/DataPlatform/entities/${id}`, payload).then((r) => r.data);
+}
+
+export function deleteSyncEntity(id: number) {
+	return service.delete<{ id: number }>(`/api/DataPlatform/entities/${id}`).then((r) => r.data);
+}
+
+export function fetchEntityFieldMappings(entityId: number) {
+	return service.get<MdpFieldMappingRow[]>(`/api/DataPlatform/entities/${entityId}/field-mappings`).then((r) => r.data);
+}
+
+export function createEntityFieldMapping(entityId: number, payload: MdpFieldMappingUpsertInput) {
+	return service.post<{ id: number }>(`/api/DataPlatform/entities/${entityId}/field-mappings`, payload).then((r) => r.data);
+}
+
+export function updateFieldMapping(id: number, payload: MdpFieldMappingUpsertInput) {
+	return service.put<{ id: number }>(`/api/DataPlatform/field-mappings/${id}`, payload).then((r) => r.data);
+}
+
+export function deleteFieldMapping(id: number) {
+	return service.delete<{ id: number }>(`/api/DataPlatform/field-mappings/${id}`).then((r) => r.data);
+}

+ 214 - 0
Web/src/views/aidop/data-platform/components/FieldMappingEditor.vue

@@ -0,0 +1,214 @@
+<template>
+	<el-drawer :model-value="visible" :title="`字段映射 - ${entity?.entityName || ''}`" size="880px" destroy-on-close @close="emit('close')">
+		<div class="toolbar">
+			<el-button type="primary" icon="ele-Plus" @click="openCreate">新增映射</el-button>
+			<el-button :loading="loading" icon="ele-Refresh" @click="loadMappings">刷新</el-button>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe size="small">
+			<el-table-column prop="sortOrder" label="序" width="55" align="center" />
+			<el-table-column prop="sourceField" label="源字段" min-width="130" show-overflow-tooltip />
+			<el-table-column prop="targetField" label="目标字段" min-width="130" show-overflow-tooltip />
+			<el-table-column prop="fieldType" label="映射类型" width="100" />
+			<el-table-column prop="transformScript" label="转换脚本" min-width="120" show-overflow-tooltip />
+			<el-table-column prop="constValue" label="常量值" width="100" show-overflow-tooltip />
+			<el-table-column prop="lookupTable" label="字典表" width="100" show-overflow-tooltip />
+			<el-table-column prop="defaultValue" label="默认值" width="90" show-overflow-tooltip />
+			<el-table-column label="必填" width="60" align="center">
+				<template #default="{ row }">{{ row.isRequired ? '是' : '否' }}</template>
+			</el-table-column>
+			<el-table-column label="操作" width="120" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" @click="confirmDelete(row)">删除</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<el-dialog v-model="formVisible" :title="formMode === 'create' ? '新增字段映射' : '编辑字段映射'" width="560px" append-to-body destroy-on-close>
+			<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+				<el-form-item label="源字段" prop="sourceField">
+					<el-input v-model="form.sourceField" placeholder="源列名或 JSONPath" />
+				</el-form-item>
+				<el-form-item label="目标字段" prop="targetField">
+					<el-input v-model="form.targetField" placeholder="目标列名" />
+				</el-form-item>
+				<el-form-item label="映射类型">
+					<el-select v-model="form.fieldType" style="width: 100%">
+						<el-option v-for="t in fieldTypes" :key="t" :label="t" :value="t" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="转换脚本">
+					<el-input v-model="form.transformScript" type="textarea" :rows="2" />
+				</el-form-item>
+				<el-form-item label="常量值">
+					<el-input v-model="form.constValue" />
+				</el-form-item>
+				<el-form-item label="字典表">
+					<el-input v-model="form.lookupTable" />
+				</el-form-item>
+				<el-form-item label="默认值">
+					<el-input v-model="form.defaultValue" />
+				</el-form-item>
+				<el-form-item label="排序">
+					<el-input-number v-model="form.sortOrder" :min="0" :step="10" />
+				</el-form-item>
+				<el-form-item label="必填">
+					<el-switch v-model="form.isRequired" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="formVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+	</el-drawer>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch } from 'vue';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import {
+	createEntityFieldMapping,
+	deleteFieldMapping,
+	fetchEntityFieldMappings,
+	updateFieldMapping,
+	type MdpEntityRow,
+	type MdpFieldMappingRow,
+	type MdpFieldMappingUpsertInput,
+} from '../api/syncTasks';
+
+const props = defineProps<{
+	visible: boolean;
+	entity: MdpEntityRow | null;
+}>();
+
+const emit = defineEmits<{
+	close: [];
+	saved: [];
+}>();
+
+const fieldTypes = ['DIRECT', 'JSONPATH', 'SCRIPT', 'CONST', 'LOOKUP'];
+const loading = ref(false);
+const saving = ref(false);
+const rows = ref<MdpFieldMappingRow[]>([]);
+const formVisible = ref(false);
+const formMode = ref<'create' | 'edit'>('create');
+const editingId = ref<number | null>(null);
+const formRef = ref<FormInstance>();
+const form = reactive<MdpFieldMappingUpsertInput>(emptyForm());
+
+const formRules: FormRules = {
+	sourceField: [{ required: true, message: '请输入源字段', trigger: 'blur' }],
+	targetField: [{ required: true, message: '请输入目标字段', trigger: 'blur' }],
+};
+
+function emptyForm(): MdpFieldMappingUpsertInput {
+	return {
+		sourceField: '',
+		targetField: '',
+		fieldType: 'DIRECT',
+		transformScript: '',
+		constValue: '',
+		lookupTable: '',
+		defaultValue: '',
+		sortOrder: 10,
+		isRequired: false,
+	};
+}
+
+async function loadMappings() {
+	if (!props.entity) return;
+	loading.value = true;
+	try {
+		rows.value = (await fetchEntityFieldMappings(props.entity.id)) ?? [];
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '加载字段映射失败');
+		rows.value = [];
+	} finally {
+		loading.value = false;
+	}
+}
+
+function openCreate() {
+	formMode.value = 'create';
+	editingId.value = null;
+	Object.assign(form, emptyForm());
+	formVisible.value = true;
+}
+
+function openEdit(row: MdpFieldMappingRow) {
+	formMode.value = 'edit';
+	editingId.value = row.id;
+	Object.assign(form, {
+		sourceField: row.sourceField,
+		targetField: row.targetField,
+		fieldType: row.fieldType,
+		transformScript: row.transformScript ?? '',
+		constValue: row.constValue ?? '',
+		lookupTable: row.lookupTable ?? '',
+		defaultValue: row.defaultValue ?? '',
+		sortOrder: row.sortOrder,
+		isRequired: row.isRequired,
+	});
+	formVisible.value = true;
+}
+
+async function submitForm() {
+	if (!props.entity) return;
+	const valid = await formRef.value?.validate().catch(() => false);
+	if (!valid) return;
+
+	saving.value = true;
+	try {
+		const payload: MdpFieldMappingUpsertInput = {
+			...form,
+			sourceField: form.sourceField.trim(),
+			targetField: form.targetField.trim(),
+			fieldType: form.fieldType || 'DIRECT',
+		};
+		if (formMode.value === 'create') {
+			await createEntityFieldMapping(props.entity.id, payload);
+			ElMessage.success('映射已添加');
+		} else if (editingId.value != null) {
+			await updateFieldMapping(editingId.value, payload);
+			ElMessage.success('映射已保存');
+		}
+		formVisible.value = false;
+		await loadMappings();
+		emit('saved');
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存失败');
+	} finally {
+		saving.value = false;
+	}
+}
+
+async function confirmDelete(row: MdpFieldMappingRow) {
+	try {
+		await ElMessageBox.confirm(`确定删除映射「${row.targetField}」?`, '确认删除', { type: 'warning' });
+		await deleteFieldMapping(row.id);
+		ElMessage.success('已删除');
+		await loadMappings();
+		emit('saved');
+	} catch {
+		/* cancelled */
+	}
+}
+
+watch(
+	() => [props.visible, props.entity?.id] as const,
+	([vis, id]) => {
+		if (vis && id) loadMappings();
+	},
+	{ immediate: true }
+);
+</script>
+
+<style scoped>
+.toolbar {
+	margin-bottom: 12px;
+	display: flex;
+	gap: 8px;
+}
+</style>

+ 250 - 0
Web/src/views/aidop/data-platform/components/SyncTaskEntityDrawer.vue

@@ -0,0 +1,250 @@
+<template>
+	<el-drawer :model-value="visible" :title="`同步实体 - ${task?.taskName || ''}`" size="920px" destroy-on-close @close="emit('close')">
+		<div class="toolbar">
+			<el-button type="primary" icon="ele-Plus" @click="openCreate">新增实体</el-button>
+			<el-button :loading="loading" icon="ele-Refresh" @click="loadEntities">刷新</el-button>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe size="small" highlight-current-row @row-click="onRowClick">
+			<el-table-column prop="entityCode" label="实体编码" min-width="150" show-overflow-tooltip />
+			<el-table-column prop="entityName" label="实体名称" min-width="140" show-overflow-tooltip />
+			<el-table-column prop="sourceTableName" label="源表/API" min-width="130" show-overflow-tooltip />
+			<el-table-column prop="targetTableName" label="目标表" min-width="140" show-overflow-tooltip />
+			<el-table-column prop="syncMode" label="模式" width="90" align="center" />
+			<el-table-column prop="fieldMappingCount" label="映射数" width="70" align="center" />
+			<el-table-column label="状态" width="70" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '启用' : '停用' }}</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" @click.stop="openMappings(row)">字段映射</el-button>
+					<el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isProtected(row)" @click.stop="confirmDelete(row)">删除</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<el-dialog v-model="formVisible" :title="formMode === 'create' ? '新增同步实体' : '编辑同步实体'" width="620px" append-to-body destroy-on-close>
+			<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
+				<el-form-item label="实体编码" prop="entityCode">
+					<el-input v-model="form.entityCode" :disabled="formMode === 'edit' && isProtectedForm" />
+				</el-form-item>
+				<el-form-item label="实体名称" prop="entityName">
+					<el-input v-model="form.entityName" />
+				</el-form-item>
+				<el-form-item label="数据源">
+					<el-input v-model="form.sourceCode" placeholder="mdp_source.source_code,如 AIDOPDEV_MYSQL" />
+				</el-form-item>
+				<el-form-item label="源表名">
+					<el-input v-model="form.sourceTableName" />
+				</el-form-item>
+				<el-form-item label="目标表">
+					<el-input v-model="form.targetTableName" />
+				</el-form-item>
+				<el-form-item label="同步模式">
+					<el-select v-model="form.syncMode" style="width: 100%">
+						<el-option label="增量 INCR" value="INCR" />
+						<el-option label="全量 FULL" value="FULL" />
+						<el-option label="分页 PAGE" value="PAGE" />
+						<el-option label="游标 CURSOR" value="CURSOR" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="增量字段">
+					<el-input v-model="form.incrColumn" />
+				</el-form-item>
+				<el-form-item label="说明">
+					<el-input v-model="form.remark" type="textarea" :rows="2" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="formVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+
+		<FieldMappingEditor
+			:visible="mappingVisible"
+			:entity="mappingEntity"
+			@close="mappingVisible = false"
+			@saved="onMappingSaved"
+		/>
+	</el-drawer>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref, watch } from 'vue';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import FieldMappingEditor from './FieldMappingEditor.vue';
+import {
+	createSyncTaskEntity,
+	deleteSyncEntity,
+	fetchSyncTaskEntities,
+	updateSyncEntity,
+	type MdpEntityRow,
+	type MdpEntityUpsertInput,
+	type MdpSyncTaskRow,
+} from '../api/syncTasks';
+
+const props = defineProps<{
+	visible: boolean;
+	task: MdpSyncTaskRow | null;
+}>();
+
+const emit = defineEmits<{
+	close: [];
+	saved: [];
+}>();
+
+const loading = ref(false);
+const saving = ref(false);
+const rows = ref<MdpEntityRow[]>([]);
+const formVisible = ref(false);
+const formMode = ref<'create' | 'edit'>('create');
+const editingId = ref<number | null>(null);
+const formRef = ref<FormInstance>();
+const form = reactive<MdpEntityUpsertInput>(emptyForm());
+const isProtectedForm = computed(() => isProtectedCode(form.entityCode));
+
+const mappingVisible = ref(false);
+const mappingEntity = ref<MdpEntityRow | null>(null);
+
+const formRules: FormRules = {
+	entityCode: [{ required: true, message: '请输入实体编码', trigger: 'blur' }],
+	entityName: [{ required: true, message: '请输入实体名称', trigger: 'blur' }],
+};
+
+function emptyForm(): MdpEntityUpsertInput {
+	return {
+		entityCode: '',
+		entityName: '',
+		entityType: 'TABLE',
+		sourceCode: 'AIDOPDEV_MYSQL',
+		sourceTableName: '',
+		targetTableName: '',
+		syncMode: 'INCR',
+		incrColumn: '',
+		status: 1,
+		remark: '',
+	};
+}
+
+function isProtectedCode(code: string) {
+	return /^S[1-4]_/i.test(code);
+}
+
+function isProtected(row: MdpEntityRow) {
+	return isProtectedCode(row.entityCode) && (row.tenantId ?? 0) === 0;
+}
+
+async function loadEntities() {
+	if (!props.task) return;
+	loading.value = true;
+	try {
+		rows.value = (await fetchSyncTaskEntities(props.task.taskCode)) ?? [];
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '加载实体失败');
+		rows.value = [];
+	} finally {
+		loading.value = false;
+	}
+}
+
+function openCreate() {
+	formMode.value = 'create';
+	editingId.value = null;
+	Object.assign(form, emptyForm());
+	formVisible.value = true;
+}
+
+function openEdit(row: MdpEntityRow) {
+	formMode.value = 'edit';
+	editingId.value = row.id;
+	Object.assign(form, {
+		entityCode: row.entityCode,
+		entityName: row.entityName,
+		entityType: row.entityType,
+		sourceId: row.sourceId,
+		sourceCode: row.sourceCode ?? 'AIDOPDEV_MYSQL',
+		sourceTableName: row.sourceTableName ?? '',
+		targetTableName: row.targetTableName ?? '',
+		syncMode: row.syncMode,
+		incrColumn: row.incrColumn ?? '',
+		status: row.status,
+		remark: row.remark ?? '',
+	});
+	formVisible.value = true;
+}
+
+function openMappings(row: MdpEntityRow) {
+	mappingEntity.value = row;
+	mappingVisible.value = true;
+}
+
+function onRowClick(row: MdpEntityRow) {
+	openMappings(row);
+}
+
+async function submitForm() {
+	if (!props.task) return;
+	const valid = await formRef.value?.validate().catch(() => false);
+	if (!valid) return;
+
+	saving.value = true;
+	try {
+		const payload: MdpEntityUpsertInput = {
+			...form,
+			entityCode: form.entityCode.trim(),
+			entityName: form.entityName.trim(),
+		};
+		if (formMode.value === 'create') {
+			await createSyncTaskEntity(props.task.taskCode, payload);
+			ElMessage.success('实体已创建');
+		} else if (editingId.value != null) {
+			await updateSyncEntity(editingId.value, payload);
+			ElMessage.success('实体已保存');
+		}
+		formVisible.value = false;
+		await loadEntities();
+		emit('saved');
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存失败');
+	} finally {
+		saving.value = false;
+	}
+}
+
+async function confirmDelete(row: MdpEntityRow) {
+	try {
+		await ElMessageBox.confirm(`确定停用实体「${row.entityName}」?`, '确认删除', { type: 'warning' });
+		await deleteSyncEntity(row.id);
+		ElMessage.success('已停用');
+		await loadEntities();
+		emit('saved');
+	} catch {
+		/* cancelled */
+	}
+}
+
+function onMappingSaved() {
+	loadEntities();
+	emit('saved');
+}
+
+watch(
+	() => [props.visible, props.task?.taskCode] as const,
+	([vis, code]) => {
+		if (vis && code) loadEntities();
+	},
+	{ immediate: true }
+);
+</script>
+
+<style scoped>
+.toolbar {
+	margin-bottom: 12px;
+	display: flex;
+	gap: 8px;
+}
+</style>

+ 425 - 32
Web/src/views/aidop/data-platform/syncTasks.vue

@@ -1,73 +1,466 @@
 <template>
-	<AidopDemoShell title="数据任务管理" subtitle="DB 同步、API 入站、API 拉取、API 出站任务集中查看;调度计划跳转到 Admin.NET 任务调度维护">
+	<AidopDemoShell
+		title="数据中台同步配置中心"
+		subtitle="外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪"
+	>
 		<template #bar-right>
-			<el-button type="primary" icon="ele-AlarmClock" @click="goJob">去任务调度</el-button>
-			<el-button icon="ele-CirclePlus" plain>新建数据任务</el-button>
+			<el-button :loading="loading" icon="ele-Refresh" @click="loadList">刷新</el-button>
+			<el-button type="primary" icon="ele-CirclePlus" @click="openCreate">新建同步任务</el-button>
 		</template>
 
 		<el-alert
 			class="mb12"
-			type="success"
+			type="info"
 			show-icon
 			:closable="false"
-			title="这里不重复实现 CRON、启停和调度历史;数据任务支持入站、出站和双向集成,需要维护执行计划时跳到平台管理 / 任务调度。"
+			title="Admin.NET 任务调度作为底层引擎复用;本页维护同步业务配置,Cron 与触发器细节请通过「高级调度设置」跳转维护。"
 		/>
 
-		<el-table :data="rows" border>
-			<el-table-column prop="entityName" label="数据任务" min-width="150" />
-			<el-table-column prop="direction" label="方向" width="90" align="center">
+		<el-card shadow="never" class="mb12">
+			<el-form :inline="true" :model="query" @submit.prevent>
+				<el-form-item label="关键词">
+					<el-input v-model="query.keyword" clearable placeholder="任务编码/名称/业务域" style="width: 220px" @keyup.enter="doSearch" />
+				</el-form-item>
+				<el-form-item label="业务域">
+					<el-select v-model="query.businessDomainCode" clearable placeholder="全部" style="width: 170px">
+						<el-option v-for="item in domainOptions" :key="item.value" :label="item.label" :value="item.value" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" icon="ele-Search" @click="doSearch">查询</el-button>
+					<el-button icon="ele-RefreshLeft" @click="resetQuery">重置</el-button>
+				</el-form-item>
+			</el-form>
+		</el-card>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%">
+			<el-table-column prop="taskName" label="同步任务" min-width="180" fixed="left" show-overflow-tooltip />
+			<el-table-column prop="taskType" label="类型" width="120" />
+			<el-table-column prop="businessDomainName" label="业务域" min-width="120" show-overflow-tooltip />
+			<el-table-column label="消费模块" min-width="160" show-overflow-tooltip>
+				<template #default="{ row }">
+					<el-tag v-for="m in splitModules(row.consumerModules)" :key="m" size="small" class="module-tag">{{ m }}</el-tag>
+					<span v-if="!splitModules(row.consumerModules).length">--</span>
+				</template>
+			</el-table-column>
+			<el-table-column prop="serviceKey" label="Service" min-width="170" show-overflow-tooltip />
+			<el-table-column prop="scheduleJobId" label="调度任务" min-width="200" show-overflow-tooltip />
+			<el-table-column prop="scheduleMode" label="调度模式" width="100" align="center" />
+			<el-table-column label="状态" width="90" align="center">
 				<template #default="{ row }">
-					<el-tag size="small" effect="plain" :type="row.directionType">{{ row.direction }}</el-tag>
+					<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '启用' : '停用' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column prop="protocol" label="协议" width="100" />
-			<el-table-column prop="sourceName" label="数据源" width="140" />
-			<el-table-column prop="targetTable" label="目标表 / 服务" min-width="160" show-overflow-tooltip />
-			<el-table-column prop="syncMode" label="模式" width="90" align="center" />
-			<el-table-column prop="incrColumn" label="增量字段" width="130" />
-			<el-table-column prop="jobId" label="关联调度任务" min-width="220" show-overflow-tooltip />
-			<el-table-column prop="lastRun" label="最近执行" width="150" />
-			<el-table-column prop="status" label="状态" width="110" align="center">
+			<el-table-column prop="configVersion" label="配置版本" width="90" align="center" />
+			<el-table-column label="最近执行" width="170">
+				<template #default="{ row }">{{ fmtDateTime(row.lastRunTime) }}</template>
+			</el-table-column>
+			<el-table-column label="最近结果" width="100" align="center">
 				<template #default="{ row }">
-					<el-tag :type="row.statusType" effect="plain">{{ row.status }}</el-tag>
+					<el-tag v-if="row.lastRunStatus" :type="runStatusTag(row.lastRunStatus)" size="small">{{ runStatusText(row.lastRunStatus) }}</el-tag>
+					<span v-else>--</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="230" fixed="right" align="center">
+			<el-table-column label="操作" width="360" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="goJob">调度管理</el-button>
-					<el-button link type="primary" @click="goLogs(row.entityCode)">日志</el-button>
-					<el-button link type="primary">映射配置</el-button>
+					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="primary" @click="openSteps(row)">步骤</el-button>
+					<el-button link type="primary" @click="openEntities(row)">实体</el-button>
+					<el-button link type="primary" @click="openEntities(row)">映射</el-button>
+					<el-dropdown trigger="click" @command="(cmd: string) => onMoreCommand(cmd, row)">
+						<el-button link type="primary">
+							更多
+							<el-icon class="el-icon--right"><ele-ArrowDown /></el-icon>
+						</el-button>
+						<template #dropdown>
+							<el-dropdown-menu>
+								<el-dropdown-item command="params" disabled>参数(待实现)</el-dropdown-item>
+								<el-dropdown-item command="formula" disabled>公式(待实现)</el-dropdown-item>
+								<el-dropdown-item command="schedule" disabled>调度策略(待实现)</el-dropdown-item>
+								<el-dropdown-item divided command="logs">日志</el-dropdown-item>
+								<el-dropdown-item command="lineage">链路</el-dropdown-item>
+								<el-dropdown-item command="adminJob" :disabled="!row.scheduleJobId">高级调度设置</el-dropdown-item>
+								<el-dropdown-item divided command="delete" :disabled="isProtected(row)">删除</el-dropdown-item>
+							</el-dropdown-menu>
+						</template>
+					</el-dropdown>
 				</template>
 			</el-table-column>
 		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				layout="total, sizes, prev, pager, next"
+				:page-sizes="[10, 20, 50]"
+				@current-change="loadList"
+				@size-change="onPageSizeChange"
+			/>
+		</div>
+
+		<el-dialog v-model="formVisible" :title="formMode === 'create' ? '新建同步任务' : '编辑同步任务'" width="640px" destroy-on-close>
+			<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
+				<el-form-item label="任务编码" prop="taskCode">
+					<el-input v-model="form.taskCode" :disabled="formMode === 'edit' && isProtectedForm" placeholder="唯一编码,如 MY_SYNC_TASK" />
+				</el-form-item>
+				<el-form-item label="任务名称" prop="taskName">
+					<el-input v-model="form.taskName" placeholder="业务可读名称" />
+				</el-form-item>
+				<el-form-item label="任务类型">
+					<el-select v-model="form.taskType" style="width: 100%">
+						<el-option label="Service 同步" value="SERVICE_SYNC" />
+						<el-option label="DB 同步" value="DB_SYNC" />
+						<el-option label="API 拉取" value="API_PULL" />
+						<el-option label="API 入站" value="API_PUSH" />
+						<el-option label="API 出站" value="API_OUT" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="业务域编码">
+					<el-input v-model="form.businessDomainCode" placeholder="如 order_delivery" />
+				</el-form-item>
+				<el-form-item label="业务域名称">
+					<el-input v-model="form.businessDomainName" placeholder="如 订单交付域" />
+				</el-form-item>
+				<el-form-item label="消费模块">
+					<el-input v-model="form.consumerModules" placeholder="逗号分隔,如 S1,S2,S3" />
+				</el-form-item>
+				<el-form-item label="来源系统">
+					<el-input v-model="form.sourceSystemCode" placeholder="mdp_source.source_code" />
+				</el-form-item>
+				<el-form-item label="Service Key">
+					<el-input v-model="form.serviceKey" placeholder="底层 Service 标识" />
+				</el-form-item>
+				<el-form-item label="Job Code">
+					<el-input v-model="form.jobCode" placeholder="运行日志 job_code,默认同任务编码" />
+				</el-form-item>
+				<el-form-item label="调度任务 ID">
+					<el-input v-model="form.scheduleJobId" placeholder="Admin.NET jobId,如 job_s1_mdp_sync_transform" />
+				</el-form-item>
+				<el-form-item label="状态">
+					<el-switch v-model="formEnabled" active-text="启用" inactive-text="停用" />
+				</el-form-item>
+				<el-form-item label="说明">
+					<el-input v-model="form.description" type="textarea" :rows="3" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="formVisible = false">取消</el-button>
+				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
+			</template>
+		</el-dialog>
+
+		<el-drawer v-model="stepsVisible" :title="`同步步骤 - ${activeTask?.taskName || ''}`" size="520px" destroy-on-close>
+			<el-table :data="steps" v-loading="stepsLoading" border size="small">
+				<el-table-column prop="sortOrder" label="序" width="50" align="center" />
+				<el-table-column prop="stepName" label="步骤" min-width="120" />
+				<el-table-column prop="stageType" label="阶段" width="120" />
+				<el-table-column label="启用" width="70" align="center">
+					<template #default="{ row }">
+						<el-switch v-model="row.enabled" size="small" />
+					</template>
+				</el-table-column>
+			</el-table>
+			<div class="drawer-footer">
+				<el-button @click="stepsVisible = false">关闭</el-button>
+				<el-button type="primary" :loading="stepsSaving" @click="saveSteps">保存步骤</el-button>
+			</div>
+		</el-drawer>
+
+		<SyncTaskEntityDrawer :visible="entityDrawerVisible" :task="entityTask" @close="entityDrawerVisible = false" @saved="loadList" />
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopDataPlatformSyncTasks">
+import { computed, onMounted, reactive, ref } from 'vue';
 import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
+import SyncTaskEntityDrawer from './components/SyncTaskEntityDrawer.vue';
+import {
+	createSyncTask,
+	deleteSyncTask,
+	fetchSyncTaskList,
+	fetchSyncTaskSteps,
+	saveSyncTaskSteps,
+	updateSyncTask,
+	type MdpSyncTaskRow,
+	type MdpSyncTaskStepRow,
+	type MdpSyncTaskUpsertInput,
+} from './api/syncTasks';
+
+const PROTECTED_CODES = new Set(['S1_MDP_SYNC_TRANSFORM', 'S2_MDP_SYNC_TRANSFORM', 'S3_MDP_SYNC_TRANSFORM', 'S4_MDP_SYNC_TRANSFORM']);
 
 const router = useRouter();
+const loading = ref(false);
+const saving = ref(false);
+const rows = ref<MdpSyncTaskRow[]>([]);
+const total = ref(0);
 
-const rows = [
-	{ entityCode: 'S3_DEMAND', entityName: 'S3 需求计划', direction: '入站', directionType: 'success', protocol: 'DB', sourceName: '旧 SQL Server', targetTable: 'ado_s3_demand_schedule', syncMode: 'INCR', incrColumn: 'update_time', jobId: 'job_s3_mdp_sync_transform', lastRun: '10 分钟前', status: '运行中', statusType: 'success' },
-	{ entityCode: 'S3_PO', entityName: 'S3 采购订单', direction: '入站', directionType: 'success', protocol: 'DB', sourceName: '旧 SQL Server', targetTable: 'ado_s3_purchase_order', syncMode: 'INCR', incrColumn: 'modified_at', jobId: 'job_s3_mdp_sync_transform', lastRun: '28 分钟前', status: '正常', statusType: 'success' },
-	{ entityCode: 'ITEM_MASTER', entityName: '物料主数据 API 拉取', direction: '入站', directionType: 'success', protocol: 'REST', sourceName: 'ERP 主库', targetTable: 'mdp_std_item', syncMode: 'PAGE', incrColumn: 'cursor', jobId: 'job_mdp_item_master_sync', lastRun: '1 小时前', status: '部分成功', statusType: 'warning' },
-	{ entityCode: 'WMS_STOCK', entityName: '库存快照 API 入站', direction: '入站', directionType: 'warning', protocol: 'Webhook', sourceName: '仓储系统 API', targetTable: 'mdp_stg_wms_stock', syncMode: 'PUSH', incrColumn: '-', jobId: 'job_mdp_wms_stock_sync', lastRun: '2 小时前', status: '待处理', statusType: 'danger' },
-	{ entityCode: 'BI_METRIC_API', entityName: '指标服务 API 出站', direction: '出站', directionType: 'primary', protocol: 'REST', sourceName: '数据中台', targetTable: '/api/mdp/metrics', syncMode: 'SERVICE', incrColumn: '-', jobId: '-', lastRun: '实时', status: '规划中', statusType: 'info' },
+const query = reactive({
+	keyword: '',
+	businessDomainCode: '',
+	page: 1,
+	pageSize: 10,
+});
+
+const domainOptions = [
+	{ label: '订单交付域', value: 'order_delivery' },
+	{ label: '工单排程域', value: 'work_schedule' },
+	{ label: '供应采购域', value: 'supply_purchase' },
+	{ label: '采购执行域', value: 'purchase_execution' },
 ];
 
-function goJob() {
-	router.push('/platform/job');
+const formVisible = ref(false);
+const formMode = ref<'create' | 'edit'>('create');
+const editingId = ref<number | null>(null);
+const formRef = ref<FormInstance>();
+const form = reactive<MdpSyncTaskUpsertInput>(emptyForm());
+const isProtectedForm = computed(() => PROTECTED_CODES.has(form.taskCode));
+
+const formEnabled = computed({
+	get: () => (form.status ?? 1) === 1,
+	set: (v: boolean) => {
+		form.status = v ? 1 : 0;
+	},
+});
+
+const formRules: FormRules = {
+	taskCode: [{ required: true, message: '请输入任务编码', trigger: 'blur' }],
+	taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
+};
+
+const stepsVisible = ref(false);
+const stepsLoading = ref(false);
+const stepsSaving = ref(false);
+const steps = ref<MdpSyncTaskStepRow[]>([]);
+const activeTask = ref<MdpSyncTaskRow | null>(null);
+
+const entityDrawerVisible = ref(false);
+const entityTask = ref<MdpSyncTaskRow | null>(null);
+
+function openEntities(row: MdpSyncTaskRow) {
+	entityTask.value = row;
+	entityDrawerVisible.value = true;
+}
+
+function emptyForm(): MdpSyncTaskUpsertInput {
+	return {
+		taskCode: '',
+		taskName: '',
+		taskType: 'SERVICE_SYNC',
+		businessDomainCode: '',
+		businessDomainName: '',
+		consumerModules: '',
+		sourceSystemCode: '',
+		serviceKey: '',
+		jobCode: '',
+		scheduleJobId: '',
+		status: 1,
+		description: '',
+	};
+}
+
+function splitModules(value?: string | null) {
+	if (!value) return [];
+	return value.split(',').map((s) => s.trim()).filter(Boolean);
+}
+
+function fmtDateTime(value?: string | null) {
+	if (!value) return '--';
+	return value.replace('T', ' ').slice(0, 19);
+}
+
+function runStatusText(status?: string | null) {
+	if (!status) return '--';
+	const map: Record<string, string> = { RUNNING: '运行中', SUCCESS: '成功', FAILED: '失败' };
+	return map[status] ?? status;
+}
+
+function runStatusTag(status?: string | null) {
+	if (status === 'SUCCESS') return 'success';
+	if (status === 'FAILED') return 'danger';
+	if (status === 'RUNNING') return 'warning';
+	return 'info';
+}
+
+function isProtected(row: MdpSyncTaskRow) {
+	return PROTECTED_CODES.has(row.taskCode) && (row.tenantId ?? 0) === 0;
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchSyncTaskList({
+			keyword: query.keyword || undefined,
+			businessDomainCode: query.businessDomainCode || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list ?? [];
+		total.value = data.total ?? 0;
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '加载同步任务失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+
+function resetQuery() {
+	query.keyword = '';
+	query.businessDomainCode = '';
+	query.page = 1;
+	loadList();
+}
+
+function onPageSizeChange() {
+	query.page = 1;
+	loadList();
+}
+
+function openCreate() {
+	formMode.value = 'create';
+	editingId.value = null;
+	Object.assign(form, emptyForm());
+	formVisible.value = true;
+}
+
+function openEdit(row: MdpSyncTaskRow) {
+	formMode.value = 'edit';
+	editingId.value = row.id;
+	Object.assign(form, {
+		taskCode: row.taskCode,
+		taskName: row.taskName,
+		taskType: row.taskType || 'SERVICE_SYNC',
+		businessDomainCode: row.businessDomainCode ?? '',
+		businessDomainName: row.businessDomainName ?? '',
+		consumerModules: row.consumerModules ?? '',
+		sourceSystemCode: row.sourceSystemCode ?? '',
+		serviceKey: row.serviceKey ?? '',
+		jobCode: row.jobCode ?? row.taskCode,
+		scheduleJobId: row.scheduleJobId ?? '',
+		status: row.status,
+		description: row.description ?? '',
+	});
+	formVisible.value = true;
+}
+
+async function submitForm() {
+	const valid = await formRef.value?.validate().catch(() => false);
+	if (!valid) return;
+
+	saving.value = true;
+	try {
+		const payload: MdpSyncTaskUpsertInput = {
+			...form,
+			taskCode: form.taskCode.trim(),
+			taskName: form.taskName.trim(),
+			jobCode: form.jobCode?.trim() || form.taskCode.trim(),
+		};
+		if (formMode.value === 'create') {
+			await createSyncTask(payload);
+			ElMessage.success('创建成功');
+		} else if (editingId.value != null) {
+			await updateSyncTask(editingId.value, payload);
+			ElMessage.success('保存成功');
+		}
+		formVisible.value = false;
+		loadList();
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存失败');
+	} finally {
+		saving.value = false;
+	}
+}
+
+async function openSteps(row: MdpSyncTaskRow) {
+	activeTask.value = row;
+	stepsVisible.value = true;
+	stepsLoading.value = true;
+	try {
+		steps.value = (await fetchSyncTaskSteps(row.taskCode)) ?? [];
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '加载步骤失败');
+		steps.value = [];
+	} finally {
+		stepsLoading.value = false;
+	}
+}
+
+async function saveSteps() {
+	if (!activeTask.value) return;
+	stepsSaving.value = true;
+	try {
+		await saveSyncTaskSteps(activeTask.value.taskCode, steps.value);
+		ElMessage.success('步骤已保存');
+		stepsVisible.value = false;
+		loadList();
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存步骤失败');
+	} finally {
+		stepsSaving.value = false;
+	}
 }
 
-function goLogs(entityCode: string) {
-	router.push({ path: '/aidop/data-platform/sync-logs', query: { entityCode } });
+function goLogs(row: MdpSyncTaskRow) {
+	router.push({ path: '/aidop/data-platform/sync-logs', query: { jobCode: row.jobCode || row.taskCode } });
 }
+
+function goLineage(row: MdpSyncTaskRow) {
+	router.push({ path: '/aidop/data-platform/mdp-monitor', query: { jobCode: row.jobCode || row.taskCode } });
+}
+
+function goAdminJob(row: MdpSyncTaskRow) {
+	if (!row.scheduleJobId) return;
+	router.push({ path: '/platform/job', query: { jobId: row.scheduleJobId } });
+}
+
+async function confirmDelete(row: MdpSyncTaskRow) {
+	try {
+		await ElMessageBox.confirm(`确定停用同步任务「${row.taskName}」?`, '确认删除', { type: 'warning' });
+		await deleteSyncTask(row.id);
+		ElMessage.success('已停用');
+		loadList();
+	} catch {
+		/* cancelled */
+	}
+}
+
+function onMoreCommand(cmd: string, row: MdpSyncTaskRow) {
+	if (cmd === 'logs') goLogs(row);
+	else if (cmd === 'lineage') goLineage(row);
+	else if (cmd === 'adminJob') goAdminJob(row);
+	else if (cmd === 'delete') confirmDelete(row);
+}
+
+onMounted(loadList);
 </script>
 
 <style scoped>
 .mb12 {
 	margin-bottom: 12px;
 }
+
+.module-tag + .module-tag {
+	margin-left: 4px;
+}
+
+.pager {
+	margin-top: 12px;
+	display: flex;
+	justify-content: flex-end;
+}
+
+.drawer-footer {
+	margin-top: 16px;
+	display: flex;
+	justify-content: flex-end;
+	gap: 8px;
+}
 </style>

+ 179 - 0
doc/plan/待执行任务列表.md

@@ -0,0 +1,179 @@
+# 待执行任务列表
+
+> 当前状态:统一待执行入口  
+> 维护日期:2026-06-12  
+> 适用对象:开发人员、大语言模型执行代理、测试负责人  
+> 使用原则:执行任务前先读本文件,再按「对应任务书」中的指定章节执行。
+
+---
+
+## 1. 使用说明
+
+本文件用于统一列明当前仍需执行、确认或暂缓的任务,避免执行代理从多份历史任务书中误判范围。
+
+执行规则:
+
+1. 本文件是当前待执行范围的优先入口。
+2. 如果任务书主体已标记为「已执行完成」,不要重做主体章节,只执行本文件列出的新增任务或指定章节。
+3. 执行前先确认任务状态,状态不是 `待执行` 的任务不要直接动手。
+4. 执行时按「对应任务书」列出的章节读取详细方案、验收用例和非目标范围。
+5. 完成后应回写本文件状态,并补充执行结果、提交号或验证结果。
+
+---
+
+## 2. 状态约定
+
+| 状态 | 含义 |
+|------|------|
+| `待执行` | 已确认需求和范围,可按对应任务书执行 |
+| `执行中` | 已有人员或模型正在处理,其他执行代理不要并行改同一范围 |
+| `已完成` | 已完成实现和基础验证 |
+| `待确认` | 方向已提出,但范围或优先级还需负责人确认 |
+| `暂缓` | 当前不执行,后续再评估 |
+
+---
+
+## 3. 当前待执行任务
+
+| 编号 | 状态 | 任务名称 | 对应任务书 | 执行范围摘要 |
+|------|------|----------|------------|--------------|
+| `TODO-001` | `已完成` | MDP运行监控关联任务调度配置 | `doc/plan/MDP运行监控按业务域与消费模块改造任务书.md` 第 12 节;`doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md` 第 14 节 | 在 MDP 运行监控列表/详情增加「查看调度配置」入口;任务调度页支持 `/platform/job?jobId=...` 自动查询 |
+| `TODO-002` | `已完成` | MDP运行监控详情页口径补齐 | `doc/plan/MDP运行监控按业务域与消费模块改造任务书.md` | 详情/链路弹窗补齐「同步业务域 + 消费模块」展示口径,保留技术追踪字段 |
+| `TODO-003` | `已完成` | MDP链路实体配置租户兼容修复 | `server/Plugins/Admin.NET.Plugin.AiDOP/Order/MdpMonitorService.cs` | 链路详情查询 `mdp_entity` 与批次 `mdp_sync_log` 时兼容当前租户和 `tenant_id=0`,恢复来源库表与目标库表显示 |
+| `TODO-004` | `已完成` | MDP链路字段映射兜底展示 | `doc/plan/MDP链路字段映射兜底展示任务书.md` | 当 `mdp_field_mapping` 无逐字段配置时,链路详情展开区返回并展示贴源通用字段映射兜底,避免展开为空 |
+| `TODO-005` | `执行中` | 数据中台同步配置中心 | `doc/plan/数据中台同步配置中心实施任务书-执行版.md` | 建设同步任务业务配置中心;**TODO-SYNC-001~003 已完成**,TODO-SYNC-004~006 待后续分包 |
+
+---
+
+## 4. TODO-001 执行提示
+
+### 4.1 目标
+
+让用户在「MDP运行监控」看到某条同步记录时,可以一键跳转到对应的「任务调度」配置,快速查看 Cron、触发器、执行方式和业务标签。
+
+### 4.2 需要阅读的任务书章节
+
+必须阅读:
+
+- `doc/plan/MDP运行监控按业务域与消费模块改造任务书.md` 第 12 节「增补任务:MDP运行监控关联任务调度配置」
+- `doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md` 第 14 节「增补任务:MDP运行监控反向关联任务调度」
+
+不要重做:
+
+- `doc/plan/MDP运行监控按业务域与消费模块改造任务书.md` 第 1-11 节主体任务
+- `doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md` 第 1-13 节主体任务
+
+### 4.3 最小实现范围
+
+后端:
+
+1. MDP job catalog 返回 `scheduleJobId`。
+2. MDP 运行监控列表、详情、latest 接口补齐 `scheduleJobId`。
+
+前端:
+
+1. `Web/src/views/aidop/data-platform/mdpMonitor.vue` 增加「查看调度配置」入口。
+2. 点击后跳转:
+
+```text
+/platform/job?jobId=job_s4_mdp_sync_transform
+```
+
+3. `Web/src/views/system/job/index.vue` 支持从 `route.query.jobId` 初始化查询条件并自动查询。
+
+### 4.4 验收重点
+
+1. 从 MDP 运行监控的 `S4_MDP_SYNC_TRANSFORM` 记录点击「查看调度配置」。
+2. 跳转到 `/platform/job?jobId=job_s4_mdp_sync_transform`。
+3. 任务调度页自动查询并展示对应作业。
+4. 用户无需手工复制或换算 `job_code` / `jobId`。
+5. 任务调度原有查询、重置、分页和操作按钮不受影响。
+
+---
+
+## 5. TODO-002 确认点
+
+### 5.1 背景
+
+MDP 运行监控列表已按「同步业务域 + 消费模块」调整,但详情弹窗可能仍显示偏旧的模块视角或技术任务文案,例如 `S4 MDP_SYNC_TRANSFORM`、`S4 单据同步` 等。
+
+### 5.2 待确认问题
+
+是否将详情弹窗也作为新增执行任务,统一补齐以下内容:
+
+1. 顶部展示同步业务域、消费模块、任务类型、调度任务。
+2. 链路说明从「归属模块」口径调整为「同步业务域 + 消费模块」口径。
+3. 保留物理任务名、输入表、输出表等技术追踪信息。
+4. 与 TODO-001 的「查看调度配置」入口合并展示。
+
+负责人确认后,再把状态从 `待确认` 改为 `待执行`,并补充对应任务书增补章节。
+
+---
+
+## 6. TODO-005 执行提示
+
+### 6.1 目标
+
+把 `数据任务管理` 从静态展示升级为真实的“数据中台同步配置中心”,支撑第三方系统标准化接入、可靠同步和业务可维护配置。
+
+### 6.2 需要阅读的任务书章节
+
+必须阅读:
+
+- `doc/plan/数据中台同步配置中心总体方案.md`
+- `doc/plan/数据中台同步配置中心实施任务书.md`
+- `doc/plan/数据中台同步配置中心实施任务书-执行版.md`
+
+### 6.3 执行原则
+
+1. 第一阶段先建设配置中心,不直接改写 S1-S4 `*MdpSyncTransformService` 内复杂 SQL。
+2. Admin.NET 任务调度作为底层调度能力复用,但数据中台同步配置中心必须完整表达调度业务配置。
+3. 公式配置采用“任务公式表 + 关联 KPI Master”,复用现有 SmartOps 公式解析、校验和预览能力。
+4. 新增配置表包括 `mdp_sync_task`、`mdp_sync_task_step`、`mdp_sync_task_param`、`mdp_sync_task_formula`、`mdp_sync_task_schedule`。
+5. 后续执行应按任务书中的 `TODO-SYNC-001` 至 `TODO-SYNC-006` 分包推进。
+
+### 6.4 非目标范围
+
+1. 不替换 Admin.NET 调度引擎。
+2. 不把页面做成任意 SQL 执行器。
+3. 不一次性将所有 KPI 改为公式配置驱动。
+4. 不重做已完成的 MDP 运行监控和链路详情基础能力。
+
+### 6.5 执行进度(2026-06-12)
+
+| 子任务 | 状态 | 说明 |
+|--------|------|------|
+| `TODO-SYNC-001` | `已完成` | `1.0.159.sql` 五表 DDL + S1-S4 种子 + 菜单更名;实体/DTO 骨架 |
+| `TODO-SYNC-002` | `已完成` | `MdpSyncTaskConfigService` 任务/步骤/调度 CRUD API |
+| `TODO-SYNC-003` | `已完成` | `syncTasks.vue` + `syncTasks.ts` 对接真实列表与编辑/步骤/跳转 |
+| `TODO-SYNC-004` | `已完成` | 实体与字段映射 CRUD API + SyncTaskEntityDrawer / FieldMappingEditor |
+| `TODO-SYNC-005` | `待执行` | 参数、公式、调度策略 UI |
+| `TODO-SYNC-006` | `待确认` | Service 绑定与配置快照,需负责人确认 |
+
+---
+
+## 7. 历史已执行基线
+
+以下任务书主体已作为历史基线理解,除非本文件明确列为待执行,否则不要重做:
+
+| 任务书 | 已执行范围说明 |
+|--------|----------------|
+| `doc/plan/MDP运行监控按业务域与消费模块改造任务书.md` | 第 1-11 节主体任务已执行完成;新增待执行见第 12 节 |
+| `doc/plan/任务调度按业务域与消费模块业务标签化改造任务书.md` | 第 1-13 节主体任务已执行完成;新增待执行见第 14 节 |
+
+---
+
+## 8. 变更记录
+
+| 日期 | 变更 |
+|------|------|
+| 2026-06-12 | 新增统一待执行任务列表;登记 `TODO-001` 和 `TODO-002` |
+| 2026-06-12 | 完成 `TODO-001`:MDP catalog 返回 `scheduleJobId`;MDP 监控列表/详情增加「查看调度配置」;任务调度页支持 `?jobId=` 自动查询 |
+| 2026-06-12 | 完成 `TODO-002`:MDP 运行详情/链路弹窗补齐同步业务域、消费模块、任务类型与调度配置入口;链路阶段说明改为业务域口径 |
+| 2026-06-12 | 完成 `TODO-003`:修复 MDP 链路实体查询只按当前租户查导致实体表为空的问题,兼容全局配置 `tenant_id=0` |
+| 2026-06-12 | 新增 `TODO-004`:生成 `MDP链路字段映射兜底展示任务书.md`,登记为待执行;本轮不实现 |
+| 2026-06-12 | 完成 `TODO-004`:链路详情无 `mdp_field_mapping` 时返回贴源通用字段映射兜底;前端展开区展示映射来源与兜底提示 |
+| 2026-06-12 | 新增 `TODO-005`:完成 `数据中台同步配置中心总体方案.md` 与 `数据中台同步配置中心实施任务书.md`,登记后续配置中心建设任务 |
+| 2026-06-12 | 补充 `TODO-005` 执行版任务书:明确菜单整合、禁改范围、DDL 草案、后端/前端文件清单、API、验收与验证要求 |
+| 2026-06-12 | `TODO-005` 第一阶段:`TODO-SYNC-001~003` 完成(配置表/种子/API/同步配置中心页面);`TODO-SYNC-004~006` 待后续 |
+| 2026-06-12 | `TODO-SYNC-004` 完成:实体与字段映射配置 API、SyncTaskEntityDrawer、FieldMappingEditor;链路详情优先展示 CONFIG 映射 |

+ 956 - 0
doc/plan/数据中台同步配置中心实施任务书-执行版.md

@@ -0,0 +1,956 @@
+# 数据中台同步配置中心实施任务书 - 执行版
+
+> 对应方案:`doc/plan/数据中台同步配置中心总体方案.md`  
+> 上游任务书:`doc/plan/数据中台同步配置中心实施任务书.md`  
+> 编写日期:2026-06-12  
+> 当前状态:待执行  
+> 执行原则:按本文件分包实施;第一阶段不改写 S1-S4 现有 MDP Service 复杂逻辑。
+
+---
+
+## 1. 执行入口与菜单定位
+
+### 1.1 菜单定位
+
+本任务必须整合在现有截图中的:
+
+```text
+Ai-DOP
+  数据中台管理
+    数据任务管理
+```
+
+实施后:
+
+```text
+Ai-DOP
+  数据中台管理
+    同步配置中心
+```
+
+要求:
+
+1. 不新增割裂菜单。
+2. 原 `数据任务管理` 升级/更名为 `同步配置中心`。
+3. 页面路由优先沿用 `/aidop/data-platform/sync-tasks`。
+4. 页面标题使用 `数据中台同步配置中心`。
+5. 页面副标题使用:
+
+```text
+外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪
+```
+
+### 1.2 与 Admin.NET 调度关系
+
+1. 数据中台同步配置中心是同步任务业务配置主入口。
+2. Admin.NET 任务调度只作为底层调度引擎复用。
+3. 不在本任务中重建 Cron 调度引擎。
+4. 数据中台页面必须完整表达调度业务配置。
+5. 原生 Admin.NET 调度页仅作为“高级调度设置”入口。
+6. 高级跳转使用:
+
+```text
+/platform/job?jobId=<schedule_job_id>
+```
+
+---
+
+## 2. 禁改范围
+
+执行 `TODO-SYNC-001` 至 `TODO-SYNC-005` 时禁止修改以下复杂执行逻辑:
+
+```text
+server/Plugins/Admin.NET.Plugin.AiDOP/Order/S1MdpSyncTransformService.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Production/S2MdpSyncTransformService.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Supply/S3MdpSyncTransformService.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/S4MdpSyncTransformService.cs
+```
+
+例外:
+
+1. 只允许读取这些文件理解现有 Service Key、JobCode、实体编码。
+2. 只有执行 `TODO-SYNC-006` 且负责人确认后,才允许接入配置快照或参数读取。
+
+第一阶段禁止:
+
+1. 页面直接执行任意 SQL。
+2. 替换 Admin.NET 任务调度。
+3. 一次性把 KPI 计算改成公式执行。
+4. 重做 MDP 运行监控。
+5. 改变现有 S1-S4 同步结果口径。
+
+---
+
+## 3. 分包执行顺序
+
+严格按以下顺序实施:
+
+1. `TODO-SYNC-001`:配置模型与种子数据。
+2. `TODO-SYNC-002`:后端配置 API。
+3. `TODO-SYNC-003`:同步配置中心页面。
+4. `TODO-SYNC-004`:实体与字段映射配置。
+5. `TODO-SYNC-005`:参数、公式、调度策略配置。
+6. `TODO-SYNC-006`:Service 绑定与配置快照,需单独确认后执行。
+
+如果执行代理只被要求完成某一个任务包,不要顺手实施后续任务包。
+
+---
+
+## 4. TODO-SYNC-001:配置模型与种子数据
+
+### 4.1 新增数据库迁移脚本
+
+新增脚本位置建议:
+
+```text
+server/Admin.NET.Web.Entry/UpdateScripts/1.0.xxx.sql
+```
+
+版本号 `1.0.xxx` 按仓库当前最新脚本顺延,不能覆盖已有脚本。
+
+### 4.2 DDL 草案
+
+执行时可按实际 MySQL 版本调整 JSON 类型;如需兼容性优先,JSON 字段可用 `TEXT` 存储。
+
+#### 4.2.1 `mdp_sync_task`
+
+```sql
+CREATE TABLE IF NOT EXISTS mdp_sync_task (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    task_name VARCHAR(200) NOT NULL,
+    task_type VARCHAR(40) NOT NULL DEFAULT 'SERVICE_SYNC',
+    business_domain_code VARCHAR(100) NULL,
+    business_domain_name VARCHAR(200) NULL,
+    consumer_modules VARCHAR(500) NULL,
+    source_system_code VARCHAR(100) NULL,
+    service_key VARCHAR(100) NULL,
+    job_code VARCHAR(100) NULL,
+    schedule_job_id VARCHAR(100) NULL,
+    status TINYINT NOT NULL DEFAULT 1,
+    owner_role VARCHAR(100) NULL,
+    config_version INT NOT NULL DEFAULT 1,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_tenant_code (tenant_id, task_code),
+    KEY idx_mdp_sync_task_job_code (job_code),
+    KEY idx_mdp_sync_task_schedule_job (schedule_job_id),
+    KEY idx_mdp_sync_task_domain (business_domain_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务业务配置';
+```
+
+#### 4.2.2 `mdp_sync_task_step`
+
+```sql
+CREATE TABLE IF NOT EXISTS mdp_sync_task_step (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    step_code VARCHAR(100) NOT NULL,
+    step_name VARCHAR(200) NOT NULL,
+    stage_type VARCHAR(40) NOT NULL,
+    service_method_key VARCHAR(100) NULL,
+    enabled TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_step (tenant_id, task_code, step_code),
+    KEY idx_mdp_sync_task_step_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务步骤配置';
+```
+
+#### 4.2.3 `mdp_sync_task_param`
+
+```sql
+CREATE TABLE IF NOT EXISTS mdp_sync_task_param (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    scope_type VARCHAR(40) NOT NULL DEFAULT 'TASK',
+    scope_code VARCHAR(100) NULL,
+    param_key VARCHAR(100) NOT NULL,
+    param_name VARCHAR(200) NOT NULL,
+    param_type VARCHAR(40) NOT NULL DEFAULT 'STRING',
+    param_value TEXT NULL,
+    default_value TEXT NULL,
+    required TINYINT NOT NULL DEFAULT 0,
+    editable TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_param (tenant_id, task_code, scope_type, scope_code, param_key),
+    KEY idx_mdp_sync_task_param_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务参数配置';
+```
+
+#### 4.2.4 `mdp_sync_task_formula`
+
+```sql
+CREATE TABLE IF NOT EXISTS mdp_sync_task_formula (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    step_code VARCHAR(100) NULL,
+    formula_code VARCHAR(100) NOT NULL,
+    formula_name VARCHAR(200) NOT NULL,
+    metric_code VARCHAR(100) NULL,
+    formula_expr VARCHAR(1000) NULL,
+    formula_preview VARCHAR(1000) NULL,
+    formula_refs TEXT NULL,
+    calc_rule TEXT NULL,
+    direction VARCHAR(40) NOT NULL DEFAULT 'higher_is_better',
+    yellow_threshold DECIMAL(18,6) NULL,
+    red_threshold DECIMAL(18,6) NULL,
+    version_no INT NOT NULL DEFAULT 1,
+    is_enabled TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_formula (tenant_id, task_code, formula_code, version_no),
+    KEY idx_mdp_sync_task_formula_metric (tenant_id, metric_code),
+    KEY idx_mdp_sync_task_formula_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务公式配置';
+```
+
+#### 4.2.5 `mdp_sync_task_schedule`
+
+```sql
+CREATE TABLE IF NOT EXISTS mdp_sync_task_schedule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    schedule_job_id VARCHAR(100) NULL,
+    schedule_mode VARCHAR(40) NOT NULL DEFAULT 'CRON',
+    cron_expr VARCHAR(200) NULL,
+    cron_desc VARCHAR(500) NULL,
+    timezone VARCHAR(100) NOT NULL DEFAULT 'Asia/Shanghai',
+    auto_enabled TINYINT NOT NULL DEFAULT 1,
+    manual_enabled TINYINT NOT NULL DEFAULT 1,
+    retry_enabled TINYINT NOT NULL DEFAULT 1,
+    max_retry_count INT NOT NULL DEFAULT 3,
+    retry_interval_seconds INT NOT NULL DEFAULT 300,
+    timeout_seconds INT NOT NULL DEFAULT 3600,
+    misfire_policy VARCHAR(40) NULL,
+    sync_window_type VARCHAR(40) NOT NULL DEFAULT 'FULL',
+    sync_window_value VARCHAR(200) NULL,
+    last_schedule_time DATETIME NULL,
+    next_schedule_time DATETIME NULL,
+    admin_job_config_json TEXT NULL,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_schedule (tenant_id, task_code),
+    KEY idx_mdp_sync_task_schedule_job (schedule_job_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务调度业务配置';
+```
+
+### 4.3 枚举约定
+
+`task_type`:
+
+```text
+DB_SYNC
+API_PULL
+API_PUSH
+API_OUT
+SERVICE_SYNC
+```
+
+`stage_type`:
+
+```text
+SOURCE_CONNECT
+STAGING
+STANDARD
+DWD
+KPI
+QUALITY_CHECK
+NOTIFY
+```
+
+`scope_type`:
+
+```text
+TASK
+STEP
+ENTITY
+FORMULA
+```
+
+`param_type`:
+
+```text
+STRING
+NUMBER
+BOOLEAN
+DATE
+DATETIME
+ENUM
+JSON
+```
+
+`schedule_mode`:
+
+```text
+MANUAL
+CRON
+INTERVAL
+EVENT
+WEBHOOK
+```
+
+`sync_window_type`:
+
+```text
+FULL
+INCREMENTAL
+ROLLING_WINDOW
+CUSTOM
+```
+
+### 4.4 S1-S4 种子数据
+
+迁移脚本必须幂等写入以下任务:
+
+| task_code | task_name | business_domain_code | consumer_modules | service_key | schedule_job_id |
+|-----------|-----------|----------------------|------------------|-------------|-----------------|
+| `S1_MDP_SYNC_TRANSFORM` | 订单交付域 MDP 同步 | `order_delivery` | `S1,S2,S3,S4,S7,S9` | `S1_MDP_SYNC_TRANSFORM` | `job_s1_mdp_sync_transform` |
+| `S2_MDP_SYNC_TRANSFORM` | 工单排程域 MDP 同步 | `work_schedule` | `S2,S3,S5,S6,S8,S9` | `S2_MDP_SYNC_TRANSFORM` | `job_s2_mdp_sync_transform` |
+| `S3_MDP_SYNC_TRANSFORM` | 供应采购域 MDP 同步 | `supply_purchase` | `S3,S4,S5,S8,S9` | `S3_MDP_SYNC_TRANSFORM` | `job_s3_mdp_sync_transform` |
+| `S4_MDP_SYNC_TRANSFORM` | 采购执行域 MDP 同步 | `purchase_execution` | `S4,S5,S8,S9` | `S4_MDP_SYNC_TRANSFORM` | `job_s4_mdp_sync_transform` |
+
+每个任务必须幂等写入默认步骤:
+
+```text
+SOURCE_CONNECT
+STAGING
+STANDARD
+DWD
+KPI
+QUALITY_CHECK
+```
+
+`QUALITY_CHECK` 第一阶段可 `enabled=0`。
+
+### 4.5 TODO-SYNC-001 验收
+
+验收 SQL:
+
+```sql
+SELECT task_code, task_name, schedule_job_id, status
+FROM mdp_sync_task
+WHERE task_code IN (
+  'S1_MDP_SYNC_TRANSFORM',
+  'S2_MDP_SYNC_TRANSFORM',
+  'S3_MDP_SYNC_TRANSFORM',
+  'S4_MDP_SYNC_TRANSFORM'
+);
+
+SELECT task_code, COUNT(*) AS step_count
+FROM mdp_sync_task_step
+GROUP BY task_code;
+
+SELECT task_code, schedule_job_id, schedule_mode
+FROM mdp_sync_task_schedule;
+```
+
+期望:
+
+1. S1-S4 四条任务存在。
+2. 每条任务至少 6 个步骤。
+3. 每条任务有调度业务配置。
+
+---
+
+## 5. TODO-SYNC-002:后端配置 API
+
+### 5.1 新增文件建议
+
+新增目录建议:
+
+```text
+server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/
+server/Plugins/Admin.NET.Plugin.AiDOP/Dto/DataPlatform/
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/
+```
+
+新增文件建议:
+
+```text
+server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/MdpSyncTaskConfigService.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Dto/DataPlatform/MdpSyncTaskDtos.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncTask.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncTaskStep.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncTaskParam.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncTaskFormula.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncTaskSchedule.cs
+```
+
+也可以使用 raw SQL,但执行版建议新增实体类,便于后续维护。
+
+### 5.2 API 路由
+
+统一路由前缀:
+
+```text
+/api/DataPlatform
+```
+
+建议在 `MdpSyncTaskConfigService` 上使用:
+
+```csharp
+[ApiDescriptionSettings(Order = 323, Description = "数据中台同步配置中心")]
+[Route("api/DataPlatform")]
+[AllowAnonymous]
+[NonUnify]
+public class MdpSyncTaskConfigService : IDynamicApiController, ITransient
+```
+
+### 5.3 DTO 最小结构
+
+列表行 DTO:
+
+```csharp
+public sealed class MdpSyncTaskListRow
+{
+    public long Id { get; set; }
+    public string TaskCode { get; set; } = string.Empty;
+    public string TaskName { get; set; } = string.Empty;
+    public string TaskType { get; set; } = string.Empty;
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? SourceSystemCode { get; set; }
+    public string? ServiceKey { get; set; }
+    public string? JobCode { get; set; }
+    public string? ScheduleJobId { get; set; }
+    public int Status { get; set; }
+    public int ConfigVersion { get; set; }
+    public string? LastRunStatus { get; set; }
+    public DateTime? LastRunTime { get; set; }
+    public string? ScheduleMode { get; set; }
+    public bool AutoEnabled { get; set; }
+    public bool ManualEnabled { get; set; }
+}
+```
+
+分页输出可复用现有前端 `Paged<T>` 结构:
+
+```csharp
+public sealed class PagedResult<T>
+{
+    public int Total { get; set; }
+    public int Page { get; set; }
+    public int PageSize { get; set; }
+    public List<T> List { get; set; } = new();
+}
+```
+
+### 5.4 API 清单
+
+必须实现:
+
+```text
+GET    /api/DataPlatform/sync-tasks
+GET    /api/DataPlatform/sync-tasks/{id}
+POST   /api/DataPlatform/sync-tasks
+PUT    /api/DataPlatform/sync-tasks/{id}
+DELETE /api/DataPlatform/sync-tasks/{id}
+```
+
+步骤:
+
+```text
+GET /api/DataPlatform/sync-tasks/{taskCode}/steps
+PUT /api/DataPlatform/sync-tasks/{taskCode}/steps
+```
+
+调度:
+
+```text
+GET  /api/DataPlatform/sync-tasks/{taskCode}/schedule
+PUT  /api/DataPlatform/sync-tasks/{taskCode}/schedule
+POST /api/DataPlatform/sync-tasks/{taskCode}/schedule/sync-admin-job
+```
+
+第一阶段 `sync-admin-job` 可以只返回当前保存的 `schedule_job_id` 与提示信息,不必写回 Admin.NET Job。
+
+### 5.5 列表 SQL 要求
+
+任务列表必须 left join 最近运行日志:
+
+```sql
+SELECT ...
+FROM mdp_sync_task t
+LEFT JOIN mdp_sync_task_schedule sch
+  ON sch.tenant_id = t.tenant_id AND sch.task_code = t.task_code
+LEFT JOIN (
+  SELECT job_code, MAX(start_time) AS last_run_time
+  FROM mdp_transform_run_log
+  GROUP BY job_code
+) latest ON latest.job_code = t.job_code
+LEFT JOIN mdp_transform_run_log run
+  ON run.job_code = latest.job_code AND run.start_time = latest.last_run_time
+```
+
+租户规则:
+
+1. 查询配置时兼容 `tenant_id = 当前租户` 与 `tenant_id = 0`。
+2. 同一 `task_code` 如果当前租户有专属配置,优先当前租户。
+3. 第一阶段可先仅使用 `tenant_id=0` 全局配置,但代码应预留租户参数。
+
+### 5.6 删除策略
+
+第一阶段删除任务建议软删除:
+
+```text
+status = 0
+```
+
+不要物理删除 S1-S4 种子任务。
+
+### 5.7 TODO-SYNC-002 验收
+
+接口验收:
+
+```text
+GET /api/DataPlatform/sync-tasks
+```
+
+期望:
+
+1. 返回 S1-S4 四条任务。
+2. S4 的 `scheduleJobId` 为 `job_s4_mdp_sync_transform`。
+3. 包含最近运行状态字段。
+4. 不影响 `/api/DataPlatform/mdp-monitor/*` 既有接口。
+
+---
+
+## 6. TODO-SYNC-003:同步配置中心页面
+
+### 6.1 修改文件
+
+必须修改:
+
+```text
+Web/src/views/aidop/data-platform/syncTasks.vue
+```
+
+新增:
+
+```text
+Web/src/views/aidop/data-platform/api/syncTasks.ts
+```
+
+可选拆分组件:
+
+```text
+Web/src/views/aidop/data-platform/components/SyncTaskFormDialog.vue
+Web/src/views/aidop/data-platform/components/SyncTaskStepDrawer.vue
+Web/src/views/aidop/data-platform/components/SyncTaskScheduleEditor.vue
+```
+
+第一轮可先在 `syncTasks.vue` 内实现,后续再拆分。
+
+### 6.2 菜单与标题要求
+
+菜单:
+
+```text
+同步配置中心
+```
+
+页面标题:
+
+```text
+数据中台同步配置中心
+```
+
+页面副标题:
+
+```text
+外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪
+```
+
+如本轮不改菜单种子,至少页面标题必须更新。
+
+### 6.3 列表字段
+
+必须显示:
+
+1. 同步任务。
+2. 类型。
+3. 业务域。
+4. 消费模块。
+5. Service。
+6. 调度任务。
+7. 调度模式。
+8. 状态。
+9. 配置版本。
+10. 最近执行。
+11. 最近结果。
+
+操作按钮:
+
+1. 编辑。
+2. 步骤。
+3. 实体。
+4. 字段映射。
+5. 参数。
+6. 公式。
+7. 调度策略。
+8. 日志。
+9. 链路。
+10. 高级调度设置。
+
+第一阶段如果按钮功能未全部实现,未实现按钮应禁用或显示“待实现”,不要静默无响应。
+
+### 6.4 前端 API 类型
+
+`syncTasks.ts` 至少定义:
+
+```ts
+export interface MdpSyncTaskRow {
+  id: number;
+  taskCode: string;
+  taskName: string;
+  taskType: string;
+  businessDomainCode?: string | null;
+  businessDomainName?: string | null;
+  consumerModules?: string | null;
+  sourceSystemCode?: string | null;
+  serviceKey?: string | null;
+  jobCode?: string | null;
+  scheduleJobId?: string | null;
+  scheduleMode?: string | null;
+  status: number;
+  configVersion: number;
+  autoEnabled?: boolean;
+  manualEnabled?: boolean;
+  lastRunStatus?: string | null;
+  lastRunTime?: string | null;
+}
+```
+
+API 函数:
+
+```ts
+fetchSyncTaskList(params)
+fetchSyncTaskDetail(id)
+createSyncTask(payload)
+updateSyncTask(id, payload)
+deleteSyncTask(id)
+```
+
+### 6.5 跳转要求
+
+高级调度设置:
+
+```ts
+router.push({ path: '/platform/job', query: { jobId: row.scheduleJobId } });
+```
+
+日志:
+
+```ts
+router.push({ path: '/aidop/data-platform/sync-logs', query: { jobCode: row.jobCode || row.taskCode } });
+```
+
+链路:
+
+可复用 MDP 运行监控现有链路能力,第一阶段可跳转到:
+
+```text
+/aidop/data-platform/mdp-monitor?jobCode=<jobCode>
+```
+
+### 6.6 TODO-SYNC-003 验收
+
+页面验收:
+
+1. `/aidop/data-platform/sync-tasks` 打开后标题为“数据中台同步配置中心”。
+2. 列表显示 S1-S4。
+3. 不再显示旧静态示例行,如 `ITEM_MASTER`、`WMS_STOCK`。
+4. S4 显示 `job_s4_mdp_sync_transform`。
+5. 高级调度设置能跳转 `/platform/job?jobId=job_s4_mdp_sync_transform`。
+
+---
+
+## 7. TODO-SYNC-004:实体与字段映射配置
+
+### 7.1 后端 API
+
+实体:
+
+```text
+GET    /api/DataPlatform/sync-tasks/{taskCode}/entities
+POST   /api/DataPlatform/sync-tasks/{taskCode}/entities
+PUT    /api/DataPlatform/entities/{id}
+DELETE /api/DataPlatform/entities/{id}
+```
+
+字段映射:
+
+```text
+GET    /api/DataPlatform/entities/{entityId}/field-mappings
+POST   /api/DataPlatform/entities/{entityId}/field-mappings
+PUT    /api/DataPlatform/field-mappings/{id}
+DELETE /api/DataPlatform/field-mappings/{id}
+```
+
+### 7.2 前端组件
+
+建议新增:
+
+```text
+Web/src/views/aidop/data-platform/components/SyncTaskEntityDrawer.vue
+Web/src/views/aidop/data-platform/components/FieldMappingEditor.vue
+```
+
+### 7.3 字段映射类型
+
+沿用:
+
+```text
+DIRECT
+JSONPATH
+SCRIPT
+CONST
+LOOKUP
+```
+
+字段映射编辑列:
+
+1. 源字段。
+2. 目标字段。
+3. 映射类型。
+4. 转换脚本。
+5. 常量值。
+6. 字典表。
+7. 默认值。
+8. 是否必填。
+9. 排序。
+
+### 7.4 链路详情联动
+
+保存真实 `mdp_field_mapping` 后:
+
+1. `MDP运行监控 -> 链路详情` 应优先显示配置映射。
+2. 不应再显示系统兜底提示。
+3. `mappingSource` 应为 `CONFIG`。
+4. `isFallback` 应为 `false`。
+
+### 7.5 TODO-SYNC-004 验收
+
+以 `S4_IQC_RECEIPT` 为例:
+
+1. 在同步配置中心进入 S4 任务。
+2. 打开实体配置。
+3. 找到 `S4_IQC_RECEIPT`。
+4. 添加至少一条字段映射。
+5. 打开 MDP 运行监控链路详情。
+6. 展开 `S4_IQC_RECEIPT`。
+7. 看到真实配置映射,且不是系统兜底。
+
+---
+
+## 8. TODO-SYNC-005:参数、公式、调度策略配置
+
+### 8.1 参数配置
+
+API:
+
+```text
+GET /api/DataPlatform/sync-tasks/{taskCode}/params
+PUT /api/DataPlatform/sync-tasks/{taskCode}/params
+```
+
+页面字段:
+
+1. 参数名称。
+2. 参数 Key。
+3. 参数类型。
+4. 参数值。
+5. 默认值。
+6. 作用域。
+7. 是否必填。
+8. 是否可编辑。
+
+第一阶段参数只保存和展示,不要求现有 Service 读取。
+
+### 8.2 公式配置
+
+API:
+
+```text
+GET    /api/DataPlatform/sync-tasks/{taskCode}/formulas
+POST   /api/DataPlatform/sync-tasks/{taskCode}/formulas
+PUT    /api/DataPlatform/sync-task-formulas/{id}
+DELETE /api/DataPlatform/sync-task-formulas/{id}
+POST   /api/DataPlatform/sync-task-formulas/preview
+```
+
+必须复用:
+
+```text
+server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FormulaExpr/FormulaParser.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FormulaExpr/FormulaValidator.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FormulaExpr/FormulaPreviewBuilder.cs
+Web/src/views/aidop/kanban/components/FormulaEditor.vue
+```
+
+保存规则:
+
+1. 保存 `formula_expr`。
+2. 保存 `formula_preview`。
+3. 保存 `formula_refs`。
+4. 可选关联 `metric_code`。
+5. 如果关联 `metric_code`,不覆盖 `ado_smart_ops_kpi_master`,只记录引用关系。
+
+### 8.3 调度策略配置
+
+API:
+
+```text
+GET  /api/DataPlatform/sync-tasks/{taskCode}/schedule
+PUT  /api/DataPlatform/sync-tasks/{taskCode}/schedule
+POST /api/DataPlatform/sync-tasks/{taskCode}/schedule/sync-admin-job
+```
+
+页面必须包含:
+
+1. 调度模式。
+2. Cron 表达式。
+3. Cron 说明。
+4. 自动同步。
+5. 允许手工执行。
+6. 重试开关。
+7. 最大重试次数。
+8. 重试间隔。
+9. 超时时间。
+10. 同步窗口类型。
+11. 同步窗口值。
+12. Admin.NET JobId。
+13. 高级调度设置按钮。
+
+第一阶段:
+
+1. 允许保存数据中台业务调度配置。
+2. 不要求写回 Admin.NET Job Cron。
+3. `sync-admin-job` 可先做读取/快照,不能破坏现有调度。
+
+### 8.4 TODO-SYNC-005 验收
+
+1. S4 任务可以保存参数 `late_days_threshold=3`。
+2. S4 任务可以保存一条公式配置。
+3. 公式预览能返回中文预览或引用替换结果。
+4. S4 调度策略显示 `job_s4_mdp_sync_transform`。
+5. 高级调度设置跳转正常。
+6. 保存参数/公式/调度策略不改变现有 S4 同步执行结果。
+
+---
+
+## 9. TODO-SYNC-006:Service 绑定与配置快照
+
+该任务暂不默认执行,需负责人单独确认。
+
+目标:
+
+1. 建设 `MdpSyncTaskConfigProvider`。
+2. 建设 Service 注册表。
+3. 执行时读取配置版本。
+4. 运行日志写入配置快照。
+5. 逐步让低风险参数参与运行。
+
+建议新增文件:
+
+```text
+server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/MdpSyncTaskConfigProvider.cs
+server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/MdpSyncServiceRegistry.cs
+```
+
+第一批可接入的低风险能力:
+
+1. 实体启停。
+2. 空表是否成功。
+3. 同步窗口天数。
+4. 质量校验是否启用。
+5. 主键/业务键表达式展示与快照。
+
+禁止一开始接入:
+
+1. 任意 SQL 模板执行。
+2. DWD 复杂 SQL 动态替换。
+3. KPI 全量公式执行替换。
+
+---
+
+## 10. 构建与验证
+
+### 10.1 后端验证
+
+按实际改动范围运行:
+
+```powershell
+dotnet build "server/Plugins/Admin.NET.Plugin.AiDOP/Admin.NET.Plugin.AiDOP.csproj"
+dotnet build "server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj"
+```
+
+如果后端正在运行导致 DLL 锁定,需要先停止后端进程再构建。
+
+### 10.2 前端验证
+
+按实际改动范围运行:
+
+```powershell
+Set-Location "Web"
+pnpm exec vue-tsc --noEmit --pretty false
+```
+
+如果仓库存在历史类型错误,应记录是否与本次改动相关。不得把既存错误当成本次新问题。
+
+### 10.3 页面回归
+
+必须回归:
+
+1. `/aidop/data-platform/sync-tasks`
+2. `/aidop/data-platform/sync-logs`
+3. `/aidop/data-platform/mdp-monitor`
+4. `/platform/job?jobId=job_s4_mdp_sync_transform`
+
+### 10.4 接口回归
+
+必须回归:
+
+1. `GET /api/DataPlatform/sync-tasks`
+2. `GET /api/DataPlatform/mdp-monitor/catalog`
+3. `GET /api/DataPlatform/mdp-monitor/lineage?jobCode=S4_MDP_SYNC_TRANSFORM&moduleCode=S4`
+
+---
+
+## 11. 提交与版本号提醒
+
+若后续执行产生代码提交:
+
+1. 修改前端代码或配置,提交时必须递增 `Web/package.json` patch 版本。
+2. 修改后端代码或配置,提交时必须递增 `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 中的 `<Version>`、`<AssemblyVersion>`、`<FileVersion>` patch 版本。
+3. 仅修改本文档或其他 Markdown,不递增前后端版本号。
+
+---
+
+## 12. 执行摘要
+
+给后续执行代理的最短提示:
+
+```text
+请按 doc/plan/数据中台同步配置中心实施任务书-执行版.md 执行。
+先执行 TODO-SYNC-001 至 TODO-SYNC-003,不要改 S1-S4 MDP Service 复杂逻辑。
+同步配置中心整合在 数据中台管理 -> 同步配置中心,沿用 /aidop/data-platform/sync-tasks。
+Admin.NET 任务调度只作为底层调度能力,数据中台页面必须完整表达调度业务配置。
+公式配置采用任务公式表 + 关联 KPI Master,复用现有 FormulaEditor/FormulaParser/FormulaValidator/FormulaPreviewBuilder。
+```

+ 562 - 0
doc/plan/数据中台同步配置中心实施任务书.md

@@ -0,0 +1,562 @@
+# 数据中台同步配置中心实施任务书
+
+> 对应方案:`doc/plan/数据中台同步配置中心总体方案.md`  
+> 编写日期:2026-06-12  
+> 当前状态:待执行  
+> 任务类型:数据中台配置中心 / 第三方系统标准化接入 / 可靠同步 / 参数公式配置
+
+---
+
+## 1. 执行目标
+
+建设数据中台同步配置中心,把现有 `数据任务管理` 从静态展示升级为真实业务配置入口。
+
+第一阶段目标:
+
+1. 新增同步任务配置模型。
+2. 初始化 S1-S4 同步任务配置。
+3. 前端提供任务、实体、字段映射、参数、公式、调度策略的配置入口。
+4. 复用 Admin.NET 任务调度,但不让同步任务配置割裂到调度页面。
+5. 复用现有 SmartOps 公式能力。
+6. 不改动现有 S1-S4 MDP Service 的复杂执行逻辑。
+
+---
+
+## 2. 关键原则
+
+1. 数据中台同步配置中心是业务主入口。
+2. Admin.NET 任务调度是底层调度执行能力,不是业务配置主入口。
+3. 复杂同步逻辑继续留在 `*MdpSyncTransformService`。
+4. 计算公式、阈值、时间窗口、启停、实体、字段映射等常变内容进入配置中心。
+5. 第一阶段先可维护、可追踪、可展示;后续逐步参与运行。
+6. 所有配置应考虑第三方系统标准化对接和可靠同步扩展。
+
+---
+
+## 3. 新增数据表任务
+
+### 3.1 新增 `mdp_sync_task`
+
+任务主配置表。
+
+必须支持:
+
+1. 任务编码、名称、类型。
+2. 业务域、消费模块。
+3. 外部系统编码。
+4. 绑定 Service Key。
+5. 绑定运行日志 JobCode。
+6. 绑定 Admin.NET JobId。
+7. 状态和配置版本。
+
+建议字段:
+
+```sql
+id
+tenant_id
+task_code
+task_name
+task_type
+business_domain_code
+business_domain_name
+consumer_modules
+source_system_code
+service_key
+job_code
+schedule_job_id
+status
+owner_role
+config_version
+description
+create_time
+update_time
+```
+
+### 3.2 新增 `mdp_sync_task_step`
+
+任务步骤配置表。
+
+必须支持:
+
+1. STAGING / STANDARD / DWD / KPI / QUALITY_CHECK 等阶段。
+2. 排序。
+3. 启停。
+4. 阶段说明。
+5. 预留 Service 方法绑定。
+
+### 3.3 新增 `mdp_sync_task_param`
+
+任务参数配置表。
+
+必须支持:
+
+1. 任务级参数。
+2. 步骤级参数。
+3. 实体级参数。
+4. 公式级参数。
+5. 参数类型和默认值。
+6. 是否必填、是否可编辑。
+
+参数类型:
+
+```text
+STRING
+NUMBER
+BOOLEAN
+DATE
+DATETIME
+ENUM
+JSON
+```
+
+### 3.4 新增 `mdp_sync_task_formula`
+
+任务公式配置表。
+
+必须支持:
+
+1. 任务与步骤。
+2. 公式编码、名称。
+3. 可关联 `ado_smart_ops_kpi_master.metric_code`。
+4. 保存 `formula_expr`、`formula_preview`、`formula_refs`。
+5. 保存阈值、方向、版本和启停。
+
+公式语法复用现有 SmartOps 公式体系,不另造语法。
+
+### 3.5 新增 `mdp_sync_task_schedule`
+
+同步任务调度业务配置表。
+
+必须支持:
+
+1. 绑定 Admin.NET JobId。
+2. 调度模式。
+3. Cron 表达式和说明。
+4. 自动/手工执行开关。
+5. 重试策略。
+6. 超时策略。
+7. 同步窗口策略。
+8. 最近/下次调度时间。
+9. 底层 Job 配置快照。
+
+调度模式:
+
+```text
+MANUAL
+CRON
+INTERVAL
+EVENT
+WEBHOOK
+```
+
+同步窗口:
+
+```text
+FULL
+INCREMENTAL
+ROLLING_WINDOW
+CUSTOM
+```
+
+---
+
+## 4. 种子数据任务
+
+### 4.1 初始化 S1-S4 任务
+
+需要为以下任务写入 `mdp_sync_task`:
+
+| task_code | task_name | service_key | schedule_job_id |
+|-----------|-----------|-------------|-----------------|
+| `S1_MDP_SYNC_TRANSFORM` | 订单交付域 MDP 同步 | `S1_MDP_SYNC_TRANSFORM` | `job_s1_mdp_sync_transform` |
+| `S2_MDP_SYNC_TRANSFORM` | 工单排程域 MDP 同步 | `S2_MDP_SYNC_TRANSFORM` | `job_s2_mdp_sync_transform` |
+| `S3_MDP_SYNC_TRANSFORM` | 供应采购域 MDP 同步 | `S3_MDP_SYNC_TRANSFORM` | `job_s3_mdp_sync_transform` |
+| `S4_MDP_SYNC_TRANSFORM` | 采购执行域 MDP 同步 | `S4_MDP_SYNC_TRANSFORM` | `job_s4_mdp_sync_transform` |
+
+### 4.2 初始化步骤
+
+每个任务默认初始化:
+
+1. `SOURCE_CONNECT`
+2. `STAGING`
+3. `STANDARD`
+4. `DWD`
+5. `KPI`
+6. `QUALITY_CHECK`
+
+第一阶段可将 `QUALITY_CHECK` 标记为未启用或预留。
+
+### 4.3 初始化调度业务配置
+
+每个任务写入 `mdp_sync_task_schedule`:
+
+1. `schedule_job_id` 绑定现有 Admin.NET Job。
+2. `schedule_mode` 默认 `CRON`。
+3. `manual_enabled` 默认启用。
+4. `auto_enabled` 依当前环境调度状态设定。
+5. 重试、超时、同步窗口先给默认值。
+
+---
+
+## 5. 后端 API 任务
+
+### 5.1 任务配置 API
+
+新增数据中台配置 API,建议统一前缀:
+
+```text
+/api/DataPlatform/sync-tasks
+```
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks`
+2. `GET /api/DataPlatform/sync-tasks/{id}`
+3. `POST /api/DataPlatform/sync-tasks`
+4. `PUT /api/DataPlatform/sync-tasks/{id}`
+5. `DELETE /api/DataPlatform/sync-tasks/{id}`
+
+列表需聚合:
+
+1. 最近运行时间。
+2. 最近运行状态。
+3. 调度 JobId。
+4. 业务域。
+5. 消费模块。
+
+### 5.2 步骤配置 API
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks/{taskCode}/steps`
+2. `PUT /api/DataPlatform/sync-tasks/{taskCode}/steps`
+
+### 5.3 实体配置 API
+
+复用 `mdp_entity`。
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks/{taskCode}/entities`
+2. `POST /api/DataPlatform/sync-tasks/{taskCode}/entities`
+3. `PUT /api/DataPlatform/entities/{id}`
+4. `DELETE /api/DataPlatform/entities/{id}`
+
+### 5.4 字段映射 API
+
+复用 `mdp_field_mapping`。
+
+接口:
+
+1. `GET /api/DataPlatform/entities/{entityId}/field-mappings`
+2. `POST /api/DataPlatform/entities/{entityId}/field-mappings`
+3. `PUT /api/DataPlatform/field-mappings/{id}`
+4. `DELETE /api/DataPlatform/field-mappings/{id}`
+
+### 5.5 参数配置 API
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks/{taskCode}/params`
+2. `PUT /api/DataPlatform/sync-tasks/{taskCode}/params`
+
+### 5.6 公式配置 API
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks/{taskCode}/formulas`
+2. `POST /api/DataPlatform/sync-tasks/{taskCode}/formulas`
+3. `PUT /api/DataPlatform/sync-task-formulas/{id}`
+4. `DELETE /api/DataPlatform/sync-task-formulas/{id}`
+5. `POST /api/DataPlatform/sync-task-formulas/preview`
+
+`preview` 应复用现有 `FormulaParser`、`FormulaValidator`、`FormulaPreviewBuilder`。
+
+### 5.7 调度业务配置 API
+
+接口:
+
+1. `GET /api/DataPlatform/sync-tasks/{taskCode}/schedule`
+2. `PUT /api/DataPlatform/sync-tasks/{taskCode}/schedule`
+3. `POST /api/DataPlatform/sync-tasks/{taskCode}/schedule/sync-admin-job`
+
+第一阶段不必直接修改 Admin.NET Job 的 Cron,可先读取/展示/绑定/跳转。
+
+---
+
+## 6. 前端页面任务
+
+### 6.1 改造数据任务管理页
+
+文件:
+
+```text
+Web/src/views/aidop/data-platform/syncTasks.vue
+```
+
+改造为真实配置中心:
+
+1. 移除静态 `rows`。
+2. 接入真实 API。
+3. 增加查询条件。
+4. 增加分页。
+5. 增加任务状态、配置版本、最近运行。
+6. 增加操作按钮。
+
+页面标题:
+
+```text
+数据中台同步配置中心
+```
+
+副标题:
+
+```text
+外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪
+```
+
+### 6.2 新增前端 API
+
+新增:
+
+```text
+Web/src/views/aidop/data-platform/api/syncTasks.ts
+```
+
+封装:
+
+1. 任务配置 API。
+2. 步骤 API。
+3. 实体 API。
+4. 字段映射 API。
+5. 参数 API。
+6. 公式 API。
+7. 调度 API。
+
+### 6.3 新增配置组件
+
+建议新增:
+
+```text
+Web/src/views/aidop/data-platform/components/SyncTaskFormDialog.vue
+Web/src/views/aidop/data-platform/components/SyncTaskStepDrawer.vue
+Web/src/views/aidop/data-platform/components/SyncTaskEntityDrawer.vue
+Web/src/views/aidop/data-platform/components/FieldMappingEditor.vue
+Web/src/views/aidop/data-platform/components/SyncTaskParamEditor.vue
+Web/src/views/aidop/data-platform/components/SyncTaskFormulaEditor.vue
+Web/src/views/aidop/data-platform/components/SyncTaskScheduleEditor.vue
+```
+
+如实现时希望控制首轮范围,可先把组件写在 `syncTasks.vue` 内,后续再拆分。
+
+### 6.4 公式编辑复用
+
+复用现有:
+
+```text
+Web/src/views/aidop/kanban/components/FormulaEditor.vue
+```
+
+要求:
+
+1. 支持 `@MetricCode`。
+2. 支持 `$FactCode`。
+3. 支持公式预览。
+4. 支持保存 `formula_expr`、`formula_preview`、`formula_refs`。
+
+### 6.5 调度策略页面
+
+调度策略不是简单跳转。
+
+页面需展示:
+
+1. 调度模式。
+2. Cron 表达式。
+3. 自动启停。
+4. 手工执行开关。
+5. 重试策略。
+6. 超时。
+7. 同步窗口。
+8. Admin.NET JobId。
+9. 最近/下次调度时间。
+10. 高级调度设置入口。
+
+按钮文案建议:
+
+```text
+高级调度设置
+同步 Admin.NET 调度配置
+```
+
+---
+
+## 7. 与现有 Service 的关系
+
+第一阶段不改以下复杂逻辑:
+
+1. `S1MdpSyncTransformService`
+2. `S2MdpSyncTransformService`
+3. `S3MdpSyncTransformService`
+4. `S4MdpSyncTransformService`
+
+配置中心只做:
+
+1. 配置记录。
+2. 展示。
+3. 校验。
+4. 链路可视化补充。
+5. 调度绑定。
+6. 公式配置。
+
+后续 Service 接入建议:
+
+1. 新增 `MdpSyncTaskConfigProvider`。
+2. Service 执行前读取任务配置快照。
+3. 运行日志写入配置版本。
+4. 逐步支持实体启停、参数、字段表达式、公式。
+
+---
+
+## 8. 验收用例
+
+### 8.1 任务列表
+
+打开:
+
+```text
+数据中台 -> 数据任务管理
+```
+
+期望:
+
+1. 页面标题为“数据中台同步配置中心”。
+2. 能看到 S1-S4 四条真实同步任务。
+3. 每条任务显示业务域、消费模块、调度 Job、状态、最近执行。
+4. 不再使用静态演示数据。
+
+### 8.2 任务基础配置
+
+期望:
+
+1. 能编辑任务名称、业务域、消费模块、Service Key、说明。
+2. 保存后刷新不丢失。
+3. 不影响现有 MDP 运行监控。
+
+### 8.3 字段映射配置
+
+以 `S4_IQC_RECEIPT` 为例:
+
+1. 打开 S4 任务实体配置。
+2. 进入字段映射。
+3. 新增 `RecID -> source_row_id` 等映射。
+4. 保存后链路详情显示“配置”映射。
+5. 无配置时仍显示系统兜底。
+
+### 8.4 公式配置
+
+期望:
+
+1. 打开公式配置。
+2. 输入公式表达式。
+3. 能预览中文公式。
+4. 能关联 KPI Master。
+5. 保存后显示版本和启停状态。
+
+### 8.5 调度策略
+
+期望:
+
+1. S4 任务显示绑定 `job_s4_mdp_sync_transform`。
+2. 能看到业务调度策略。
+3. 能跳转高级调度设置。
+4. 不需要用户离开数据中台才能理解同步策略。
+
+### 8.6 不改 Service 回归
+
+期望:
+
+1. S1-S4 原有同步任务仍可运行。
+2. MDP 运行监控仍可查询。
+3. 链路详情仍可展示。
+4. 不因配置中心上线改变原有执行结果。
+
+---
+
+## 9. 非目标范围
+
+第一阶段不做:
+
+1. 不替换 Admin.NET 调度引擎。
+2. 不直接改写 S1-S4 Service 内复杂 SQL。
+3. 不实现完整 `mdp_pipeline` 配置驱动执行器。
+4. 不允许任意 SQL 由页面直接执行。
+5. 不强制所有 KPI 立即由公式配置驱动。
+6. 不重做 MDP 运行监控已有能力。
+
+---
+
+## 10. 分阶段任务拆解
+
+### TODO-SYNC-001:配置模型与种子数据
+
+范围:
+
+1. 新增 5 张配置表。
+2. 初始化 S1-S4 任务。
+3. 初始化步骤。
+4. 初始化调度业务配置。
+
+### TODO-SYNC-002:配置中心任务列表
+
+范围:
+
+1. 后端任务列表 API。
+2. 前端 `syncTasks.vue` 真实化。
+3. 展示任务、调度、运行状态。
+
+### TODO-SYNC-003:实体与字段映射配置
+
+范围:
+
+1. `mdp_entity` CRUD。
+2. `mdp_field_mapping` CRUD。
+3. 链路详情联动验证。
+
+### TODO-SYNC-004:参数与公式配置
+
+范围:
+
+1. 参数配置 API 和页面。
+2. 公式配置 API 和页面。
+3. 复用公式预览和校验。
+
+### TODO-SYNC-005:调度策略配置
+
+范围:
+
+1. `mdp_sync_task_schedule` API。
+2. 调度策略页面。
+3. Admin.NET Job 信息同步和高级跳转。
+
+### TODO-SYNC-006:Service 绑定与配置快照
+
+范围:
+
+1. 建设 Service 注册表。
+2. 执行时读取配置快照。
+3. 运行日志记录配置版本。
+4. 后续逐步让参数和公式参与运行。
+
+---
+
+## 11. 执行提示
+
+执行本任务书时应按任务包推进。第一阶段优先执行:
+
+1. `TODO-SYNC-001`
+2. `TODO-SYNC-002`
+3. `TODO-SYNC-003`
+4. `TODO-SYNC-004`
+5. `TODO-SYNC-005`
+
+`TODO-SYNC-006` 涉及运行链路,应在配置中心稳定后单独评审。

+ 531 - 0
doc/plan/数据中台同步配置中心总体方案.md

@@ -0,0 +1,531 @@
+# 数据中台同步配置中心总体方案
+
+> 适用范围:数据中台 / 第三方系统接入 / MDP 同步任务 / 任务调度 / 参数公式配置  
+> 编写日期:2026-06-12  
+> 当前状态:方案确认版  
+> 目标读者:产品负责人、架构负责人、后端开发、前端开发、大语言模型执行代理、测试负责人
+
+---
+
+## 1. 背景
+
+当前数据中台已具备 MDP 运行监控、链路详情、批次日志和部分实体/字段映射展示能力。S1-S4 的 MDP 同步任务已经能通过现有 Service 完成源表抽取、贴源、标准层、DWD 和 KPI 写入。
+
+但现状仍存在几个问题:
+
+1. `数据任务管理` 页面仍偏静态展示,尚未成为真实配置入口。
+2. `mdp_source`、`mdp_entity`、`mdp_field_mapping` 等配置表已有基础,但缺少面向用户的统一维护页面。
+3. 同步任务的调度、实体、字段映射、参数、公式、日志、链路分散在不同页面或代码中,业务人员感知割裂。
+4. 复杂同步逻辑主要在 `*MdpSyncTransformService` 中,适合承载复杂事务,但计算公式、阈值、时间窗口、启停开关等常变口径需要配置化。
+5. 后续目标是支撑第三方系统标准化对接和可靠同步,需要统一配置控制面。
+
+因此需要建设“数据中台同步配置中心”,作为同步任务的业务主入口。
+
+---
+
+## 2. 设计目标
+
+### 2.1 业务目标
+
+建设统一的同步任务业务配置中心,使用户能够维护:
+
+1. 外部系统接入配置。
+2. 同步任务基础信息。
+3. 同步实体和源/目标对象。
+4. 逐字段映射。
+5. 业务参数、阈值、开关、同步窗口。
+6. 计算公式及指标口径。
+7. 调度策略和 Admin.NET Job 绑定。
+8. 重试、超时、空表策略等可靠同步配置。
+9. 运行日志、链路追踪和配置快照。
+
+### 2.2 技术目标
+
+采用混合执行模型:
+
+1. 复杂逻辑继续由现有 Service 承载。
+2. 常变配置进入数据库和页面。
+3. Admin.NET 任务调度作为底层调度能力复用。
+4. 数据中台同步配置中心作为业务控制面,保存完整业务配置。
+5. 第一阶段不直接替换现有 Service 内复杂 SQL,先实现配置可维护、可追踪、可展示。
+6. 后续逐步让 Service 读取配置快照,推动可安全抽象的部分配置驱动。
+
+---
+
+## 3. 总体架构
+
+```mermaid
+flowchart TD
+    user["用户"] --> center["数据中台同步配置中心"]
+    center --> taskConfig["同步任务配置"]
+    center --> sourceConfig["外部系统与实体配置"]
+    center --> mappingConfig["字段映射配置"]
+    center --> paramConfig["参数配置"]
+    center --> formulaConfig["公式配置"]
+    center --> scheduleConfig["调度策略配置"]
+    scheduleConfig --> adminJob["Admin.NET 任务调度"]
+    taskConfig --> serviceBinding["Service 绑定适配"]
+    serviceBinding --> mdpService["现有 MDP Service"]
+    mdpService --> runLog["运行日志"]
+    mdpService --> lineage["链路详情"]
+    formulaConfig --> kpiMaster["KPI Master / BusinessFact"]
+```
+
+核心分工:
+
+| 层级 | 职责 |
+|------|------|
+| 数据中台同步配置中心 | 业务配置主入口,统一维护任务、实体、字段、参数、公式、调度和可靠性策略 |
+| Admin.NET 任务调度 | 底层 Job 注册、Cron、启停、触发能力 |
+| MDP Service | 复杂同步逻辑、标准层/DWD/KPI 写入、事务处理 |
+| MDP 配置表 | 保存任务、实体、字段映射、参数、公式、调度业务配置 |
+| 运行日志/链路 | 展示每次执行结果、实体状态、字段映射、配置生效情况 |
+
+---
+
+## 4. 执行模型
+
+### 4.1 当前阶段
+
+当前 S1-S4 运行链路仍按代码执行:
+
+```text
+Admin.NET Job / 手工触发
+  -> *MdpSyncTransformService.RunFullAsync
+  -> STAGING
+  -> STANDARD
+  -> DWD
+  -> KPI
+  -> mdp_transform_run_log / mdp_sync_log
+```
+
+### 4.2 目标阶段
+
+目标链路:
+
+```text
+同步配置中心
+  -> 读取任务配置
+  -> 绑定 Admin.NET Job
+  -> 绑定 MDP Service
+  -> 执行前加载配置快照
+  -> Service 执行复杂逻辑
+  -> 运行日志记录配置版本
+  -> 链路详情展示配置与执行结果
+```
+
+### 4.3 第一阶段原则
+
+1. 不直接改写 S1-S4 复杂 SQL。
+2. 不把页面做成任意 SQL 执行器。
+3. 不重复建设 Cron 调度引擎。
+4. 优先把常变动配置管起来。
+5. 优先保证配置中心体验完整。
+
+---
+
+## 5. 配置模型
+
+### 5.1 新增配置表
+
+建议新增 5 张核心表:
+
+1. `mdp_sync_task`
+2. `mdp_sync_task_step`
+3. `mdp_sync_task_param`
+4. `mdp_sync_task_formula`
+5. `mdp_sync_task_schedule`
+
+### 5.2 复用现有表
+
+复用以下现有表:
+
+| 表 | 用途 |
+|----|------|
+| `mdp_source` | 外部系统 / 数据源配置 |
+| `mdp_entity` | 同步实体、源对象、目标对象 |
+| `mdp_field_mapping` | 逐字段映射 |
+| `mdp_sync_log` | 单实体同步日志 |
+| `mdp_transform_run_log` | 整批运行日志 |
+| `ado_smart_ops_kpi_master` | KPI 主数据和指标公式 |
+| `ado_smart_ops_business_fact` | 业务事实字典,作为公式 `$FactCode` 来源 |
+| Admin.NET Job 表/接口 | 底层任务调度 |
+
+---
+
+## 6. 表设计建议
+
+### 6.1 `mdp_sync_task`
+
+同步任务主配置。
+
+| 字段 | 说明 |
+|------|------|
+| `id` | 主键 |
+| `tenant_id` | 租户 |
+| `task_code` | 同步任务编码,如 `S4_MDP_SYNC_TRANSFORM` |
+| `task_name` | 同步任务名称 |
+| `task_type` | `DB_SYNC` / `API_PULL` / `API_PUSH` / `API_OUT` / `SERVICE_SYNC` |
+| `business_domain_code` | 业务域编码 |
+| `business_domain_name` | 业务域名称 |
+| `consumer_modules` | 消费模块,建议 JSON 或逗号分隔 |
+| `source_system_code` | 来源系统编码 |
+| `service_key` | 绑定后端 Service 的 Key |
+| `job_code` | 运行日志中的技术任务编码 |
+| `schedule_job_id` | Admin.NET JobId |
+| `status` | 启用状态 |
+| `owner_role` | 负责角色 |
+| `config_version` | 当前配置版本 |
+| `description` | 说明 |
+| `create_time` / `update_time` | 时间戳 |
+
+### 6.2 `mdp_sync_task_step`
+
+同步任务步骤配置。
+
+| 字段 | 说明 |
+|------|------|
+| `task_code` | 任务编码 |
+| `step_code` | 步骤编码 |
+| `step_name` | 步骤名称 |
+| `stage_type` | `SOURCE_CONNECT` / `STAGING` / `STANDARD` / `DWD` / `KPI` / `QUALITY_CHECK` / `NOTIFY` |
+| `service_method_key` | 可选,绑定 Service 方法或阶段 Key |
+| `enabled` | 是否启用 |
+| `sort_order` | 排序 |
+| `description` | 说明 |
+
+第一阶段主要用于展示、排序、启停和后续扩展,不强制驱动现有 Service。
+
+### 6.3 `mdp_sync_task_param`
+
+任务参数配置。
+
+| 字段 | 说明 |
+|------|------|
+| `task_code` | 任务编码 |
+| `scope_type` | `TASK` / `STEP` / `ENTITY` / `FORMULA` |
+| `scope_code` | 对应步骤、实体或公式编码 |
+| `param_key` | 参数 Key |
+| `param_name` | 参数名称 |
+| `param_type` | `STRING` / `NUMBER` / `BOOLEAN` / `DATE` / `DATETIME` / `ENUM` / `JSON` |
+| `param_value` | 参数值 |
+| `default_value` | 默认值 |
+| `required` | 是否必填 |
+| `editable` | 是否允许用户编辑 |
+| `description` | 说明 |
+
+典型参数:
+
+```text
+allow_empty_source = true
+sync_window_days = 7
+late_days_threshold = 3
+enable_quality_check = false
+```
+
+### 6.4 `mdp_sync_task_formula`
+
+同步任务公式配置。
+
+| 字段 | 说明 |
+|------|------|
+| `task_code` | 任务编码 |
+| `step_code` | 步骤编码,通常为 `KPI` 或 `QUALITY_CHECK` |
+| `formula_code` | 公式编码 |
+| `formula_name` | 公式名称 |
+| `metric_code` | 可选,关联 `ado_smart_ops_kpi_master.metric_code` |
+| `formula_expr` | 结构化公式表达式 |
+| `formula_preview` | 人读预览 |
+| `formula_refs` | 引用清单 JSON |
+| `calc_rule` | 计算规则说明 |
+| `direction` | 优劣方向 |
+| `yellow_threshold` | 黄色阈值 |
+| `red_threshold` | 红色阈值 |
+| `version_no` | 版本号 |
+| `is_enabled` | 是否启用 |
+
+公式语法和校验复用现有:
+
+- `FormulaParser`
+- `FormulaValidator`
+- `FormulaPreviewBuilder`
+- `FormulaEditor.vue`
+
+### 6.5 `mdp_sync_task_schedule`
+
+同步任务调度业务配置。
+
+| 字段 | 说明 |
+|------|------|
+| `task_code` | 任务编码 |
+| `schedule_job_id` | Admin.NET JobId |
+| `schedule_mode` | `MANUAL` / `CRON` / `INTERVAL` / `EVENT` / `WEBHOOK` |
+| `cron_expr` | Cron 表达式 |
+| `cron_desc` | Cron 中文说明 |
+| `timezone` | 时区 |
+| `auto_enabled` | 自动同步是否启用 |
+| `manual_enabled` | 是否允许手工执行 |
+| `retry_enabled` | 是否启用重试 |
+| `max_retry_count` | 最大重试次数 |
+| `retry_interval_seconds` | 重试间隔 |
+| `timeout_seconds` | 超时时间 |
+| `misfire_policy` | 错过触发策略 |
+| `sync_window_type` | `FULL` / `INCREMENTAL` / `ROLLING_WINDOW` / `CUSTOM` |
+| `sync_window_value` | 同步窗口值 |
+| `last_schedule_time` | 最近调度时间 |
+| `next_schedule_time` | 下次调度时间 |
+| `admin_job_config_json` | 底层 Job 配置快照 |
+
+说明:Admin.NET 仍是底层调度执行器,但数据中台保存业务侧调度策略,避免配置割裂。
+
+---
+
+## 7. 第三方系统标准化对接
+
+任务类型需要覆盖多种接入模式。
+
+### 7.1 DB 同步
+
+适用于 SQL Server、MySQL、Oracle、PostgreSQL。
+
+配置重点:
+
+1. 数据源连接。
+2. 源表/视图。
+3. 目标表。
+4. 增量字段。
+5. 主键/业务键。
+6. 字段映射。
+7. 同步窗口。
+8. 空表策略。
+
+### 7.2 API 拉取
+
+适用于 ERP、MES、WMS、SRM 等第三方 REST API。
+
+配置重点:
+
+1. API 地址。
+2. 请求方法。
+3. 认证方式。
+4. Header / Query / Body 模板。
+5. 分页或游标。
+6. 响应 JSONPath。
+7. 字段映射。
+8. 重试和超时。
+
+### 7.3 API 推送 / 入站
+
+适用于 Webhook、外部事件、状态回传。
+
+配置重点:
+
+1. 接收端点。
+2. 认证方式。
+3. 幂等键。
+4. 请求映射。
+5. 异常处理。
+6. 回执策略。
+
+### 7.4 API 出站
+
+适用于外部系统、BI、客户侧或政府侧统计服务读取数据。
+
+配置重点:
+
+1. 服务路径。
+2. 输出字段。
+3. 数据过滤。
+4. 权限控制。
+5. 调用日志。
+
+---
+
+## 8. 页面设计
+
+### 8.1 列表页
+
+路径:
+
+```text
+/aidop/data-platform/sync-tasks
+```
+
+页面标题建议:
+
+```text
+数据中台同步配置中心
+```
+
+副标题建议:
+
+```text
+外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪
+```
+
+列表列:
+
+1. 任务名称。
+2. 任务类型。
+3. 业务域。
+4. 消费模块。
+5. 外部系统。
+6. Service。
+7. 调度策略。
+8. 状态。
+9. 配置版本。
+10. 最近执行。
+11. 最近结果。
+
+操作:
+
+1. 编辑。
+2. 步骤。
+3. 实体。
+4. 字段映射。
+5. 参数。
+6. 公式。
+7. 调度策略。
+8. 立即执行。
+9. 运行日志。
+10. 链路详情。
+11. 高级调度设置。
+
+### 8.2 详情页 / 抽屉
+
+建议分 Tab:
+
+1. 基础信息。
+2. 执行步骤。
+3. 外部系统。
+4. 同步实体。
+5. 字段映射。
+6. 参数配置。
+7. 公式配置。
+8. 调度策略。
+9. 可靠性策略。
+10. 运行记录。
+11. 链路预览。
+
+### 8.3 调度策略交互
+
+调度策略在数据中台内完整展示和维护:
+
+1. 自动同步启停。
+2. 调度模式。
+3. Cron 表达式。
+4. Cron 中文说明。
+5. 同步窗口。
+6. 重试策略。
+7. 超时时间。
+8. 手工执行权限。
+9. 绑定 Admin.NET Job。
+10. 最近/下次调度时间。
+
+Admin.NET 原生任务调度页作为高级入口:
+
+```text
+高级调度设置
+```
+
+---
+
+## 9. 可靠同步能力
+
+方案应预留以下能力:
+
+1. 幂等键。
+2. 批次号。
+3. 重试策略。
+4. 失败补偿。
+5. 断点续传。
+6. 空表策略。
+7. 数据质量校验。
+8. 异常样本记录。
+9. 配置版本。
+10. 配置快照。
+11. 审计日志。
+12. 手工重跑。
+13. 运行对账。
+
+第一阶段可先实现配置记录和展示,运行驱动逐步接入。
+
+---
+
+## 10. 与现有模块关系
+
+| 模块 | 当前状态 | 配置中心关系 |
+|------|----------|--------------|
+| S1 | Service 已实现 MDP 同步 | 种子化为同步任务,逐步维护实体/字段/公式 |
+| S2 | Service 已实现 MDP 同步 | 种子化为同步任务 |
+| S3 | Service 已实现 MDP 同步,S3 范式较完整 | 作为配置中心参考样板 |
+| S4 | Service 已实现 MDP 同步,字段映射目前依赖兜底展示 | 通过字段映射编辑补齐真实配置 |
+| 后续 S5-S9 | 部分有业务页面或监控 | 按同一任务模型接入 |
+
+---
+
+## 11. 分阶段路线
+
+### Phase 1:配置模型与任务列表真实化
+
+1. 新增 5 张配置表。
+2. 初始化 S1-S4 任务配置。
+3. 数据任务页面读取真实任务。
+4. 展示调度绑定、最近运行、状态。
+5. 不改变现有 Service 执行逻辑。
+
+### Phase 2:实体与字段映射配置
+
+1. 维护 `mdp_entity`。
+2. 维护 `mdp_field_mapping`。
+3. 链路详情优先显示真实配置。
+4. S4 由系统兜底逐步转为真实配置。
+
+### Phase 3:参数与调度策略配置
+
+1. 维护任务参数。
+2. 维护调度业务配置。
+3. 同步 Admin.NET Job 基础信息。
+4. 预留 Service 读取参数快照接口。
+
+### Phase 4:公式配置复用
+
+1. 复用 SmartOps 公式编辑器。
+2. 复用公式校验和预览。
+3. 支持关联 KPI Master。
+4. 保存任务公式版本。
+5. 第一阶段先配置化和追踪,不直接替代 KPI SQL。
+
+### Phase 5:Service 绑定与配置快照
+
+1. 建设 Service 注册表。
+2. 任务配置绑定现有 `RunFullAsync`。
+3. 执行时记录配置版本。
+4. 运行日志可追溯配置快照。
+
+### Phase 6:逐步配置驱动
+
+1. STAGING 主键/业务键表达式逐步读配置。
+2. 简单字段映射逐步驱动执行。
+3. 数据质量规则配置化。
+4. 简单 KPI 公式配置化。
+5. 复杂 DWD 与事务逻辑继续留在 Service。
+
+---
+
+## 12. 非目标范围
+
+第一阶段不做以下事项:
+
+1. 不替换 Admin.NET 任务调度。
+2. 不把页面做成任意 SQL 执行器。
+3. 不一次性改造 S1-S4 复杂 Service。
+4. 不强制将所有 KPI 立即改为公式执行。
+5. 不重构现有 MDP 运行监控。
+6. 不改变现有稳定同步结果口径。
+
+---
+
+## 13. 结论
+
+数据中台同步配置中心应作为第三方系统标准化接入和可靠同步的业务控制面。它统一管理同步任务、实体、字段映射、参数、公式、调度和可观测能力;底层复用 Admin.NET 调度和现有 MDP Service,先实现配置可维护、可追踪,再逐步推动配置参与运行。

+ 12 - 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.168</AssemblyVersion>
-    <FileVersion>1.0.168</FileVersion>
-    <Version>1.0.168</Version>
+    <AssemblyVersion>1.0.169</AssemblyVersion>
+    <FileVersion>1.0.169</FileVersion>
+    <Version>1.0.169</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -115,6 +115,15 @@
     <None Update="UpdateScripts\1.0.157.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+<<<<<<< HEAD
+=======
+    <None Update="UpdateScripts\1.0.158.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.159.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+>>>>>>> f3661a943 (feat(data-platform): sync config center with entity and field mapping CRUD)
   </ItemGroup>
 
   <ItemGroup>

+ 204 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.159.sql

@@ -0,0 +1,204 @@
+-- MDP-SYNC-CONFIG-CENTER-1
+-- 数据中台同步配置中心:任务/步骤/参数/公式/调度业务配置表 + S1-S4 种子数据 + 菜单更名
+
+SET NAMES utf8mb4;
+
+CREATE TABLE IF NOT EXISTS mdp_sync_task (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    task_name VARCHAR(200) NOT NULL,
+    task_type VARCHAR(40) NOT NULL DEFAULT 'SERVICE_SYNC',
+    business_domain_code VARCHAR(100) NULL,
+    business_domain_name VARCHAR(200) NULL,
+    consumer_modules VARCHAR(500) NULL,
+    source_system_code VARCHAR(100) NULL,
+    service_key VARCHAR(100) NULL,
+    job_code VARCHAR(100) NULL,
+    schedule_job_id VARCHAR(100) NULL,
+    status TINYINT NOT NULL DEFAULT 1,
+    owner_role VARCHAR(100) NULL,
+    config_version INT NOT NULL DEFAULT 1,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_tenant_code (tenant_id, task_code),
+    KEY idx_mdp_sync_task_job_code (job_code),
+    KEY idx_mdp_sync_task_schedule_job (schedule_job_id),
+    KEY idx_mdp_sync_task_domain (business_domain_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务业务配置';
+
+CREATE TABLE IF NOT EXISTS mdp_sync_task_step (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    step_code VARCHAR(100) NOT NULL,
+    step_name VARCHAR(200) NOT NULL,
+    stage_type VARCHAR(40) NOT NULL,
+    service_method_key VARCHAR(100) NULL,
+    enabled TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_step (tenant_id, task_code, step_code),
+    KEY idx_mdp_sync_task_step_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务步骤配置';
+
+CREATE TABLE IF NOT EXISTS mdp_sync_task_param (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    scope_type VARCHAR(40) NOT NULL DEFAULT 'TASK',
+    scope_code VARCHAR(100) NOT NULL DEFAULT '',
+    param_key VARCHAR(100) NOT NULL,
+    param_name VARCHAR(200) NOT NULL,
+    param_type VARCHAR(40) NOT NULL DEFAULT 'STRING',
+    param_value TEXT NULL,
+    default_value TEXT NULL,
+    required TINYINT NOT NULL DEFAULT 0,
+    editable TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_param (tenant_id, task_code, scope_type, scope_code, param_key),
+    KEY idx_mdp_sync_task_param_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务参数配置';
+
+CREATE TABLE IF NOT EXISTS mdp_sync_task_formula (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    step_code VARCHAR(100) NULL,
+    formula_code VARCHAR(100) NOT NULL,
+    formula_name VARCHAR(200) NOT NULL,
+    metric_code VARCHAR(100) NULL,
+    formula_expr VARCHAR(1000) NULL,
+    formula_preview VARCHAR(1000) NULL,
+    formula_refs TEXT NULL,
+    calc_rule TEXT NULL,
+    direction VARCHAR(40) NOT NULL DEFAULT 'higher_is_better',
+    yellow_threshold DECIMAL(18,6) NULL,
+    red_threshold DECIMAL(18,6) NULL,
+    version_no INT NOT NULL DEFAULT 1,
+    is_enabled TINYINT NOT NULL DEFAULT 1,
+    sort_order INT NOT NULL DEFAULT 0,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_formula (tenant_id, task_code, formula_code, version_no),
+    KEY idx_mdp_sync_task_formula_metric (tenant_id, metric_code),
+    KEY idx_mdp_sync_task_formula_task (tenant_id, task_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务公式配置';
+
+CREATE TABLE IF NOT EXISTS mdp_sync_task_schedule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    task_code VARCHAR(100) NOT NULL,
+    schedule_job_id VARCHAR(100) NULL,
+    schedule_mode VARCHAR(40) NOT NULL DEFAULT 'CRON',
+    cron_expr VARCHAR(200) NULL,
+    cron_desc VARCHAR(500) NULL,
+    timezone VARCHAR(100) NOT NULL DEFAULT 'Asia/Shanghai',
+    auto_enabled TINYINT NOT NULL DEFAULT 1,
+    manual_enabled TINYINT NOT NULL DEFAULT 1,
+    retry_enabled TINYINT NOT NULL DEFAULT 1,
+    max_retry_count INT NOT NULL DEFAULT 3,
+    retry_interval_seconds INT NOT NULL DEFAULT 300,
+    timeout_seconds INT NOT NULL DEFAULT 3600,
+    misfire_policy VARCHAR(40) NULL,
+    sync_window_type VARCHAR(40) NOT NULL DEFAULT 'FULL',
+    sync_window_value VARCHAR(200) NULL,
+    last_schedule_time DATETIME NULL,
+    next_schedule_time DATETIME NULL,
+    admin_job_config_json TEXT NULL,
+    description VARCHAR(1000) NULL,
+    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_sync_task_schedule (tenant_id, task_code),
+    KEY idx_mdp_sync_task_schedule_job (schedule_job_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MDP同步任务调度业务配置';
+
+INSERT INTO mdp_sync_task
+(tenant_id, task_code, task_name, task_type, business_domain_code, business_domain_name, consumer_modules, source_system_code, service_key, job_code, schedule_job_id, status, config_version, description)
+VALUES
+(0, 'S1_MDP_SYNC_TRANSFORM', '订单交付域 MDP 同步', 'SERVICE_SYNC', 'order_delivery', '订单交付域', 'S1,S2,S3,S4,S7,S9', 'AIDOPDEV_MYSQL', 'S1_MDP_SYNC_TRANSFORM', 'S1_MDP_SYNC_TRANSFORM', 'job_s1_mdp_sync_transform', 1, 1, '订单交付域贴源/标准/DWD/KPI 同步任务'),
+(0, 'S2_MDP_SYNC_TRANSFORM', '工单排程域 MDP 同步', 'SERVICE_SYNC', 'work_schedule', '工单排程域', 'S2,S3,S5,S6,S8,S9', 'AIDOPDEV_MYSQL', 'S2_MDP_SYNC_TRANSFORM', 'S2_MDP_SYNC_TRANSFORM', 'job_s2_mdp_sync_transform', 1, 1, '工单排程域贴源/标准/DWD/KPI 同步任务'),
+(0, 'S3_MDP_SYNC_TRANSFORM', '供应采购域 MDP 同步', 'SERVICE_SYNC', 'supply_purchase', '供应采购域', 'S3,S4,S5,S8,S9', 'AIDOPDEV_MYSQL', 'S3_MDP_SYNC_TRANSFORM', 'S3_MDP_SYNC_TRANSFORM', 'job_s3_mdp_sync_transform', 1, 1, '供应采购域贴源/标准/DWD/KPI 同步任务'),
+(0, 'S4_MDP_SYNC_TRANSFORM', '采购执行域 MDP 同步', 'SERVICE_SYNC', 'purchase_execution', '采购执行域', 'S4,S5,S8,S9', 'AIDOPDEV_MYSQL', 'S4_MDP_SYNC_TRANSFORM', 'S4_MDP_SYNC_TRANSFORM', 'job_s4_mdp_sync_transform', 1, 1, '采购执行域贴源/标准/DWD/KPI 同步任务')
+ON DUPLICATE KEY UPDATE
+    task_name = VALUES(task_name),
+    business_domain_code = VALUES(business_domain_code),
+    business_domain_name = VALUES(business_domain_name),
+    consumer_modules = VALUES(consumer_modules),
+    source_system_code = VALUES(source_system_code),
+    service_key = VALUES(service_key),
+    job_code = VALUES(job_code),
+    schedule_job_id = VALUES(schedule_job_id),
+    status = VALUES(status),
+    description = VALUES(description),
+    update_time = CURRENT_TIMESTAMP;
+
+INSERT INTO mdp_sync_task_schedule
+(tenant_id, task_code, schedule_job_id, schedule_mode, cron_desc, auto_enabled, manual_enabled, retry_enabled, max_retry_count, retry_interval_seconds, timeout_seconds, sync_window_type, description)
+VALUES
+(0, 'S1_MDP_SYNC_TRANSFORM', 'job_s1_mdp_sync_transform', 'CRON', '按 Admin.NET 任务调度执行', 1, 1, 1, 3, 300, 3600, 'FULL', 'S1 同步调度业务配置'),
+(0, 'S2_MDP_SYNC_TRANSFORM', 'job_s2_mdp_sync_transform', 'CRON', '按 Admin.NET 任务调度执行', 1, 1, 1, 3, 300, 3600, 'FULL', 'S2 同步调度业务配置'),
+(0, 'S3_MDP_SYNC_TRANSFORM', 'job_s3_mdp_sync_transform', 'CRON', '按 Admin.NET 任务调度执行', 1, 1, 1, 3, 300, 3600, 'FULL', 'S3 同步调度业务配置'),
+(0, 'S4_MDP_SYNC_TRANSFORM', 'job_s4_mdp_sync_transform', 'CRON', '按 Admin.NET 任务调度执行', 1, 1, 1, 3, 300, 3600, 'FULL', 'S4 同步调度业务配置')
+ON DUPLICATE KEY UPDATE
+    schedule_job_id = VALUES(schedule_job_id),
+    schedule_mode = VALUES(schedule_mode),
+    cron_desc = VALUES(cron_desc),
+    auto_enabled = VALUES(auto_enabled),
+    manual_enabled = VALUES(manual_enabled),
+    retry_enabled = VALUES(retry_enabled),
+    max_retry_count = VALUES(max_retry_count),
+    retry_interval_seconds = VALUES(retry_interval_seconds),
+    timeout_seconds = VALUES(timeout_seconds),
+    sync_window_type = VALUES(sync_window_type),
+    description = VALUES(description),
+    update_time = CURRENT_TIMESTAMP;
+
+INSERT INTO mdp_sync_task_step
+(tenant_id, task_code, step_code, step_name, stage_type, enabled, sort_order, description)
+SELECT 0, v.task_code, v.step_code, v.step_name, v.stage_type, v.enabled, v.sort_order, v.description
+FROM (
+    SELECT 'S1_MDP_SYNC_TRANSFORM' AS task_code, 'SOURCE_CONNECT' AS step_code, '来源连接' AS step_name, 'SOURCE_CONNECT' AS stage_type, 1 AS enabled, 10 AS sort_order, '登记数据源与实体连接' AS description
+    UNION ALL SELECT 'S1_MDP_SYNC_TRANSFORM', 'STAGING', '贴源同步', 'STAGING', 1, 20, '源表抽取写入 mdp_stg_*'
+    UNION ALL SELECT 'S1_MDP_SYNC_TRANSFORM', 'STANDARD', '标准层转换', 'STANDARD', 1, 30, 'mdp_stg_* -> mdp_std_*'
+    UNION ALL SELECT 'S1_MDP_SYNC_TRANSFORM', 'DWD', 'DWD宽表', 'DWD', 1, 40, '标准层沉淀 DWD 宽表'
+    UNION ALL SELECT 'S1_MDP_SYNC_TRANSFORM', 'KPI', '指标写入', 'KPI', 1, 50, '计算并写入 KPI 指标'
+    UNION ALL SELECT 'S1_MDP_SYNC_TRANSFORM', 'QUALITY_CHECK', '质量校验', 'QUALITY_CHECK', 0, 60, '数据质量校验(预留)'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'SOURCE_CONNECT', '来源连接', 'SOURCE_CONNECT', 1, 10, '登记数据源与实体连接'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'STAGING', '贴源同步', 'STAGING', 1, 20, '源表抽取写入 mdp_stg_*'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'STANDARD', '标准层转换', 'STANDARD', 1, 30, 'mdp_stg_* -> mdp_std_*'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'DWD', 'DWD宽表', 'DWD', 1, 40, '标准层沉淀 DWD 宽表'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'KPI', '指标写入', 'KPI', 1, 50, '计算并写入 KPI 指标'
+    UNION ALL SELECT 'S2_MDP_SYNC_TRANSFORM', 'QUALITY_CHECK', '质量校验', 'QUALITY_CHECK', 0, 60, '数据质量校验(预留)'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'SOURCE_CONNECT', '来源连接', 'SOURCE_CONNECT', 1, 10, '登记数据源与实体连接'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'STAGING', '贴源同步', 'STAGING', 1, 20, '源表抽取写入 mdp_stg_*'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'STANDARD', '标准层转换', 'STANDARD', 1, 30, 'mdp_stg_* -> mdp_std_*'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'DWD', 'DWD宽表', 'DWD', 1, 40, '标准层沉淀 DWD 宽表'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'KPI', '指标写入', 'KPI', 1, 50, '计算并写入 KPI 指标'
+    UNION ALL SELECT 'S3_MDP_SYNC_TRANSFORM', 'QUALITY_CHECK', '质量校验', 'QUALITY_CHECK', 0, 60, '数据质量校验(预留)'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'SOURCE_CONNECT', '来源连接', 'SOURCE_CONNECT', 1, 10, '登记数据源与实体连接'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'STAGING', '贴源同步', 'STAGING', 1, 20, '源表抽取写入 mdp_stg_s4_*'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'STANDARD', '标准层转换', 'STANDARD', 1, 30, 'mdp_stg_s4_* -> mdp_std_s4_*'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'DWD', 'DWD宽表', 'DWD', 1, 40, '采购执行 DWD 宽表'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'KPI', '指标写入', 'KPI', 1, 50, '计算并写入 KPI 指标'
+    UNION ALL SELECT 'S4_MDP_SYNC_TRANSFORM', 'QUALITY_CHECK', '质量校验', 'QUALITY_CHECK', 0, 60, '数据质量校验(预留)'
+) v
+ON DUPLICATE KEY UPDATE
+    step_name = VALUES(step_name),
+    stage_type = VALUES(stage_type),
+    enabled = VALUES(enabled),
+    sort_order = VALUES(sort_order),
+    description = VALUES(description),
+    update_time = CURRENT_TIMESTAMP;
+
+UPDATE SysMenu
+SET Title = '同步配置中心',
+    Remark = '外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪'
+WHERE Id = 1320990000404;

+ 373 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/MdpSyncEntityConfigService.cs

@@ -0,0 +1,373 @@
+using Admin.NET.Plugin.AiDOP.Dto.DataPlatform;
+using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
+
+namespace Admin.NET.Plugin.AiDOP.DataPlatform;
+
+/// <summary>
+/// 同步任务实体与字段映射配置 API。
+/// </summary>
+[ApiDescriptionSettings(Order = 324, Description = "数据中台同步实体与字段映射")]
+[Route("api/DataPlatform")]
+[AllowAnonymous]
+[NonUnify]
+public class MdpSyncEntityConfigService : IDynamicApiController, ITransient
+{
+    private static readonly HashSet<string> ValidFieldTypes = new(StringComparer.OrdinalIgnoreCase)
+    {
+        "DIRECT", "JSONPATH", "SCRIPT", "CONST", "LOOKUP"
+    };
+
+    private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
+
+    public MdpSyncEntityConfigService(ISqlSugarClient db, UserManager userManager)
+    {
+        _db = db;
+        _userManager = userManager;
+    }
+
+    [DisplayName("同步任务实体列表")]
+    [HttpGet("sync-tasks/{taskCode}/entities")]
+    public async Task<object> GetEntities(string taskCode)
+    {
+        var tenantId = _userManager.TenantId;
+        var task = await FindTaskAsync(taskCode, tenantId);
+        var prefix = ResolveEntityCodePrefix(task.TaskCode);
+        var jobCode = string.IsNullOrWhiteSpace(task.JobCode) ? task.TaskCode : task.JobCode;
+
+        var where = new List<string> { "(e.tenant_id = @TenantId OR e.tenant_id = 0)", "e.status = 1" };
+        var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
+
+        if (!string.IsNullOrWhiteSpace(prefix))
+        {
+            where.Add("(e.entity_code LIKE @EntityPrefix OR e.job_id = @JobCode)");
+            pars.Add(new SugarParameter("@EntityPrefix", prefix + "%"));
+            pars.Add(new SugarParameter("@JobCode", jobCode));
+        }
+        else
+        {
+            where.Add("e.job_id = @JobCode");
+            pars.Add(new SugarParameter("@JobCode", jobCode));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+        var list = await _db.Ado.SqlQueryAsync<MdpEntityRow>(
+            $"""
+            SELECT e.id AS Id, e.tenant_id AS TenantId, e.source_id AS SourceId,
+                   s.source_code AS SourceCode, s.source_name AS SourceName, s.source_type AS SourceType,
+                   e.entity_code AS EntityCode, e.entity_name AS EntityName, e.entity_type AS EntityType,
+                   e.source_table_name AS SourceTableName, e.source_api_path AS SourceApiPath,
+                   e.target_table_name AS TargetTableName, e.sync_mode AS SyncMode, e.incr_column AS IncrColumn,
+                   e.batch_size AS BatchSize, e.job_id AS JobId, e.status AS Status, e.remark AS Remark,
+                   IFNULL(m.cnt, 0) AS FieldMappingCount
+            FROM mdp_entity e
+            LEFT JOIN mdp_source s ON s.id = e.source_id
+            LEFT JOIN (
+                SELECT entity_id, COUNT(1) AS cnt FROM mdp_field_mapping GROUP BY entity_id
+            ) m ON m.entity_id = e.id
+            WHERE {whereSql}
+            ORDER BY e.entity_code
+            """,
+            pars);
+
+        return list;
+    }
+
+    [DisplayName("新增同步任务实体")]
+    [HttpPost("sync-tasks/{taskCode}/entities")]
+    public async Task<object> CreateEntity(string taskCode, [FromBody] MdpEntityUpsertInput input)
+    {
+        ValidateEntityUpsert(input);
+        var tenantId = _userManager.TenantId;
+        var task = await FindTaskAsync(taskCode, tenantId);
+        var sourceId = await ResolveSourceIdAsync(input, tenantId);
+        var entityTenantId = task.TenantId == 0 ? 0L : tenantId;
+        var jobCode = string.IsNullOrWhiteSpace(task.JobCode) ? task.TaskCode : task.JobCode;
+
+        var exists = await _db.Queryable<MdpEntity>()
+            .Where(u => u.TenantId == entityTenantId && u.EntityCode == input.EntityCode.Trim())
+            .AnyAsync();
+        if (exists)
+            throw Oops.Oh("实体编码已存在");
+
+        var now = DateTime.Now;
+        var entity = new MdpEntity
+        {
+            TenantId = entityTenantId,
+            SourceId = sourceId,
+            EntityCode = input.EntityCode.Trim(),
+            EntityName = input.EntityName.Trim(),
+            EntityType = string.IsNullOrWhiteSpace(input.EntityType) ? "TABLE" : input.EntityType.Trim(),
+            SourceTableName = input.SourceTableName?.Trim(),
+            SourceApiPath = input.SourceApiPath?.Trim(),
+            TargetTableName = input.TargetTableName?.Trim(),
+            SyncMode = string.IsNullOrWhiteSpace(input.SyncMode) ? "INCR" : input.SyncMode.Trim(),
+            IncrColumn = input.IncrColumn?.Trim(),
+            BatchSize = input.BatchSize <= 0 ? 5000 : input.BatchSize,
+            JobId = string.IsNullOrWhiteSpace(input.JobId) ? jobCode : input.JobId.Trim(),
+            Status = input.Status <= 0 ? 0 : 1,
+            Remark = input.Remark?.Trim(),
+            CreateTime = now,
+            UpdateTime = now
+        };
+
+        var id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
+        await BumpTaskConfigVersionAsync(task);
+        return new { id };
+    }
+
+    [DisplayName("更新同步实体")]
+    [HttpPut("entities/{id:long}")]
+    public async Task<object> UpdateEntity(long id, [FromBody] MdpEntityUpsertInput input)
+    {
+        ValidateEntityUpsert(input);
+        var tenantId = _userManager.TenantId;
+        var entity = await FindEditableEntityAsync(id, tenantId);
+
+        entity.EntityName = input.EntityName.Trim();
+        entity.EntityType = string.IsNullOrWhiteSpace(input.EntityType) ? "TABLE" : input.EntityType.Trim();
+        entity.SourceId = await ResolveSourceIdAsync(input, tenantId, entity.SourceId);
+        entity.SourceTableName = input.SourceTableName?.Trim();
+        entity.SourceApiPath = input.SourceApiPath?.Trim();
+        entity.TargetTableName = input.TargetTableName?.Trim();
+        entity.SyncMode = string.IsNullOrWhiteSpace(input.SyncMode) ? "INCR" : input.SyncMode.Trim();
+        entity.IncrColumn = input.IncrColumn?.Trim();
+        entity.BatchSize = input.BatchSize <= 0 ? 5000 : input.BatchSize;
+        entity.JobId = input.JobId?.Trim();
+        entity.Status = input.Status <= 0 ? 0 : 1;
+        entity.Remark = input.Remark?.Trim();
+        entity.UpdateTime = DateTime.Now;
+
+        if (!IsBuiltInEntity(entity))
+            entity.EntityCode = input.EntityCode.Trim();
+
+        await _db.Updateable(entity).ExecuteCommandAsync();
+        return new { id = entity.Id };
+    }
+
+    [DisplayName("删除同步实体")]
+    [HttpDelete("entities/{id:long}")]
+    public async Task<object> DeleteEntity(long id)
+    {
+        var tenantId = _userManager.TenantId;
+        var entity = await FindEditableEntityAsync(id, tenantId);
+        if (IsBuiltInEntity(entity) && entity.TenantId == 0)
+            throw Oops.Oh("内置 MDP 实体不允许删除");
+
+        entity.Status = 0;
+        entity.UpdateTime = DateTime.Now;
+        await _db.Updateable(entity).UpdateColumns(u => new { u.Status, u.UpdateTime }).ExecuteCommandAsync();
+        return new { id = entity.Id };
+    }
+
+    [DisplayName("实体字段映射列表")]
+    [HttpGet("entities/{entityId:long}/field-mappings")]
+    public async Task<object> GetFieldMappings(long entityId)
+    {
+        var tenantId = _userManager.TenantId;
+        await FindEditableEntityAsync(entityId, tenantId);
+        var list = await _db.Queryable<MdpFieldMapping>()
+            .Where(u => u.EntityId == entityId)
+            .OrderBy(u => u.SortOrder)
+            .OrderBy(u => u.TargetField)
+            .ToListAsync();
+        return list.Select(MapFieldMappingRow).ToList();
+    }
+
+    [DisplayName("新增实体字段映射")]
+    [HttpPost("entities/{entityId:long}/field-mappings")]
+    public async Task<object> CreateFieldMapping(long entityId, [FromBody] MdpFieldMappingUpsertInput input)
+    {
+        ValidateFieldMappingUpsert(input);
+        var tenantId = _userManager.TenantId;
+        await FindEditableEntityAsync(entityId, tenantId);
+
+        var exists = await _db.Queryable<MdpFieldMapping>()
+            .Where(u => u.EntityId == entityId && u.TargetField == input.TargetField.Trim())
+            .AnyAsync();
+        if (exists)
+            throw Oops.Oh("目标字段映射已存在");
+
+        var entity = MapFieldMappingInsert(entityId, input);
+        var id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
+        return new { id };
+    }
+
+    [DisplayName("更新字段映射")]
+    [HttpPut("field-mappings/{id:long}")]
+    public async Task<object> UpdateFieldMapping(long id, [FromBody] MdpFieldMappingUpsertInput input)
+    {
+        ValidateFieldMappingUpsert(input);
+        var tenantId = _userManager.TenantId;
+        var mapping = await _db.Queryable<MdpFieldMapping>().Where(u => u.Id == id).FirstAsync();
+        if (mapping == null)
+            throw Oops.Oh("字段映射不存在");
+
+        await FindEditableEntityAsync(mapping.EntityId, tenantId);
+
+        var duplicate = await _db.Queryable<MdpFieldMapping>()
+            .Where(u => u.EntityId == mapping.EntityId && u.TargetField == input.TargetField.Trim() && u.Id != id)
+            .AnyAsync();
+        if (duplicate)
+            throw Oops.Oh("目标字段映射已存在");
+
+        mapping.SourceField = input.SourceField.Trim();
+        mapping.TargetField = input.TargetField.Trim();
+        mapping.FieldType = NormalizeFieldType(input.FieldType);
+        mapping.TransformScript = input.TransformScript?.Trim();
+        mapping.ConstValue = input.ConstValue?.Trim();
+        mapping.LookupTable = input.LookupTable?.Trim();
+        mapping.IsRequired = input.IsRequired ? 1 : 0;
+        mapping.DefaultValue = input.DefaultValue?.Trim();
+        mapping.SortOrder = input.SortOrder;
+        await _db.Updateable(mapping).ExecuteCommandAsync();
+        return new { id = mapping.Id };
+    }
+
+    [DisplayName("删除字段映射")]
+    [HttpDelete("field-mappings/{id:long}")]
+    public async Task<object> DeleteFieldMapping(long id)
+    {
+        var tenantId = _userManager.TenantId;
+        var mapping = await _db.Queryable<MdpFieldMapping>().Where(u => u.Id == id).FirstAsync();
+        if (mapping == null)
+            throw Oops.Oh("字段映射不存在");
+
+        await FindEditableEntityAsync(mapping.EntityId, tenantId);
+        await _db.Deleteable<MdpFieldMapping>().Where(u => u.Id == id).ExecuteCommandAsync();
+        return new { id };
+    }
+
+    private async Task<MdpSyncTask> FindTaskAsync(string taskCode, long tenantId)
+    {
+        if (string.IsNullOrWhiteSpace(taskCode))
+            throw Oops.Oh("任务编码不能为空");
+
+        var task = await _db.Queryable<MdpSyncTask>()
+            .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0) && u.Status == 1)
+            .OrderBy(u => u.TenantId, OrderByType.Desc)
+            .FirstAsync();
+        if (task == null)
+            throw Oops.Oh("同步任务不存在");
+        return task;
+    }
+
+    private async Task<MdpEntity> FindEditableEntityAsync(long id, long tenantId)
+    {
+        var entity = await _db.Queryable<MdpEntity>()
+            .Where(u => u.Id == id && (u.TenantId == tenantId || u.TenantId == 0))
+            .FirstAsync();
+        if (entity == null)
+            throw Oops.Oh("同步实体不存在");
+        return entity;
+    }
+
+    private async Task<long> ResolveSourceIdAsync(MdpEntityUpsertInput input, long tenantId, long? fallbackSourceId = null)
+    {
+        if (input.SourceId > 0)
+            return input.SourceId;
+
+        if (!string.IsNullOrWhiteSpace(input.SourceCode))
+        {
+            var sourceId = await _db.Ado.GetLongAsync(
+                """
+                SELECT id FROM mdp_source
+                WHERE source_code = @SourceCode AND (tenant_id = @TenantId OR tenant_id = 0)
+                ORDER BY tenant_id DESC
+                LIMIT 1
+                """,
+                new List<SugarParameter>
+                {
+                    new("@SourceCode", input.SourceCode.Trim()),
+                    new("@TenantId", tenantId)
+                });
+            if (sourceId > 0)
+                return sourceId;
+            throw Oops.Oh($"数据源 {input.SourceCode} 不存在");
+        }
+
+        if (fallbackSourceId is > 0)
+            return fallbackSourceId.Value;
+
+        throw Oops.Oh("请指定 sourceId 或 sourceCode");
+    }
+
+    private async Task BumpTaskConfigVersionAsync(MdpSyncTask task)
+    {
+        task.ConfigVersion += 1;
+        task.UpdateTime = DateTime.Now;
+        await _db.Updateable(task).UpdateColumns(u => new { u.ConfigVersion, u.UpdateTime }).ExecuteCommandAsync();
+    }
+
+    private static string? ResolveEntityCodePrefix(string taskCode)
+    {
+        if (string.IsNullOrWhiteSpace(taskCode))
+            return null;
+
+        var first = taskCode.Split('_', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
+        if (string.IsNullOrWhiteSpace(first) || first.Length < 2 || first[0] != 'S' || !char.IsDigit(first[1]))
+            return null;
+
+        return first + "_";
+    }
+
+    private static bool IsBuiltInEntity(MdpEntity entity) =>
+        entity.EntityCode.StartsWith("S1_", StringComparison.OrdinalIgnoreCase)
+        || entity.EntityCode.StartsWith("S2_", StringComparison.OrdinalIgnoreCase)
+        || entity.EntityCode.StartsWith("S3_", StringComparison.OrdinalIgnoreCase)
+        || entity.EntityCode.StartsWith("S4_", StringComparison.OrdinalIgnoreCase);
+
+    private static void ValidateEntityUpsert(MdpEntityUpsertInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.EntityCode))
+            throw Oops.Oh("实体编码不能为空");
+        if (string.IsNullOrWhiteSpace(input.EntityName))
+            throw Oops.Oh("实体名称不能为空");
+    }
+
+    private static void ValidateFieldMappingUpsert(MdpFieldMappingUpsertInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.SourceField))
+            throw Oops.Oh("源字段不能为空");
+        if (string.IsNullOrWhiteSpace(input.TargetField))
+            throw Oops.Oh("目标字段不能为空");
+    }
+
+    private static string NormalizeFieldType(string? fieldType)
+    {
+        var normalized = string.IsNullOrWhiteSpace(fieldType) ? "DIRECT" : fieldType.Trim().ToUpperInvariant();
+        return ValidFieldTypes.Contains(normalized) ? normalized : "DIRECT";
+    }
+
+    private static MdpFieldMapping MapFieldMappingInsert(long entityId, MdpFieldMappingUpsertInput input) =>
+        new()
+        {
+            EntityId = entityId,
+            SourceField = input.SourceField.Trim(),
+            TargetField = input.TargetField.Trim(),
+            FieldType = NormalizeFieldType(input.FieldType),
+            TransformScript = input.TransformScript?.Trim(),
+            ConstValue = input.ConstValue?.Trim(),
+            LookupTable = input.LookupTable?.Trim(),
+            IsRequired = input.IsRequired ? 1 : 0,
+            DefaultValue = input.DefaultValue?.Trim(),
+            SortOrder = input.SortOrder,
+            CreateTime = DateTime.Now
+        };
+
+    private static MdpFieldMappingRow MapFieldMappingRow(MdpFieldMapping row) =>
+        new()
+        {
+            Id = row.Id,
+            EntityId = row.EntityId,
+            SourceField = row.SourceField,
+            TargetField = row.TargetField,
+            FieldType = row.FieldType,
+            TransformScript = row.TransformScript,
+            ConstValue = row.ConstValue,
+            LookupTable = row.LookupTable,
+            IsRequired = row.IsRequired != 0,
+            DefaultValue = row.DefaultValue,
+            SortOrder = row.SortOrder
+        };
+}

+ 502 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/MdpSyncTaskConfigService.cs

@@ -0,0 +1,502 @@
+using Admin.NET.Plugin.AiDOP.Dto.DataPlatform;
+using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
+using Admin.NET.Plugin.AiDOP.Order;
+
+namespace Admin.NET.Plugin.AiDOP.DataPlatform;
+
+/// <summary>
+/// 数据中台同步配置中心 API。
+/// </summary>
+[ApiDescriptionSettings(Order = 323, Description = "数据中台同步配置中心")]
+[Route("api/DataPlatform")]
+[AllowAnonymous]
+[NonUnify]
+public class MdpSyncTaskConfigService : IDynamicApiController, ITransient
+{
+    private static readonly HashSet<string> ProtectedTaskCodes = new(StringComparer.OrdinalIgnoreCase)
+    {
+        "S1_MDP_SYNC_TRANSFORM",
+        "S2_MDP_SYNC_TRANSFORM",
+        "S3_MDP_SYNC_TRANSFORM",
+        "S4_MDP_SYNC_TRANSFORM"
+    };
+
+    private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
+
+    public MdpSyncTaskConfigService(ISqlSugarClient db, UserManager userManager)
+    {
+        _db = db;
+        _userManager = userManager;
+    }
+
+    [DisplayName("同步任务列表")]
+    [HttpGet("sync-tasks")]
+    public async Task<object> GetList([FromQuery] MdpSyncTaskListQuery input)
+    {
+        var tenantId = _userManager.TenantId;
+        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> { "(t.tenant_id = @TenantId OR t.tenant_id = 0)", "t.status = 1" };
+        var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
+
+        if (!string.IsNullOrWhiteSpace(input.Keyword))
+        {
+            where.Add("(t.task_code LIKE @Keyword OR t.task_name LIKE @Keyword OR t.business_domain_name LIKE @Keyword)");
+            pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.BusinessDomainCode))
+        {
+            where.Add("t.business_domain_code = @BusinessDomainCode");
+            pars.Add(new SugarParameter("@BusinessDomainCode", input.BusinessDomainCode.Trim()));
+        }
+        if (input.Status.HasValue)
+        {
+            where[1] = "t.status = @Status";
+            pars.Add(new SugarParameter("@Status", input.Status.Value));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM mdp_sync_task t WHERE {whereSql}", pars);
+        var list = await _db.Ado.SqlQueryAsync<MdpSyncTaskListRow>(
+            $"""
+            SELECT t.id AS Id, t.tenant_id AS TenantId, t.task_code AS TaskCode, t.task_name AS TaskName,
+                   t.task_type AS TaskType, t.business_domain_code AS BusinessDomainCode,
+                   t.business_domain_name AS BusinessDomainName, t.consumer_modules AS ConsumerModules,
+                   t.source_system_code AS SourceSystemCode, t.service_key AS ServiceKey,
+                   t.job_code AS JobCode, t.schedule_job_id AS ScheduleJobId, t.status AS Status,
+                   t.config_version AS ConfigVersion, t.description AS Description,
+                   sch.schedule_mode AS ScheduleMode,
+                   IFNULL(sch.auto_enabled, 1) AS AutoEnabled,
+                   IFNULL(sch.manual_enabled, 1) AS ManualEnabled,
+                   run.status AS LastRunStatus, run.start_time AS LastRunTime
+            FROM mdp_sync_task t
+            LEFT JOIN mdp_sync_task_schedule sch
+              ON sch.tenant_id = t.tenant_id AND sch.task_code = t.task_code
+            LEFT JOIN (
+                SELECT job_code, MAX(start_time) AS last_run_time
+                FROM mdp_transform_run_log
+                WHERE {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)}
+                GROUP BY job_code
+            ) latest ON latest.job_code = IFNULL(t.job_code, t.task_code)
+            LEFT JOIN mdp_transform_run_log run
+              ON run.job_code = latest.job_code AND run.start_time = latest.last_run_time
+            WHERE {whereSql}
+            ORDER BY t.task_code
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    [DisplayName("同步任务详情")]
+    [HttpGet("sync-tasks/{id:long}")]
+    public async Task<object> GetDetail(long id)
+    {
+        var tenantId = _userManager.TenantId;
+        var row = await _db.Ado.SqlQuerySingleAsync<MdpSyncTaskListRow>(
+            $"""
+            SELECT t.id AS Id, t.tenant_id AS TenantId, t.task_code AS TaskCode, t.task_name AS TaskName,
+                   t.task_type AS TaskType, t.business_domain_code AS BusinessDomainCode,
+                   t.business_domain_name AS BusinessDomainName, t.consumer_modules AS ConsumerModules,
+                   t.source_system_code AS SourceSystemCode, t.service_key AS ServiceKey,
+                   t.job_code AS JobCode, t.schedule_job_id AS ScheduleJobId, t.status AS Status,
+                   t.config_version AS ConfigVersion, t.description AS Description,
+                   sch.schedule_mode AS ScheduleMode,
+                   IFNULL(sch.auto_enabled, 1) AS AutoEnabled,
+                   IFNULL(sch.manual_enabled, 1) AS ManualEnabled,
+                   run.status AS LastRunStatus, run.start_time AS LastRunTime
+            FROM mdp_sync_task t
+            LEFT JOIN mdp_sync_task_schedule sch
+              ON sch.tenant_id = t.tenant_id AND sch.task_code = t.task_code
+            LEFT JOIN (
+                SELECT job_code, MAX(start_time) AS last_run_time
+                FROM mdp_transform_run_log
+                WHERE {MdpMonitorService.BuildMdpRunLogTenantWhere(tenantId)}
+                GROUP BY job_code
+            ) latest ON latest.job_code = IFNULL(t.job_code, t.task_code)
+            LEFT JOIN mdp_transform_run_log run
+              ON run.job_code = latest.job_code AND run.start_time = latest.last_run_time
+            WHERE t.id = @Id AND (t.tenant_id = @TenantId OR t.tenant_id = 0)
+            LIMIT 1
+            """,
+            new SugarParameter("@Id", id),
+            new SugarParameter("@TenantId", tenantId));
+
+        if (row == null)
+            throw Oops.Oh("同步任务不存在");
+        return row;
+    }
+
+    [DisplayName("新增同步任务")]
+    [HttpPost("sync-tasks")]
+    public async Task<object> Create([FromBody] MdpSyncTaskUpsertInput input)
+    {
+        ValidateUpsert(input);
+        var tenantId = _userManager.TenantId;
+        var exists = await _db.Queryable<MdpSyncTask>()
+            .Where(u => u.TenantId == tenantId && u.TaskCode == input.TaskCode.Trim())
+            .AnyAsync();
+        if (exists)
+            throw Oops.Oh("任务编码已存在");
+
+        var entity = MapUpsert(input, tenantId);
+        entity.ConfigVersion = 1;
+        entity.CreateTime = DateTime.Now;
+        entity.UpdateTime = DateTime.Now;
+        var id = await _db.Insertable(entity).ExecuteReturnBigIdentityAsync();
+        await EnsureDefaultScheduleAsync(tenantId, entity.TaskCode, entity.ScheduleJobId);
+        return new { id };
+    }
+
+    [DisplayName("更新同步任务")]
+    [HttpPut("sync-tasks/{id:long}")]
+    public async Task<object> Update(long id, [FromBody] MdpSyncTaskUpsertInput input)
+    {
+        ValidateUpsert(input);
+        var tenantId = _userManager.TenantId;
+        var entity = await FindEditableTaskAsync(id, tenantId);
+        entity.TaskName = input.TaskName.Trim();
+        entity.TaskType = string.IsNullOrWhiteSpace(input.TaskType) ? "SERVICE_SYNC" : input.TaskType.Trim();
+        entity.BusinessDomainCode = input.BusinessDomainCode?.Trim();
+        entity.BusinessDomainName = input.BusinessDomainName?.Trim();
+        entity.ConsumerModules = input.ConsumerModules?.Trim();
+        entity.SourceSystemCode = input.SourceSystemCode?.Trim();
+        entity.ServiceKey = input.ServiceKey?.Trim();
+        entity.JobCode = string.IsNullOrWhiteSpace(input.JobCode) ? input.TaskCode.Trim() : input.JobCode.Trim();
+        entity.ScheduleJobId = input.ScheduleJobId?.Trim();
+        entity.Status = input.Status <= 0 ? 0 : 1;
+        entity.OwnerRole = input.OwnerRole?.Trim();
+        entity.Description = input.Description?.Trim();
+        entity.ConfigVersion += 1;
+        entity.UpdateTime = DateTime.Now;
+
+        if (!ProtectedTaskCodes.Contains(entity.TaskCode))
+            entity.TaskCode = input.TaskCode.Trim();
+
+        await _db.Updateable(entity).ExecuteCommandAsync();
+        await SyncScheduleJobIdAsync(entity.TenantId, entity.TaskCode, entity.ScheduleJobId);
+        return new { id = entity.Id };
+    }
+
+    [DisplayName("删除同步任务")]
+    [HttpDelete("sync-tasks/{id:long}")]
+    public async Task<object> Delete(long id)
+    {
+        var tenantId = _userManager.TenantId;
+        var entity = await FindEditableTaskAsync(id, tenantId);
+        if (ProtectedTaskCodes.Contains(entity.TaskCode) && entity.TenantId == 0)
+            throw Oops.Oh("内置 MDP 同步任务不允许删除");
+
+        entity.Status = 0;
+        entity.UpdateTime = DateTime.Now;
+        await _db.Updateable(entity).UpdateColumns(u => new { u.Status, u.UpdateTime }).ExecuteCommandAsync();
+        return new { id };
+    }
+
+    [DisplayName("同步任务步骤列表")]
+    [HttpGet("sync-tasks/{taskCode}/steps")]
+    public async Task<object> GetSteps(string taskCode)
+    {
+        var tenantId = _userManager.TenantId;
+        await EnsureTaskExistsAsync(taskCode, tenantId);
+        var steps = await _db.Queryable<MdpSyncTaskStep>()
+            .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
+            .OrderBy(u => u.SortOrder)
+            .ToListAsync();
+
+        return steps.Select(MapStepRow).ToList();
+    }
+
+    [DisplayName("保存同步任务步骤")]
+    [HttpPut("sync-tasks/{taskCode}/steps")]
+    public async Task<object> SaveSteps(string taskCode, [FromBody] List<MdpSyncTaskStepRow> input)
+    {
+        var tenantId = _userManager.TenantId;
+        var task = await EnsureTaskExistsAsync(taskCode, tenantId);
+        var rows = input ?? new List<MdpSyncTaskStepRow>();
+        var now = DateTime.Now;
+
+        foreach (var row in rows)
+        {
+            if (string.IsNullOrWhiteSpace(row.StepCode) || string.IsNullOrWhiteSpace(row.StepName))
+                throw Oops.Oh("步骤编码和名称不能为空");
+
+            var entity = await _db.Queryable<MdpSyncTaskStep>()
+                .Where(u => u.TaskCode == taskCode && u.StepCode == row.StepCode && (u.TenantId == tenantId || u.TenantId == 0))
+                .FirstAsync();
+
+            if (entity == null)
+            {
+                entity = new MdpSyncTaskStep
+                {
+                    TenantId = task.TenantId == 0 ? 0 : tenantId,
+                    TaskCode = taskCode,
+                    StepCode = row.StepCode.Trim(),
+                    StepName = row.StepName.Trim(),
+                    StageType = string.IsNullOrWhiteSpace(row.StageType) ? row.StepCode.Trim() : row.StageType.Trim(),
+                    ServiceMethodKey = row.ServiceMethodKey?.Trim(),
+                    Enabled = row.Enabled ? 1 : 0,
+                    SortOrder = row.SortOrder,
+                    Description = row.Description?.Trim(),
+                    CreateTime = now,
+                    UpdateTime = now
+                };
+                await _db.Insertable(entity).ExecuteCommandAsync();
+            }
+            else
+            {
+                entity.StepName = row.StepName.Trim();
+                entity.StageType = string.IsNullOrWhiteSpace(row.StageType) ? row.StepCode.Trim() : row.StageType.Trim();
+                entity.ServiceMethodKey = row.ServiceMethodKey?.Trim();
+                entity.Enabled = row.Enabled ? 1 : 0;
+                entity.SortOrder = row.SortOrder;
+                entity.Description = row.Description?.Trim();
+                entity.UpdateTime = now;
+                await _db.Updateable(entity).ExecuteCommandAsync();
+            }
+        }
+
+        task.ConfigVersion += 1;
+        task.UpdateTime = now;
+        await _db.Updateable(task).UpdateColumns(u => new { u.ConfigVersion, u.UpdateTime }).ExecuteCommandAsync();
+        return new { count = rows.Count };
+    }
+
+    [DisplayName("同步任务调度配置")]
+    [HttpGet("sync-tasks/{taskCode}/schedule")]
+    public async Task<object> GetSchedule(string taskCode)
+    {
+        var tenantId = _userManager.TenantId;
+        await EnsureTaskExistsAsync(taskCode, tenantId);
+        var schedule = await FindScheduleAsync(taskCode, tenantId);
+        if (schedule == null)
+            return new MdpSyncTaskScheduleRow { TaskCode = taskCode, ScheduleMode = "CRON" };
+        return MapScheduleRow(schedule);
+    }
+
+    [DisplayName("保存同步任务调度配置")]
+    [HttpPut("sync-tasks/{taskCode}/schedule")]
+    public async Task<object> SaveSchedule(string taskCode, [FromBody] MdpSyncTaskScheduleRow input)
+    {
+        var tenantId = _userManager.TenantId;
+        var task = await EnsureTaskExistsAsync(taskCode, tenantId);
+        var schedule = await FindScheduleAsync(taskCode, tenantId);
+        var now = DateTime.Now;
+
+        if (schedule == null)
+        {
+            schedule = new MdpSyncTaskSchedule
+            {
+                TenantId = task.TenantId == 0 ? 0 : tenantId,
+                TaskCode = taskCode,
+                CreateTime = now,
+                UpdateTime = now
+            };
+        }
+
+        schedule.ScheduleJobId = input.ScheduleJobId?.Trim() ?? task.ScheduleJobId;
+        schedule.ScheduleMode = string.IsNullOrWhiteSpace(input.ScheduleMode) ? "CRON" : input.ScheduleMode.Trim();
+        schedule.CronExpr = input.CronExpr?.Trim();
+        schedule.CronDesc = input.CronDesc?.Trim();
+        schedule.Timezone = string.IsNullOrWhiteSpace(input.Timezone) ? "Asia/Shanghai" : input.Timezone.Trim();
+        schedule.AutoEnabled = input.AutoEnabled ? 1 : 0;
+        schedule.ManualEnabled = input.ManualEnabled ? 1 : 0;
+        schedule.RetryEnabled = input.RetryEnabled ? 1 : 0;
+        schedule.MaxRetryCount = input.MaxRetryCount <= 0 ? 3 : input.MaxRetryCount;
+        schedule.RetryIntervalSeconds = input.RetryIntervalSeconds <= 0 ? 300 : input.RetryIntervalSeconds;
+        schedule.TimeoutSeconds = input.TimeoutSeconds <= 0 ? 3600 : input.TimeoutSeconds;
+        schedule.MisfirePolicy = input.MisfirePolicy?.Trim();
+        schedule.SyncWindowType = string.IsNullOrWhiteSpace(input.SyncWindowType) ? "FULL" : input.SyncWindowType.Trim();
+        schedule.SyncWindowValue = input.SyncWindowValue?.Trim();
+        schedule.LastScheduleTime = input.LastScheduleTime;
+        schedule.NextScheduleTime = input.NextScheduleTime;
+        schedule.AdminJobConfigJson = input.AdminJobConfigJson;
+        schedule.Description = input.Description?.Trim();
+        schedule.UpdateTime = now;
+
+        if (schedule.Id <= 0)
+            await _db.Insertable(schedule).ExecuteCommandAsync();
+        else
+            await _db.Updateable(schedule).ExecuteCommandAsync();
+
+        task.ScheduleJobId = schedule.ScheduleJobId;
+        task.ConfigVersion += 1;
+        task.UpdateTime = now;
+        await _db.Updateable(task).UpdateColumns(u => new { u.ScheduleJobId, u.ConfigVersion, u.UpdateTime }).ExecuteCommandAsync();
+        return MapScheduleRow(schedule);
+    }
+
+    [DisplayName("同步 Admin.NET 调度快照")]
+    [HttpPost("sync-tasks/{taskCode}/schedule/sync-admin-job")]
+    public async Task<object> SyncAdminJob(string taskCode)
+    {
+        var tenantId = _userManager.TenantId;
+        var task = await EnsureTaskExistsAsync(taskCode, tenantId);
+        var schedule = await FindScheduleAsync(taskCode, tenantId) ?? new MdpSyncTaskSchedule
+        {
+            TenantId = task.TenantId == 0 ? 0 : tenantId,
+            TaskCode = taskCode,
+            ScheduleJobId = task.ScheduleJobId,
+            ScheduleMode = "CRON",
+            CreateTime = DateTime.Now,
+            UpdateTime = DateTime.Now
+        };
+
+        schedule.AdminJobConfigJson = System.Text.Json.JsonSerializer.Serialize(new
+        {
+            scheduleJobId = schedule.ScheduleJobId ?? task.ScheduleJobId,
+            message = "第一阶段仅保存 Admin.NET Job 绑定快照,不写回底层 Cron 配置。"
+        });
+        schedule.UpdateTime = DateTime.Now;
+
+        if (schedule.Id <= 0)
+            await _db.Insertable(schedule).ExecuteCommandAsync();
+        else
+            await _db.Updateable(schedule).UpdateColumns(u => new { u.AdminJobConfigJson, u.UpdateTime }).ExecuteCommandAsync();
+
+        return new
+        {
+            taskCode,
+            scheduleJobId = schedule.ScheduleJobId ?? task.ScheduleJobId,
+            adminJobConfigJson = schedule.AdminJobConfigJson
+        };
+    }
+
+    private async Task<MdpSyncTask> EnsureTaskExistsAsync(string taskCode, long tenantId)
+    {
+        if (string.IsNullOrWhiteSpace(taskCode))
+            throw Oops.Oh("任务编码不能为空");
+
+        var task = await _db.Queryable<MdpSyncTask>()
+            .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0) && u.Status == 1)
+            .OrderBy(u => u.TenantId, OrderByType.Desc)
+            .FirstAsync();
+        if (task == null)
+            throw Oops.Oh("同步任务不存在");
+        return task;
+    }
+
+    private async Task<MdpSyncTask> FindEditableTaskAsync(long id, long tenantId)
+    {
+        var entity = await _db.Queryable<MdpSyncTask>()
+            .Where(u => u.Id == id && (u.TenantId == tenantId || u.TenantId == 0))
+            .FirstAsync();
+        if (entity == null)
+            throw Oops.Oh("同步任务不存在");
+        return entity;
+    }
+
+    private async Task<MdpSyncTaskSchedule?> FindScheduleAsync(string taskCode, long tenantId) =>
+        await _db.Queryable<MdpSyncTaskSchedule>()
+            .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
+            .OrderBy(u => u.TenantId, OrderByType.Desc)
+            .FirstAsync();
+
+    private async Task EnsureDefaultScheduleAsync(long tenantId, string taskCode, string? scheduleJobId)
+    {
+        var exists = await _db.Queryable<MdpSyncTaskSchedule>()
+            .Where(u => u.TaskCode == taskCode && (u.TenantId == tenantId || u.TenantId == 0))
+            .AnyAsync();
+        if (exists)
+            return;
+
+        await _db.Insertable(new MdpSyncTaskSchedule
+        {
+            TenantId = tenantId,
+            TaskCode = taskCode,
+            ScheduleJobId = scheduleJobId,
+            ScheduleMode = "CRON",
+            CronDesc = "按 Admin.NET 任务调度执行",
+            AutoEnabled = 1,
+            ManualEnabled = 1,
+            RetryEnabled = 1,
+            MaxRetryCount = 3,
+            RetryIntervalSeconds = 300,
+            TimeoutSeconds = 3600,
+            SyncWindowType = "FULL",
+            CreateTime = DateTime.Now,
+            UpdateTime = DateTime.Now
+        }).ExecuteCommandAsync();
+    }
+
+    private async Task SyncScheduleJobIdAsync(long tenantId, string taskCode, string? scheduleJobId)
+    {
+        var schedule = await FindScheduleAsync(taskCode, tenantId);
+        if (schedule == null)
+        {
+            await EnsureDefaultScheduleAsync(tenantId, taskCode, scheduleJobId);
+            return;
+        }
+
+        schedule.ScheduleJobId = scheduleJobId;
+        schedule.UpdateTime = DateTime.Now;
+        await _db.Updateable(schedule).UpdateColumns(u => new { u.ScheduleJobId, u.UpdateTime }).ExecuteCommandAsync();
+    }
+
+    private static void ValidateUpsert(MdpSyncTaskUpsertInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.TaskCode))
+            throw Oops.Oh("任务编码不能为空");
+        if (string.IsNullOrWhiteSpace(input.TaskName))
+            throw Oops.Oh("任务名称不能为空");
+    }
+
+    private static MdpSyncTask MapUpsert(MdpSyncTaskUpsertInput input, long tenantId) =>
+        new()
+        {
+            TenantId = tenantId,
+            TaskCode = input.TaskCode.Trim(),
+            TaskName = input.TaskName.Trim(),
+            TaskType = string.IsNullOrWhiteSpace(input.TaskType) ? "SERVICE_SYNC" : input.TaskType.Trim(),
+            BusinessDomainCode = input.BusinessDomainCode?.Trim(),
+            BusinessDomainName = input.BusinessDomainName?.Trim(),
+            ConsumerModules = input.ConsumerModules?.Trim(),
+            SourceSystemCode = input.SourceSystemCode?.Trim(),
+            ServiceKey = input.ServiceKey?.Trim(),
+            JobCode = string.IsNullOrWhiteSpace(input.JobCode) ? input.TaskCode.Trim() : input.JobCode.Trim(),
+            ScheduleJobId = input.ScheduleJobId?.Trim(),
+            Status = input.Status <= 0 ? 0 : 1,
+            OwnerRole = input.OwnerRole?.Trim(),
+            Description = input.Description?.Trim()
+        };
+
+    private static MdpSyncTaskStepRow MapStepRow(MdpSyncTaskStep step) =>
+        new()
+        {
+            Id = step.Id,
+            StepCode = step.StepCode,
+            StepName = step.StepName,
+            StageType = step.StageType,
+            ServiceMethodKey = step.ServiceMethodKey,
+            Enabled = step.Enabled != 0,
+            SortOrder = step.SortOrder,
+            Description = step.Description
+        };
+
+    private static MdpSyncTaskScheduleRow MapScheduleRow(MdpSyncTaskSchedule schedule) =>
+        new()
+        {
+            TaskCode = schedule.TaskCode,
+            ScheduleJobId = schedule.ScheduleJobId,
+            ScheduleMode = schedule.ScheduleMode,
+            CronExpr = schedule.CronExpr,
+            CronDesc = schedule.CronDesc,
+            Timezone = schedule.Timezone,
+            AutoEnabled = schedule.AutoEnabled != 0,
+            ManualEnabled = schedule.ManualEnabled != 0,
+            RetryEnabled = schedule.RetryEnabled != 0,
+            MaxRetryCount = schedule.MaxRetryCount,
+            RetryIntervalSeconds = schedule.RetryIntervalSeconds,
+            TimeoutSeconds = schedule.TimeoutSeconds,
+            MisfirePolicy = schedule.MisfirePolicy,
+            SyncWindowType = schedule.SyncWindowType,
+            SyncWindowValue = schedule.SyncWindowValue,
+            LastScheduleTime = schedule.LastScheduleTime,
+            NextScheduleTime = schedule.NextScheduleTime,
+            AdminJobConfigJson = schedule.AdminJobConfigJson,
+            Description = schedule.Description
+        };
+}

+ 70 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/DataPlatform/MdpEntityDtos.cs

@@ -0,0 +1,70 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.DataPlatform;
+
+public sealed class MdpEntityRow
+{
+    public long Id { get; set; }
+    public long TenantId { get; set; }
+    public long SourceId { get; set; }
+    public string? SourceCode { get; set; }
+    public string? SourceName { get; set; }
+    public string? SourceType { get; set; }
+    public string EntityCode { get; set; } = string.Empty;
+    public string EntityName { get; set; } = string.Empty;
+    public string EntityType { get; set; } = "TABLE";
+    public string? SourceTableName { get; set; }
+    public string? SourceApiPath { get; set; }
+    public string? TargetTableName { get; set; }
+    public string SyncMode { get; set; } = "INCR";
+    public string? IncrColumn { get; set; }
+    public int BatchSize { get; set; } = 5000;
+    public string? JobId { get; set; }
+    public int Status { get; set; } = 1;
+    public string? Remark { get; set; }
+    public int FieldMappingCount { get; set; }
+}
+
+public sealed class MdpEntityUpsertInput
+{
+    public string EntityCode { get; set; } = string.Empty;
+    public string EntityName { get; set; } = string.Empty;
+    public string EntityType { get; set; } = "TABLE";
+    public long SourceId { get; set; }
+    public string? SourceCode { get; set; }
+    public string? SourceTableName { get; set; }
+    public string? SourceApiPath { get; set; }
+    public string? TargetTableName { get; set; }
+    public string SyncMode { get; set; } = "INCR";
+    public string? IncrColumn { get; set; }
+    public int BatchSize { get; set; } = 5000;
+    public string? JobId { get; set; }
+    public int Status { get; set; } = 1;
+    public string? Remark { get; set; }
+}
+
+public sealed class MdpFieldMappingRow
+{
+    public long Id { get; set; }
+    public long EntityId { get; set; }
+    public string SourceField { get; set; } = string.Empty;
+    public string TargetField { get; set; } = string.Empty;
+    public string FieldType { get; set; } = "DIRECT";
+    public string? TransformScript { get; set; }
+    public string? ConstValue { get; set; }
+    public string? LookupTable { get; set; }
+    public bool IsRequired { get; set; }
+    public string? DefaultValue { get; set; }
+    public int SortOrder { get; set; }
+}
+
+public sealed class MdpFieldMappingUpsertInput
+{
+    public string SourceField { get; set; } = string.Empty;
+    public string TargetField { get; set; } = string.Empty;
+    public string FieldType { get; set; } = "DIRECT";
+    public string? TransformScript { get; set; }
+    public string? ConstValue { get; set; }
+    public string? LookupTable { get; set; }
+    public bool IsRequired { get; set; }
+    public string? DefaultValue { get; set; }
+    public int SortOrder { get; set; }
+}

+ 86 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/DataPlatform/MdpSyncTaskDtos.cs

@@ -0,0 +1,86 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.DataPlatform;
+
+public sealed class MdpSyncTaskListQuery
+{
+    public string? Keyword { get; set; }
+    public string? BusinessDomainCode { get; set; }
+    public int? Status { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+}
+
+public sealed class MdpSyncTaskListRow
+{
+    public long Id { get; set; }
+    public long TenantId { get; set; }
+    public string TaskCode { get; set; } = string.Empty;
+    public string TaskName { get; set; } = string.Empty;
+    public string TaskType { get; set; } = string.Empty;
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? SourceSystemCode { get; set; }
+    public string? ServiceKey { get; set; }
+    public string? JobCode { get; set; }
+    public string? ScheduleJobId { get; set; }
+    public int Status { get; set; }
+    public int ConfigVersion { get; set; }
+    public string? Description { get; set; }
+    public string? ScheduleMode { get; set; }
+    public bool AutoEnabled { get; set; }
+    public bool ManualEnabled { get; set; }
+    public string? LastRunStatus { get; set; }
+    public DateTime? LastRunTime { get; set; }
+}
+
+public sealed class MdpSyncTaskUpsertInput
+{
+    public string TaskCode { get; set; } = string.Empty;
+    public string TaskName { get; set; } = string.Empty;
+    public string TaskType { get; set; } = "SERVICE_SYNC";
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? SourceSystemCode { get; set; }
+    public string? ServiceKey { get; set; }
+    public string? JobCode { get; set; }
+    public string? ScheduleJobId { get; set; }
+    public int Status { get; set; } = 1;
+    public string? OwnerRole { get; set; }
+    public string? Description { get; set; }
+}
+
+public sealed class MdpSyncTaskStepRow
+{
+    public long? Id { get; set; }
+    public string StepCode { get; set; } = string.Empty;
+    public string StepName { get; set; } = string.Empty;
+    public string StageType { get; set; } = string.Empty;
+    public string? ServiceMethodKey { get; set; }
+    public bool Enabled { get; set; } = true;
+    public int SortOrder { get; set; }
+    public string? Description { get; set; }
+}
+
+public sealed class MdpSyncTaskScheduleRow
+{
+    public string TaskCode { get; set; } = string.Empty;
+    public string? ScheduleJobId { get; set; }
+    public string ScheduleMode { get; set; } = "CRON";
+    public string? CronExpr { get; set; }
+    public string? CronDesc { get; set; }
+    public string Timezone { get; set; } = "Asia/Shanghai";
+    public bool AutoEnabled { get; set; } = true;
+    public bool ManualEnabled { get; set; } = true;
+    public bool RetryEnabled { get; set; } = true;
+    public int MaxRetryCount { get; set; } = 3;
+    public int RetryIntervalSeconds { get; set; } = 300;
+    public int TimeoutSeconds { get; set; } = 3600;
+    public string? MisfirePolicy { get; set; }
+    public string SyncWindowType { get; set; } = "FULL";
+    public string? SyncWindowValue { get; set; }
+    public DateTime? LastScheduleTime { get; set; }
+    public DateTime? NextScheduleTime { get; set; }
+    public string? AdminJobConfigJson { get; set; }
+    public string? Description { get; set; }
+}

+ 49 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpEntity.cs

@@ -0,0 +1,49 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
+
+[SugarTable("mdp_entity", "MDP同步实体配置")]
+public class MdpEntity
+{
+    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    public long TenantId { get; set; }
+    public long SourceId { get; set; }
+    public string EntityCode { get; set; } = string.Empty;
+    public string EntityName { get; set; } = string.Empty;
+    public string EntityType { get; set; } = "TABLE";
+    public string? SourceTableName { get; set; }
+    public string? SourceApiPath { get; set; }
+    public long? ApiConfigId { get; set; }
+    public string? TargetTableName { get; set; }
+    public string SyncMode { get; set; } = "INCR";
+    public string? IncrColumn { get; set; }
+    public int BatchSize { get; set; } = 5000;
+    public string? ResponseDataPath { get; set; }
+    public string? DedupKeyPath { get; set; }
+    public string? LastCursor { get; set; }
+    public DateTime? LastSyncTo { get; set; }
+    public string? JobId { get; set; }
+    public int Status { get; set; } = 1;
+    public string? Remark { get; set; }
+    public DateTime CreateTime { get; set; }
+    public DateTime UpdateTime { get; set; }
+}
+
+[SugarTable("mdp_field_mapping", "MDP字段映射配置")]
+public class MdpFieldMapping
+{
+    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    public long EntityId { get; set; }
+    public string SourceField { get; set; } = string.Empty;
+    public string TargetField { get; set; } = string.Empty;
+    public string FieldType { get; set; } = "DIRECT";
+    public string? TransformScript { get; set; }
+    public string? ConstValue { get; set; }
+    public string? LookupTable { get; set; }
+    public int IsRequired { get; set; }
+    public string? DefaultValue { get; set; }
+    public int SortOrder { get; set; }
+    public DateTime CreateTime { get; set; }
+}

+ 75 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/DataPlatform/MdpSyncConfigEntities.cs

@@ -0,0 +1,75 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
+
+[SugarTable("mdp_sync_task", "MDP同步任务业务配置")]
+public class MdpSyncTask
+{
+    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    public long TenantId { get; set; }
+    public string TaskCode { get; set; } = string.Empty;
+    public string TaskName { get; set; } = string.Empty;
+    public string TaskType { get; set; } = "SERVICE_SYNC";
+    public string? BusinessDomainCode { get; set; }
+    public string? BusinessDomainName { get; set; }
+    public string? ConsumerModules { get; set; }
+    public string? SourceSystemCode { get; set; }
+    public string? ServiceKey { get; set; }
+    public string? JobCode { get; set; }
+    public string? ScheduleJobId { get; set; }
+    public int Status { get; set; } = 1;
+    public string? OwnerRole { get; set; }
+    public int ConfigVersion { get; set; } = 1;
+    public string? Description { get; set; }
+    public DateTime CreateTime { get; set; }
+    public DateTime UpdateTime { get; set; }
+}
+
+[SugarTable("mdp_sync_task_step", "MDP同步任务步骤配置")]
+public class MdpSyncTaskStep
+{
+    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    public long TenantId { get; set; }
+    public string TaskCode { get; set; } = string.Empty;
+    public string StepCode { get; set; } = string.Empty;
+    public string StepName { get; set; } = string.Empty;
+    public string StageType { get; set; } = string.Empty;
+    public string? ServiceMethodKey { get; set; }
+    public int Enabled { get; set; } = 1;
+    public int SortOrder { get; set; }
+    public string? Description { get; set; }
+    public DateTime CreateTime { get; set; }
+    public DateTime UpdateTime { get; set; }
+}
+
+[SugarTable("mdp_sync_task_schedule", "MDP同步任务调度业务配置")]
+public class MdpSyncTaskSchedule
+{
+    [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
+    public long Id { get; set; }
+
+    public long TenantId { get; set; }
+    public string TaskCode { get; set; } = string.Empty;
+    public string? ScheduleJobId { get; set; }
+    public string ScheduleMode { get; set; } = "CRON";
+    public string? CronExpr { get; set; }
+    public string? CronDesc { get; set; }
+    public string Timezone { get; set; } = "Asia/Shanghai";
+    public int AutoEnabled { get; set; } = 1;
+    public int ManualEnabled { get; set; } = 1;
+    public int RetryEnabled { get; set; } = 1;
+    public int MaxRetryCount { get; set; } = 3;
+    public int RetryIntervalSeconds { get; set; } = 300;
+    public int TimeoutSeconds { get; set; } = 3600;
+    public string? MisfirePolicy { get; set; }
+    public string SyncWindowType { get; set; } = "FULL";
+    public string? SyncWindowValue { get; set; }
+    public DateTime? LastScheduleTime { get; set; }
+    public DateTime? NextScheduleTime { get; set; }
+    public string? AdminJobConfigJson { get; set; }
+    public string? Description { get; set; }
+    public DateTime CreateTime { get; set; }
+    public DateTime UpdateTime { get; set; }
+}

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -302,7 +302,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             (1320990000401L, "/aidop/data-platform/overview", "aidopDataPlatformOverview", "数据中台工作台", "/aidop/data-platform/overview", "ele-DataBoard", 10, "数据中台质量与运行概览"),
             (1320990000402L, "/aidop/data-platform/data-map", "aidopDataPlatformDataMap", "数据地图", "/aidop/data-platform/dataMap", "ele-Share", 20, "数据源、接入方式、处理层级和服务出口可视化"),
             (1320990000403L, "/aidop/data-platform/sources", "aidopDataPlatformSources", "数据源管理", "/aidop/data-platform/sources", "ele-Coin", 30, "外部系统、数据库与 API 连接配置入口"),
-            (1320990000404L, "/aidop/data-platform/sync-tasks", "aidopDataPlatformSyncTasks", "数据任务管理", "/aidop/data-platform/syncTasks", "ele-Operation", 40, "DB/API 入站出站任务与 Admin.NET 调度任务关联入口"),
+            (1320990000404L, "/aidop/data-platform/sync-tasks", "aidopDataPlatformSyncTasks", "同步配置中心", "/aidop/data-platform/syncTasks", "ele-Operation", 40, "外部系统标准化接入、同步任务、字段映射、参数公式、调度策略与运行追踪"),
             (1320990000405L, "/aidop/data-platform/sync-logs", "aidopDataPlatformSyncLogs", "数据任务日志", "/aidop/data-platform/syncLogs", "ele-Tickets", 50, "数据任务批次日志与异常样本入口"),
             (1320990000406L, "/aidop/data-platform/mdp-monitor", "aidopDataPlatformMdpMonitor", "MDP运行监控", "/aidop/data-platform/mdpMonitor", "ele-Monitor", 60, "统一查看 S1/S3 等模块 MDP 同步与转换运行状态"),
         };