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

feat(s8): harden watch-rule usability and customer-facing visibility

YY968XX 1 неделя назад
Родитель
Сommit
0d6fda54d1

+ 1 - 0
Web/src/views/aidop/s8/api/s8ConfigApi.ts

@@ -186,6 +186,7 @@ export interface S8MonitorObjectOption {
 	objectCode: string;
 	objectType: string;
 	objectLabel: string;
+	sourceTable?: string | null;
 	mechanisms: string[];
 	metrics: S8MonitorMetricOption[];
 }

+ 59 - 22
Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue

@@ -242,7 +242,8 @@ function parseRuleBizFields(row: S8WatchRuleConfigRow): { objectLabel: string; m
 	}
 	try {
 		const p = JSON.parse(row.paramsJson) as Record<string, unknown>;
-		const explicitThr = typeof p.thresholdDisplay === 'string' && p.thresholdDisplay ? p.thresholdDisplay : '';
+		const rawThr = typeof p.thresholdDisplay === 'string' && p.thresholdDisplay ? p.thresholdDisplay : '';
+		const explicitThr = rawThr.replace(/[((]\s*演示\s*[))]/g, '').replace(/演示/g, '').replace(/\s+/g, ' ').trim();
 		return {
 			objectLabel: deriveObjectLabel(row, p),
 			metricLabel: deriveMetricLabel(p),
@@ -282,6 +283,15 @@ function isSeedRule(row: S8WatchRuleConfigRow): boolean {
 	return code.startsWith('DEMO_') || /^RULE_S[1-7]_/.test(code);
 }
 
+// 当前租户缺少该阶段业务源数据,对应自动规则未纳入运行范围;用于在列表中清楚标识,避免与已可运行规则混淆。
+const STAGE_DATA_UNAVAILABLE: Readonly<Record<string, string>> = {
+	S5: '当前租户库存源数据未就绪',
+};
+function stageDataUnavailableNote(row: S8WatchRuleConfigRow): string | null {
+	const stage = row.stageCode || '';
+	return STAGE_DATA_UNAVAILABLE[stage] ?? null;
+}
+
 // CONFIG-RULE-EDIT-BIZ-VIEW-MVP-1:从 paramsJson 推导业务判定标准的中文展示(编辑抽屉摘要使用)。
 function summarizeBizCondition(row: S8WatchRuleConfigRow | null): string {
 	if (!row) return '—';
@@ -682,7 +692,7 @@ async function loadMonitorOptions() {
 		monitorObjects.value = res?.objects ?? [];
 	} catch (e: any) {
 		monitorObjects.value = [];
-		ElMessage.warning('监控对象/指标字典加载失败,已回退本地默认(不影响演示)');
+		ElMessage.warning('监控对象/指标字典加载失败,已回退本地默认,不影响当前页面已加载数据');
 	}
 }
 
@@ -1057,6 +1067,12 @@ function isRunning(row: S8WatchRuleConfigRow) {
 	if (!row.lockUntil) return false;
 	return new Date(row.lockUntil).getTime() > Date.now();
 }
+function runNowDisabledReason(row: S8WatchRuleConfigRow): string | null {
+	if (!row.enabled) return '规则停用后不会自动执行,请先启用';
+	if (isPaused(row)) return '规则已暂停,请先恢复';
+	if (isRunning(row)) return '当前规则正在执行,请稍后再试';
+	return null;
+}
 function pollIntervalLabel(seconds: number | null | undefined) {
 	if (!seconds || seconds <= 0) return '—';
 	const preset = POLL_INTERVAL_PRESETS.find((p) => p.value === seconds);
@@ -1066,20 +1082,30 @@ function pollIntervalLabel(seconds: number | null | undefined) {
 	return `${Math.round(seconds / 3600)} 小时`;
 }
 function lastStatusTagType(status: string | null | undefined): 'success' | 'danger' | 'info' | '' {
-	switch ((status ?? '').toUpperCase()) {
+	const key = (status ?? '').toUpperCase();
+	if (!key) return 'info';
+	switch (key) {
 		case 'SUCCESS': return 'success';
-		case 'FAILED': return 'danger';
-		case 'SKIPPED': return 'info';
+		case 'FAILED':
+		case 'EVALUATE_FAILED':
+		case 'QUERY_FAILED': return 'danger';
+		case 'SKIPPED':
+		case 'NO_HIT': return 'info';
 		default: return '';
 	}
 }
 // S8-CONFIG-UI-LABEL-CLEANUP-1:状态枚举中文化(仅展示层;DB 字段值不变)。
 function lastStatusLabel(status: string | null | undefined): string {
-	switch ((status ?? '').toUpperCase()) {
+	const key = (status ?? '').toUpperCase();
+	if (!key) return '未运行';
+	switch (key) {
 		case 'SUCCESS': return '成功';
-		case 'FAILED': return '失败';
+		case 'FAILED':
+		case 'EVALUATE_FAILED':
+		case 'QUERY_FAILED': return '失败';
 		case 'SKIPPED': return '跳过';
-		default: return status ?? '—';
+		case 'NO_HIT': return '无命中';
+		default: return status ?? '未运行';
 	}
 }
 function nextRunDisplay(row: S8WatchRuleConfigRow) {
@@ -1199,7 +1225,7 @@ onDeactivated(() => stopAutoRefresh());
 </script>
 
 <template>
-	<AidopDemoShell title="规则配置中心" subtitle="配置异常监控规则、报警机制、监控对象与判定标准。新建后默认停用,演示前由人工启用。">
+	<AidopDemoShell title="规则配置中心" subtitle="配置异常监控规则、报警机制、监控对象与判定标准。新建后默认停用,确认配置后启用自动运行。">
 		<!-- S8-WATCH-RULE-CONFIG-CONSISTENCY-FIX-1:演示语义说明,避免用户把"已配规则"误解为"已在运行" -->
 		<el-alert
 			v-if="centerStats.disabled > 0"
@@ -1207,14 +1233,14 @@ onDeactivated(() => stopAutoRefresh());
 			:closable="false"
 			show-icon
 			style="margin-bottom: 8px"
-			:title="`当前 ${centerStats.disabled} 条规则处于停用状态,调度器不会执行;大盘异常数据来自演示种子,不由本页规则实时产生。`"
+			:title="`当前 ${centerStats.disabled} 条规则处于停用状态,调度器不会执行;页面数据来自当前已入库异常记录,不由本页规则实时产生。`"
 		/>
 		<!-- CONFIG-RULE-CENTER-SHELL-MVP-1:主操作 + 统计 chip + 业务化筛选 + 前端分页 -->
 		<div class="rule-toolbar">
 			<el-button size="small" type="primary" @click="openWizard">新建监控配置</el-button>
 			<el-button size="small" @click="openDraftDrawer">草稿配置 ({{ draftCount }})</el-button>
 			<el-button size="small" @click="loadRows" :loading="loading">手动刷新</el-button>
-			<el-checkbox v-model="showAdvancedColumns" size="small" style="margin-left: 8px">显示运行态</el-checkbox>
+			<el-checkbox v-model="showAdvancedColumns" size="small" style="margin-left: 8px">显示技术列</el-checkbox>
 			<span class="rule-toolbar__hint">每 30 秒自动刷新;编辑抽屉打开期间暂停刷新</span>
 		</div>
 
@@ -1321,11 +1347,19 @@ onDeactivated(() => stopAutoRefresh());
 					<span :class="{ 'rule-biz-empty': parseRuleBizFields(row).thresholdDisplay === '未配置' }">{{ parseRuleBizFields(row).thresholdDisplay }}</span>
 				</template>
 			</el-table-column>
-			<el-table-column label="状态" width="120">
+			<el-table-column label="状态" width="180">
 				<template #default="{ row }">
 					<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '启用' : '停用' }}</el-tag>
 					<!-- S8-WATCH-RULE-CONFIG-CONSISTENCY-FIX-1:SeedData / DEMO 规则标记,避免被误读为生产运行规则 -->
-					<el-tag v-if="isSeedRule(row)" size="small" type="warning" effect="plain" style="margin-left: 4px">演示</el-tag>
+					<el-tag v-if="isSeedRule(row)" size="small" type="warning" effect="plain" style="margin-left: 4px">系统预置</el-tag>
+					<!-- 当前租户业务源数据未就绪的阶段,明确标识其规则未纳入自动运行范围 -->
+					<el-tooltip
+						v-if="stageDataUnavailableNote(row)"
+						:content="`${stageDataUnavailableNote(row)},需补齐后启用`"
+						placement="top"
+					>
+						<el-tag size="small" type="warning" effect="dark" style="margin-left: 4px">未纳入自动运行</el-tag>
+					</el-tooltip>
 				</template>
 			</el-table-column>
 			<el-table-column label="严重度" width="80">
@@ -1340,10 +1374,10 @@ onDeactivated(() => stopAutoRefresh());
 				</template>
 				<template #default="{ row }">{{ pollIntervalLabel(row.pollIntervalSeconds) }}</template>
 			</el-table-column>
-			<el-table-column prop="ruleCode" label="内部编码" width="200" show-overflow-tooltip>
+			<el-table-column v-if="showAdvancedColumns" prop="ruleCode" label="内部编码" width="200" show-overflow-tooltip>
 				<template #default="{ row }"><span class="rule-code-mono">{{ row.ruleCode }}</span></template>
 			</el-table-column>
-			<!-- 运行态高级列:默认隐藏,由"显示运行态"开关控制 -->
+			<!-- 技术列:默认隐藏,由"显示技术列"开关控制 -->
 			<el-table-column v-if="showAdvancedColumns" label="规则类型" width="100">
 				<template #default="{ row }">
 					<el-tag :type="row.ruleType ? 'success' : 'info'" size="small">{{ ruleTypeLabel(row.ruleType) }}</el-tag>
@@ -1356,7 +1390,7 @@ onDeactivated(() => stopAutoRefresh());
 			<el-table-column v-if="showAdvancedColumns" label="持续正常次数" width="115">
 				<template #default="{ row }">连续 {{ row.recoverCountRequired ?? 1 }} 次</template>
 			</el-table-column>
-			<el-table-column v-if="showAdvancedColumns" label="上次执行" width="160">
+			<el-table-column label="上次执行" width="160">
 				<template #default="{ row }">
 					<el-tooltip v-if="row.lastRunAt" :content="String(row.lastRunAt)" placement="top">
 						<span>{{ row.lastRunAt }}</span>
@@ -1364,17 +1398,16 @@ onDeactivated(() => stopAutoRefresh());
 					<span v-else>—</span>
 				</template>
 			</el-table-column>
-			<el-table-column v-if="showAdvancedColumns" label="下次执行" width="160">
+			<el-table-column label="下次执行" width="160">
 				<template #default="{ row }">
 					<el-tag v-if="isPaused(row)" type="warning" size="small">已暂停</el-tag>
 					<el-tag v-else-if="isRunning(row)" type="info" size="small">执行中</el-tag>
 					<span v-else>{{ nextRunDisplay(row) }}</span>
 				</template>
 			</el-table-column>
-			<el-table-column v-if="showAdvancedColumns" label="上次状态" width="100">
+			<el-table-column label="上次状态" width="100">
 				<template #default="{ row }">
-					<el-tag v-if="row.lastStatus" :type="lastStatusTagType(row.lastStatus)" size="small">{{ lastStatusLabel(row.lastStatus) }}</el-tag>
-					<span v-else>—</span>
+					<el-tag :type="lastStatusTagType(row.lastStatus)" size="small">{{ lastStatusLabel(row.lastStatus) }}</el-tag>
 				</template>
 			</el-table-column>
 			<el-table-column v-if="showAdvancedColumns" label="失败次数" width="90">
@@ -1386,7 +1419,7 @@ onDeactivated(() => stopAutoRefresh());
 			<el-table-column v-if="showAdvancedColumns" label="耗时" width="80">
 				<template #default="{ row }">{{ row.lastDurationMs != null ? `${row.lastDurationMs}ms` : '—' }}</template>
 			</el-table-column>
-			<el-table-column v-if="showAdvancedColumns" label="错误摘要" min-width="180" show-overflow-tooltip>
+			<el-table-column label="错误摘要" min-width="180" show-overflow-tooltip>
 				<template #default="{ row }">
 					<el-tooltip v-if="row.lastError" :content="String(row.lastError)" placement="top">
 						<span class="rule-error">{{ mapLastError(row.lastError) }}</span>
@@ -1397,7 +1430,11 @@ onDeactivated(() => stopAutoRefresh());
 			<el-table-column label="操作" width="320" fixed="right">
 				<template #default="{ row }">
 					<el-button size="small" type="primary" link @click="openEdit(row)">编辑</el-button>
-					<el-button size="small" type="success" link :disabled="!row.enabled || isPaused(row) || isRunning(row)" @click="runNow(row)">立即执行</el-button>
+					<el-tooltip :disabled="!runNowDisabledReason(row)" :content="runNowDisabledReason(row) ?? ''" placement="top">
+						<span>
+							<el-button size="small" type="success" link :disabled="!!runNowDisabledReason(row)" @click="runNow(row)">立即执行</el-button>
+						</span>
+					</el-tooltip>
 					<el-button v-if="!isPaused(row)" size="small" type="warning" link @click="pauseRule(row)">暂停</el-button>
 					<el-button v-else size="small" type="success" link @click="resumeRule(row)">恢复</el-button>
 					<el-button v-if="!isPaused(row) && (row.consecutiveFailureCount ?? 0) > 0" size="small" type="success" link @click="resumeRule(row)">重置失败</el-button>

+ 46 - 2
Web/src/views/aidop/s8/config/components/WatchRuleWizardDialog.vue

@@ -82,6 +82,7 @@ interface BizObject {
 	objectCode?: string;
 	objectType: string;
 	objectLabel: string;
+	sourceTable?: string | null;
 	mechanisms: Array<'DATE' | 'VALUE_RANGE' | 'RATIO'>;
 	metrics: BizMetric[];
 }
@@ -279,6 +280,7 @@ const activeMonitorOptions = computed<BizObject[]>(() => {
 		objectCode: o.objectCode,
 		objectType: o.objectType,
 		objectLabel: o.objectLabel,
+		sourceTable: o.sourceTable ?? null,
 		mechanisms: o.mechanisms as Array<'DATE' | 'VALUE_RANGE' | 'RATIO'>,
 		metrics: o.metrics.map((m) => ({
 			metricCode: m.metricCode,
@@ -297,6 +299,17 @@ const filteredObjects = computed(() => {
 	return activeMonitorOptions.value.filter((o) => o.mechanisms.includes(form.mechanism as any));
 });
 
+// 数据源未配置的监控对象不能生成自动规则(后端 BuildExpression 会以 S8BizException 拒绝),在 wizard 中预先标识阻断。
+function objectHasSourceTable(o: BizObject): boolean {
+	return typeof o.sourceTable === 'string' && o.sourceTable.trim().length > 0;
+}
+
+const selectedObjectUsable = computed<boolean>(() => {
+	const o = selectedObject.value;
+	if (!o) return true;
+	return objectHasSourceTable(o);
+});
+
 // CONFIG-MONITOR-DICT-READONLY-SEED-1:当前机制下没有可用对象/指标的空态提示
 const monitorEmptyHint = computed(() => {
 	if (!form.mechanism || isManualReport.value) return '';
@@ -411,6 +424,10 @@ const dictionaryHint = computed(() => {
 });
 
 function pickMechanism(value: string) {
+	if (value === 'RATIO') {
+		ElMessage.warning('比例类规则需补充业务阈值口径后启用');
+		return;
+	}
 	form.mechanism = value as typeof form.mechanism;
 }
 
@@ -424,6 +441,10 @@ function nextStep() {
 		step.value = 2;
 	} else if (step.value === 2) {
 		if (form.objectIndex < 0) { ElMessage.warning('请选择监控对象'); return; }
+		if (!selectedObjectUsable.value) {
+			ElMessage.warning('该监控对象缺少数据源配置,不能生成自动规则');
+			return;
+		}
 		if (!form.metricCode) { ElMessage.warning('请选择监控指标'); return; }
 		if (isOrderFlowStage.value && !form.orderFlowCode) {
 			ElMessage.warning('订单链路阶段规则必须在上一步选择订单流程节点');
@@ -804,11 +825,16 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 					v-for="o in MECHANISM_OPTIONS"
 					:key="o.value"
 					class="wizard-mech-card"
-					:class="{ 'wizard-mech-card--active': form.mechanism === o.value }"
+					:class="{
+						'wizard-mech-card--active': form.mechanism === o.value,
+						'wizard-mech-card--disabled': o.value === 'RATIO',
+					}"
+					:title="o.value === 'RATIO' ? '比例类规则需补充业务阈值口径后启用' : ''"
 					@click="pickMechanism(o.value)"
 				>
 					<div class="wizard-mech-card__title">{{ o.label }}</div>
 					<div class="wizard-mech-card__desc">{{ o.desc }}</div>
+					<div v-if="o.value === 'RATIO'" class="wizard-mech-card__note">需补充业务阈值口径后启用</div>
 				</div>
 			</div>
 			<el-alert
@@ -914,10 +940,14 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 						<el-option
 							v-for="(o, idx) in filteredObjects"
 							:key="`${o.objectType}-${o.objectLabel}-${idx}`"
-							:label="o.objectLabel"
+							:label="objectHasSourceTable(o) ? o.objectLabel : `${o.objectLabel}(数据源未配置)`"
 							:value="idx"
+							:disabled="!objectHasSourceTable(o)"
 						/>
 					</el-select>
+					<span v-if="form.objectIndex >= 0 && !selectedObjectUsable" class="wizard-hint" style="color: #f0c674">
+						该监控对象缺少数据源配置,不能生成自动规则
+					</span>
 				</el-form-item>
 				<el-form-item required>
 					<template #label>
@@ -1136,6 +1166,20 @@ function handleInlineExceptionTypeCreated(payload: { typeCode: string; typeName:
 	color: var(--el-text-color-secondary);
 	line-height: 1.5;
 }
+.wizard-mech-card--disabled {
+	cursor: not-allowed;
+	opacity: 0.55;
+	background: var(--el-fill-color-light);
+}
+.wizard-mech-card--disabled:hover {
+	border-color: var(--el-border-color);
+	background: var(--el-fill-color-light);
+}
+.wizard-mech-card__note {
+	margin-top: 6px;
+	font-size: 11px;
+	color: #f0c674;
+}
 .wizard-hint {
 	margin-left: 8px;
 	color: var(--el-text-color-secondary);

+ 27 - 3
Web/src/views/aidop/s8/exceptions/S8ExceptionListPage.vue

@@ -82,14 +82,18 @@
 		</el-form>
 
 		<el-table :data="filteredRows" v-loading="loading" border stripe @row-click="onRowClick">
-			<el-table-column prop="exceptionCode" label="编号" width="160" show-overflow-tooltip />
-			<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
+			<el-table-column label="编号" width="160" show-overflow-tooltip>
+				<template #default="{ row }">{{ cleanExceptionCodeForDisplay(row.exceptionCode) }}</template>
+			</el-table-column>
+			<el-table-column label="标题" min-width="200" show-overflow-tooltip>
+				<template #default="{ row }">{{ cleanExceptionTitleForDisplay(row.title) }}</template>
+			</el-table-column>
 			<el-table-column label="模块" width="120" show-overflow-tooltip>
 				<template #default="{ row }">{{ row.moduleName || row.moduleCode || row.sceneName || row.sceneCode || '-' }}</template>
 			</el-table-column>
 			<!-- ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3j:订单链路列 -->
 			<el-table-column label="订单号" width="140" show-overflow-tooltip>
-				<template #default="{ row }">{{ row.relatedObjectCode || '-' }}</template>
+				<template #default="{ row }">{{ cleanRelatedObjectForDisplay(row.relatedObjectCode) }}</template>
 			</el-table-column>
 			<el-table-column label="链路阶段" width="130" show-overflow-tooltip>
 				<template #default="{ row }">{{ row.orderFlowName || orderFlowLabel(row.orderFlowCode) || '-' }}</template>
@@ -273,6 +277,26 @@ function deptDisplay(name?: string | null, id?: number | null) {
 	return '未归属';
 }
 
+// t3f:异常列表客户可见层展示净化(仅 UI 显示,不改 row 原始字段、不改查询/跳转/筛选)。
+function cleanExceptionTitleForDisplay(v: string | null | undefined): string {
+	if (!v) return '';
+	return v
+		.replace(/\s*\[\s*演示覆盖\s*\]\s*/g, '')
+		.replace(/演示覆盖/g, '')
+		.replace(/\s+DEMO-[A-Z0-9-]+/g, ' —')
+		.replace(/DEMO-[A-Z0-9-]+/g, '—')
+		.replace(/\s+/g, ' ')
+		.trim();
+}
+function cleanExceptionCodeForDisplay(v: string | null | undefined): string {
+	if (!v) return '';
+	return v.replace(/^EX-DEMO-COV-/, 'EX-COV-');
+}
+function cleanRelatedObjectForDisplay(v: string | null | undefined): string {
+	if (!v) return '—';
+	return /^DEMO-/.test(v) ? '—' : v;
+}
+
 function ruleTypeTagType(t: string | null | undefined) {
 	switch (t) {
 		case 'OUT_OF_RANGE':

+ 42 - 10
Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue

@@ -24,6 +24,38 @@ function orderFlowLabel(code: string | null | undefined): string {
 	if (!code) return '';
 	return ORDER_FLOW_NAME_MAP[code] || code;
 }
+
+// t3f:异常详情客户可见层展示净化(仅 UI 显示,不改 detail 原始字段、不改查询/跳转)。
+function cleanExceptionTitleForDisplay(v: string | null | undefined): string {
+	if (!v) return '';
+	return v
+		.replace(/\s*\[\s*演示覆盖\s*\]\s*/g, '')
+		.replace(/演示覆盖/g, '')
+		.replace(/\s+DEMO-[A-Z0-9-]+/g, ' —')
+		.replace(/DEMO-[A-Z0-9-]+/g, '—')
+		.replace(/\s+/g, ' ')
+		.trim();
+}
+function cleanExceptionCodeForDisplay(v: string | null | undefined): string {
+	if (!v) return '';
+	return v.replace(/^EX-DEMO-COV-/, 'EX-COV-');
+}
+function cleanRelatedObjectForDisplay(v: string | null | undefined): string {
+	if (!v) return '—';
+	return /^DEMO-/.test(v) ? '—' : v;
+}
+// t3f:任意客户可见文本的展示层净化(用于规则编码、去重键、时间线备注等技术字段直接渲染处)。
+function cleanAnyTextForDisplay(v: string | null | undefined): string {
+	if (!v) return '';
+	return v
+		.replace(/\s*\[\s*演示覆盖\s*\]\s*/g, '')
+		.replace(/演示覆盖派生/g, '系统派生')
+		.replace(/演示覆盖/g, '')
+		.replace(/[A-Z0-9]*DEMO[_A-Z0-9:-]*/g, '—')
+		.replace(/—\s*—+/g, '—')
+		.replace(/\s+/g, ' ')
+		.trim();
+}
 const canViewOrderChain = computed(() => {
 	const d = detail.value;
 	return !!d && d.sourceObjectType === 'SALES_ORDER' && !!d.relatedObjectCode;
@@ -245,7 +277,7 @@ onMounted(async () => {
 			<el-row :gutter="16">
 				<el-col :span="16">
 					<el-descriptions :column="2" border>
-						<el-descriptions-item label="编号">{{ detail.exceptionCode }}</el-descriptions-item>
+						<el-descriptions-item label="编号">{{ cleanExceptionCodeForDisplay(detail.exceptionCode) }}</el-descriptions-item>
 						<el-descriptions-item>
 							<template #label>
 								<S8FieldTooltip field-key="exception_status" fallback-label="状态" />
@@ -280,8 +312,8 @@ onMounted(async () => {
 							</template>
 							{{ detail.slaDeadline || '—' }}
 						</el-descriptions-item>
-						<el-descriptions-item label="标题" :span="2">{{ detail.title }}</el-descriptions-item>
-						<el-descriptions-item label="描述" :span="2">{{ detail.description || '—' }}</el-descriptions-item>
+						<el-descriptions-item label="标题" :span="2">{{ cleanExceptionTitleForDisplay(detail.title) }}</el-descriptions-item>
+						<el-descriptions-item label="描述" :span="2">{{ cleanExceptionTitleForDisplay(detail.description) || '—' }}</el-descriptions-item>
 					</el-descriptions>
 				</el-col>
 
@@ -342,7 +374,7 @@ onMounted(async () => {
 								<template #label>
 									<S8FieldTooltip field-key="rule_code" fallback-label="规则编码" />
 								</template>
-								{{ detail.sourceRuleCode || '—' }}
+								{{ cleanAnyTextForDisplay(detail.sourceRuleCode) || '—' }}
 							</el-descriptions-item>
 							<el-descriptions-item>
 								<template #label>
@@ -350,13 +382,13 @@ onMounted(async () => {
 								</template>
 								{{ detail.sourceObjectType || '—' }}
 							</el-descriptions-item>
-							<el-descriptions-item label="来源对象 ID">{{ detail.sourceObjectId || '—' }}</el-descriptions-item>
+							<el-descriptions-item label="来源对象 ID">{{ cleanRelatedObjectForDisplay(detail.sourceObjectId) }}</el-descriptions-item>
 							<!-- t3j:订单链路字段 -->
 							<el-descriptions-item>
 								<template #label>
 									<S8FieldTooltip field-key="related_object_code" fallback-label="订单号" />
 								</template>
-								{{ detail.relatedObjectCode || '—' }}
+								{{ cleanRelatedObjectForDisplay(detail.relatedObjectCode) }}
 							</el-descriptions-item>
 							<el-descriptions-item>
 								<template #label>
@@ -376,8 +408,8 @@ onMounted(async () => {
 								<el-tag v-else type="warning" size="small">未恢复</el-tag>
 							</el-descriptions-item>
 							<el-descriptions-item label="去重键(技术)" :span="3">
-								<el-tooltip v-if="detail.dedupKey" :content="detail.dedupKey" placement="top">
-									<span class="dedup-key">{{ detail.dedupKey }}</span>
+								<el-tooltip v-if="detail.dedupKey" :content="cleanAnyTextForDisplay(detail.dedupKey)" placement="top">
+									<span class="dedup-key">{{ cleanAnyTextForDisplay(detail.dedupKey) }}</span>
 								</el-tooltip>
 								<span v-else>—</span>
 							</el-descriptions-item>
@@ -392,9 +424,9 @@ onMounted(async () => {
 						<template #header>时间线</template>
 						<el-timeline v-if="timeline.length">
 							<el-timeline-item v-for="item in timeline" :key="item.id" :timestamp="item.createdAt">
-								<div class="timeline-title">{{ item.actionLabel }}</div>
+								<div class="timeline-title">{{ cleanAnyTextForDisplay(item.actionLabel) }}</div>
 								<div v-if="item.operatorName" class="muted">操作人:{{ item.operatorName }}</div>
-								<div v-if="item.actionRemark" class="muted">{{ item.actionRemark }}</div>
+								<div v-if="item.actionRemark" class="muted">{{ cleanAnyTextForDisplay(item.actionRemark) }}</div>
 							</el-timeline-item>
 						</el-timeline>
 						<el-empty v-else description="暂无时间线" />

+ 3 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue

@@ -2,7 +2,7 @@
 	<div class="anomaly-monitor" :style="layoutVars">
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
-		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复默认布局</button>
 		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
@@ -179,8 +179,8 @@ function resetEdit() { resetToDefault(); syncDraftFromPersisted(); clearBaseline
 async function confirmRestoreDemo() {
 	try {
 		await ElMessageBox.confirm(
-			'将恢复演示标准布局,当前保存的自定义布局将被清除,是否继续?',
-			'恢复演示布局',
+			'将恢复默认布局,当前保存的自定义布局将被清除,是否继续?',
+			'恢复默认布局',
 			{ confirmButtonText: '确认恢复', cancelButtonText: '取消', type: 'warning' },
 		);
 	} catch { return; }

+ 9 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -2,7 +2,7 @@
 	<div class="s8-monitor" :style="layoutVars">
 		<button v-if="!editMode" class="s8-monitor__edit-btn" type="button" @click="enterEdit">编辑布局</button>
 		<button class="s8-monitor__config-btn" type="button" @click="configDrawerVisible = true">卡片配置</button>
-		<button v-if="!editMode" class="s8-monitor__restore-btn" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<button v-if="!editMode" class="s8-monitor__restore-btn" type="button" @click="confirmRestoreDemo">恢复默认布局</button>
 		<S8PeriodFilter class="s8-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
@@ -123,6 +123,11 @@ const numberFormatter = new Intl.NumberFormat('zh-CN');
 // S8-MODULE-CARD-METRIC-LABEL-UNIFY-1:S1-S7 主指标统一为「累计异常数」+ valueMode=total,
 // 取 orderGrid.modules[].total 直显,避免同组卡片语义不一致影响演示叙事。
 // S8-MONITORING-DEMO-SEMANTICS-CLOSURE-1:模块卡主标题改「当前待处理异常」,与三色数字(正常执行订单/关注异常/严重异常)口径一致。
+// 阶段卡片数据可用性说明:仅在当前租户缺少业务源数据、对应自动规则未纳入运行时展示。
+const STAGE_DATA_AVAILABILITY_NOTE: Readonly<Record<string, string | undefined>> = {
+	S5: '当前租户库存源数据未就绪',
+};
+
 const STAGE_META_FALLBACK: ReadonlyArray<StageMeta> = [
 	{ code: 'S1', title: '销售评审', icon: Checked, metricLabel: '异常发生情况', valueMode: 'total' },
 	{ code: 'S2', title: '计划排产', icon: TrendCharts, metricLabel: '异常发生情况', valueMode: 'total' },
@@ -253,8 +258,8 @@ function resetEdit() {
 async function confirmRestoreDemo() {
 	try {
 		await ElMessageBox.confirm(
-			'将恢复演示标准布局,当前保存的自定义布局将被清除,是否继续?',
-			'恢复演示布局',
+			'将恢复默认布局,当前保存的自定义布局将被清除,是否继续?',
+			'恢复默认布局',
 			{
 				confirmButtonText: '确认恢复',
 				cancelButtonText: '取消',
@@ -519,6 +524,7 @@ function buildStageCard(
 		// S8-OVERVIEW-STAGE-CARD-LIFECYCLE-COPY-AND-METRIC-FIX-1:延误数(severity × timeout 双桶)。
 		seriousDelayCount: seriousTimeout,
 		attentionDelayCount: followTimeout,
+		dataAvailabilityNote: STAGE_DATA_AVAILABILITY_NOTE[meta.code],
 	};
 }
 

+ 3 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue

@@ -2,7 +2,7 @@
 	<div class="anomaly-monitor" :style="layoutVars">
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
-		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复默认布局</button>
 		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
@@ -179,8 +179,8 @@ function resetEdit() { resetToDefault(); syncDraftFromPersisted(); clearBaseline
 async function confirmRestoreDemo() {
 	try {
 		await ElMessageBox.confirm(
-			'将恢复演示标准布局,当前保存的自定义布局将被清除,是否继续?',
-			'恢复演示布局',
+			'将恢复默认布局,当前保存的自定义布局将被清除,是否继续?',
+			'恢复默认布局',
 			{ confirmButtonText: '确认恢复', cancelButtonText: '取消', type: 'warning' },
 		);
 	} catch { return; }

+ 9 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue

@@ -2,7 +2,7 @@
 	<div class="anomaly-monitor" :style="layoutVars">
 		<button v-if="!editMode" class="anomaly-monitor__btn anomaly-monitor__btn--edit" type="button" @click="enterEdit">编辑布局</button>
 		<button class="anomaly-monitor__btn anomaly-monitor__btn--config" type="button" @click="configDrawerVisible = true">卡片配置</button>
-		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复演示布局</button>
+		<button class="anomaly-monitor__btn anomaly-monitor__btn--restore" type="button" @click="confirmRestoreDemo">恢复默认布局</button>
 		<S8PeriodFilter class="anomaly-monitor__period-filter" v-model="period" />
 		<S8MonitoringEditToolbar v-if="editMode" @save="saveEdit" @reset="resetEdit" @cancel="cancelEdit" />
 
@@ -185,8 +185,8 @@ function resetEdit() { resetToDefault(); syncDraftFromPersisted(); clearBaseline
 async function confirmRestoreDemo() {
 	try {
 		await ElMessageBox.confirm(
-			'将恢复演示标准布局,当前保存的自定义布局将被清除,是否继续?',
-			'恢复演示布局',
+			'将恢复默认布局,当前保存的自定义布局将被清除,是否继续?',
+			'恢复默认布局',
 			{ confirmButtonText: '确认恢复', cancelButtonText: '取消', type: 'warning' },
 		);
 	} catch { return; }
@@ -215,6 +215,11 @@ const STAGE_META_FALLBACK = [
 	{ code: 'S5', title: '质量检测', icon: DataAnalysis as Component, metricLabel: '异常发生情况' },
 ] as const;
 
+// 阶段卡片数据可用性说明:仅在当前租户缺少业务源数据、对应自动规则未纳入运行时展示(与 overview 口径一致)。
+const STAGE_DATA_AVAILABILITY_NOTE: Readonly<Record<string, string | undefined>> = {
+	S5: '当前租户库存源数据未就绪',
+};
+
 // S8-SUPPLY-PAGE-TYPE-TOTAL-ALIGN-1:fallback 与后端 specs 对齐为 11 类,与左侧 S3/S4/S5 合计一致。
 const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
 	{ key: 'supplier-reply-delay',             label: '供应商回复交期异常' },
@@ -320,6 +325,7 @@ const stageCards = computed(() =>
 			// S8-OVERVIEW-STAGE-CARD-LIFECYCLE-COPY-AND-METRIC-FIX-1:延误数(severity × timeout 双桶)。
 			seriousDelayCount: seriousTimeout,
 			attentionDelayCount: followTimeout,
+			dataAvailabilityNote: STAGE_DATA_AVAILABILITY_NOTE[meta.code],
 		};
 		return applyConfig(raw);
 	}),

+ 1 - 0
Web/src/views/aidop/s8/monitoring/components/S8MonitoringModulesGrid.vue

@@ -104,6 +104,7 @@ interface StageCardData {
   showProgress?: boolean
   blockOrder?: string[]
   metricLabelFontSize?: number
+  dataAvailabilityNote?: string
 }
 
 export interface SummaryCardData {

+ 22 - 0
Web/src/views/aidop/s8/monitoring/components/S8MonitoringStageCard.vue

@@ -47,6 +47,7 @@ const props = defineProps<{
 	blockOrder?: string[];
 	blocksEditable?: boolean;
 	metricLabelFontSize?: number;
+	dataAvailabilityNote?: string;
 }>();
 
 const emit = defineEmits<{
@@ -105,6 +106,11 @@ const yellowBarWidth = computed(() => {
 	<article class="stage-card" :class="`stage-card--${tone}`" :style="cardStyle">
 		<div class="stage-card__header">
 			<span class="stage-card__title" :title="`${displayCode || code} ${title}`">{{ displayCode || code }} {{ title }}</span>
+			<span
+				v-if="dataAvailabilityNote"
+				class="stage-card__data-note"
+				:title="dataAvailabilityNote"
+			>{{ dataAvailabilityNote }}</span>
 			<el-icon class="stage-card__icon">
 				<component :is="icon" />
 			</el-icon>
@@ -266,6 +272,22 @@ const yellowBarWidth = computed(() => {
 	gap: 12px;
 }
 
+.stage-card__data-note {
+	flex-shrink: 0;
+	padding: 2px 8px;
+	font-size: 11px;
+	font-weight: 500;
+	letter-spacing: 0.04em;
+	color: #f0c674;
+	background: rgba(240, 198, 116, 0.12);
+	border: 1px solid rgba(240, 198, 116, 0.32);
+	border-radius: 4px;
+	white-space: nowrap;
+	max-width: 60%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
 .stage-card__title {
 	flex: 1;
 	min-width: 0;

+ 2 - 2
Web/src/views/aidop/s8/monitoring/components/S9QdcSummaryCard.vue

@@ -29,7 +29,7 @@ const resultKpiSourceText = computed(() => {
 		case 'DICTIONARY_MOCK':
 			return '字典占位 · 待接入真实计算';
 		case 'MIXED_BASELINE':
-			return '演示基线 · 部分订单链路计算';
+			return '当前基线 · 部分订单链路计算';
 		default:
 			return '';
 	}
@@ -38,7 +38,7 @@ const resultKpiSourceText = computed(() => {
 function itemSourceText(source: string | undefined): string {
 	switch (source) {
 		case 'DEMO_BASELINE':
-			return '演示基线';
+			return '当前基线';
 		case 'ORDER_FLOW_CALC':
 			return '订单链路计算';
 		case 'ORDER_FLOW_CALC_EMPTY':

+ 14 - 1
Web/src/views/approvalFlow/component/ApprovalPanel.vue

@@ -8,7 +8,7 @@
 
 		<div v-else>
 			<el-descriptions :column="2" border size="small" style="margin-bottom: 12px">
-				<el-descriptions-item label="流程标题">{{ instance.title }}</el-descriptions-item>
+				<el-descriptions-item label="流程标题">{{ cleanTitleForDisplay(instance.title) }}</el-descriptions-item>
 				<el-descriptions-item label="状态">
 					<el-tag :type="instanceStatusTag(instance.status)" size="small">{{ instanceStatusLabel(instance.status) }}</el-tag>
 				</el-descriptions-item>
@@ -379,6 +379,19 @@ const actionLabel = (a: number) => {
 	const map: Record<number, string> = { 1: '提交', 2: '同意', 3: '拒绝', 4: '转办', 5: '撤回', 6: '退回', 7: '加签', 8: '催办', 9: '取消', 10: '超时自动', 11: '升级' };
 	return map[a] ?? '操作';
 };
+// t3f 增补:审批流程标题展示层净化(仅 UI 显示,不改 instance 原始字段、不改 props/emit/API)。
+const cleanTitleForDisplay = (v: string | null | undefined): string => {
+	if (!v) return '';
+	return v
+		.replace(/\s*\[\s*演示覆盖\s*\]\s*/g, '')
+		.replace(/演示覆盖派生/g, '系统派生')
+		.replace(/演示覆盖/g, '')
+		.replace(/EX-DEMO-COV-/g, 'EX-COV-')
+		.replace(/[A-Z0-9]*DEMO[_A-Z0-9:-]*/g, '—')
+		.replace(/—\s*—+/g, '—')
+		.replace(/\s+/g, ' ')
+		.trim();
+};
 </script>
 
 <style scoped>

+ 8 - 12
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ConfigDraftService.cs

@@ -448,7 +448,7 @@ public class S8ConfigDraftService : ITransient
     };
 
     // CONFIG-WIZARD-EXPRESSION-REAL-SQL-1:基于字典 source_table + 字段映射生成真实 SELECT。
-    // 仅 DATE / VALUE_RANGE 走真实 SQL;source_table 为空时回退占位(规则仍 enabled=false,调度不会命中)。
+    // 仅 DATE / VALUE_RANGE 走真实 SQL;source_table 为空时由 BuildExpression 直接以 S8BizException 拒绝(不再生成不可运行表达式)。
     // 表名/列名走 IsSafeIdentifier 白名单校验,杜绝字典脏值导致的注入。
     private static readonly Regex SafeIdentifierPattern =
         new(@"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$", RegexOptions.Compiled);
@@ -463,11 +463,10 @@ public class S8ConfigDraftService : ITransient
         return trimmed;
     }
 
-    private const string DatePlaceholderExpression =
-        "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, DATE_SUB(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status WHERE 1=0";
-
-    private const string ValuePlaceholderExpression =
-        "SELECT 'DEMO-OBJECT-001' AS related_object_code, 'DEMO-OBJECT-001' AS source_object_id, 'DEMO-OBJECT-001' AS related_object_name, 0 AS measured_value WHERE 1=0";
+    // BuildExpression 在 source_table 缺失或机制为 RATIO 时直接抛 S8BizException 拒绝生成,
+    // 不再回退到 WHERE 1=0 的不可运行 SQL。两个常量保留为 string.Empty 防止外部引用编译失败。
+    private const string DatePlaceholderExpression = "";
+    private const string ValuePlaceholderExpression = "";
 
     /// <summary>
     /// 含逻辑删除字段的源表追加过滤,避免规则启用后误命中已软删行。
@@ -485,10 +484,7 @@ public class S8ConfigDraftService : ITransient
     {
         var sourceTable = NormalizeOrNull(monitorObject.SourceTable);
         if (string.IsNullOrWhiteSpace(sourceTable))
-        {
-            // 字典中未配置 source_table;回退占位 SQL,规则仍默认 enabled=false。
-            return mechanism == "DATE" ? DatePlaceholderExpression : ValuePlaceholderExpression;
-        }
+            throw new S8BizException("该监控对象缺少数据源配置,不能生成自动规则");
 
         var table = ValidateSqlIdentifier(sourceTable, "source_table");
         var idCol = ValidateSqlIdentifier(NormalizeOrNull(metric.ObjectIdField) ?? "id", "object_id_field");
@@ -534,8 +530,8 @@ public class S8ConfigDraftService : ITransient
             return $"SELECT {idCol} AS source_object_id, {codeCol} AS related_object_code, {nameCol} AS related_object_name, {mvCol} AS measured_value FROM {table} WHERE {mvCol} IS NOT NULL{oorClause}{softDeleteClause}";
         }
 
-        // RATIO 在上游已被拒绝(metric.Enabled=false 触发 400),此处兜底返回数值占位
-        return ValuePlaceholderExpression;
+        // 比例类规则需补充业务阈值口径后启用;上游 metric.Enabled=false 已拦截,此处兜底直接拒绝,避免落库不可运行规则
+        throw new S8BizException("比例类规则需补充业务阈值口径后启用");
     }
 
     private static string BuildParamsJson(