Prechádzať zdrojové kódy

feat: 合同评审/产品设计/订单交付功能增强,新增S1/S2蓝图设计与业务需求文档

Pengxy 3 týždňov pred
rodič
commit
8540646a65

+ 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,

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"
+  }
+}

+ 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 ──