Forráskód Böngészése

merge: integrate remote master and preserve AIDOP UAT updates

Keep remote S0/S8 updates, move local S4/SysTenant migrations to server 1.0.158, and bump Web to 2.4.184 after conflict resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
skygu 1 hónapja
szülő
commit
4131c20404
48 módosított fájl, 2498 hozzáadás és 377 törlés
  1. 1 1
      Web/package.json
  2. 7 0
      Web/src/views/aidop/api/contractReview.ts
  3. 82 26
      Web/src/views/aidop/business/contractReviewForm.vue
  4. 21 5
      Web/src/views/aidop/business/orderDeliveryList.vue
  5. 42 3
      Web/src/views/aidop/business/productDesignForm.vue
  6. 5 0
      Web/src/views/aidop/business/salesOrderForm.vue
  7. 82 0
      Web/src/views/aidop/s0/api/s0SalesApi.ts
  8. 369 0
      Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue
  9. 117 12
      Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue
  10. 11 0
      Web/src/views/aidop/s0/sales/ProductDesignCycleList.vue
  11. 4 0
      Web/src/views/aidop/s8/api/s8MonitoringApi.ts
  12. 2 3
      Web/src/views/aidop/s8/monitoring/S8MonitoringDeliveryPage.vue
  13. 2 3
      Web/src/views/aidop/s8/monitoring/S8MonitoringProductionPage.vue
  14. 2 3
      Web/src/views/aidop/s8/monitoring/S8MonitoringSupplyPage.vue
  15. 2 1
      Web/src/views/aidop/s8/monitoring/components/S8DeliveryTrendChart.vue
  16. 1 0
      Web/src/views/aidop/s8/monitoring/components/S8PeriodFilter.vue
  17. 6 5
      Web/src/views/aidop/s8/monitoring/components/S8ProductionTrendChart.vue
  18. 2 1
      Web/src/views/aidop/s8/monitoring/components/S8SupplyTrendChart.vue
  19. 1 1
      Web/src/views/aidop/s8/monitoring/useS8PeriodFilter.ts
  20. BIN
      doc/S1产销协同_UAT测试报告v1.0.xlsx
  21. BIN
      doc/S1产销协同业务需求描述.docx
  22. BIN
      doc/S1产销协同模块蓝图设计方案.docx
  23. BIN
      doc/S2制造协同业务需求描述.docx
  24. BIN
      doc/S2制造协同模块蓝图设计方案.docx
  25. 573 0
      doc/package-lock.json
  26. 16 0
      doc/package.json
  27. 9 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  28. 25 243
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.154.sql
  29. 76 19
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.155.sql
  30. 45 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.157.sql
  31. 266 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.158.sql
  32. 121 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleBreakdownsController.cs
  33. 89 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleSyncFlagsController.cs
  34. 60 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCyclesController.cs
  35. 4 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs
  36. 83 3
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ProductDesignCyclesController.cs
  37. 129 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs
  38. 6 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs
  39. 60 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleBreakdown.cs
  40. 40 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleSyncFlag.cs
  41. 6 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0PriorityCode.cs
  42. 17 8
      server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8PeriodHelper.cs
  43. 43 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewService.cs
  44. 10 10
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs
  45. 21 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/ProductDesignService.cs
  46. 4 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs
  47. 10 6
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictDataSeedData.cs
  48. 26 16
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MonitoringService.cs

+ 1 - 1
Web/package.json

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

+ 7 - 0
Web/src/views/aidop/api/contractReview.ts

@@ -122,3 +122,10 @@ export function fetchResponsibleUsers(keyword?: string) {
 		.get<unknown>('/api/Order/contract/responsible-users', { params: { keyword } })
 		.then((r) => unwrapFurionBody<{ value: string; label: string }[]>(r.data));
 }
+
+export function checkContractProjectCode(projectCode: string, excludeId: number = 0) {
+	return service
+		.get<unknown>('/api/Order/contract/check-project-code', { params: { projectCode, excludeId } })
+		.then((r) => unwrapFurionBody<{ exists: boolean }>(r.data))
+		.then((d) => d.exists);
+}

+ 82 - 26
Web/src/views/aidop/business/contractReviewForm.vue

@@ -12,47 +12,55 @@
 		</div>
 
 		<!-- 基础信息(仿图片表格布局) -->
+		<el-form ref="formRef" :model="form" :rules="rules" :disabled="!canEdit">
 		<div class="cr-info-table">
 			<div class="cr-row">
-				<div class="cr-label">销售公司</div>
+				<div class="cr-label required">销售公司</div>
 				<div class="cr-value">
-					<el-input v-model="form.salesCompany" :disabled="!canEdit" placeholder="请输入" />
+					<el-form-item prop="salesCompany">
+						<el-input v-model="form.salesCompany" placeholder="请输入" />
+					</el-form-item>
 				</div>
 				<div class="cr-label">销售区域</div>
 				<div class="cr-value">
-					<el-input v-model="form.salesArea" :disabled="!canEdit" placeholder="请输入" />
+					<el-input v-model="form.salesArea" placeholder="请输入" />
 				</div>
-				<div class="cr-label">合同编号</div>
+				<div class="cr-label required">合同编号</div>
 				<div class="cr-value">
-					<el-input v-model="form.projectCode" :disabled="!canEdit" placeholder="请输入" />
+					<el-form-item prop="projectCode">
+						<el-input v-model="form.projectCode" placeholder="请输入" />
+					</el-form-item>
 				</div>
 			</div>
 			<div class="cr-row">
-				<div class="cr-label">客户名称</div>
+				<div class="cr-label required">客户名称</div>
 				<div class="cr-value">
-					<el-input v-model="form.customerName" :disabled="!canEdit" placeholder="请输入" />
+					<el-form-item prop="customerName">
+						<el-input v-model="form.customerName" placeholder="请输入" />
+					</el-form-item>
 				</div>
 				<div class="cr-label">合同负责人</div>
 				<div class="cr-value">
-					<el-select v-model="form.responsibleAccount" filterable remote :remote-method="searchResponsibleUsers" placeholder="请选择合同负责人" clearable :disabled="!canEdit" style="width: 100%" @change="onResponsibleChange">
+					<el-select v-model="form.responsibleAccount" filterable remote :remote-method="searchResponsibleUsers" placeholder="请选择合同负责人" clearable style="width: 100%" @change="onResponsibleChange">
 						<el-option v-for="item in responsibleUserOptions" :key="item.value" :label="item.label" :value="item.value" />
 					</el-select>
 				</div>
-				<div class="cr-label">合同开始时间</div>
+				<div class="cr-label required">合同开始时间</div>
 				<div class="cr-value">
-					<el-date-picker
-						v-model="form.projectStartDate"
-						type="date"
-						value-format="YYYY-MM-DD"
-						style="width: 100%"
-						:disabled="!canEdit"
-					/>
+					<el-form-item prop="projectStartDate">
+						<el-date-picker
+							v-model="form.projectStartDate"
+							type="date"
+							value-format="YYYY-MM-DD"
+							style="width: 100%"
+						/>
+					</el-form-item>
 				</div>
 			</div>
 			<div class="cr-row">
 				<div class="cr-label">项目孵化状态</div>
 				<div class="cr-value">
-					<el-select v-model="form.projectStatus" :disabled="!canEdit" clearable style="width: 100%">
+					<el-select v-model="form.projectStatus" clearable style="width: 100%">
 						<el-option label="方案沟通阶段" value="方案沟通阶段" />
 						<el-option label="订单暂停" value="订单暂停" />
 						<el-option label="合同签订" value="合同签订" />
@@ -60,7 +68,7 @@
 				</div>
 				<div class="cr-label">CRM机会编号</div>
 				<div class="cr-value">
-					<el-input v-model="form.crmNo" :disabled="!canEdit" placeholder="请输入" />
+					<el-input v-model="form.crmNo" placeholder="请输入" />
 				</div>
 				<div class="cr-label">流程状态</div>
 				<div class="cr-value">
@@ -72,7 +80,7 @@
 			<div class="cr-row">
 				<div class="cr-label">成单概率</div>
 				<div class="cr-value">
-					<el-select v-model="form.winRate" :disabled="!canEdit" clearable style="width: 100%">
+					<el-select v-model="form.winRate" clearable style="width: 100%">
 						<el-option label="高" value="高" />
 						<el-option label="中" value="中" />
 						<el-option label="低" value="低" />
@@ -85,7 +93,6 @@
 						type="month"
 						value-format="YYYY-MM"
 						style="width: 100%"
-						:disabled="!canEdit"
 					/>
 				</div>
 				<div class="cr-label">预计发货日期</div>
@@ -95,14 +102,15 @@
 						type="date"
 						value-format="YYYY-MM-DD"
 						style="width: 100%"
-						:disabled="!canEdit"
 					/>
 				</div>
 			</div>
 			<div class="cr-row">
-				<div class="cr-label">合同标题</div>
+				<div class="cr-label required">合同标题</div>
 				<div class="cr-value cr-span2">
-					<el-input v-model="form.title" :disabled="!canEdit" placeholder="请输入合同标题" />
+					<el-form-item prop="title">
+						<el-input v-model="form.title" placeholder="请输入合同标题" />
+					</el-form-item>
 				</div>
 				<div class="cr-label">单据编号</div>
 				<div class="cr-value">
@@ -114,7 +122,6 @@
 				<div class="cr-value cr-full">
 					<el-input
 						v-model="form.projectRequirement"
-						:disabled="!canEdit"
 						type="textarea"
 						:rows="2"
 						placeholder="请描述项目需求"
@@ -126,7 +133,6 @@
 				<div class="cr-value cr-full">
 					<el-input
 						v-model="form.remark"
-						:disabled="!canEdit"
 						type="textarea"
 						:rows="2"
 						placeholder="备注信息"
@@ -134,6 +140,7 @@
 				</div>
 			</div>
 		</div>
+		</el-form>
 
 		<!-- 审批流(插件:与业务类型 CONTRACT_REVIEW 绑定,见 /doc/审批流集成开发指南.md) -->
 		<div v-if="detail && detail.recID > 0" class="cr-approval-wrap">
@@ -191,6 +198,7 @@
 <script setup lang="ts">
 import { computed, reactive, ref, watch } from 'vue';
 import { ElMessage } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
 import { Document } from '@element-plus/icons-vue';
 import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
 import { useUserInfo } from '/@/stores/userInfo';
@@ -198,6 +206,7 @@ import {
 	fetchContractReviewDetail,
 	fetchResponsibleUsers,
 	saveContractReview,
+	checkContractProjectCode,
 	type ContractReviewDetail,
 	type ContractReviewFlowNode,
 } from '../api/contractReview';
@@ -213,11 +222,40 @@ const emit = defineEmits<{
 }>();
 
 const userInfo = useUserInfo();
-const currentUserId = computed(() => (userInfo.userInfos?.id as number | undefined) ?? undefined);
+const currentUserId = computed(() => userInfo.userInfos?.id != null ? Number(userInfo.userInfos.id) : undefined);
 
 const loading = ref(false);
 const saving = ref(false);
 
+const formRef = ref<FormInstance>();
+
+// 合同编号唯一性异步校验
+const validateProjectCode = (_rule: any, value: any, callback: any) => {
+	if (!value) {
+		callback(new Error('请输入合同编号'));
+		return;
+	}
+	const excludeId = detail.value?.recID ?? 0;
+	checkContractProjectCode(value, excludeId).then((exists) => {
+		if (exists) {
+			callback(new Error('合同编号已存在,请使用其他编号'));
+		} else {
+			callback();
+		}
+	}).catch(() => callback());
+};
+
+const rules = reactive<FormRules>({
+	salesCompany: [{ required: true, message: '请输入销售公司', trigger: 'blur' }],
+	projectCode: [
+		{ required: true, message: '请输入合同编号', trigger: 'blur' },
+		{ validator: validateProjectCode, trigger: 'blur' },
+	],
+	customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
+	projectStartDate: [{ required: true, message: '请选择合同开始时间', trigger: 'change' }],
+	title: [{ required: true, message: '请输入合同标题', trigger: 'blur' }],
+});
+
 const responsibleUserOptions = ref<{ value: string; label: string }[]>([]);
 
 async function searchResponsibleUsers(query: string) {
@@ -359,6 +397,13 @@ async function loadDetail() {
 }
 
 async function onSave() {
+	if (!formRef.value) return;
+	try {
+		await formRef.value.validate();
+	} catch {
+		return; // 校验不通过,停止提交
+	}
+
 	saving.value = true;
 	try {
 		const res: any = await saveContractReview({
@@ -497,6 +542,12 @@ watch(
 	font-weight: 500;
 	white-space: nowrap;
 
+	&.required::before {
+		content: '*';
+		color: #f56c6c;
+		margin-right: 4px;
+	}
+
 	& + .cr-value {
 		border-right: 1px solid #e4e7ed;
 	}
@@ -508,6 +559,11 @@ watch(
 	display: flex;
 	align-items: center;
 
+	:deep(.el-form-item) {
+		width: 100%;
+		margin-bottom: 0;
+	}
+
 	:deep(.el-input__wrapper),
 	:deep(.el-textarea__inner),
 	:deep(.el-select),

+ 21 - 5
Web/src/views/aidop/business/orderDeliveryList.vue

@@ -52,11 +52,21 @@
 			stripe
 			style="width: 100%"
 		>
-			<el-table-column v-if="col.customNo" prop="customNo" label="客户编码" width="120" fixed="left" show-overflow-tooltip sortable />
-			<el-table-column v-if="col.billNo" prop="billNo" label="订单号" width="160" fixed="left" show-overflow-tooltip sortable />
-			<el-table-column v-if="col.moentryMono" prop="moentryMono" label="工单编号" width="150" show-overflow-tooltip sortable />
-			<el-table-column v-if="col.itemNumber" prop="itemNumber" label="物料编码" width="130" show-overflow-tooltip sortable />
-			<el-table-column v-if="col.itemName" prop="itemName" label="物料名称" min-width="140" show-overflow-tooltip sortable />
+			<el-table-column v-if="col.customNo" label="客户编码" width="120" fixed="left" show-overflow-tooltip sortable>
+				<template #default="{ row }">{{ nv(row.customNo) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.billNo" label="订单号" width="160" fixed="left" show-overflow-tooltip sortable>
+				<template #default="{ row }">{{ nv(row.billNo) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.moentryMono" label="工单编号" width="150" show-overflow-tooltip sortable>
+				<template #default="{ row }">{{ nv(row.moentryMono) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.itemNumber" label="物料编码" width="130" show-overflow-tooltip sortable>
+				<template #default="{ row }">{{ nv(row.itemNumber) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.itemName" label="物料名称" min-width="140" show-overflow-tooltip sortable>
+				<template #default="{ row }">{{ nv(row.itemName) }}</template>
+			</el-table-column>
 			<el-table-column v-if="col.qty" prop="qty" label="订单数量" width="100" align="right" sortable />
 			<el-table-column v-if="col.planDate" label="客户交期" width="115" align="center" sortable>
 				<template #default="{ row }">{{ fmtDate(row.planDate) }}</template>
@@ -184,6 +194,12 @@ function fmtDate(v?: string | null) {
 	return v.slice(0, 10);
 }
 
+/** 空值处理:null / undefined / 字符串"null" → 空字符串 */
+function nv(v?: string | null) {
+	if (v == null || v === 'null' || v === 'NULL') return '';
+	return v;
+}
+
 function progressActive(p?: string | null): number {
 	const n = parseInt(p ?? '0');
 	return isNaN(n) ? 0 : n;

+ 42 - 3
Web/src/views/aidop/business/productDesignForm.vue

@@ -158,7 +158,7 @@
 			<div class="pd-section-main">
 				<div class="cr-info-table">
 					<div class="cr-row">
-						<div class="cr-label">产品编码</div>
+						<div class="cr-label required">产品编码</div>
 						<div class="cr-value pd-with-suffix">
 							<el-input v-model="form.itemNum" :disabled="isView" placeholder="产品编码" clearable />
 							<el-button v-if="!isView" :icon="Search" @click="openItemPick('main')" />
@@ -648,9 +648,14 @@ async function loadBomAndRouting(itemNum: string) {
 			form.drawingDesignCycle = result.drawingDesignCycle;
 		}
 
-		// 构建工艺路线
+		// 构建工艺路线(按工序代码数字顺序排列)
+		const sortedRoutings = [...result.routings].sort((a, b) => {
+			const na = a.op ?? 0;
+			const nb = b.op ?? 0;
+			return na - nb;
+		});
 		let routingSeq = 0;
-		form.routings = result.routings.map((r) => ({
+		form.routings = sortedRoutings.map((r) => ({
 			id: undefined,
 			seq: ++routingSeq,
 			opName: r.descr ?? '',
@@ -785,6 +790,34 @@ watch(
 );
 
 async function onSave() {
+	// 1. 产品编码必填
+	if (!form.itemNum || !form.itemNum.trim()) {
+		ElMessage.warning('产品编码不能为空');
+		return;
+	}
+	// 2. BOM 子表至少一条
+	if (!form.boms || form.boms.length === 0) {
+		ElMessage.warning('制造 BOM 至少需要一条数据');
+		return;
+	}
+	// 3. 工艺子表至少一条
+	if (!form.routings || form.routings.length === 0) {
+		ElMessage.warning('工艺路线至少需要一条数据');
+		return;
+	}
+	// 4. BOM 物料编码必填
+	const emptyBom = form.boms.find((b) => !b.itemNum || !b.itemNum.trim());
+	if (emptyBom) {
+		ElMessage.warning('制造 BOM 中物料编码不能为空');
+		return;
+	}
+	// 5. 工艺工序代码必填
+	const emptyRouting = form.routings.find((r) => !r.opCode || !r.opCode.trim());
+	if (emptyRouting) {
+		ElMessage.warning('工艺路线中工序代码不能为空');
+		return;
+	}
+
 	saving.value = true;
 	try {
 		await saveProductDesign({
@@ -951,6 +984,12 @@ async function onSave() {
 	color: #606266;
 	font-weight: 500;
 	white-space: nowrap;
+
+	&.required::before {
+		content: '*';
+		color: #f56c6c;
+		margin-right: 4px;
+	}
 }
 
 .cr-value {

+ 5 - 0
Web/src/views/aidop/business/salesOrderForm.vue

@@ -235,6 +235,11 @@ function onPickItem(v: ItemRow) {
 
 async function onSave() {
 	await formRef.value?.validate().catch(() => Promise.reject());
+	// 子表订单明细至少一条
+	if (!form.entries || form.entries.length === 0) {
+		ElMessage.warning('订单明细至少需要一条数据');
+		return;
+	}
 	if (props.mode === 'create') {
 		form.entries = (form.entries || []).map((x) => ({
 			...x,

+ 82 - 0
Web/src/views/aidop/s0/api/s0SalesApi.ts

@@ -228,6 +228,8 @@ export interface S0PriorityCodeRow {
 	sourceId?: string | null;
 	valueType?: string | null;
 	valueId?: string | null;
+	customerTypeCode?: string | null;
+	orderTypeCode?: string | null;
 	isActive: boolean;
 	createUser?: string | null;
 	createTime?: string;
@@ -247,6 +249,8 @@ export interface S0PriorityCodeUpsert {
 	sourceId?: string;
 	valueType?: string;
 	valueId?: string;
+	customerTypeCode?: string;
+	orderTypeCode?: string;
 	isActive: boolean;
 	createUser?: string;
 	updateUser?: string;
@@ -420,6 +424,80 @@ export const s0ContractReviewCyclesApi = {
 		service.patch<S0ContractReviewCycleRow>(`/api/s0/sales/contract-review-cycles/${id}/toggle-enabled`, body).then(unwrap),
 };
 
+// ── B1.2: 合同评审周期同步开关(工厂级一行) ──────────────────────────
+
+export interface S0ContractReviewCycleSyncFlag {
+	id?: number | null;
+	companyRefId?: string | null;
+	factoryRefId: string;
+	domainCode?: string | null;
+	isSyncEnabled: boolean;
+}
+
+export interface S0ContractReviewCycleSyncFlagUpsert {
+	companyRefId: string;
+	factoryRefId: string;
+	domainCode?: string;
+	isSyncEnabled: boolean;
+	createUser?: string;
+	updateUser?: string;
+}
+
+export const s0ContractReviewCycleSyncFlagApi = {
+	get: (params: { factoryRefId: string }) =>
+		service.get<S0ContractReviewCycleSyncFlag>('/api/s0/sales/contract-review-cycle-sync-flag', { params }).then(unwrap),
+	update: (body: S0ContractReviewCycleSyncFlagUpsert) =>
+		service.put<S0ContractReviewCycleSyncFlag>('/api/s0/sales/contract-review-cycle-sync-flag', body).then(unwrap),
+};
+
+// ── B1.3: 合同评审周期下钻配置(按 parent_stage_code 分组) ──────────
+
+export interface S0ContractReviewCycleBreakdownRow {
+	id: number;
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string | null;
+	parentStageCode: string;
+	groupCode: string;
+	groupName: string;
+	stdHours: number;
+	orderNo: number;
+	isActive: boolean;
+	createUser?: string | null;
+	createTime?: string;
+	updateUser?: string | null;
+	updateTime?: string | null;
+}
+
+export interface S0ContractReviewCycleBreakdownUpsert {
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string;
+	parentStageCode: string;
+	groupCode: string;
+	groupName: string;
+	stdHours: number;
+	orderNo: number;
+	isActive: boolean;
+	createUser?: string;
+	updateUser?: string;
+}
+
+export const s0ContractReviewCycleBreakdownsApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0ContractReviewCycleBreakdownRow>>('/api/s0/sales/contract-review-cycle-breakdowns', { params }).then(unwrap),
+	get: (id: number) =>
+		service.get<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`).then(unwrap),
+	create: (body: S0ContractReviewCycleBreakdownUpsert) =>
+		service.post<S0ContractReviewCycleBreakdownRow>('/api/s0/sales/contract-review-cycle-breakdowns', body).then(unwrap),
+	update: (id: number, body: S0ContractReviewCycleBreakdownUpsert) =>
+		service.put<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`, body).then(unwrap),
+	delete: (id: number) =>
+		service.delete(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: { isActive: boolean }) =>
+		service.patch<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}/toggle-enabled`, body).then(unwrap),
+};
+
 // ── B2: 产品设计周期标准 ──────────────────────────────────────────────
 
 export interface S0ProductDesignCycleRow {
@@ -435,6 +513,10 @@ export interface S0ProductDesignCycleRow {
 	createTime?: string;
 	updateUser?: string | null;
 	updateTime?: string | null;
+	/** 后端列表附加:当前 (ItemType[, Domain]) 组合在 ItemMaster 命中物料行数。 */
+	itemMasterMatchedCount?: number;
+	/** 后端列表附加:前 3 个示例物料,格式 "ItemNum / Descr"(无 Descr 时仅 ItemNum)。 */
+	itemMasterSampleItems?: string[];
 }
 
 export interface S0ProductDesignCycleUpsert {

+ 369 - 0
Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue

@@ -1,5 +1,17 @@
 <template>
 	<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 合同评审周期">
+		<!-- ── 同步当前配置开关(工厂级) ─────────────────────────────────── -->
+		<div class="sync-bar">
+			<span class="sync-bar__label">同步当前配置:</span>
+			<el-switch
+				v-model="syncEnabled"
+				:disabled="syncDisabled"
+				:loading="syncLoading"
+				@change="onSyncChange"
+			/>
+			<span class="sync-bar__text">{{ syncBarText }}</span>
+		</div>
+
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="公司">
 				<el-select v-model="query.companyRefId" clearable filterable placeholder="全部" style="width: 180px">
@@ -62,6 +74,51 @@
 			/>
 		</div>
 
+		<!-- ── 意见反馈下钻配置 ──────────────────────────────────────── -->
+		<section class="breakdown-section">
+			<div class="breakdown-section__head">
+				<h3 class="breakdown-section__title">意见反馈 — 部门/组 PI 配置</h3>
+				<div class="breakdown-section__actions">
+					<el-button :disabled="!query.factoryRefId" @click="loadBreakdown">刷新</el-button>
+					<el-button type="success" :disabled="!query.factoryRefId" @click="openBreakdownCreate">新增下钻</el-button>
+				</div>
+			</div>
+
+			<div v-if="!query.factoryRefId" class="breakdown-section__hint">
+				请先选择工厂后维护意见反馈下钻配置
+			</div>
+			<el-table
+				v-else
+				:data="breakdownRows"
+				v-loading="breakdownLoading"
+				border
+				stripe
+				size="small"
+				style="width: 100%"
+			>
+				<el-table-column prop="groupCode" label="组编码" width="160" class-name="breakdown-section__wrap-cell" />
+				<el-table-column prop="groupName" label="组名称" min-width="160" class-name="breakdown-section__wrap-cell" />
+				<el-table-column prop="stdHours" label="标准时长(h)" width="130" align="right" />
+				<el-table-column prop="orderNo" label="顺序" width="80" align="center" />
+				<el-table-column label="是否启用" width="100" align="center">
+					<template #default="{ row }">
+						<el-tag :type="row.isActive ? 'success' : 'info'" size="small">
+							{{ row.isActive ? '是' : '否' }}
+						</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="220" fixed="right" align="center">
+					<template #default="{ row }">
+						<el-button link type="primary" @click="openBreakdownEdit(row)">编辑</el-button>
+						<el-button link :type="row.isActive ? 'warning' : 'success'" @click="breakdownToggleActive(row)">
+							{{ row.isActive ? '禁用' : '启用' }}
+						</el-button>
+						<el-button link type="danger" @click="onBreakdownDelete(row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</section>
+
 		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close @closed="resetForm">
 			<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
 				<el-row :gutter="16">
@@ -117,6 +174,40 @@
 				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
 			</template>
 		</el-dialog>
+
+		<!-- ── 下钻 新增/编辑 弹窗 ──────────────────────────────────── -->
+		<el-dialog
+			v-model="breakdownDialogVisible"
+			:title="breakdownDialogTitle"
+			width="560px"
+			destroy-on-close
+			@closed="resetBreakdownForm"
+		>
+			<el-form ref="breakdownFormRef" :model="breakdownForm" :rules="breakdownRules" label-width="110px">
+				<el-form-item label="父阶段">
+					<el-input :model-value="STAGE_NAME_MAP[BREAKDOWN_PARENT_STAGE_CODE]" disabled />
+				</el-form-item>
+				<el-form-item label="组编码" prop="groupCode">
+					<el-input v-model="breakdownForm.groupCode" placeholder="例如:market" />
+				</el-form-item>
+				<el-form-item label="组名称" prop="groupName">
+					<el-input v-model="breakdownForm.groupName" placeholder="例如:市场部" />
+				</el-form-item>
+				<el-form-item label="标准时长(h)" prop="stdHours">
+					<el-input-number v-model="breakdownForm.stdHours" :min="0" :step="0.1" :precision="2" style="width: 100%" />
+				</el-form-item>
+				<el-form-item label="顺序">
+					<el-input-number v-model="breakdownForm.orderNo" :min="1" style="width: 100%" />
+				</el-form-item>
+				<el-form-item label="是否启用">
+					<el-switch v-model="breakdownForm.isActive" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="breakdownDialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="breakdownSaving" @click="submitBreakdownForm">保存</el-button>
+			</template>
+		</el-dialog>
 	</AidopDemoShell>
 </template>
 
@@ -128,9 +219,13 @@ import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import {
 	loadOrgList,
 	s0ContractReviewCyclesApi,
+	s0ContractReviewCycleSyncFlagApi,
+	s0ContractReviewCycleBreakdownsApi,
 	type OrgOption,
 	type S0ContractReviewCycleRow,
 	type S0ContractReviewCycleUpsert,
+	type S0ContractReviewCycleBreakdownRow,
+	type S0ContractReviewCycleBreakdownUpsert,
 } from '../api/s0SalesApi';
 
 const route = useRoute();
@@ -165,6 +260,25 @@ const formRef = ref<FormInstance>();
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 
+// ── 同步开关(工厂级) ─────────────────────────────────────────────────
+const syncEnabled = ref(false);
+const syncLoading = ref(false);
+const syncDisabled = computed(() => !query.factoryRefId);
+const syncBarText = computed(() => {
+	if (!query.factoryRefId) return '请先选择工厂';
+	return syncEnabled.value ? '当前工厂配置已同步到 S8' : '当前工厂未同步';
+});
+
+// ── 意见反馈下钻配置 ───────────────────────────────────────────────────
+const BREAKDOWN_PARENT_STAGE_CODE = 'feedback';
+const breakdownRows = ref<S0ContractReviewCycleBreakdownRow[]>([]);
+const breakdownLoading = ref(false);
+const breakdownDialogVisible = ref(false);
+const breakdownDialogTitle = ref('新增下钻');
+const breakdownEditingId = ref<number | null>(null);
+const breakdownSaving = ref(false);
+const breakdownFormRef = ref<FormInstance>();
+
 const filteredFactoryOptions = computed(() => {
 	if (!query.companyRefId) return factoryOptions.value;
 	return factoryOptions.value.filter((item) => item.pid === query.companyRefId);
@@ -199,6 +313,30 @@ const rules: FormRules = {
 	stageName: [{ required: true, message: '请填写阶段名称', trigger: 'blur' }],
 };
 
+function emptyBreakdownForm(): S0ContractReviewCycleBreakdownUpsert {
+	return {
+		companyRefId: undefined,
+		factoryRefId: undefined,
+		domainCode: '',
+		parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
+		groupCode: '',
+		groupName: '',
+		stdHours: 0,
+		orderNo: 1,
+		isActive: true,
+		createUser: '',
+		updateUser: '',
+	};
+}
+
+const breakdownForm = reactive<S0ContractReviewCycleBreakdownUpsert>(emptyBreakdownForm());
+
+const breakdownRules: FormRules = {
+	groupCode: [{ required: true, message: '请填写组编码', trigger: 'blur' }],
+	groupName: [{ required: true, message: '请填写组名称', trigger: 'blur' }],
+	stdHours: [{ required: true, message: '请填写标准时长', trigger: 'change' }],
+};
+
 function syncDomainCode() {
 	const f = factoryOptions.value.find((item) => item.id === form.factoryRefId);
 	form.domainCode = f?.code ?? '';
@@ -228,6 +366,15 @@ watch(
 	}
 );
 
+// 工厂切换时重新读取该工厂的 sync flag 和意见反馈下钻(未选工厂时清空)
+watch(
+	() => query.factoryRefId,
+	() => {
+		void loadSyncFlag();
+		void loadBreakdown();
+	}
+);
+
 async function loadOptions() {
 	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
 	companyOptions.value = companies;
@@ -338,9 +485,169 @@ function toggleActive(row: S0ContractReviewCycleRow) {
 		.catch(() => {});
 }
 
+// ── 同步开关加载与切换 ────────────────────────────────────────────────
+
+async function loadSyncFlag() {
+	if (!query.factoryRefId) {
+		// 未选工厂:reset 到关闭状态;不发请求
+		syncEnabled.value = false;
+		return;
+	}
+	try {
+		const data = await s0ContractReviewCycleSyncFlagApi.get({ factoryRefId: query.factoryRefId });
+		syncEnabled.value = data?.isSyncEnabled ?? false;
+	} catch {
+		syncEnabled.value = false;
+	}
+}
+
+async function onSyncChange(next: boolean | string | number) {
+	const nextBool = next === true;
+	if (!query.factoryRefId || !query.companyRefId) {
+		// 双保险:模板已 disabled,但工厂被外部清空时回滚
+		syncEnabled.value = !nextBool;
+		ElMessage.warning('请先选择公司与工厂');
+		return;
+	}
+	const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
+	syncLoading.value = true;
+	try {
+		await s0ContractReviewCycleSyncFlagApi.update({
+			companyRefId: query.companyRefId,
+			factoryRefId: query.factoryRefId,
+			domainCode: factory?.code ?? undefined,
+			isSyncEnabled: nextBool,
+		});
+		syncEnabled.value = nextBool;
+		ElMessage.success(nextBool ? '已开启同步' : '已关闭同步');
+	} catch {
+		syncEnabled.value = !nextBool;
+		ElMessage.error('同步开关切换失败,请稍后重试');
+	} finally {
+		syncLoading.value = false;
+	}
+}
+
+// ── 意见反馈下钻加载与 CRUD ─────────────────────────────────────────
+
+async function loadBreakdown() {
+	if (!query.factoryRefId) {
+		breakdownRows.value = [];
+		return;
+	}
+	breakdownLoading.value = true;
+	try {
+		const data = await s0ContractReviewCycleBreakdownsApi.list({
+			factoryRefId: query.factoryRefId,
+			parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
+			page: 1,
+			pageSize: 100,
+		});
+		breakdownRows.value = data.list;
+	} catch {
+		breakdownRows.value = [];
+	} finally {
+		breakdownLoading.value = false;
+	}
+}
+
+function resetBreakdownForm() {
+	breakdownEditingId.value = null;
+	Object.assign(breakdownForm, emptyBreakdownForm());
+	breakdownFormRef.value?.clearValidate();
+}
+
+function openBreakdownCreate() {
+	if (!query.factoryRefId || !query.companyRefId) {
+		ElMessage.warning('请先选择公司与工厂');
+		return;
+	}
+	resetBreakdownForm();
+	const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
+	breakdownForm.companyRefId = query.companyRefId;
+	breakdownForm.factoryRefId = query.factoryRefId;
+	breakdownForm.domainCode = factory?.code ?? '';
+	breakdownForm.orderNo = breakdownRows.value.length + 1;
+	breakdownDialogTitle.value = '新增下钻';
+	breakdownDialogVisible.value = true;
+}
+
+function openBreakdownEdit(row: S0ContractReviewCycleBreakdownRow) {
+	resetBreakdownForm();
+	breakdownEditingId.value = row.id;
+	breakdownDialogTitle.value = `编辑:${row.groupName}`;
+	Object.assign(breakdownForm, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		domainCode: row.domainCode ?? '',
+		parentStageCode: row.parentStageCode,
+		groupCode: row.groupCode,
+		groupName: row.groupName,
+		stdHours: row.stdHours,
+		orderNo: row.orderNo,
+		isActive: row.isActive,
+		createUser: row.createUser ?? '',
+		updateUser: row.updateUser ?? '',
+	});
+	breakdownDialogVisible.value = true;
+}
+
+async function submitBreakdownForm() {
+	await breakdownFormRef.value?.validate();
+	breakdownSaving.value = true;
+	try {
+		const payload: S0ContractReviewCycleBreakdownUpsert = { ...breakdownForm };
+		if (breakdownEditingId.value !== null) {
+			await s0ContractReviewCycleBreakdownsApi.update(breakdownEditingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0ContractReviewCycleBreakdownsApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		breakdownDialogVisible.value = false;
+		await loadBreakdown();
+	} catch {
+		ElMessage.error('保存下钻配置失败,请检查填写内容后重试');
+	} finally {
+		breakdownSaving.value = false;
+	}
+}
+
+function onBreakdownDelete(row: S0ContractReviewCycleBreakdownRow) {
+	ElMessageBox.confirm(`确认删除该部门/组 PI 配置吗?「${row.groupName}」`, '确认', { type: 'warning' })
+		.then(async () => {
+			try {
+				await s0ContractReviewCycleBreakdownsApi.delete(row.id);
+				ElMessage.success('已删除');
+				await loadBreakdown();
+			} catch {
+				ElMessage.error('删除失败,请稍后重试');
+			}
+		})
+		.catch(() => {});
+}
+
+function breakdownToggleActive(row: S0ContractReviewCycleBreakdownRow) {
+	const next = !row.isActive;
+	const actionText = next ? '启用' : '禁用';
+	ElMessageBox.confirm(`确定${actionText}「${row.groupName}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			try {
+				await s0ContractReviewCycleBreakdownsApi.toggleEnabled(row.id, { isActive: next });
+				ElMessage.success(`${actionText}成功`);
+				await loadBreakdown();
+			} catch {
+				ElMessage.error('启停失败,请稍后重试');
+			}
+		})
+		.catch(() => {});
+}
+
 onMounted(async () => {
 	await loadOptions();
 	await loadList();
+	await loadSyncFlag();
+	await loadBreakdown();
 });
 </script>
 
@@ -356,4 +663,66 @@ onMounted(async () => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+.sync-bar {
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	margin-bottom: 12px;
+	padding: 10px 14px;
+	background: var(--el-color-info-light-9);
+	border-left: 4px solid var(--el-color-primary);
+	border-radius: 4px;
+
+	&__label {
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	&__text {
+		color: var(--el-text-color-secondary);
+		font-size: 13px;
+	}
+}
+
+.breakdown-section {
+	margin-top: 24px;
+	padding-top: 16px;
+	border-top: 1px dashed var(--el-border-color);
+
+	&__head {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin-bottom: 12px;
+		gap: 12px;
+	}
+
+	&__title {
+		margin: 0;
+		font-size: 16px;
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	&__actions {
+		display: flex;
+		gap: 8px;
+	}
+
+	&__hint {
+		padding: 24px 0;
+		text-align: center;
+		color: var(--el-text-color-secondary);
+		font-size: 13px;
+		background: var(--el-color-info-light-9);
+		border-radius: 4px;
+	}
+}
+
+:deep(.breakdown-section__wrap-cell .cell) {
+	white-space: normal;
+	word-break: break-word;
+	line-height: 1.4;
+}
 </style>

+ 117 - 12
Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue

@@ -15,36 +15,43 @@
 				<el-button type="primary" @click="loadList">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
 				<el-button type="success" @click="openCreate">新增配置</el-button>
+				<el-button @click="configDialogVisible = true">配置维护</el-button>
 			</el-form-item>
 		</el-form>
 
 		<!-- ── 主列表 ── -->
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
-			<el-table-column prop="descr" label="名称" min-width="150" show-overflow-tooltip>
+			<el-table-column prop="descr" label="名称" width="200" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.descr || '—' }}</template>
 			</el-table-column>
-			<el-table-column prop="value" label="工单字段" width="130" show-overflow-tooltip>
+			<el-table-column label="客户类型" width="140" class-name="wrap-cell">
+				<template #default="{ row }">{{ customerTypeLabel(row.customerTypeCode) }}</template>
+			</el-table-column>
+			<el-table-column label="订单类型" width="140" class-name="wrap-cell">
+				<template #default="{ row }">{{ orderTypeLabel(row.orderTypeCode) }}</template>
+			</el-table-column>
+			<el-table-column prop="value" label="工单字段" width="130" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.value || '—' }}</template>
 			</el-table-column>
-			<el-table-column prop="priority" label="优先级" width="80" align="center" />
-			<el-table-column prop="sourceTable" label="来源表" width="140" show-overflow-tooltip>
+			<el-table-column prop="priority" label="优先级" width="90" align="center" />
+			<el-table-column prop="sourceTable" label="来源表" width="160" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.sourceTable || '—' }}</template>
 			</el-table-column>
-			<el-table-column prop="sourceColumn" label="来源字段" width="130" show-overflow-tooltip>
+			<el-table-column prop="sourceColumn" label="来源字段" width="150" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.sourceColumn || '—' }}</template>
 			</el-table-column>
-			<el-table-column label="排序" width="80" align="center">
+			<el-table-column label="排序" width="90" align="center">
 				<template #default="{ row }">{{ orderByDisplay(row) }}</template>
 			</el-table-column>
-			<el-table-column label="是否生效" width="88" align="center">
+			<el-table-column label="是否生效" width="100" align="center">
 				<template #default="{ row }">
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column prop="domainCode" label="工厂域" width="100" show-overflow-tooltip>
+			<el-table-column prop="domainCode" label="工厂域" width="100" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.domainCode || '—' }}</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+			<el-table-column label="操作" width="180" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
 					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
@@ -87,6 +94,30 @@
 							<el-input v-model="form.value" placeholder="工单表字段(Value)" />
 						</el-form-item>
 					</el-col>
+					<el-col :span="12">
+						<el-form-item label="客户类型">
+							<el-select v-model="form.customerTypeCode" clearable placeholder="不限" style="width: 100%">
+								<el-option
+									v-for="opt in dictOptions.customerType"
+									:key="opt.value"
+									:label="opt.label"
+									:value="String(opt.value)"
+								/>
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="订单类型">
+							<el-select v-model="form.orderTypeCode" clearable placeholder="不限" style="width: 100%">
+								<el-option
+									v-for="opt in dictOptions.orderType"
+									:key="opt.value"
+									:label="opt.label"
+									:value="String(opt.value)"
+								/>
+							</el-select>
+						</el-form-item>
+					</el-col>
 					<el-col :span="12">
 						<el-form-item label="优先级" prop="priority">
 							<el-input-number v-model="form.priority" :min="0" style="width: 100%" controls-position="right" />
@@ -147,6 +178,24 @@
 				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
 			</template>
 		</el-dialog>
+
+		<!-- ── 配置维护弹窗(仅客户类型 / 订单类型)── -->
+		<el-dialog v-model="configDialogVisible" title="订单优先级配置维护" width="820px" destroy-on-close>
+			<el-tabs v-model="configActiveTab">
+				<el-tab-pane label="客户类型" name="customerType">
+					<DictDataMaintainPanel
+						dict-code="s0_order_priority_customer_type"
+						@changed="loadDictSelects"
+					/>
+				</el-tab-pane>
+				<el-tab-pane label="订单类型" name="orderType">
+					<DictDataMaintainPanel
+						dict-code="s0_order_type"
+						@changed="loadDictSelects"
+					/>
+				</el-tab-pane>
+			</el-tabs>
+		</el-dialog>
 	</AidopDemoShell>
 </template>
 
@@ -159,7 +208,10 @@ import {
 	s0OrderPriorityRulesApi,
 	type S0PriorityCodeRow,
 	type S0PriorityCodeUpsert,
+	loadDictOptions,
+	type OptionItem,
 } from '../api/s0SalesApi';
+import DictDataMaintainPanel from './components/DictDataMaintainPanel.vue';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '订单优先级配置');
@@ -206,6 +258,38 @@ function resetQuery() {
 	void loadList();
 }
 
+// ── 字典选项(客户类型 / 订单类型)─────────────────────────────────────────
+const dictOptions = reactive({
+	customerType: [] as OptionItem[],
+	orderType: [] as OptionItem[],
+});
+
+const customerTypeMap = computed(
+	() => new Map(dictOptions.customerType.map((o) => [String(o.value), o.label])),
+);
+const orderTypeMap = computed(
+	() => new Map(dictOptions.orderType.map((o) => [String(o.value), o.label])),
+);
+
+/** code → label;空值显示 —;字典缺 label 时回退显示 code,不编造文案。 */
+function customerTypeLabel(code?: string | null): string {
+	if (!code) return '—';
+	return customerTypeMap.value.get(code) ?? code;
+}
+function orderTypeLabel(code?: string | null): string {
+	if (!code) return '—';
+	return orderTypeMap.value.get(code) ?? code;
+}
+
+async function loadDictSelects() {
+	const [ct, ot] = await Promise.all([
+		loadDictOptions('s0_order_priority_customer_type'),
+		loadDictOptions('s0_order_type'),
+	]);
+	dictOptions.customerType = ct;
+	dictOptions.orderType = ot;
+}
+
 // ── 配置表单 ──────────────────────────────────────────────────────────────
 const formDialogVisible = ref(false);
 const formDialogTitle = ref('新增配置');
@@ -216,6 +300,8 @@ const formRef = ref<FormInstance>();
 interface PriorityCodeForm {
 	descr: string;
 	value: string;
+	customerTypeCode: string;
+	orderTypeCode: string;
 	priority: number;
 	orderBy: boolean | null;
 	sourceTable: string;
@@ -232,6 +318,8 @@ function emptyForm(): PriorityCodeForm {
 	return {
 		descr: '',
 		value: '',
+		customerTypeCode: '',
+		orderTypeCode: '',
 		priority: 1,
 		orderBy: null,
 		sourceTable: '',
@@ -271,6 +359,8 @@ function openEdit(row: S0PriorityCodeRow) {
 	Object.assign(form, {
 		descr: row.descr ?? '',
 		value: row.value ?? '',
+		customerTypeCode: row.customerTypeCode ?? '',
+		orderTypeCode: row.orderTypeCode ?? '',
 		priority: row.priority,
 		orderBy: row.orderBy ?? null,
 		sourceTable: row.sourceTable ?? '',
@@ -285,8 +375,10 @@ function openEdit(row: S0PriorityCodeRow) {
 	formDialogVisible.value = true;
 }
 
-/** 空字符串归一为 undefined,避免把空值写库。 */
-function blankToUndefined(v: string): string | undefined {
+/** 空串 / undefined / null 统一归一为 undefined,避免把空值写库;
+ *  el-select 清空后 v-model 变为 undefined,必须容忍。 */
+function blankToUndefined(v: string | null | undefined): string | undefined {
+	if (v === null || v === undefined) return undefined;
 	return v.trim() === '' ? undefined : v;
 }
 
@@ -297,6 +389,8 @@ async function submitForm() {
 		const payload: S0PriorityCodeUpsert = {
 			descr: form.descr,
 			value: blankToUndefined(form.value),
+			customerTypeCode: blankToUndefined(form.customerTypeCode),
+			orderTypeCode: blankToUndefined(form.orderTypeCode),
 			priority: form.priority,
 			orderBy: form.orderBy,
 			sourceTable: blankToUndefined(form.sourceTable),
@@ -346,8 +440,12 @@ function toggleActive(row: S0PriorityCodeRow) {
 		.catch(() => {});
 }
 
+// ── 配置维护弹窗 ──────────────────────────────────────────────────────────
+const configDialogVisible = ref(false);
+const configActiveTab = ref('customerType');
+
 onMounted(async () => {
-	await loadList();
+	await Promise.all([loadList(), loadDictSelects()]);
 });
 </script>
 
@@ -363,4 +461,11 @@ onMounted(async () => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+// 长文本列自动换行(替代 tooltip 截断),仅作用于本页表格
+:deep(.wrap-cell .cell) {
+	white-space: normal;
+	word-break: break-word;
+	line-height: 1.4;
+}
 </style>

+ 11 - 0
Web/src/views/aidop/s0/sales/ProductDesignCycleList.vue

@@ -37,6 +37,17 @@
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
 			<el-table-column prop="itemType" label="物料类型" width="160" show-overflow-tooltip />
 			<el-table-column prop="ownerApplication" label="物料属性" width="120" show-overflow-tooltip />
+			<el-table-column label="类型命中物料数" width="130" align="right">
+				<template #default="{ row }">{{ row.itemMasterMatchedCount ?? 0 }}</template>
+			</el-table-column>
+			<el-table-column label="示例物料" min-width="240" show-overflow-tooltip>
+				<template #default="{ row }">
+					<span v-if="row.itemMasterSampleItems && row.itemMasterSampleItems.length > 0">
+						{{ row.itemMasterSampleItems.join('、') }}
+					</span>
+					<span v-else>—</span>
+				</template>
+			</el-table-column>
 			<el-table-column prop="stdHours" label="标准设计时长(h)" width="150" align="right" />
 			<el-table-column label="启用" width="80" align="center">
 				<template #default="{ row }">

+ 4 - 0
Web/src/views/aidop/s8/api/s8MonitoringApi.ts

@@ -34,6 +34,7 @@ export type S8Period =
 	| 'today'
 	| 'this_week'
 	| 'this_month'
+	| 'this_quarter'
 	| 'last_24h'
 	| 'last_7d'
 	| 'last_30d';
@@ -88,6 +89,7 @@ export interface S8DeliveryTrendDay {
 	orderDueDateDelay: number;
 	productDesignDelay: number;
 	deliveryDelayWarning: number;
+	shipmentAbnormal: number;
 	total: number;
 }
 
@@ -112,6 +114,7 @@ export interface S8ProductionTrendDay {
 	materialFault: number;
 	qualityFault: number;
 	bodyProductionDelay: number;
+	productionWorkOrderCompletionDelay: number;
 	total: number;
 }
 
@@ -135,6 +138,7 @@ export interface S8SupplyTrendDay {
 	purchaseExecutionDelay: number;
 	materialStockAbnormal: number;
 	inventoryTurnoverAbnormal: number;
+	purchaseDeliveryAbnormal: number;
 	total: number;
 }
 

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

@@ -360,10 +360,9 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 }
 
 // ─── 工具(趋势标题)──────────────────────────────────────────────────────
-const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', this_quarter: '本季度', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
 function trendTitle(domain: string) {
-	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
-	return `${label}${domain}异常趋势`;
+	return `${domain}异常趋势`;
 }
 
 // ─── 数据加载 ──────────────────────────────────────────────────────────────

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

@@ -356,10 +356,9 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 }
 
 // ─── 工具(趋势标题)──────────────────────────────────────────────────────
-const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', this_quarter: '本季度', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
 function trendTitle(domain: string) {
-	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
-	return `${label}${domain}异常趋势`;
+	return `${domain}异常趋势`;
 }
 
 // ─── 数据加载 ──────────────────────────────────────────────────────────────

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

@@ -376,10 +376,9 @@ function anomalyToneClass(closeRate: number | null | undefined) {
 }
 
 // ─── 工具(趋势标题)──────────────────────────────────────────────────────
-const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
+const PERIOD_LABEL: Record<string, string> = { today: '本日', this_week: '本周', this_month: '本月', this_quarter: '本季度', last_24h: '过去24h', last_7d: '过去7天', last_30d: '过去30天' };
 function trendTitle(domain: string) {
-	const label = PERIOD_LABEL[period.value ?? ''] ?? '本月';
-	return `${label}${domain}异常趋势`;
+	return `${domain}异常趋势`;
 }
 
 // ─── 数据加载 ──────────────────────────────────────────────────────────────

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

@@ -43,7 +43,7 @@ const props = defineProps<{
 	title?: string;
 }>();
 
-const title = computed(() => props.title ?? '本月交付异常趋势');
+const title = computed(() => props.title ?? '交付异常趋势');
 
 const chartRef = ref();
 
@@ -68,6 +68,7 @@ const SERIES_META = [
 	{ key: 'orderDueDateDelay', name: '订单交期延期', color: '#7bd0ff' },
 	{ key: 'productDesignDelay', name: '产品设计延期', color: '#a78bfa' },
 	{ key: 'deliveryDelayWarning', name: '总装发货延期', color: '#ffc107' },
+	{ key: 'shipmentAbnormal', name: '发运异常', color: '#fb7185' },
 ] as const;
 
 const chartOption = computed(() => {

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

@@ -21,6 +21,7 @@ const OPTIONS = [
 	{ value: 'today' as S8Period, label: '本日' },
 	{ value: 'this_week' as S8Period, label: '本周' },
 	{ value: 'this_month' as S8Period, label: '本月' },
+	{ value: 'this_quarter' as S8Period, label: '本季度' },
 	{ value: 'last_24h' as S8Period, label: '过去24h' },
 	{ value: 'last_7d' as S8Period, label: '过去7天' },
 	{ value: 'last_30d' as S8Period, label: '过去30天' },

+ 6 - 5
Web/src/views/aidop/s8/monitoring/components/S8ProductionTrendChart.vue

@@ -44,7 +44,7 @@ const props = defineProps<{
 	title?: string;
 }>();
 
-const title = computed(() => props.title ?? '本月生产异常趋势');
+const title = computed(() => props.title ?? '生产异常趋势');
 
 const chartRef = ref();
 
@@ -66,10 +66,11 @@ const todayText = computed(() => String(summary.value?.todayValue ?? 0));
 
 // S8-PRODUCTION-PAGE-TYPE-TITLE-AND-TREND-ALIGN-1:series 扩为 4 类(含本体生产延期)。
 const SERIES_META = [
-	{ key: 'equipmentFault',      name: '设备异常',     color: '#ffc107' },
-	{ key: 'materialFault',       name: '物料异常',     color: '#7bd0ff' },
-	{ key: 'qualityFault',        name: '质量异常',     color: '#ff9d6c' },
-	{ key: 'bodyProductionDelay', name: '本体生产延期', color: '#a78bfa' },
+	{ key: 'equipmentFault',                     name: '设备异常',     color: '#ffc107' },
+	{ key: 'materialFault',                      name: '物料异常',     color: '#7bd0ff' },
+	{ key: 'qualityFault',                       name: '质量异常',     color: '#ff9d6c' },
+	{ key: 'bodyProductionDelay',                name: '本体生产延期', color: '#a78bfa' },
+	{ key: 'productionWorkOrderCompletionDelay', name: '工单完工延期', color: '#fb7185' },
 ] as const;
 
 const chartOption = computed(() => {

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

@@ -44,7 +44,7 @@ const props = defineProps<{
 	title?: string;
 }>();
 
-const title = computed(() => props.title ?? '本月供应异常趋势');
+const title = computed(() => props.title ?? '供应异常趋势');
 
 const chartRef = ref();
 
@@ -77,6 +77,7 @@ const ALL_SERIES_META = [
 	{ key: 'purchaseExecutionDelay',       name: '采购执行延期',       color: '#a78bfa' },
 	{ key: 'materialStockAbnormal',        name: '物料库存异常',       color: '#60a5fa' },
 	{ key: 'inventoryTurnoverAbnormal',    name: '库存周转异常',       color: '#facc15' },
+	{ key: 'purchaseDeliveryAbnormal',     name: '采购到货异常',       color: '#fb7185' },
 ] as const;
 
 type SeriesMeta = typeof ALL_SERIES_META[number];

+ 1 - 1
Web/src/views/aidop/s8/monitoring/useS8PeriodFilter.ts

@@ -2,7 +2,7 @@ import { shallowRef } from 'vue';
 import type { S8Period } from '../api/s8MonitoringApi';
 
 export function useS8PeriodFilter() {
-	const period = shallowRef<S8Period | undefined>('this_month');
+	const period = shallowRef<S8Period | undefined>('this_quarter');
 	const setPeriod = (next: S8Period) => { period.value = next; };
 	return { period, setPeriod };
 }

BIN
doc/S1产销协同_UAT测试报告v1.0.xlsx


BIN
doc/S1产销协同业务需求描述.docx


BIN
doc/S1产销协同模块蓝图设计方案.docx


BIN
doc/S2制造协同业务需求描述.docx


BIN
doc/S2制造协同模块蓝图设计方案.docx


+ 573 - 0
doc/package-lock.json

@@ -0,0 +1,573 @@
+{
+  "name": "doc",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "doc",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "adm-zip": "^0.5.17",
+        "sharp": "^0.34.5"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.0.tgz",
+      "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/adm-zip": {
+      "version": "0.5.17",
+      "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.17.tgz",
+      "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.2",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.2.tgz",
+      "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD",
+      "optional": true
+    }
+  }
+}

+ 16 - 0
doc/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "doc",
+  "version": "1.0.0",
+  "description": "本仓库**主文档根目录为 `doc/`**。请勿在仓库根目录再新建并列的 `docs/` 存放本仓库文档(历史 `docs/` 已并入此处)。",
+  "main": "generate_s1_blueprint_docx.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "adm-zip": "^0.5.17",
+    "sharp": "^0.34.5"
+  }
+}

+ 9 - 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.155</AssemblyVersion>
-    <FileVersion>1.0.155</FileVersion>
-    <Version>1.0.155</Version>
+    <AssemblyVersion>1.0.158</AssemblyVersion>
+    <FileVersion>1.0.158</FileVersion>
+    <Version>1.0.158</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -112,6 +112,12 @@
     <None Update="UpdateScripts\1.0.155.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.157.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.158.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 25 - 243
server/Admin.NET.Web.Entry/UpdateScripts/1.0.154.sql

@@ -1,243 +1,25 @@
--- S4-MDP-PROCUREMENT-EXECUTION-1
--- S4 采购执行专属贴源/标准/DWD 与 mdp_entity 登记(幂等)
--- 采购订单主链仍由 S3 维护;S4 仅补 IQC/发货执行/退货/欠料执行事实。
-
-SET NAMES utf8mb4;
-
-CREATE TABLE IF NOT EXISTS mdp_stg_s4_iqc (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    source_table VARCHAR(100) NOT NULL,
-    source_row_id VARCHAR(100) NOT NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-    raw_data JSON NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_mdp_stg_s4_iqc (tenant_id, source_table, source_row_id),
-    KEY idx_mdp_stg_s4_iqc_batch (sync_batch_id),
-    KEY idx_mdp_stg_s4_iqc_biz (source_biz_key)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 IQC/收货贴源';
-
-CREATE TABLE IF NOT EXISTS mdp_stg_s4_shipment (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    source_table VARCHAR(100) NOT NULL,
-    source_row_id VARCHAR(100) NOT NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-    raw_data JSON NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_mdp_stg_s4_shipment (tenant_id, source_table, source_row_id),
-    KEY idx_mdp_stg_s4_shipment_batch (sync_batch_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 发货执行贴源';
-
-CREATE TABLE IF NOT EXISTS mdp_stg_s4_return (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    source_table VARCHAR(100) NOT NULL,
-    source_row_id VARCHAR(100) NOT NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-    raw_data JSON NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_mdp_stg_s4_return (tenant_id, source_table, source_row_id),
-    KEY idx_mdp_stg_s4_return_batch (sync_batch_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 采购退货贴源';
-
-CREATE TABLE IF NOT EXISTS mdp_stg_s4_shortage (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    source_table VARCHAR(100) NOT NULL,
-    source_row_id VARCHAR(100) NOT NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-    raw_data JSON NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_mdp_stg_s4_shortage (tenant_id, source_table, source_row_id),
-    KEY idx_mdp_stg_s4_shortage_batch (sync_batch_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 欠料执行贴源';
-
-CREATE TABLE IF NOT EXISTS mdp_std_s4_iqc (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    factory_id BIGINT NULL DEFAULT 1,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    po_no VARCHAR(50) NULL,
-    po_line VARCHAR(50) NULL,
-    supplier_code VARCHAR(50) NULL,
-    item_code VARCHAR(50) NULL,
-    receipt_qty DECIMAL(18,6) NULL DEFAULT 0,
-    sample_qty DECIMAL(18,6) NULL DEFAULT 0,
-    defect_qty DECIMAL(18,6) NULL DEFAULT 0,
-    qc_result VARCHAR(20) NULL,
-    receipt_date DATETIME NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_std_s4_iqc (tenant_id, source_biz_key),
-    KEY idx_std_s4_iqc_po (tenant_id, po_no, po_line),
-    KEY idx_std_s4_iqc_supplier (tenant_id, supplier_code)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 IQC标准层';
-
-CREATE TABLE IF NOT EXISTS mdp_std_s4_shipment (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    factory_id BIGINT NULL DEFAULT 1,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    shipment_no VARCHAR(50) NULL,
-    po_no VARCHAR(50) NULL,
-    po_line VARCHAR(50) NULL,
-    supplier_code VARCHAR(50) NULL,
-    item_code VARCHAR(50) NULL,
-    ship_qty DECIMAL(18,6) NULL DEFAULT 0,
-    ship_date DATETIME NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_std_s4_shipment (tenant_id, source_biz_key),
-    KEY idx_std_s4_shipment_po (tenant_id, po_no),
-    KEY idx_std_s4_shipment_supplier (tenant_id, supplier_code)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 发货执行标准层';
-
-CREATE TABLE IF NOT EXISTS mdp_std_s4_return (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    factory_id BIGINT NULL DEFAULT 1,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    po_no VARCHAR(50) NULL,
-    po_line VARCHAR(50) NULL,
-    supplier_code VARCHAR(50) NULL,
-    item_code VARCHAR(50) NULL,
-    return_qty DECIMAL(18,6) NULL DEFAULT 0,
-    return_reason VARCHAR(200) NULL,
-    return_status VARCHAR(50) NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_std_s4_return (tenant_id, source_biz_key),
-    KEY idx_std_s4_return_po (tenant_id, po_no)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 采购退货标准层';
-
-CREATE TABLE IF NOT EXISTS mdp_std_s4_shortage (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL DEFAULT 0,
-    factory_id BIGINT NULL DEFAULT 1,
-    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
-    work_order VARCHAR(100) NULL,
-    supplier_code VARCHAR(50) NULL,
-    item_code VARCHAR(50) NULL,
-    shortage_qty DECIMAL(18,6) NULL DEFAULT 0,
-    risk_level VARCHAR(20) NULL,
-    need_date DATETIME NULL,
-    source_biz_key VARCHAR(200) NULL,
-    sync_batch_id VARCHAR(100) NOT NULL,
-    sync_time DATETIME NOT NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_std_s4_shortage (tenant_id, source_biz_key),
-    KEY idx_std_s4_shortage_supplier (tenant_id, supplier_code),
-    KEY idx_std_s4_shortage_item (tenant_id, item_code)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 欠料执行标准层';
-
-CREATE TABLE IF NOT EXISTS dwd_s4_purchase_execution (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    tenant_id BIGINT NOT NULL,
-    factory_id BIGINT NOT NULL DEFAULT 1,
-    stat_date DATE NOT NULL,
-    po_no VARCHAR(50) NULL,
-    po_line VARCHAR(50) NULL,
-    supplier_code VARCHAR(50) NULL,
-    item_code VARCHAR(50) NULL,
-    order_qty DECIMAL(12,3) NULL DEFAULT 0,
-    delivery_qty DECIMAL(12,3) NULL DEFAULT 0,
-    received_qty DECIMAL(12,3) NULL DEFAULT 0,
-    returned_qty DECIMAL(12,3) NULL DEFAULT 0,
-    shortage_qty DECIMAL(12,3) NULL DEFAULT 0,
-    due_date DATE NULL,
-    actual_arrival_date DATE NULL,
-    risk_level VARCHAR(20) NULL,
-    source_system VARCHAR(20) NULL DEFAULT 'AIDOP',
-    sync_batch_id VARCHAR(100) NULL,
-    sync_time DATETIME NULL,
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_dwd_s4_pe (tenant_id, stat_date, po_no, po_line, item_code),
-    KEY idx_dwd_s4_pe_date (tenant_id, stat_date),
-    KEY idx_dwd_s4_pe_supplier (tenant_id, supplier_code),
-    KEY idx_dwd_s4_pe_po (tenant_id, po_no)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4采购执行宽表';
-
--- 扩展 dwd_po_trans(幂等加列)
-SET @db := DATABASE();
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='factory_id'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN factory_id BIGINT NULL DEFAULT 1 AFTER tenant_id');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='po_line'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN po_line VARCHAR(50) NULL AFTER po_no');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='returned_qty'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN returned_qty DECIMAL(12,3) NULL DEFAULT 0 AFTER received_qty');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='shortage_qty'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN shortage_qty DECIMAL(12,3) NULL DEFAULT 0 AFTER returned_qty');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='due_date'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN due_date DATE NULL AFTER shortage_qty');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='actual_arrival_date'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN actual_arrival_date DATE NULL AFTER due_date');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='risk_level'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN risk_level VARCHAR(20) NULL AFTER actual_arrival_date');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET @sql := IF(
-    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='sync_batch_id'),
-    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN sync_batch_id VARCHAR(100) NULL AFTER source_system');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-INSERT INTO mdp_entity
-(tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name, sync_mode, batch_size, status, remark)
-SELECT 0, s.id, v.entity_code, v.entity_name, 'TABLE', v.source_table_name, v.target_table_name, 'FULL', 5000, 1, v.remark
-FROM mdp_source s
-JOIN (
-    SELECT 'S4_IQC_RECEIPT' AS entity_code, 'S4 IQC收货明细' AS entity_name, 'PurOrdRctDetail' AS source_table_name, 'mdp_stg_s4_iqc' AS target_table_name, 'S4 采购执行 IQC/收货贴源,不重复 S3 采购订单主链' AS remark
-    UNION ALL SELECT 'S4_SHIPMENT_EXEC', 'S4 发货执行明细', 'scm_shdzb', 'mdp_stg_s4_shipment', 'S4 供应商发货单执行贴源'
-    UNION ALL SELECT 'S4_RETURN_EXEC', 'S4 采购退货行', 'srm_polist_ds', 'mdp_stg_s4_return', 'S4 交货计划退货数量贴源'
-    UNION ALL SELECT 'S4_SHORTAGE_EXEC', 'S4 欠料执行', 'dwd_material_shortage', 'mdp_stg_s4_shortage', 'S4 消费 S3 缺料 DWD 快照,不新增第三套工单贴源'
-) v ON 1=1
-WHERE s.tenant_id = 0 AND s.source_code = 'AIDOPDEV_MYSQL'
-ON DUPLICATE KEY UPDATE
-    source_id = VALUES(source_id),
-    entity_name = VALUES(entity_name),
-    source_table_name = VALUES(source_table_name),
-    target_table_name = VALUES(target_table_name),
-    remark = VALUES(remark),
-    update_time = CURRENT_TIMESTAMP;
+-- S0-PRIORITYCODE-CUSTOMER-ORDER-TYPE-CONFIG-1
+-- 为 PriorityCode 表增加 客户类型/订单类型 两个可空编码列(字典 Value)。
+-- 幂等:用 information_schema 守卫,列已存在则跳过;不更新现有数据、不写默认值、不做破坏性操作。
+
+SET @ddl_customer := (SELECT IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS
+           WHERE TABLE_SCHEMA = DATABASE()
+             AND TABLE_NAME = 'PriorityCode'
+             AND COLUMN_NAME = 'CustomerTypeCode'),
+    'SELECT 1',
+    'ALTER TABLE `PriorityCode` ADD COLUMN `CustomerTypeCode` varchar(50) NULL COMMENT ''客户类型编码(字典 s0_order_priority_customer_type 的 Value)'''));
+PREPARE stmt_customer FROM @ddl_customer;
+EXECUTE stmt_customer;
+DEALLOCATE PREPARE stmt_customer;
+
+SET @ddl_order := (SELECT IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS
+           WHERE TABLE_SCHEMA = DATABASE()
+             AND TABLE_NAME = 'PriorityCode'
+             AND COLUMN_NAME = 'OrderTypeCode'),
+    'SELECT 1',
+    'ALTER TABLE `PriorityCode` ADD COLUMN `OrderTypeCode` varchar(50) NULL COMMENT ''订单类型编码(字典 s0_order_type 的 Value)'''));
+PREPARE stmt_order FROM @ddl_order;
+EXECUTE stmt_order;
+DEALLOCATE PREPARE stmt_order;

+ 76 - 19
server/Admin.NET.Web.Entry/UpdateScripts/1.0.155.sql

@@ -1,21 +1,78 @@
--- SYS-TENANT-CONNECTION-LENGTH-1
--- 放大 SysTenant.Connection,避免 ID 隔离租户写入主库连接串时超过 varchar(256)。
+-- ──────────────────────────────────────────────────────────────────────
+-- S0-CONTRACT-REVIEW-CYCLE-PI-CONFIG-SYNC-1
+-- 1. 主表 S0ContractReviewCycle 现有 10 行 stage_code 从 CR1-CR5 → 语义编码
+--    (opinion_review/feedback/second_review/leader_opinion/sign)。
+--    并在 std_hours=0 时填初值(8/12/8/10/2),不覆盖用户已配置的非零值。
+-- 2. 新建 S0ContractReviewCycleBreakdown 下钻配置表。
+-- 3. 新建 S0ContractReviewCycleSyncFlag 工厂级同步开关表。
+-- 4. seed 意见反馈下钻 6 行(3 组 × 2 现有工厂)。
+--
+-- 幂等:
+--   - UPDATE WHERE 子句迁移后不匹配 → 第二次执行空操作;
+--   - std_hours 附加 AND std_hours=0 守卫,保留用户后续编辑;
+--   - CREATE TABLE IF NOT EXISTS;
+--   - INSERT IGNORE 跳过 (factory, parent_stage, group) 唯一冲突。
+-- ──────────────────────────────────────────────────────────────────────
 
-SET NAMES utf8mb4;
-SET @db := DATABASE();
+-- 1a. PI 初值填入(仅当 std_hours=0 时;保留用户已配置的非零值)
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 8  WHERE `stage_code` = 'CR1' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 12 WHERE `stage_code` = 'CR2' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 8  WHERE `stage_code` = 'CR3' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 10 WHERE `stage_code` = 'CR4' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 2  WHERE `stage_code` = 'CR5' AND `std_hours` = 0;
 
-SET @sql := IF(
-    EXISTS(
-        SELECT 1
-        FROM information_schema.COLUMNS
-        WHERE TABLE_SCHEMA = @db
-          AND LOWER(TABLE_NAME) = LOWER('SysTenant')
-          AND COLUMN_NAME = 'Connection'
-          AND DATA_TYPE <> 'text'
-    ),
-    'ALTER TABLE `SysTenant` MODIFY COLUMN `Connection` TEXT NULL COMMENT ''数据库连接''',
-    'SELECT 1'
-);
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
+-- 1b. stage_code 语义化迁移(无条件,迁移后再跑空操作)
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'opinion_review' WHERE `stage_code` = 'CR1';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'feedback'       WHERE `stage_code` = 'CR2';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'second_review'  WHERE `stage_code` = 'CR3';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'leader_opinion' WHERE `stage_code` = 'CR4';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'sign'           WHERE `stage_code` = 'CR5';
+
+-- 2a. 下钻配置表
+CREATE TABLE IF NOT EXISTS `S0ContractReviewCycleBreakdown` (
+  `rec_id`            BIGINT       NOT NULL AUTO_INCREMENT,
+  `company_ref_id`    BIGINT       NOT NULL,
+  `factory_ref_id`    BIGINT       NOT NULL,
+  `domain_code`       VARCHAR(50)  NULL,
+  `parent_stage_code` VARCHAR(50)  NOT NULL,
+  `group_code`        VARCHAR(50)  NOT NULL,
+  `group_name`        VARCHAR(200) NOT NULL,
+  `std_hours`         DECIMAL(6,2) NOT NULL DEFAULT 0,
+  `order_no`          INT          NOT NULL DEFAULT 1,
+  `is_active`         TINYINT(1)   NOT NULL DEFAULT 1,
+  `create_user`       VARCHAR(100) NULL,
+  `create_time`       DATETIME     NOT NULL,
+  `update_user`       VARCHAR(100) NULL,
+  `update_time`       DATETIME     NULL,
+  PRIMARY KEY (`rec_id`),
+  UNIQUE KEY `uk_S0CRCBreakdown_factory_parent_group` (`factory_ref_id`, `parent_stage_code`, `group_code`),
+  KEY `idx_S0CRCBreakdown_factory_parent` (`factory_ref_id`, `parent_stage_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同评审周期下钻配置(按 parent_stage_code 分组)';
+
+-- 2b. 工厂级同步开关表
+CREATE TABLE IF NOT EXISTS `S0ContractReviewCycleSyncFlag` (
+  `rec_id`          BIGINT       NOT NULL AUTO_INCREMENT,
+  `company_ref_id`  BIGINT       NOT NULL,
+  `factory_ref_id`  BIGINT       NOT NULL,
+  `domain_code`     VARCHAR(50)  NULL,
+  `is_sync_enabled` TINYINT(1)   NOT NULL DEFAULT 0,
+  `create_user`     VARCHAR(100) NULL,
+  `create_time`     DATETIME     NOT NULL,
+  `update_user`     VARCHAR(100) NULL,
+  `update_time`     DATETIME     NULL,
+  PRIMARY KEY (`rec_id`),
+  UNIQUE KEY `uk_S0CRCSyncFlag_factory` (`factory_ref_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同评审周期同步开关(工厂级一行)';
+
+-- 3. 意见反馈下钻 seed(3 组 × 2 现有工厂 = 6 行),INSERT IGNORE 跳过唯一冲突
+INSERT IGNORE INTO `S0ContractReviewCycleBreakdown` (
+  `company_ref_id`, `factory_ref_id`, `domain_code`, `parent_stage_code`,
+  `group_code`, `group_name`, `std_hours`, `order_no`, `is_active`,
+  `create_user`, `create_time`
+) VALUES
+  (1329900200001, 1329900200002, NULL, 'feedback', 'market',        '市场部',     4.80, 1, 1, 'system', NOW()),
+  (1329900200001, 1329900200002, NULL, 'feedback', 'tech_presales', '技术售前组', 4.20, 2, 1, 'system', NOW()),
+  (1329900200001, 1329900200002, NULL, 'feedback', 'planning',      '综合计划',   3.00, 3, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'market',        '市场部',     4.80, 1, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'tech_presales', '技术售前组', 4.20, 2, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'planning',      '综合计划',   3.00, 3, 1, 'system', NOW());

+ 45 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.157.sql

@@ -0,0 +1,45 @@
+-- ──────────────────────────────────────────────────────────────────────
+-- S0-PRODUCT-DESIGN-CYCLE-ITEMMASTER-TYPE-LINK-1
+-- 字典 s0_material_type 与 ItemMaster.ItemType 真实 RS 编码对齐。
+--
+-- 1) 禁用旧语义 6 项(raw/semi/finished/aux/fixture/packaging)— 仅当 Status=1 时
+--    UPDATE,保留行不物理删除;不覆盖人工恢复。
+-- 2) 插入新 9 项 RS 编码(RS11/12/13/14/15/30/50/60/99),label = code(无正式中文映射)。
+--
+-- 幂等:
+--   - 旧码 UPDATE WHERE Status=1 → 第二次执行命中 0 行(Status 已 0)→ 空操作;
+--   - 新码 INSERT IGNORE 走 (DictTypeId, Value) UNIQUE 索引(index_SysDictData_TV)防重;
+--   - 仅作用于 s0_material_type 字典,不影响 s0_owner_application 或其他字典类型;
+--   - 不使用 DROP / TRUNCATE / 无条件 DELETE;不更新 ProductDesignCycle / ItemMaster 业务数据。
+-- ──────────────────────────────────────────────────────────────────────
+
+-- 1. 禁用旧 6 项(仅 Status=1 时)
+UPDATE `SysDictData`
+SET    `Status` = 0,
+       `UpdateTime` = NOW()
+WHERE  `DictTypeId` = (SELECT `Id` FROM `SysDictType` WHERE `Code` = 's0_material_type')
+  AND  `Value` IN ('raw', 'semi', 'finished', 'aux', 'fixture', 'packaging')
+  AND  `Status` = 1;
+
+-- 2. 插入新 9 项 RS 编码(label=code;走唯一索引幂等)
+INSERT IGNORE INTO `SysDictData`
+  (`Id`, `DictTypeId`, `Label`, `Value`, `OrderNo`, `Status`, `CreateTime`)
+SELECT
+  1329900100100 + v.ord_offset                                        AS `Id`,
+  (SELECT `Id` FROM `SysDictType` WHERE `Code` = 's0_material_type')   AS `DictTypeId`,
+  v.code                                                              AS `Label`,
+  v.code                                                              AS `Value`,
+  100 + v.ord_offset                                                  AS `OrderNo`,
+  1                                                                   AS `Status`,
+  NOW()                                                               AS `CreateTime`
+FROM (
+  SELECT 'RS11' AS code, 0 AS ord_offset UNION ALL
+  SELECT 'RS12', 1 UNION ALL
+  SELECT 'RS13', 2 UNION ALL
+  SELECT 'RS14', 3 UNION ALL
+  SELECT 'RS15', 4 UNION ALL
+  SELECT 'RS30', 5 UNION ALL
+  SELECT 'RS50', 6 UNION ALL
+  SELECT 'RS60', 7 UNION ALL
+  SELECT 'RS99', 8
+) v;

+ 266 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.158.sql

@@ -0,0 +1,266 @@
+-- S4-MDP-PROCUREMENT-EXECUTION-1
+-- S4 采购执行专属贴源/标准/DWD 与 mdp_entity 登记(幂等)
+-- 采购订单主链仍由 S3 维护;S4 仅补 IQC/发货执行/退货/欠料执行事实。
+
+SET NAMES utf8mb4;
+
+CREATE TABLE IF NOT EXISTS mdp_stg_s4_iqc (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    source_table VARCHAR(100) NOT NULL,
+    source_row_id VARCHAR(100) NOT NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+    raw_data JSON NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_stg_s4_iqc (tenant_id, source_table, source_row_id),
+    KEY idx_mdp_stg_s4_iqc_batch (sync_batch_id),
+    KEY idx_mdp_stg_s4_iqc_biz (source_biz_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 IQC/收货贴源';
+
+CREATE TABLE IF NOT EXISTS mdp_stg_s4_shipment (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    source_table VARCHAR(100) NOT NULL,
+    source_row_id VARCHAR(100) NOT NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+    raw_data JSON NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_stg_s4_shipment (tenant_id, source_table, source_row_id),
+    KEY idx_mdp_stg_s4_shipment_batch (sync_batch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 发货执行贴源';
+
+CREATE TABLE IF NOT EXISTS mdp_stg_s4_return (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    source_table VARCHAR(100) NOT NULL,
+    source_row_id VARCHAR(100) NOT NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+    raw_data JSON NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_stg_s4_return (tenant_id, source_table, source_row_id),
+    KEY idx_mdp_stg_s4_return_batch (sync_batch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 采购退货贴源';
+
+CREATE TABLE IF NOT EXISTS mdp_stg_s4_shortage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    source_table VARCHAR(100) NOT NULL,
+    source_row_id VARCHAR(100) NOT NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+    raw_data JSON NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_mdp_stg_s4_shortage (tenant_id, source_table, source_row_id),
+    KEY idx_mdp_stg_s4_shortage_batch (sync_batch_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 欠料执行贴源';
+
+CREATE TABLE IF NOT EXISTS mdp_std_s4_iqc (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    factory_id BIGINT NULL DEFAULT 1,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    po_no VARCHAR(50) NULL,
+    po_line VARCHAR(50) NULL,
+    supplier_code VARCHAR(50) NULL,
+    item_code VARCHAR(50) NULL,
+    receipt_qty DECIMAL(18,6) NULL DEFAULT 0,
+    sample_qty DECIMAL(18,6) NULL DEFAULT 0,
+    defect_qty DECIMAL(18,6) NULL DEFAULT 0,
+    qc_result VARCHAR(20) NULL,
+    receipt_date DATETIME NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_std_s4_iqc (tenant_id, source_biz_key),
+    KEY idx_std_s4_iqc_po (tenant_id, po_no, po_line),
+    KEY idx_std_s4_iqc_supplier (tenant_id, supplier_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 IQC标准层';
+
+CREATE TABLE IF NOT EXISTS mdp_std_s4_shipment (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    factory_id BIGINT NULL DEFAULT 1,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    shipment_no VARCHAR(50) NULL,
+    po_no VARCHAR(50) NULL,
+    po_line VARCHAR(50) NULL,
+    supplier_code VARCHAR(50) NULL,
+    item_code VARCHAR(50) NULL,
+    ship_qty DECIMAL(18,6) NULL DEFAULT 0,
+    ship_date DATETIME NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_std_s4_shipment (tenant_id, source_biz_key),
+    KEY idx_std_s4_shipment_po (tenant_id, po_no),
+    KEY idx_std_s4_shipment_supplier (tenant_id, supplier_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 发货执行标准层';
+
+CREATE TABLE IF NOT EXISTS mdp_std_s4_return (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    factory_id BIGINT NULL DEFAULT 1,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    po_no VARCHAR(50) NULL,
+    po_line VARCHAR(50) NULL,
+    supplier_code VARCHAR(50) NULL,
+    item_code VARCHAR(50) NULL,
+    return_qty DECIMAL(18,6) NULL DEFAULT 0,
+    return_reason VARCHAR(200) NULL,
+    return_status VARCHAR(50) NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_std_s4_return (tenant_id, source_biz_key),
+    KEY idx_std_s4_return_po (tenant_id, po_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 采购退货标准层';
+
+CREATE TABLE IF NOT EXISTS mdp_std_s4_shortage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL DEFAULT 0,
+    factory_id BIGINT NULL DEFAULT 1,
+    source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+    work_order VARCHAR(100) NULL,
+    supplier_code VARCHAR(50) NULL,
+    item_code VARCHAR(50) NULL,
+    shortage_qty DECIMAL(18,6) NULL DEFAULT 0,
+    risk_level VARCHAR(20) NULL,
+    need_date DATETIME NULL,
+    source_biz_key VARCHAR(200) NULL,
+    sync_batch_id VARCHAR(100) NOT NULL,
+    sync_time DATETIME NOT NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_std_s4_shortage (tenant_id, source_biz_key),
+    KEY idx_std_s4_shortage_supplier (tenant_id, supplier_code),
+    KEY idx_std_s4_shortage_item (tenant_id, item_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4 欠料执行标准层';
+
+CREATE TABLE IF NOT EXISTS dwd_s4_purchase_execution (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tenant_id BIGINT NOT NULL,
+    factory_id BIGINT NOT NULL DEFAULT 1,
+    stat_date DATE NOT NULL,
+    po_no VARCHAR(50) NULL,
+    po_line VARCHAR(50) NULL,
+    supplier_code VARCHAR(50) NULL,
+    item_code VARCHAR(50) NULL,
+    order_qty DECIMAL(12,3) NULL DEFAULT 0,
+    delivery_qty DECIMAL(12,3) NULL DEFAULT 0,
+    received_qty DECIMAL(12,3) NULL DEFAULT 0,
+    returned_qty DECIMAL(12,3) NULL DEFAULT 0,
+    shortage_qty DECIMAL(12,3) NULL DEFAULT 0,
+    due_date DATE NULL,
+    actual_arrival_date DATE NULL,
+    risk_level VARCHAR(20) NULL,
+    source_system VARCHAR(20) NULL DEFAULT 'AIDOP',
+    sync_batch_id VARCHAR(100) NULL,
+    sync_time DATETIME NULL,
+    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_dwd_s4_pe (tenant_id, stat_date, po_no, po_line, item_code),
+    KEY idx_dwd_s4_pe_date (tenant_id, stat_date),
+    KEY idx_dwd_s4_pe_supplier (tenant_id, supplier_code),
+    KEY idx_dwd_s4_pe_po (tenant_id, po_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S4采购执行宽表';
+
+-- 扩展 dwd_po_trans(幂等加列)
+SET @db := DATABASE();
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='factory_id'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN factory_id BIGINT NULL DEFAULT 1 AFTER tenant_id');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='po_line'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN po_line VARCHAR(50) NULL AFTER po_no');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='returned_qty'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN returned_qty DECIMAL(12,3) NULL DEFAULT 0 AFTER received_qty');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='shortage_qty'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN shortage_qty DECIMAL(12,3) NULL DEFAULT 0 AFTER returned_qty');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='due_date'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN due_date DATE NULL AFTER shortage_qty');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='actual_arrival_date'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN actual_arrival_date DATE NULL AFTER due_date');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='risk_level'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN risk_level VARCHAR(20) NULL AFTER actual_arrival_date');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF(
+    EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='dwd_po_trans' AND COLUMN_NAME='sync_batch_id'),
+    'SELECT 1', 'ALTER TABLE dwd_po_trans ADD COLUMN sync_batch_id VARCHAR(100) NULL AFTER source_system');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+INSERT INTO mdp_entity
+(tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name, sync_mode, batch_size, status, remark)
+SELECT 0, s.id, v.entity_code, v.entity_name, 'TABLE', v.source_table_name, v.target_table_name, 'FULL', 5000, 1, v.remark
+FROM mdp_source s
+JOIN (
+    SELECT 'S4_IQC_RECEIPT' AS entity_code, 'S4 IQC收货明细' AS entity_name, 'PurOrdRctDetail' AS source_table_name, 'mdp_stg_s4_iqc' AS target_table_name, 'S4 采购执行 IQC/收货贴源,不重复 S3 采购订单主链' AS remark
+    UNION ALL SELECT 'S4_SHIPMENT_EXEC', 'S4 发货执行明细', 'scm_shdzb', 'mdp_stg_s4_shipment', 'S4 供应商发货单执行贴源'
+    UNION ALL SELECT 'S4_RETURN_EXEC', 'S4 采购退货行', 'srm_polist_ds', 'mdp_stg_s4_return', 'S4 交货计划退货数量贴源'
+    UNION ALL SELECT 'S4_SHORTAGE_EXEC', 'S4 欠料执行', 'dwd_material_shortage', 'mdp_stg_s4_shortage', 'S4 消费 S3 缺料 DWD 快照,不新增第三套工单贴源'
+) v ON 1=1
+WHERE s.tenant_id = 0 AND s.source_code = 'AIDOPDEV_MYSQL'
+ON DUPLICATE KEY UPDATE
+    source_id = VALUES(source_id),
+    entity_name = VALUES(entity_name),
+    source_table_name = VALUES(source_table_name),
+    target_table_name = VALUES(target_table_name),
+    remark = VALUES(remark),
+    update_time = CURRENT_TIMESTAMP;
+
+-- ----------------------------------------------------------------------
+-- SYS-TENANT-CONNECTION-LENGTH-1
+-- 放大 SysTenant.Connection,避免 ID 隔离租户写入主库连接串时超过 varchar(256)。
+
+SET NAMES utf8mb4;
+SET @db := DATABASE();
+
+SET @sql := IF(
+    EXISTS(
+        SELECT 1
+        FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = @db
+          AND LOWER(TABLE_NAME) = LOWER('SysTenant')
+          AND COLUMN_NAME = 'Connection'
+          AND DATA_TYPE <> 'text'
+    ),
+    'ALTER TABLE `SysTenant` MODIFY COLUMN `Connection` TEXT NULL COMMENT ''数据库连接''',
+    'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 121 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleBreakdownsController.cs

@@ -0,0 +1,121 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// 合同评审周期下钻配置 CRUD(按 parent_stage_code + group_code 分组)。
+/// 本批仅维护 parent_stage_code='feedback'(意见反馈)下的部门/组 PI 标准。
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/contract-review-cycle-breakdowns")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0ContractReviewCycleBreakdownsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> _rep;
+
+    public AdoS0ContractReviewCycleBreakdownsController(SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> rep)
+        => _rep = rep;
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ContractReviewCycleBreakdownQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.ParentStageCode), x => x.ParentStageCode == q.ParentStageCode)
+            .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value);
+
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.OrderNo).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? NotFound() : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0ContractReviewCycleBreakdownUpsertDto dto)
+    {
+        if (await _rep.IsAnyAsync(x =>
+                x.FactoryRefId == dto.FactoryRefId &&
+                x.ParentStageCode == dto.ParentStageCode &&
+                x.GroupCode == dto.GroupCode))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂此父阶段下的组编码已存在");
+
+        var entity = new AdoS0ContractReviewCycleBreakdown
+        {
+            CompanyRefId = dto.CompanyRefId,
+            FactoryRefId = dto.FactoryRefId,
+            DomainCode = dto.DomainCode,
+            ParentStageCode = dto.ParentStageCode,
+            GroupCode = dto.GroupCode,
+            GroupName = dto.GroupName,
+            StdHours = dto.StdHours,
+            OrderNo = dto.OrderNo,
+            IsActive = dto.IsActive,
+            CreateUser = dto.CreateUser,
+            CreateTime = DateTime.Now,
+        };
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ContractReviewCycleBreakdownUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        if (await _rep.IsAnyAsync(x =>
+                x.Id != id &&
+                x.FactoryRefId == dto.FactoryRefId &&
+                x.ParentStageCode == dto.ParentStageCode &&
+                x.GroupCode == dto.GroupCode))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂此父阶段下的组编码已存在");
+
+        entity.CompanyRefId = dto.CompanyRefId;
+        entity.FactoryRefId = dto.FactoryRefId;
+        entity.DomainCode = dto.DomainCode;
+        entity.ParentStageCode = dto.ParentStageCode;
+        entity.GroupCode = dto.GroupCode;
+        entity.GroupName = dto.GroupName;
+        entity.StdHours = dto.StdHours;
+        entity.OrderNo = dto.OrderNo;
+        entity.IsActive = dto.IsActive;
+        entity.UpdateUser = dto.UpdateUser;
+        entity.UpdateTime = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleActiveAsync(long id, [FromBody] AdoS0ContractReviewCycleBreakdownToggleActiveDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        entity.IsActive = dto.IsActive;
+        entity.UpdateTime = DateTime.Now;
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return NotFound();
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+}

+ 89 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleSyncFlagsController.cs

@@ -0,0 +1,89 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// 合同评审周期同步开关:工厂级一行。
+/// GET 行不存在时返回 IsSyncEnabled=false 兜底;PUT 走 upsert。
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/contract-review-cycle-sync-flag")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0ContractReviewCycleSyncFlagsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> _rep;
+
+    public AdoS0ContractReviewCycleSyncFlagsController(SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> rep)
+        => _rep = rep;
+
+    [HttpGet]
+    public async Task<IActionResult> GetByFactoryAsync([FromQuery] long factoryRefId)
+    {
+        if (factoryRefId <= 0) return BadRequest(new { message = "factoryRefId 不能为空" });
+
+        var entity = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId)
+            .FirstAsync();
+
+        if (entity == null)
+        {
+            return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+            {
+                FactoryRefId = factoryRefId,
+                IsSyncEnabled = false,
+            });
+        }
+
+        return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+        {
+            Id = entity.Id,
+            CompanyRefId = entity.CompanyRefId,
+            FactoryRefId = entity.FactoryRefId,
+            DomainCode = entity.DomainCode,
+            IsSyncEnabled = entity.IsSyncEnabled,
+        });
+    }
+
+    [HttpPut]
+    public async Task<IActionResult> UpsertAsync([FromBody] AdoS0ContractReviewCycleSyncFlagUpsertDto dto)
+    {
+        var now = DateTime.Now;
+        var entity = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == dto.FactoryRefId)
+            .FirstAsync();
+
+        if (entity == null)
+        {
+            entity = new AdoS0ContractReviewCycleSyncFlag
+            {
+                CompanyRefId = dto.CompanyRefId,
+                FactoryRefId = dto.FactoryRefId,
+                DomainCode = dto.DomainCode,
+                IsSyncEnabled = dto.IsSyncEnabled,
+                CreateUser = dto.CreateUser,
+                CreateTime = now,
+            };
+            await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        }
+        else
+        {
+            entity.CompanyRefId = dto.CompanyRefId;
+            entity.DomainCode = dto.DomainCode;
+            entity.IsSyncEnabled = dto.IsSyncEnabled;
+            entity.UpdateUser = dto.UpdateUser;
+            entity.UpdateTime = now;
+            await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        }
+
+        return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+        {
+            Id = entity.Id,
+            CompanyRefId = entity.CompanyRefId,
+            FactoryRefId = entity.FactoryRefId,
+            DomainCode = entity.DomainCode,
+            IsSyncEnabled = entity.IsSyncEnabled,
+        });
+    }
+}

+ 60 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCyclesController.cs

@@ -11,9 +11,18 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
 public class AdoS0ContractReviewCyclesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0ContractReviewCycle> _rep;
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> _syncRep;
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> _breakdownRep;
 
-    public AdoS0ContractReviewCyclesController(SqlSugarRepository<AdoS0ContractReviewCycle> rep)
-        => _rep = rep;
+    public AdoS0ContractReviewCyclesController(
+        SqlSugarRepository<AdoS0ContractReviewCycle> rep,
+        SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> syncRep,
+        SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> breakdownRep)
+    {
+        _rep = rep;
+        _syncRep = syncRep;
+        _breakdownRep = breakdownRep;
+    }
 
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ContractReviewCycleQueryDto q)
@@ -104,4 +113,53 @@ public class AdoS0ContractReviewCyclesController : ControllerBase
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });
     }
+
+    /// <summary>
+    /// PI 只读配置:合并主阶段 + 意见反馈下钻,仅当工厂同步开关开启时返回数据。
+    /// 为 S8 后续接入预留契约(S8-ORDER-REVIEW-PI-FROM-S0-1),本批不消费。
+    /// </summary>
+    [HttpGet("pi-config")]
+    public async Task<IActionResult> GetPiConfigAsync([FromQuery] long factoryRefId)
+    {
+        if (factoryRefId <= 0) return BadRequest(new { message = "factoryRefId 不能为空" });
+
+        var syncFlag = await _syncRep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId)
+            .FirstAsync();
+        var syncEnabled = syncFlag?.IsSyncEnabled ?? false;
+
+        var result = new AdoS0ContractReviewCyclePiConfigDto
+        {
+            FactoryRefId = factoryRefId,
+            SyncEnabled = syncEnabled,
+        };
+
+        if (!syncEnabled) return Ok(result);
+
+        var stages = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId && x.IsActive)
+            .OrderBy(x => x.OrderNo)
+            .ToListAsync();
+        result.MainStages = stages.Select(x => new AdoS0ContractReviewCyclePiConfigStageDto
+        {
+            StageCode = x.StageCode,
+            StageName = x.StageName,
+            StdHours = x.StdHours,
+            OrderNo = x.OrderNo,
+        }).ToList();
+
+        var breakdowns = await _breakdownRep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId && x.ParentStageCode == "feedback" && x.IsActive)
+            .OrderBy(x => x.OrderNo)
+            .ToListAsync();
+        result.OpinionFeedbackBreakdown = breakdowns.Select(x => new AdoS0ContractReviewCyclePiConfigBreakdownDto
+        {
+            GroupCode = x.GroupCode,
+            GroupName = x.GroupName,
+            StdHours = x.StdHours,
+            OrderNo = x.OrderNo,
+        }).ToList();
+
+        return Ok(result);
+    }
 }

+ 4 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs

@@ -80,6 +80,8 @@ public class AdoS0OrderPriorityRulesController : ControllerBase
             SourceId = dto.SourceId,
             ValueType = dto.ValueType,
             ValueId = dto.ValueId,
+            CustomerTypeCode = dto.CustomerTypeCode,
+            OrderTypeCode = dto.OrderTypeCode,
             IsActive = dto.IsActive,
             CreateUser = dto.CreateUser,
             LegacyCreateUser = dto.LegacyCreateUser,
@@ -109,6 +111,8 @@ public class AdoS0OrderPriorityRulesController : ControllerBase
         entity.SourceId = dto.SourceId;
         entity.ValueType = dto.ValueType;
         entity.ValueId = dto.ValueId;
+        entity.CustomerTypeCode = dto.CustomerTypeCode;
+        entity.OrderTypeCode = dto.OrderTypeCode;
         entity.IsActive = dto.IsActive;
         entity.UpdateUser = dto.UpdateUser;
         entity.LegacyCreateUser = dto.LegacyCreateUser;

+ 83 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ProductDesignCyclesController.cs

@@ -11,9 +11,15 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
 public class AdoS0ProductDesignCyclesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0ProductDesignCycle> _rep;
+    private readonly SqlSugarRepository<AdoS0ItemMaster> _itemMasterRep;
 
-    public AdoS0ProductDesignCyclesController(SqlSugarRepository<AdoS0ProductDesignCycle> rep)
-        => _rep = rep;
+    public AdoS0ProductDesignCyclesController(
+        SqlSugarRepository<AdoS0ProductDesignCycle> rep,
+        SqlSugarRepository<AdoS0ItemMaster> itemMasterRep)
+    {
+        _rep = rep;
+        _itemMasterRep = itemMasterRep;
+    }
 
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ProductDesignCycleQueryDto q)
@@ -29,10 +35,84 @@ public class AdoS0ProductDesignCyclesController : ControllerBase
             .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value);
 
         var total = await query.CountAsync();
-        var list = await query.OrderByDescending(x => x.CreateTime).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        var entities = await query
+            .OrderByDescending(x => x.CreateTime)
+            .Skip((q.Page - 1) * q.PageSize)
+            .Take(q.PageSize)
+            .ToListAsync();
+
+        var list = await BuildRowsWithItemMasterMatchAsync(entities);
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
 
+    /// <summary>
+    /// 在 PDC 实体基础上附带 ItemMaster 命中校验信息。
+    /// 统计口径:
+    ///   - ItemMaster.ItemType = PDC.item_type;
+    ///   - PDC.domain_code 有值 → ItemMaster.Domain = domain_code;为 NULL → 不限 Domain;
+    ///   - OwnerApplication / Status 均不参与过滤。
+    /// 实现:2 次查询(PDC 主列表上游已查 + ItemMaster 按 ItemType IN 一次性拉取相关列),
+    /// 内存按 (ItemType, Domain) 分组计算 count / top3,避免 N+1。
+    /// </summary>
+    private async Task<List<AdoS0ProductDesignCycleRowDto>> BuildRowsWithItemMasterMatchAsync(
+        List<AdoS0ProductDesignCycle> entities)
+    {
+        if (entities.Count == 0) return new List<AdoS0ProductDesignCycleRowDto>();
+
+        var distinctItemTypes = entities
+            .Select(e => e.ItemType)
+            .Where(t => !string.IsNullOrWhiteSpace(t))
+            .Distinct()
+            .ToList();
+
+        // 候选 ItemMaster 行(仅当前页相关 ItemType);空集合不发起查询。
+        var candidates = distinctItemTypes.Count == 0
+            ? new List<AdoS0ItemMaster>()
+            : await _itemMasterRep.AsQueryable()
+                .Where(x => distinctItemTypes.Contains(x.ItemType!))
+                .Select(x => new AdoS0ItemMaster
+                {
+                    Id = x.Id,
+                    ItemType = x.ItemType,
+                    DomainCode = x.DomainCode,
+                    ItemNum = x.ItemNum,
+                    Descr = x.Descr,
+                })
+                .ToListAsync();
+
+        return entities.Select(e =>
+        {
+            var matched = candidates.Where(c =>
+                c.ItemType == e.ItemType &&
+                (string.IsNullOrWhiteSpace(e.DomainCode) || c.DomainCode == e.DomainCode))
+                .ToList();
+
+            var samples = matched
+                .OrderBy(c => c.Id)
+                .Take(3)
+                .Select(c => string.IsNullOrWhiteSpace(c.Descr) ? c.ItemNum : $"{c.ItemNum} / {c.Descr}")
+                .ToList();
+
+            return new AdoS0ProductDesignCycleRowDto
+            {
+                Id = e.Id,
+                CompanyRefId = e.CompanyRefId,
+                FactoryRefId = e.FactoryRefId,
+                DomainCode = e.DomainCode,
+                ItemType = e.ItemType,
+                OwnerApplication = e.OwnerApplication,
+                StdHours = e.StdHours,
+                IsActive = e.IsActive,
+                CreateUser = e.CreateUser,
+                CreateTime = e.CreateTime,
+                UpdateUser = e.UpdateUser,
+                UpdateTime = e.UpdateTime,
+                ItemMasterMatchedCount = matched.Count,
+                ItemMasterSampleItems = samples,
+            };
+        }).ToList();
+    }
+
     [HttpGet("{id:long}")]
     public async Task<IActionResult> GetAsync(long id)
     {

+ 129 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs

@@ -259,6 +259,10 @@ public class AdoS0PriorityCodeUpsertDto
     public string? SourceId { get; set; }
     public string? ValueType { get; set; }
     public string? ValueId { get; set; }
+    /// <summary>客户类型编码(字典 s0_order_priority_customer_type 的 Value),可空</summary>
+    public string? CustomerTypeCode { get; set; }
+    /// <summary>订单类型编码(字典 s0_order_type 的 Value),可空</summary>
+    public string? OrderTypeCode { get; set; }
     public bool IsActive { get; set; } = true;
     public string? CreateUser { get; set; }
     public string? LegacyCreateUser { get; set; }
@@ -350,6 +354,32 @@ public class AdoS0ProductDesignCycleToggleActiveDto
     public bool IsActive { get; set; }
 }
 
+/// <summary>
+/// 产品设计周期列表行响应(Entity 字段 + ItemMaster 命中校验字段)。
+/// 不影响 Upsert / Query / ToggleActive DTO,仅 GetPagedAsync 列表返回使用。
+/// </summary>
+public class AdoS0ProductDesignCycleRowDto
+{
+    public long Id { get; set; }
+    public long CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string? DomainCode { get; set; }
+    public string ItemType { get; set; } = string.Empty;
+    public string OwnerApplication { get; set; } = string.Empty;
+    public int StdHours { get; set; }
+    public bool IsActive { get; set; }
+    public string? CreateUser { get; set; }
+    public DateTime CreateTime { get; set; }
+    public string? UpdateUser { get; set; }
+    public DateTime? UpdateTime { get; set; }
+
+    /// <summary>该 (ItemType[, Domain]) 组合在 ItemMaster 命中的物料行数。</summary>
+    public int ItemMasterMatchedCount { get; set; }
+
+    /// <summary>前 3 个示例物料,格式 "ItemNum / Descr";无 Descr 时仅 ItemNum;无命中时空列表。</summary>
+    public List<string> ItemMasterSampleItems { get; set; } = new();
+}
+
 // ── B3: 订单评审周期标准 ──────────────────────────────────────────────
 
 public class AdoS0OrderReviewCycleQueryDto
@@ -389,3 +419,102 @@ public class AdoS0OrderReviewCycleToggleActiveDto
 {
     public bool IsActive { get; set; }
 }
+
+// ── B1.1: 合同评审周期下钻配置 ────────────────────────────────────────
+
+public class AdoS0ContractReviewCycleBreakdownQueryDto
+{
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
+    public string? DomainCode { get; set; }
+    /// <summary>父阶段编码(如 'feedback' 意见反馈)</summary>
+    public string? ParentStageCode { get; set; }
+    public bool? IsActive { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0ContractReviewCycleBreakdownUpsertDto
+{
+    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    public long CompanyRefId { get; set; }
+
+    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    public long FactoryRefId { get; set; }
+
+    public string? DomainCode { get; set; }
+
+    [Required(ErrorMessage = "父阶段编码不能为空")]
+    public string ParentStageCode { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "组编码不能为空")]
+    public string GroupCode { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "组名称不能为空")]
+    public string GroupName { get; set; } = string.Empty;
+
+    [Range(typeof(decimal), "0", "9999.99", ErrorMessage = "标准时长必须在 0 ~ 9999.99 之间")]
+    public decimal StdHours { get; set; }
+
+    public int OrderNo { get; set; } = 1;
+    public bool IsActive { get; set; } = true;
+    public string? CreateUser { get; set; }
+    public string? UpdateUser { get; set; }
+}
+
+public class AdoS0ContractReviewCycleBreakdownToggleActiveDto
+{
+    public bool IsActive { get; set; }
+}
+
+// ── B1.2: 合同评审周期同步开关 ────────────────────────────────────────
+
+/// <summary>同步开关读取响应;行不存在时 IsSyncEnabled 默认 false。</summary>
+public class AdoS0ContractReviewCycleSyncFlagDto
+{
+    public long? Id { get; set; }
+    public long? CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string? DomainCode { get; set; }
+    public bool IsSyncEnabled { get; set; }
+}
+
+public class AdoS0ContractReviewCycleSyncFlagUpsertDto
+{
+    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    public long CompanyRefId { get; set; }
+
+    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    public long FactoryRefId { get; set; }
+
+    public string? DomainCode { get; set; }
+    public bool IsSyncEnabled { get; set; }
+    public string? CreateUser { get; set; }
+    public string? UpdateUser { get; set; }
+}
+
+// ── B1.3: PI 只读配置(S8 后续可消费) ─────────────────────────────────
+
+public class AdoS0ContractReviewCyclePiConfigStageDto
+{
+    public string StageCode { get; set; } = string.Empty;
+    public string StageName { get; set; } = string.Empty;
+    public int StdHours { get; set; }
+    public int OrderNo { get; set; }
+}
+
+public class AdoS0ContractReviewCyclePiConfigBreakdownDto
+{
+    public string GroupCode { get; set; } = string.Empty;
+    public string GroupName { get; set; } = string.Empty;
+    public decimal StdHours { get; set; }
+    public int OrderNo { get; set; }
+}
+
+public class AdoS0ContractReviewCyclePiConfigDto
+{
+    public long FactoryRefId { get; set; }
+    public bool SyncEnabled { get; set; }
+    public List<AdoS0ContractReviewCyclePiConfigStageDto> MainStages { get; set; } = new();
+    public List<AdoS0ContractReviewCyclePiConfigBreakdownDto> OpinionFeedbackBreakdown { get; set; } = new();
+}

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs

@@ -375,6 +375,8 @@ public class AdoS8DeliveryTrendDayDto
     public int ProductDesignDelay { get; set; }
     /// <summary>总装发货延期(DELIVERY_DELAY_WARNING)当日计数</summary>
     public int DeliveryDelayWarning { get; set; }
+    /// <summary>发运异常(SHIPMENT_ABNORMAL)当日计数</summary>
+    public int ShipmentAbnormal { get; set; }
     public int Total { get; set; }
 }
 
@@ -404,6 +406,8 @@ public class AdoS8ProductionTrendDayDto
     public int QualityFault { get; set; }
     /// <summary>本体生产延期(BODY_PRODUCTION_DELAY_WARNING)当日计数</summary>
     public int BodyProductionDelay { get; set; }
+    /// <summary>工单完工延期(PRODUCTION_WORK_ORDER_COMPLETION_DELAY)当日计数</summary>
+    public int ProductionWorkOrderCompletionDelay { get; set; }
     public int Total { get; set; }
 }
 
@@ -433,6 +437,8 @@ public class AdoS8SupplyTrendDayDto
     public int MaterialStockAbnormal { get; set; }
     /// <summary>库存周转异常(INVENTORY_TURNOVER_ABNORMAL)当日计数</summary>
     public int InventoryTurnoverAbnormal { get; set; }
+    /// <summary>采购到货异常(PURCHASE_DELIVERY_ABNORMAL)当日计数</summary>
+    public int PurchaseDeliveryAbnormal { get; set; }
     public int Total { get; set; }
 }
 

+ 60 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleBreakdown.cs

@@ -0,0 +1,60 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 合同评审周期下钻配置(按 parent_stage_code 分组)。
+/// 本批仅初始化 parent_stage_code = 'feedback'(意见反馈)下的部门/组 PI 标准。
+/// </summary>
+[SugarTable("S0ContractReviewCycleBreakdown", "合同评审周期下钻配置")]
+[SugarIndex("uk_S0CRCBreakdown_factory_parent_group",
+    nameof(FactoryRefId), OrderByType.Asc,
+    nameof(ParentStageCode), OrderByType.Asc,
+    nameof(GroupCode), OrderByType.Asc,
+    IsUnique = true)]
+[SugarIndex("idx_S0CRCBreakdown_factory_parent",
+    nameof(FactoryRefId), OrderByType.Asc,
+    nameof(ParentStageCode), OrderByType.Asc,
+    IsUnique = false)]
+public class AdoS0ContractReviewCycleBreakdown
+{
+    [SugarColumn(ColumnName = "rec_id", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "company_ref_id", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_ref_id", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnName = "domain_code", Length = 50, IsNullable = true)]
+    public string? DomainCode { get; set; }
+
+    [SugarColumn(ColumnName = "parent_stage_code", Length = 50)]
+    public string ParentStageCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "group_code", Length = 50)]
+    public string GroupCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "group_name", Length = 200)]
+    public string GroupName { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "std_hours", ColumnDataType = "decimal(6,2)")]
+    public decimal StdHours { get; set; }
+
+    [SugarColumn(ColumnName = "order_no")]
+    public int OrderNo { get; set; }
+
+    [SugarColumn(ColumnName = "is_active", ColumnDataType = "boolean")]
+    public bool IsActive { get; set; } = true;
+
+    [SugarColumn(ColumnName = "create_user", Length = 100, IsNullable = true)]
+    public string? CreateUser { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "update_user", Length = 100, IsNullable = true)]
+    public string? UpdateUser { get; set; }
+
+    [SugarColumn(ColumnName = "update_time", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+}

+ 40 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleSyncFlag.cs

@@ -0,0 +1,40 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 合同评审周期同步开关(工厂级一行)。
+/// 行不存在 = 未开启(默认 false);行存在则以 IsSyncEnabled 字段为准。
+/// 开启后该工厂的 S0 合同评审周期配置(主阶段 + 下钻)将被标记为 S8 可消费的 PI 标准。
+/// </summary>
+[SugarTable("S0ContractReviewCycleSyncFlag", "合同评审周期同步开关")]
+[SugarIndex("uk_S0CRCSyncFlag_factory",
+    nameof(FactoryRefId), OrderByType.Asc,
+    IsUnique = true)]
+public class AdoS0ContractReviewCycleSyncFlag
+{
+    [SugarColumn(ColumnName = "rec_id", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "company_ref_id", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_ref_id", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnName = "domain_code", Length = 50, IsNullable = true)]
+    public string? DomainCode { get; set; }
+
+    [SugarColumn(ColumnName = "is_sync_enabled", ColumnDataType = "boolean")]
+    public bool IsSyncEnabled { get; set; } = false;
+
+    [SugarColumn(ColumnName = "create_user", Length = 100, IsNullable = true)]
+    public string? CreateUser { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "update_user", Length = 100, IsNullable = true)]
+    public string? UpdateUser { get; set; }
+
+    [SugarColumn(ColumnName = "update_time", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+}

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0PriorityCode.cs

@@ -42,6 +42,12 @@ public class AdoS0PriorityCode
     [SugarColumn(ColumnName = "ValueID", ColumnDescription = "工单表关联字段", Length = 255, IsNullable = true)]
     public string? ValueId { get; set; }
 
+    [SugarColumn(ColumnName = "CustomerTypeCode", ColumnDescription = "客户类型编码(字典 s0_order_priority_customer_type 的 Value)", Length = 50, IsNullable = true)]
+    public string? CustomerTypeCode { get; set; }
+
+    [SugarColumn(ColumnName = "OrderTypeCode", ColumnDescription = "订单类型编码(字典 s0_order_type 的 Value)", Length = 50, IsNullable = true)]
+    public string? OrderTypeCode { get; set; }
+
     [SugarColumn(ColumnName = "IsActive", ColumnDescription = "是否生效", ColumnDataType = "boolean")]
     public bool IsActive { get; set; } = true;
 

+ 17 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8PeriodHelper.cs

@@ -7,7 +7,7 @@ public static class S8PeriodHelper
 {
     /// <summary>
     /// 将 period 字符串解析为半开区间 [from, to)。
-    /// 支持 today / this_week / this_month(大小写容错)。
+    /// 支持 today / this_week / this_month / this_quarter(大小写容错)。
     /// 非法或空值返回 (null, null),调用方保持全量行为。
     /// </summary>
     public static (DateTime? from, DateTime? to) Resolve(string? period)
@@ -17,13 +17,14 @@ public static class S8PeriodHelper
         var today = DateTime.Today;
         return period.Trim().ToLowerInvariant() switch
         {
-            "today"      => (today, today.AddDays(1)),
-            "this_week"  => ResolveThisWeek(today),
-            "this_month" => (new DateTime(today.Year, today.Month, 1), new DateTime(today.Year, today.Month, 1).AddMonths(1)),
-            "last_24h"   => (DateTime.Now.AddHours(-24), DateTime.Now),
-            "last_7d"    => (today.AddDays(-6), today.AddDays(1)),
-            "last_30d"   => (today.AddDays(-29), today.AddDays(1)),
-            _            => (null, null),
+            "today"        => (today, today.AddDays(1)),
+            "this_week"    => ResolveThisWeek(today),
+            "this_month"   => (new DateTime(today.Year, today.Month, 1), new DateTime(today.Year, today.Month, 1).AddMonths(1)),
+            "this_quarter" => ResolveThisQuarter(today),
+            "last_24h"     => (DateTime.Now.AddHours(-24), DateTime.Now),
+            "last_7d"      => (today.AddDays(-6), today.AddDays(1)),
+            "last_30d"     => (today.AddDays(-29), today.AddDays(1)),
+            _              => (null, null),
         };
     }
 
@@ -34,4 +35,12 @@ public static class S8PeriodHelper
         var monday = today.AddDays(-offset);
         return (monday, monday.AddDays(7));
     }
+
+    private static (DateTime? from, DateTime? to) ResolveThisQuarter(DateTime today)
+    {
+        // 季度起始月:1 / 4 / 7 / 10
+        var quarterStartMonth = ((today.Month - 1) / 3) * 3 + 1;
+        var start = new DateTime(today.Year, quarterStartMonth, 1);
+        return (start, start.AddMonths(3));
+    }
 }

+ 43 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewService.cs

@@ -142,13 +142,35 @@ public class ContractReviewService : IDynamicApiController, ITransient
     [ApiDescriptionSettings(Name = "SaveContractReview"), HttpPost("contract/save")]
     public async Task<object> SaveContractReview([FromBody] ContractReviewSaveInput input)
     {
+        // 必填校验
+        if (string.IsNullOrWhiteSpace(input.ProjectCode))
+            throw Oops.Oh("合同编号不能为空");
+        if (string.IsNullOrWhiteSpace(input.ProjectStartDate))
+            throw Oops.Oh("合同开始时间不能为空");
+        if (string.IsNullOrWhiteSpace(input.SalesCompany))
+            throw Oops.Oh("销售公司不能为空");
+        if (string.IsNullOrWhiteSpace(input.CustomerName))
+            throw Oops.Oh("客户名称不能为空");
+        if (string.IsNullOrWhiteSpace(input.Title))
+            throw Oops.Oh("合同标题不能为空");
+
+        var tenantId = _userManager.TenantId;
+
+        // 合同编号唯一性校验
+        var existingId = input.RecID ?? 0;
+        var codeExists = await _reviewRep.IsAnyAsync(u =>
+            u.ProjectCode == input.ProjectCode.Trim()
+            && u.TenantId == tenantId
+            && u.RecID != existingId);
+        if (codeExists)
+            throw Oops.Oh($"合同编号「{input.ProjectCode.Trim()}」已存在,请使用其他编号");
+
         var now = DateTime.Now;
         var user = _userManager.Account ?? "system";
 
         if (input.RecID is null or 0)
         {
             // ── 新增:参照 SysJobService.AddJobDetail ──
-            var tenantId = _userManager.TenantId;
             var month = now.ToString("yyyyMM");
             var maxSeq = await _db.Ado.GetIntAsync(
                 $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_contract_review WHERE BillNo LIKE 'CR{month}%' AND tenant_id = @TenantId",
@@ -193,7 +215,6 @@ public class ContractReviewService : IDynamicApiController, ITransient
         else
         {
             // ── 编辑:参照 SysJobService.UpdateJobDetail ──
-            var tenantId = _userManager.TenantId;
             var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID.Value && u.TenantId == tenantId)
                          ?? throw Oops.Oh("合同评审记录不存在");
 
@@ -246,6 +267,26 @@ public class ContractReviewService : IDynamicApiController, ITransient
         return new { message = "删除成功" };
     }
 
+    // ══════════════════════════════════════════════════════════════
+    // 合同编号唯一性校验 GET /api/Order/contract/check-project-code
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>校验合同编号唯一性 📋</summary>
+    [DisplayName("校验合同编号唯一性")]
+    [HttpGet("contract/check-project-code")]
+    public async Task<object> CheckProjectCode([FromQuery] string projectCode, [FromQuery] int excludeId = 0)
+    {
+        if (string.IsNullOrWhiteSpace(projectCode))
+            return new { exists = false };
+
+        var tenantId = _userManager.TenantId;
+        var exists = await _reviewRep.IsAnyAsync(u =>
+            u.ProjectCode == projectCode.Trim()
+            && u.TenantId == tenantId
+            && u.RecID != excludeId);
+
+        return new { exists };
+    }
+
     // ══════════════════════════════════════════════════════════════
     // 合同负责人下拉框 GET /api/Order/contract/responsible-users
     // ══════════════════════════════════════════════════════════════

+ 10 - 10
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs

@@ -94,11 +94,11 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             d.order_no                                                  AS BillNo,
             NULL                                                        AS Rstate,
             CAST(NULLIF(d.order_line, '') AS SIGNED)                    AS EntrySeq,
-            d.item_code                                                 AS ItemNumber,
-            d.item_name                                                 AS ItemName,
-            d.item_spec                                                 AS Specification,
+            IFNULL(NULLIF(d.item_code, 'null'), '')                       AS ItemNumber,
+            IFNULL(NULLIF(d.item_name, 'null'), '')                     AS ItemName,
+            IFNULL(NULLIF(d.item_spec, 'null'), '')                     AS Specification,
             NULL                                                        AS Unit,
-            d.customer_no                                               AS CustomNo,
+            IFNULL(NULLIF(d.customer_no, 'null'), '')                     AS CustomNo,
             NULL                                                        AS CustomOrderBillNo,
             NULL                                                        AS CustomOrderItemNo,
             NULL                                                        AS CustomLevel,
@@ -125,7 +125,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             d.actual_ship_date                                          AS ShipDate,
             NULL                                                        AS Recid,
             IF(IFNULL(d.planned_ship_qty, 0) > 0, '是', '否')          AS Spstatus,
-            me.mono_list                                                AS MoentryMono
+            IFNULL(NULLIF(me.mono_list, 'null'), '')                      AS MoentryMono
         FROM dwd_ship_trans d
         LEFT JOIN crm_seorderentry se
                ON se.id = d.order_entry_id
@@ -187,9 +187,9 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         var tenantId = _userManager.TenantId;
         const string sql = """
             SELECT
-                d.item_code          AS ItemNumber,
-                d.item_name          AS ItemName,
-                d.item_spec          AS Specification,
+                IFNULL(NULLIF(d.item_code, 'null'), '')  AS ItemNumber,
+                IFNULL(NULLIF(d.item_name, 'null'), '')  AS ItemName,
+                IFNULL(NULLIF(d.item_spec, 'null'), '')  AS Specification,
                 d.order_qty          AS Qty,
                 COALESCE(so.customer_order_no, '') AS BillFrom,
                 d.plan_delivery_date AS PlanDate,
@@ -330,8 +330,8 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             SELECT
                 ROW_NUMBER() OVER (ORDER BY d.row_id) AS Sno,
                 d.num AS Num,
-                d.item_number AS ItemNumber,
-                d.item_name AS ItemName,
+                IFNULL(NULLIF(d.item_number, 'null'), '') AS ItemNumber,
+                IFNULL(NULLIF(d.item_name, 'null'), '') AS ItemName,
                 d.bom_number AS BomNumber,
                 d.model AS Model,
                 DATE_FORMAT(d.kitting_time, '%Y-%m-%d') AS KittingTime,

+ 21 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ProductDesignService.cs

@@ -140,6 +140,26 @@ public class ProductDesignService : IDynamicApiController, ITransient
         if (input.ProductKind is not (1 or 2))
             throw Oops.Oh("产品类型无效,请选择常规产品或非标产品");
 
+        // 产品编码必填
+        if (string.IsNullOrWhiteSpace(input.ItemNum))
+            throw Oops.Oh("产品编码不能为空");
+
+        // BOM 子表至少一条
+        if (input.Boms == null || input.Boms.Count == 0)
+            throw Oops.Oh("制造 BOM 至少需要一条数据");
+
+        // 工艺子表至少一条
+        if (input.Routings == null || input.Routings.Count == 0)
+            throw Oops.Oh("工艺路线至少需要一条数据");
+
+        // BOM 物料编码必填
+        if (input.Boms.Any(b => string.IsNullOrWhiteSpace(b.ItemNum)))
+            throw Oops.Oh("制造 BOM 中物料编码不能为空");
+
+        // 工艺工序代码必填
+        if (input.Routings.Any(r => string.IsNullOrWhiteSpace(r.OpCode)))
+            throw Oops.Oh("工艺路线中工序代码不能为空");
+
         var now = DateTime.Now;
         var user = _userManager.Account ?? "system";
         var tenantId = _userManager.TenantId;
@@ -246,7 +266,7 @@ LEFT JOIN ProductStructureOp pso ON pso.ParentItem=psm.ParentItem
 
         var routingSql = @"
 SELECT r.Descr,r.Op,r.ParentOp,CAST(r.MilestoneOp AS CHAR(5)) AS MilestoneOp,p.Line,r.RouteCode
-FROM RoutingOpDetail as r left join ProdLineDetail as p on r.RoutingCode=p.Part and r.Op=p.Op WHERE RoutingCode=@itemNum";
+FROM RoutingOpDetail as r left join ProdLineDetail as p on r.RoutingCode=p.Part and r.Op=p.Op WHERE RoutingCode=@itemNum ORDER BY r.Op";
 
         var boms = await _db.Ado.SqlQueryAsync<BomQueryRow>(bomSql, new { itemNum });
         var routings = await _db.Ado.SqlQueryAsync<RoutingQueryRow>(routingSql, new { itemNum });

+ 4 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs

@@ -235,6 +235,10 @@ public class SeOrderService : IDynamicApiController, ITransient
     [ApiDescriptionSettings(Name = "SaveSeOrder"), HttpPost("seorder/save")]
     public async Task<object> SaveSeOrder([FromBody] SeOrderSaveInput input)
     {
+        // 子表订单明细至少一条
+        if (input.Entries == null || input.Entries.Count == 0)
+            throw Oops.Oh("订单明细至少需要一条数据");
+
         if (input.Id is null or 0)
         {
             // ── 新增:参照 SysJobService.AddJobDetail ──

+ 10 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/S0DictDataSeedData.cs

@@ -36,12 +36,16 @@ public class S0DictDataSeedData : ISqlSugarEntitySeedData<SysDictData>
         return new[]
         {
             // ── s0_material_type ──
-            D(seq++, materialTypeId, "原材料",   "raw",       100, ct),
-            D(seq++, materialTypeId, "半成品",   "semi",      101, ct),
-            D(seq++, materialTypeId, "成品",     "finished",  102, ct),
-            D(seq++, materialTypeId, "辅料",     "aux",       103, ct),
-            D(seq++, materialTypeId, "工装夹具", "fixture",   104, ct),
-            D(seq++, materialTypeId, "包装件",   "packaging", 105, ct),
+            // 与 ItemMaster.ItemType 真实 RS 编码对齐;label 暂用 code 本身(无正式中文映射)。
+            D(seq++, materialTypeId, "RS11", "RS11", 100, ct),
+            D(seq++, materialTypeId, "RS12", "RS12", 101, ct),
+            D(seq++, materialTypeId, "RS13", "RS13", 102, ct),
+            D(seq++, materialTypeId, "RS14", "RS14", 103, ct),
+            D(seq++, materialTypeId, "RS15", "RS15", 104, ct),
+            D(seq++, materialTypeId, "RS30", "RS30", 105, ct),
+            D(seq++, materialTypeId, "RS50", "RS50", 106, ct),
+            D(seq++, materialTypeId, "RS60", "RS60", 107, ct),
+            D(seq++, materialTypeId, "RS99", "RS99", 108, ct),
 
             // ── s0_pl_category ──
             D(seq++, plCategoryId, "采购件", "purchase",    100, ct),

+ 26 - 16
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MonitoringService.cs

@@ -406,7 +406,7 @@ public class S8MonitoringService : ITransient
         }
 
         var deliveryModules = new[] { "S1", "S7" };
-        var deliveryTypes = new[] { "ORDER_DUE_DATE_DELAY", "PRODUCT_DESIGN_DELAY", "DELIVERY_DELAY_WARNING" };
+        var deliveryTypes = new[] { "ORDER_DUE_DATE_DELAY", "PRODUCT_DESIGN_DELAY", "DELIVERY_DELAY_WARNING", "SHIPMENT_ABNORMAL" };
 
         var rows = await _rep.AsQueryable()
             .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
@@ -428,6 +428,7 @@ public class S8MonitoringService : ITransient
             var orderDueDateDelay    = bucket.Count(r => r.ExceptionTypeCode == "ORDER_DUE_DATE_DELAY");
             var productDesignDelay   = bucket.Count(r => r.ExceptionTypeCode == "PRODUCT_DESIGN_DELAY");
             var deliveryDelayWarning = bucket.Count(r => r.ExceptionTypeCode == "DELIVERY_DELAY_WARNING");
+            var shipmentAbnormal     = bucket.Count(r => r.ExceptionTypeCode == "SHIPMENT_ABNORMAL");
             dayList.Add(new AdoS8DeliveryTrendDayDto
             {
                 Date                 = d.ToString("MM/dd"),
@@ -435,7 +436,8 @@ public class S8MonitoringService : ITransient
                 OrderDueDateDelay    = orderDueDateDelay,
                 ProductDesignDelay   = productDesignDelay,
                 DeliveryDelayWarning = deliveryDelayWarning,
-                Total                = orderDueDateDelay + productDesignDelay + deliveryDelayWarning,
+                ShipmentAbnormal     = shipmentAbnormal,
+                Total                = orderDueDateDelay + productDesignDelay + deliveryDelayWarning + shipmentAbnormal,
             });
         }
 
@@ -489,7 +491,7 @@ public class S8MonitoringService : ITransient
         var prodTypes = new[]
         {
             "EQUIP_FAULT", "MFG_MATERIAL_ABNORMAL", "MFG_QUALITY_ABNORMAL",
-            "BODY_PRODUCTION_DELAY_WARNING"
+            "BODY_PRODUCTION_DELAY_WARNING", "PRODUCTION_WORK_ORDER_COMPLETION_DELAY"
         };
 
         var rows = await _rep.AsQueryable()
@@ -511,15 +513,17 @@ public class S8MonitoringService : ITransient
             var mf  = bucket.Count(r => r.ExceptionTypeCode == "MFG_MATERIAL_ABNORMAL");
             var qf  = bucket.Count(r => r.ExceptionTypeCode == "MFG_QUALITY_ABNORMAL");
             var bpd = bucket.Count(r => r.ExceptionTypeCode == "BODY_PRODUCTION_DELAY_WARNING");
+            var pwo = bucket.Count(r => r.ExceptionTypeCode == "PRODUCTION_WORK_ORDER_COMPLETION_DELAY");
             dayList.Add(new AdoS8ProductionTrendDayDto
             {
-                Date                = d.ToString("MM/dd"),
-                RawDate             = d.ToString("yyyy-MM-dd"),
-                EquipmentFault      = ef,
-                MaterialFault       = mf,
-                QualityFault        = qf,
-                BodyProductionDelay = bpd,
-                Total               = ef + mf + qf + bpd,
+                Date                                = d.ToString("MM/dd"),
+                RawDate                             = d.ToString("yyyy-MM-dd"),
+                EquipmentFault                      = ef,
+                MaterialFault                       = mf,
+                QualityFault                        = qf,
+                BodyProductionDelay                 = bpd,
+                ProductionWorkOrderCompletionDelay  = pwo,
+                Total                               = ef + mf + qf + bpd + pwo,
             });
         }
 
@@ -578,7 +582,8 @@ public class S8MonitoringService : ITransient
             "SUPPLIER_ETA_ISSUE", "SUPPLIER_SHIP_ISSUE", "SUPPLIER_DELIVERY_DELAY_WARNING",
             "WAREHOUSE_RECEIPT_ABNORMAL", "IQC_ISSUE", "WH_PUTAWAY_ISSUE",
             "WORK_ORDER_KITTING_ABNORMAL", "WORK_ORDER_ISSUE_ABNORMAL",
-            "PURCHASE_EXECUTION_DELAY", "MATERIAL_STOCK_ABNORMAL", "INVENTORY_TURNOVER_ABNORMAL"
+            "PURCHASE_EXECUTION_DELAY", "MATERIAL_STOCK_ABNORMAL", "INVENTORY_TURNOVER_ABNORMAL",
+            "PURCHASE_DELIVERY_ABNORMAL"
         };
 
         var rows = await _rep.AsQueryable()
@@ -607,6 +612,7 @@ public class S8MonitoringService : ITransient
             var ped   = bucket.Count(r => r.ExceptionTypeCode == "PURCHASE_EXECUTION_DELAY");
             var msa   = bucket.Count(r => r.ExceptionTypeCode == "MATERIAL_STOCK_ABNORMAL");
             var ita   = bucket.Count(r => r.ExceptionTypeCode == "INVENTORY_TURNOVER_ABNORMAL");
+            var pda   = bucket.Count(r => r.ExceptionTypeCode == "PURCHASE_DELIVERY_ABNORMAL");
             dayList.Add(new AdoS8SupplyTrendDayDto
             {
                 Date                          = d.ToString("MM/dd"),
@@ -622,7 +628,8 @@ public class S8MonitoringService : ITransient
                 PurchaseExecutionDelay        = ped,
                 MaterialStockAbnormal         = msa,
                 InventoryTurnoverAbnormal     = ita,
-                Total                         = sEta + sShip + sDdw + whR + iqc + whP + woK + woI + ped + msa + ita,
+                PurchaseDeliveryAbnormal      = pda,
+                Total                         = sEta + sShip + sDdw + whR + iqc + whP + woK + woI + ped + msa + ita + pda,
             });
         }
 
@@ -667,15 +674,17 @@ public class S8MonitoringService : ITransient
                 ("order-due-date-delay",    "订单交期延期", "ORDER_DUE_DATE_DELAY"),
                 ("product-design-delay",    "产品设计延期", "PRODUCT_DESIGN_DELAY"),
                 ("delivery-delay-warning",  "总装发货延期", "DELIVERY_DELAY_WARNING"),
+                ("shipment-abnormal",       "发运异常",     "SHIPMENT_ABNORMAL"),
             },
             "PRODUCTION" => new (string, string, string)[]
             {
-                ("equipment-fault",                "设备异常",         "EQUIP_FAULT"),
-                ("material-fault",                 "物料异常",         "MFG_MATERIAL_ABNORMAL"),
-                ("quality-fault",                  "质量异常",         "MFG_QUALITY_ABNORMAL"),
+                ("equipment-fault",                          "设备异常",         "EQUIP_FAULT"),
+                ("material-fault",                           "物料异常",         "MFG_MATERIAL_ABNORMAL"),
+                ("quality-fault",                            "质量异常",         "MFG_QUALITY_ABNORMAL"),
                 // S8-PRODUCTION-PAGE-TYPE-TITLE-AND-TREND-ALIGN-1:大屏显示「本体生产延期」(去预警),
                 // typeCode 保持 BODY_PRODUCTION_DELAY_WARNING,不改 ado_s8_exception_type.type_name。
-                ("body-production-delay-warning",  "本体生产延期",     "BODY_PRODUCTION_DELAY_WARNING"),
+                ("body-production-delay-warning",            "本体生产延期",     "BODY_PRODUCTION_DELAY_WARNING"),
+                ("production-work-order-completion-delay",   "工单完工延期",     "PRODUCTION_WORK_ORDER_COMPLETION_DELAY"),
             },
             "SUPPLY" => new (string, string, string)[]
             {
@@ -693,6 +702,7 @@ public class S8MonitoringService : ITransient
                 ("purchase-execution-delay",       "采购执行延期",         "PURCHASE_EXECUTION_DELAY"),
                 ("material-stock-abnormal",        "物料库存异常",         "MATERIAL_STOCK_ABNORMAL"),
                 ("inventory-turnover-abnormal",    "库存周转异常",         "INVENTORY_TURNOVER_ABNORMAL"),
+                ("purchase-delivery-abnormal",     "采购到货异常",         "PURCHASE_DELIVERY_ABNORMAL"),
             },
             _ => Array.Empty<(string, string, string)>(),
         };