소스 검색

feat(aidop): 合同评审、订单发货与审批流调整; chore: bump version 2.4.76 / 1.0.43

Made-with: Cursor
Pengxy 5 달 전
부모
커밋
27b47bd797

+ 1 - 1
Web/package.json

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

+ 12 - 25
Web/src/views/aidop/api/contractReview.ts

@@ -1,4 +1,5 @@
 import service from '/@/utils/request';
+import { unwrapFurionBody } from './linkagePlan';
 
 export interface Paged<T> {
 	total: number;
@@ -92,40 +93,26 @@ export interface ContractReviewSaveParams {
 	remark?: string | null;
 }
 
-// ── 审批入参 ──
-export interface ApproveParams {
-	reviewRecID: number;
-	opinion?: string | null;
-	reviewerAccount?: string | null;
-	reviewerName?: string | null;
-}
-
 export function fetchContractReviewList(params: Record<string, unknown>) {
 	return service
-		.get<Paged<ContractReviewRow>>('/api/Order/contract/list', { params })
-		.then((r) => r.data);
+		.get<unknown>('/api/Order/contract/list', { params })
+		.then((r) => unwrapFurionBody<Paged<ContractReviewRow>>(r.data));
 }
 
 export function fetchContractReviewDetail(id: number) {
-	return service.get<ContractReviewDetail>(`/api/Order/contract/${id}`).then((r) => r.data);
+	return service
+		.get<unknown>(`/api/Order/contract/${id}`)
+		.then((r) => unwrapFurionBody<ContractReviewDetail>(r.data));
 }
 
 export function saveContractReview(body: ContractReviewSaveParams) {
-	return service.post('/api/Order/contract/save', body).then((r) => r.data);
+	return service
+		.post<unknown>('/api/Order/contract/save', body)
+		.then((r) => unwrapFurionBody<{ id?: number; billNo?: string; message?: string }>(r.data));
 }
 
 export function deleteContractReview(recID: number) {
-	return service.post('/api/Order/contract/delete', { recID }).then((r) => r.data);
-}
-
-export function submitContractReview(recID: number) {
-	return service.post('/api/Order/contract/submit', { recID }).then((r) => r.data);
-}
-
-export function approveContractReview(body: ApproveParams) {
-	return service.post('/api/Order/contract/approve', body).then((r) => r.data);
-}
-
-export function rejectContractReview(body: ApproveParams) {
-	return service.post('/api/Order/contract/reject', body).then((r) => r.data);
+	return service
+		.post<unknown>('/api/Order/contract/delete', { recID })
+		.then((r) => unwrapFurionBody<unknown>(r.data));
 }

+ 118 - 89
Web/src/views/aidop/business/contractReviewForm.vue

@@ -7,14 +7,6 @@
 				<el-button v-if="canEdit" :loading="saving" @click="onSave">
 					<el-icon><Document /></el-icon> 暂存
 				</el-button>
-				<el-button
-					v-if="canSubmit"
-					type="primary"
-					:loading="submitting"
-					@click="onSubmit"
-				>
-					<el-icon><Promotion /></el-icon> 提交审批
-				</el-button>
 				<el-button @click="$emit('cancel')">关闭</el-button>
 			</div>
 		</div>
@@ -141,32 +133,28 @@
 			</div>
 		</div>
 
-		<!-- 审批流程进度 -->
-		<div class="cr-section-title">审批流程进度</div>
-		<div class="cr-steps-wrap">
-			<el-steps
-				:active="stepsActive"
-				align-center
-				finish-status="success"
-				process-status="process"
-			>
-				<el-step title="意见评审" description="法律/技术/计划/试验" />
-				<el-step title="意见反馈" />
-				<el-step title="二次评审" />
-				<el-step title="领导意见" />
-				<el-step title="合同盖章" />
-			</el-steps>
+		<!-- 审批流(插件:与业务类型 CONTRACT_REVIEW 绑定,见 /doc/审批流集成开发指南.md) -->
+		<div v-if="detail && detail.recID > 0" class="cr-approval-wrap">
+			<ApprovalPanel
+				bizType="CONTRACT_REVIEW"
+				:bizId="detail.recID"
+				:bizNo="detail.billNo || undefined"
+				:title="approvalTitle"
+				:currentUserId="currentUserId"
+				@refresh="onApprovalRefresh"
+			/>
 		</div>
 
-		<!-- 流程节点明细 -->
-		<div class="cr-section-title">审批节点明细</div>
+		<!-- 历史:旧版自建节点数据(迁移前);新流程以审批面板时间线为准 -->
+		<template v-if="flows.length">
+		<div class="cr-section-title">历史节点明细(旧数据)</div>
 		<el-table
 			:data="flows"
 			border
 			stripe
 			size="small"
 			style="width: 100%"
-			empty-text="暂无流程数据(提交后自动生成)"
+			empty-text="暂无"
 		>
 			<el-table-column label="环节" width="100" align="center">
 				<template #default="{ row }">
@@ -194,17 +182,19 @@
 				</template>
 			</el-table-column>
 		</el-table>
+		</template>
 	</div>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, reactive, ref } from 'vue';
+import { computed, reactive, ref, watch } from 'vue';
 import { ElMessage } from 'element-plus';
-import { Document, Promotion } from '@element-plus/icons-vue';
+import { Document } from '@element-plus/icons-vue';
+import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
+import { useUserInfo } from '/@/stores/userInfo';
 import {
 	fetchContractReviewDetail,
 	saveContractReview,
-	submitContractReview,
 	type ContractReviewDetail,
 	type ContractReviewFlowNode,
 } from '../api/contractReview';
@@ -217,15 +207,21 @@ const props = defineProps<{
 const emit = defineEmits<{
 	(e: 'cancel'): void;
 	(e: 'saved'): void;
-	(e: 'submitted'): void;
 }>();
 
-const loading  = ref(false);
-const saving   = ref(false);
-const submitting = ref(false);
+const userInfo = useUserInfo();
+const currentUserId = computed(() => (userInfo.userInfos?.id as number | undefined) ?? undefined);
+
+const loading = ref(false);
+const saving = ref(false);
 
 const detail = ref<ContractReviewDetail | null>(null);
-const flows  = ref<ContractReviewFlowNode[]>([]);
+const flows = ref<ContractReviewFlowNode[]>([]);
+
+const approvalTitle = computed(() => {
+	const b = detail.value?.billNo;
+	return b ? `合同评审-${b}` : '合同评审';
+});
 
 const form = reactive({
 	title: '',
@@ -246,14 +242,11 @@ const form = reactive({
 	remark: '',
 });
 
-const canEdit    = computed(() => props.mode !== 'view' &&
-	(detail.value?.flowStatus === 'draft' || detail.value?.flowStatus === 'rejected' || props.mode === 'create'));
-const canSubmit  = computed(() => canEdit.value && detail.value?.recID != null);
-const stepsActive = computed(() => {
-	const s = detail.value?.flowStatus;
-	if (s === 'completed') return 5;
-	return detail.value?.currentStage ?? 0;
-});
+const canEdit = computed(
+	() =>
+		props.mode !== 'view' &&
+		(detail.value?.flowStatus === 'draft' || detail.value?.flowStatus === 'rejected' || props.mode === 'create')
+);
 
 function flowStatusLabel(s?: string | null) {
 	const MAP: Record<string, string> = { draft: '草稿', reviewing: '审批中', completed: '已完成', rejected: '已驳回' };
@@ -276,31 +269,58 @@ function nodeStatusType(s?: string | null): 'info' | 'warning' | 'success' | 'da
 	return 'info';
 }
 
+function resetCreatePlaceholder() {
+	detail.value = { recID: 0, billNo: '', currentStage: 0, flowStatus: 'draft', flows: [] };
+	flows.value = [];
+	Object.assign(form, {
+		title: '',
+		customerName: '',
+		customerNo: '',
+		salesCompany: '',
+		salesArea: '',
+		projectCode: '',
+		crmNo: '',
+		responsibleAccount: '',
+		responsibleName: '',
+		projectStartDate: '',
+		projectStatus: '',
+		winRate: '',
+		expectedOrderMonth: '',
+		expectedDeliveryDate: '',
+		projectRequirement: '',
+		remark: '',
+	});
+}
+
 async function loadDetail() {
 	if (!props.recordId) return;
 	loading.value = true;
 	try {
 		const d = await fetchContractReviewDetail(props.recordId);
 		detail.value = d;
-		flows.value  = d.flows ?? [];
+		flows.value = d.flows ?? [];
 		Object.assign(form, {
-			title:               d.title ?? '',
-			customerName:        d.customerName ?? '',
-			customerNo:          d.customerNo ?? '',
-			salesCompany:        d.salesCompany ?? '',
-			salesArea:           d.salesArea ?? '',
-			projectCode:         d.projectCode ?? '',
-			crmNo:               d.crmNo ?? '',
-			responsibleAccount:  d.responsibleAccount ?? '',
-			responsibleName:     d.responsibleName ?? '',
-			projectStartDate:    d.projectStartDate ?? '',
-			projectStatus:       d.projectStatus ?? '',
-			winRate:             d.winRate ?? '',
-			expectedOrderMonth:  d.expectedOrderMonth ?? '',
+			title: d.title ?? '',
+			customerName: d.customerName ?? '',
+			customerNo: d.customerNo ?? '',
+			salesCompany: d.salesCompany ?? '',
+			salesArea: d.salesArea ?? '',
+			projectCode: d.projectCode ?? '',
+			crmNo: d.crmNo ?? '',
+			responsibleAccount: d.responsibleAccount ?? '',
+			responsibleName: d.responsibleName ?? '',
+			projectStartDate: d.projectStartDate ?? '',
+			projectStatus: d.projectStatus ?? '',
+			winRate: d.winRate ?? '',
+			expectedOrderMonth: d.expectedOrderMonth ?? '',
 			expectedDeliveryDate: d.expectedDeliveryDate ?? '',
-			projectRequirement:  d.projectRequirement ?? '',
-			remark:              d.remark ?? '',
+			projectRequirement: d.projectRequirement ?? '',
+			remark: d.remark ?? '',
 		});
+	} catch (e: any) {
+		ElMessage.error(e?.message || '加载合同评审详情失败');
+		detail.value = null;
+		flows.value = [];
 	} finally {
 		loading.value = false;
 	}
@@ -344,37 +364,49 @@ async function onSave() {
 }
 
 async function loadDetailById(id: number) {
-	const d = await fetchContractReviewDetail(id);
-	detail.value = d;
-	flows.value  = d.flows ?? [];
-}
-
-async function onSubmit() {
-	if (!detail.value?.recID) {
-		ElMessage.warning('请先保存后再提交');
-		return;
-	}
-	submitting.value = true;
+	loading.value = true;
 	try {
-		const res: any = await submitContractReview(detail.value.recID);
-		ElMessage.success(res?.message || '提交成功');
-		await loadDetail();
-		emit('submitted');
+		const d = await fetchContractReviewDetail(id);
+		detail.value = d;
+		flows.value = d.flows ?? [];
+		Object.assign(form, {
+			title: d.title ?? '',
+			customerName: d.customerName ?? '',
+			customerNo: d.customerNo ?? '',
+			salesCompany: d.salesCompany ?? '',
+			salesArea: d.salesArea ?? '',
+			projectCode: d.projectCode ?? '',
+			crmNo: d.crmNo ?? '',
+			responsibleAccount: d.responsibleAccount ?? '',
+			responsibleName: d.responsibleName ?? '',
+			projectStartDate: d.projectStartDate ?? '',
+			projectStatus: d.projectStatus ?? '',
+			winRate: d.winRate ?? '',
+			expectedOrderMonth: d.expectedOrderMonth ?? '',
+			expectedDeliveryDate: d.expectedDeliveryDate ?? '',
+			projectRequirement: d.projectRequirement ?? '',
+			remark: d.remark ?? '',
+		});
 	} catch (e: any) {
-		ElMessage.error(e?.message || '提交失败');
+		ElMessage.error(e?.message || '加载合同评审详情失败');
 	} finally {
-		submitting.value = false;
+		loading.value = false;
 	}
 }
 
-onMounted(() => {
-	if (props.recordId) {
-		loadDetail();
-	} else {
-		// 新建模式:初始化一个空 detail 占位(用于状态判断)
-		detail.value = { recID: 0, billNo: '', currentStage: 0, flowStatus: 'draft', flows: [] };
-	}
-});
+async function onApprovalRefresh() {
+	await loadDetail();
+	emit('saved');
+}
+
+watch(
+	() => props.recordId,
+	(id) => {
+		if (id) loadDetail();
+		else resetCreatePlaceholder();
+	},
+	{ immediate: true }
+);
 </script>
 
 <style scoped lang="scss">
@@ -469,11 +501,8 @@ onMounted(() => {
 	margin-bottom: 14px;
 }
 
-.cr-steps-wrap {
-	padding: 12px 0 20px;
-	background: #fafafa;
-	border: 1px solid #e4e7ed;
-	border-radius: 4px;
-	margin-bottom: 18px;
+.cr-approval-wrap {
+	margin-top: 8px;
+	margin-bottom: 8px;
 }
 </style>

+ 2 - 113
Web/src/views/aidop/business/contractReviewList.vue

@@ -54,7 +54,7 @@
 			<el-table-column label="创建时间" width="160" align="center" sortable>
 				<template #default="{ row }">{{ fmtDate(row.createTime) }}</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+			<el-table-column label="操作" width="160" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openView(row)">查看</el-button>
 					<el-button
@@ -62,16 +62,6 @@
 						link type="warning"
 						@click="openEdit(row)"
 					>编辑</el-button>
-					<el-button
-						v-if="row.flowStatus === 'draft' || row.flowStatus === 'rejected'"
-						link type="success"
-						@click="onSubmit(row)"
-					>提交</el-button>
-					<el-button
-						v-if="row.flowStatus === 'reviewing'"
-						link type="primary"
-						@click="openApprove(row)"
-					>审批</el-button>
 					<el-button
 						v-if="row.flowStatus !== 'reviewing'"
 						link type="danger"
@@ -107,29 +97,8 @@
 				:record-id="editingId"
 				@cancel="formVisible = false"
 				@saved="onSaved"
-				@submitted="onSaved"
 			/>
 		</el-dialog>
-
-		<!-- 审批弹窗 -->
-		<el-dialog v-model="approveVisible" title="审批操作" width="480px" destroy-on-close>
-			<el-form :model="approveForm" label-width="90px">
-				<el-form-item label="审批人账号">
-					<el-input v-model="approveForm.reviewerAccount" placeholder="默认取当前登录账号" />
-				</el-form-item>
-				<el-form-item label="审批人姓名">
-					<el-input v-model="approveForm.reviewerName" placeholder="默认取当前登录账号" />
-				</el-form-item>
-				<el-form-item label="审批意见">
-					<el-input v-model="approveForm.opinion" type="textarea" :rows="3" placeholder="请输入审批意见" />
-				</el-form-item>
-			</el-form>
-			<template #footer>
-				<el-button @click="approveVisible = false">取消</el-button>
-				<el-button type="danger" :loading="approving" @click="onReject">驳回</el-button>
-				<el-button type="success" :loading="approving" @click="onApprove">通过</el-button>
-			</template>
-		</el-dialog>
 	</AidopDemoShell>
 </template>
 
@@ -139,19 +108,10 @@ import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import ContractReviewForm from './contractReviewForm.vue';
-import {
-	fetchContractReviewList,
-	deleteContractReview,
-	submitContractReview,
-	approveContractReview,
-	rejectContractReview,
-	type ContractReviewRow,
-} from '../api/contractReview';
-import { useUserInfo } from '/@/stores/userInfo';
+import { fetchContractReviewList, deleteContractReview, type ContractReviewRow } from '../api/contractReview';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '合同评审');
-const userInfo = useUserInfo();
 
 const STAGE_NAMES: Record<number, string> = {
 	1: '意见评审',
@@ -225,22 +185,6 @@ function openView(row: ContractReviewRow) { editingId.value = row.recID; formMod
 
 async function onSaved() { formVisible.value = false; await loadList(); }
 
-// ── 提交 ──
-async function onSubmit(row: ContractReviewRow) {
-	try {
-		await ElMessageBox.confirm(`确认提交合同评审【${row.billNo}】进入审批流程?`, '提交确认', {
-			confirmButtonText: '确认提交', cancelButtonText: '取消', type: 'warning',
-		});
-	} catch { return; }
-	try {
-		await submitContractReview(row.recID);
-		ElMessage.success('提交成功,审批流程已启动');
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '提交失败');
-	}
-}
-
 // ── 删除 ──
 async function onDelete(row: ContractReviewRow) {
 	try {
@@ -257,61 +201,6 @@ async function onDelete(row: ContractReviewRow) {
 	}
 }
 
-// ── 审批 ──
-const approveVisible = ref(false);
-const approving = ref(false);
-const approvingRow = ref<ContractReviewRow | null>(null);
-const approveForm = reactive({ reviewerAccount: '', reviewerName: '', opinion: '' });
-
-function openApprove(row: ContractReviewRow) {
-	approvingRow.value = row;
-	approveForm.reviewerAccount = userInfo.userInfos?.account ?? '';
-	approveForm.reviewerName = '';
-	approveForm.opinion = '';
-	approveVisible.value = true;
-}
-
-async function onApprove() {
-	if (!approvingRow.value) return;
-	approving.value = true;
-	try {
-		await approveContractReview({
-			reviewRecID: approvingRow.value.recID,
-			opinion: approveForm.opinion || null,
-			reviewerAccount: approveForm.reviewerAccount || null,
-			reviewerName: approveForm.reviewerName || null,
-		});
-		ElMessage.success('审批通过');
-		approveVisible.value = false;
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '操作失败');
-	} finally {
-		approving.value = false;
-	}
-}
-
-async function onReject() {
-	if (!approvingRow.value) return;
-	if (!approveForm.opinion) { ElMessage.warning('请填写驳回原因'); return; }
-	approving.value = true;
-	try {
-		await rejectContractReview({
-			reviewRecID: approvingRow.value.recID,
-			opinion: approveForm.opinion,
-			reviewerAccount: approveForm.reviewerAccount || null,
-			reviewerName: approveForm.reviewerName || null,
-		});
-		ElMessage.success('已驳回');
-		approveVisible.value = false;
-		loadList();
-	} catch (e: any) {
-		ElMessage.error(e?.message || '操作失败');
-	} finally {
-		approving.value = false;
-	}
-}
-
 onMounted(() => loadList());
 </script>
 

+ 187 - 24
Web/src/views/aidop/business/orderDeliveryFlow.vue

@@ -1,21 +1,60 @@
 <template>
 	<div v-loading="loading" class="flow-wrap">
-		<!-- 顶部基础信息 -->
-		<el-descriptions v-if="data?.basic" :column="4" border size="small" class="mb12">
-			<el-descriptions-item label="物料编码">{{ data.basic.itemNumber }}</el-descriptions-item>
-			<el-descriptions-item label="物料名称">{{ data.basic.itemName }}</el-descriptions-item>
-			<el-descriptions-item label="规格型号">{{ data.basic.specification }}</el-descriptions-item>
-			<el-descriptions-item label="订单数量">{{ data.basic.qty }}</el-descriptions-item>
-			<el-descriptions-item label="客户单号">{{ data.basic.billFrom }}</el-descriptions-item>
-			<el-descriptions-item label="订单交期">{{ fmtDate(data.basic.planDate) }}</el-descriptions-item>
-			<el-descriptions-item label="订单进度">
-				<el-tag size="small" :type="progressTagType(data.basic.progress)">{{ progressLabel(data.basic.progress) }}</el-tag>
-			</el-descriptions-item>
-			<el-descriptions-item label="检查时间">{{ fmtDateTime(data.basic.mrptime) }}</el-descriptions-item>
-		</el-descriptions>
+		<!-- 顶部基础信息表单 -->
+		<div v-if="data?.basic" class="basic-info-section">
+			<div class="section-title">基本信息:</div>
+			<el-form label-width="100px" size="small" class="basic-form">
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="物料编码:">
+							<el-input :model-value="data.basic.itemNumber" readonly />
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="物料名称:">
+							<el-input :model-value="data.basic.itemName" readonly />
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="规格型号:">
+							<el-input :model-value="data.basic.specification" readonly />
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="订单数量:">
+							<el-input :model-value="data.basic.qty" readonly />
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="客户单号:">
+							<el-input :model-value="data.basic.billFrom" readonly />
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="订单交期:">
+							<el-input :model-value="fmtDate(data.basic.planDate)" readonly />
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-row :gutter="20">
+					<el-col :span="8">
+						<el-form-item label="订单进度:">
+							<el-input :model-value="progressLabel(data.basic.progress)" readonly />
+						</el-form-item>
+					</el-col>
+					<el-col :span="8">
+						<el-form-item label="检查时间:">
+							<el-input :model-value="fmtDateTime(data.basic.mrptime)" readonly />
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+		</div>
 
 		<!-- 8 个 Tab -->
-		<el-tabs v-model="activeTab" type="border-card">
+		<el-tabs v-model="activeTab" type="border-card" class="tabs-section">
 			<!-- Tab1 订单信息 -->
 			<el-tab-pane label="订单信息" name="tab1">
 				<el-descriptions v-if="data?.tab1" :column="3" border size="small">
@@ -36,15 +75,48 @@
 
 			<!-- Tab2 订单评审 -->
 			<el-tab-pane label="订单评审" name="tab2">
-				<el-descriptions v-if="data?.tab2?.review" :column="3" border size="small" class="mb12">
-					<el-descriptions-item label="系统预计交货日期">{{ fmtDate(data.tab2.review.sysCapacityDate) }}</el-descriptions-item>
-					<el-descriptions-item label="新增工单">{{ data.tab2.review.morderNo }}</el-descriptions-item>
-					<el-descriptions-item label="交期评审确认">{{ fmtDate(data.tab2.review.createTime) }}</el-descriptions-item>
-					<el-descriptions-item label="新增工单数量">{{ data.tab2.review.makeQty }}</el-descriptions-item>
-					<el-descriptions-item label="可用库存占用">{{ data.tab2.review.useQty }}</el-descriptions-item>
-					<el-descriptions-item label="在制占用量">{{ data.tab2.review.moQty }}</el-descriptions-item>
-				</el-descriptions>
-				<el-divider content-position="left">变更记录</el-divider>
+				<div class="review-section">
+					<div class="section-subtitle">预计交期评估信息:</div>
+					<el-form v-if="data?.tab2?.review" label-width="140px" size="small" class="review-form">
+						<el-row :gutter="20">
+							<el-col :span="8">
+								<el-form-item label="系统预计交货日期:">
+									<el-input :model-value="data.tab2.review.sysCapacityDate" readonly />
+								</el-form-item>
+							</el-col>
+							<el-col :span="8">
+								<el-form-item label="新增工单:">
+									<el-input :model-value="data.tab2.review.morderNo" readonly />
+								</el-form-item>
+							</el-col>
+							<el-col :span="8">
+								<el-form-item label="交期评审确认:">
+									<el-input :model-value="data.tab2.review.createTime" readonly />
+								</el-form-item>
+							</el-col>
+						</el-row>
+						<el-row :gutter="20">
+							<el-col :span="8">
+								<el-form-item label="新增工单数量:">
+									<el-input :model-value="data.tab2.review.makeQty" readonly />
+								</el-form-item>
+							</el-col>
+							<el-col :span="8">
+								<el-form-item label="可用库存占用:">
+									<el-input :model-value="data.tab2.review.useQty" readonly />
+								</el-form-item>
+							</el-col>
+							<el-col :span="8">
+								<el-form-item label="在制占用量:">
+									<el-input :model-value="data.tab2.review.moQty" readonly />
+								</el-form-item>
+							</el-col>
+						</el-row>
+					</el-form>
+					<div v-else class="no-data">暂无评审信息</div>
+				</div>
+
+				<div class="section-subtitle" style="margin-top: 16px">订单变更信息:</div>
 				<el-table :data="data?.tab2?.changes ?? []" border stripe size="small" style="width: 100%">
 					<el-table-column prop="sno" label="序号" width="60" align="center" />
 					<el-table-column prop="billNo" label="订单编号" width="160" show-overflow-tooltip />
@@ -52,7 +124,7 @@
 					<el-table-column prop="changeReason" label="变更原因" min-width="140" show-overflow-tooltip />
 					<el-table-column prop="changeContent" label="变更内容" min-width="160" show-overflow-tooltip />
 					<el-table-column label="申请时间" width="160" align="center">
-						<template #default="{ row }">{{ fmtDateTime(row.updateTime) }}</template>
+						<template #default="{ row }">{{ row.updateTime }}</template>
 					</el-table-column>
 				</el-table>
 			</el-tab-pane>
@@ -363,4 +435,95 @@ onMounted(() => load());
 .mb12 {
 	margin-bottom: 12px;
 }
+
+.basic-info-section {
+	background: #f5f7fa;
+	padding: 16px;
+	border-radius: 4px;
+	margin-bottom: 16px;
+
+	.section-title {
+		font-size: 14px;
+		font-weight: 500;
+		color: #303133;
+		margin-bottom: 12px;
+	}
+
+	.basic-form {
+		:deep(.el-form-item) {
+			margin-bottom: 12px;
+		}
+
+		:deep(.el-form-item__label) {
+			text-align: right;
+			color: #606266;
+		}
+
+		:deep(.el-input.is-disabled .el-input__wrapper) {
+			background-color: #fff;
+			box-shadow: 0 0 0 1px #dcdfe6 inset;
+		}
+
+		:deep(.el-input.is-disabled .el-input__inner) {
+			color: #606266;
+			cursor: default;
+		}
+	}
+}
+
+.tabs-section {
+	:deep(.el-tabs__content) {
+		padding: 12px;
+	}
+}
+
+.review-section {
+	margin-bottom: 16px;
+
+	.section-subtitle {
+		font-size: 13px;
+		font-weight: 500;
+		color: #303133;
+		margin-bottom: 12px;
+		padding-left: 8px;
+		border-left: 3px solid #409eff;
+	}
+
+	.review-form {
+		:deep(.el-form-item) {
+			margin-bottom: 12px;
+		}
+
+		:deep(.el-form-item__label) {
+			text-align: right;
+			color: #606266;
+		}
+
+		:deep(.el-input.is-disabled .el-input__wrapper) {
+			background-color: #fff;
+			box-shadow: 0 0 0 1px #dcdfe6 inset;
+		}
+
+		:deep(.el-input.is-disabled .el-input__inner) {
+			color: #606266;
+			cursor: default;
+		}
+	}
+
+	.no-data {
+		text-align: center;
+		color: #909399;
+		padding: 20px 0;
+		font-size: 13px;
+	}
+}
+
+.section-subtitle {
+	font-size: 13px;
+	font-weight: 500;
+	color: #303133;
+	margin-bottom: 12px;
+	padding-left: 8px;
+	border-left: 3px solid #409eff;
+}
 </style>

+ 13 - 12
Web/src/views/approvalFlow/component/ApprovalPanel.vue

@@ -150,30 +150,31 @@ const isInitiator = computed(() => {
 	return instance.value.initiatorId === props.currentUserId;
 });
 
-watch(
-	() => [props.bizType, props.bizId],
-	() => {
-		if (props.bizType && props.bizId) {
-			loadInstance();
-		}
-	},
-	{ immediate: true }
-);
-
+/** 须在 watch(immediate) 之前定义,避免「Cannot access before initialization」 */
 const loadInstance = async () => {
+	visible.value = true;
 	try {
 		const res = await getInstanceByBiz(props.bizType, props.bizId);
 		instance.value = res.data?.result ?? null;
-		visible.value = true;
 		if (instance.value) {
 			const tlRes = await getTimeline(instance.value.id);
 			timeline.value = tlRes.data?.result ?? [];
 		}
 	} catch {
-		visible.value = true;
+		/* 仍展示面板,便于发起人点击「提交审批」或重试 */
 	}
 };
 
+watch(
+	() => [props.bizType, props.bizId],
+	() => {
+		if (props.bizType && props.bizId) {
+			loadInstance();
+		}
+	},
+	{ immediate: true }
+);
+
 const doStartFlow = async () => {
 	starting.value = true;
 	try {

+ 4 - 4
server/Admin.NET.Application/Configuration/Database.json

@@ -10,9 +10,9 @@
         //"ConfigId": "1300000000001", // 默认库标识-禁止修改
         "DbType": "MySql",
         "DbNickName": "系统库",
-        "ConnectionString": "Server=123.60.180.165;Port=3306;Database=dopdemo;Uid=root;Pwd=5heng=uN;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
+        //"ConnectionString": "Server=123.60.180.165;Port=3306;Database=dopdemo;Uid=root;Pwd=5heng=uN;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
         //"ConnectionString": "Server=106.14.73.46;Port=3306;Database=aidopcore;Uid=aidopremote;Pwd=AidOp#Remote2026$Secure;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
-        //"ConnectionString": "Server=123.60.180.165;Port=3306;Database=aidopdev;Uid=aidopremote;Pwd=1234567890aiDOP#;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
+        "ConnectionString": "Server=123.60.180.165;Port=3306;Database=aidopdev;Uid=aidopremote;Pwd=1234567890aiDOP#;SslMode=None;Charset=utf8mb4;AllowLoadLocalInfile=true;AllowUserVariables=true;",
         // 本地 SQLite 示例(切回时改 DbType 为 Sqlite 并恢复下行连接串)
         //"DbType": "Sqlite",
         //"ConnectionString": "DataSource=./Admin.NET.db",
@@ -31,7 +31,7 @@
         //	}
         //],
         "DbSettings": {
-          "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
+          "EnableInitDb": false, // 启用库初始化(若实体没有变化建议关闭)
           "EnableInitView": false, // 启用视图初始化(远端受限账号建议关闭)
           "EnableDiffLog": false, // 启用库表差异日志
           "EnableUnderLine": false, // 启用驼峰转下划线
@@ -42,7 +42,7 @@
           "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表)
         },
         "SeedSettings": {
-          "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭)
+          "EnableInitSeed": false, // 启用种子初始化(若种子没有变化建议关闭)
           "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表)
         }
       }

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

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

+ 73 - 9
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewBizHandler.cs

@@ -4,28 +4,92 @@ using Admin.NET.Plugin.ApprovalFlow.Service;
 namespace Admin.NET.Plugin.AiDOP.Order;
 
 /// <summary>
-/// 合同评审审批业务回调
-/// 注册 BizType = "CONTRACT_REVIEW"
-/// 待合同实体创建后补齐具体逻辑
+/// 合同评审审批业务回调(与流程定义 BizType = CONTRACT_REVIEW 一致)
 /// </summary>
 public class ContractReviewBizHandler : IFlowBizHandler, ITransient
 {
     public string BizType => "CONTRACT_REVIEW";
 
-    public Task OnFlowStarted(long bizId, long instanceId)
+    private readonly SqlSugarRepository<ContractReview> _reviewRep;
+
+    public ContractReviewBizHandler(SqlSugarRepository<ContractReview> reviewRep)
     {
-        // TODO: 更新合同状态为"审批中"
-        return Task.CompletedTask;
+        _reviewRep = reviewRep;
     }
 
-    public Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus)
+    public async Task OnFlowStarted(long bizId, long instanceId)
     {
-        // TODO: 更新合同最终状态
-        return Task.CompletedTask;
+        var id = (int)bizId;
+        var now = DateTime.Now;
+        var row = await _reviewRep.GetByIdAsync(id);
+        if (row == null) return;
+
+        row.FlowStatus = "reviewing";
+        row.CurrentStage = 1;
+        row.CurrentDept = "审批流";
+        row.UpdateTime = now;
+        await _reviewRep.UpdateAsync(row);
+    }
+
+    public async Task OnFlowCompleted(long bizId, FlowInstanceStatusEnum finalStatus)
+    {
+        var id = (int)bizId;
+        var now = DateTime.Now;
+        var row = await _reviewRep.GetByIdAsync(id);
+        if (row == null) return;
+
+        switch (finalStatus)
+        {
+            case FlowInstanceStatusEnum.Approved:
+                row.FlowStatus = "completed";
+                row.CurrentStage = 5;
+                row.CurrentDept = "已完成";
+                break;
+            case FlowInstanceStatusEnum.Rejected:
+            case FlowInstanceStatusEnum.Terminated:
+                row.FlowStatus = "rejected";
+                row.CurrentStage = 0;
+                row.CurrentDept = null;
+                break;
+            case FlowInstanceStatusEnum.Cancelled:
+                row.FlowStatus = "draft";
+                row.CurrentStage = 0;
+                row.CurrentDept = null;
+                break;
+            default:
+                row.FlowStatus = "draft";
+                row.CurrentStage = 0;
+                row.CurrentDept = null;
+                break;
+        }
+
+        row.UpdateTime = now;
+        await _reviewRep.UpdateAsync(row);
     }
 
     public Task OnNodeCompleted(long bizId, string nodeId, string nodeName)
     {
         return Task.CompletedTask;
     }
+
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        var row = await _reviewRep.GetByIdAsync((int)bizId);
+        if (row == null) return new Dictionary<string, object>();
+
+        var winRateLevel = row.WinRate switch
+        {
+            "高" => 3,
+            "中" => 2,
+            "低" => 1,
+            _ => 0,
+        };
+
+        return new Dictionary<string, object>
+        {
+            ["currentStage"] = row.CurrentStage ?? 0,
+            ["winRateLevel"] = winRateLevel,
+            ["hasCrmNo"] = string.IsNullOrWhiteSpace(row.CrmNo) ? 0 : 1,
+        };
+    }
 }

+ 1 - 213
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ContractReviewService.cs

@@ -3,7 +3,7 @@ namespace Admin.NET.Plugin.AiDOP.Order;
 /// <summary>
 /// 合同评审服务 📋
 /// 路由前缀:/api/Order/contract/...
-/// 流程:意见评审(法律事务部→技术售前组→综合主计划→试验站)→意见反馈→二次评审→领导意见→合同盖章
+/// 审批:由审批流插件(BizType=CONTRACT_REVIEW)+ 前端 ApprovalPanel 发起与办理;本服务仅保留列表/详情/保存/删除。
 /// </summary>
 [ApiDescriptionSettings(Order = 250, Description = "合同评审")]
 [Route("api/Order")]
@@ -16,21 +16,6 @@ public class ContractReviewService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<ContractReviewFlow> _flowRep;
     private readonly UserManager _userManager;
 
-    // 流程节点模板(stageNo, stageName, department, deptNo, seq)
-    private static readonly (int StageNo, string StageName, string Department, string DeptNo, int Seq)[] FlowTemplate =
-    {
-        (1, "意见评审", "法律事务部", "DEPT_LAW",  1),
-        (1, "意见评审", "技术售前组", "DEPT_TECH", 2),
-        (1, "意见评审", "综合主计划", "DEPT_PLAN", 3),
-        (1, "意见评审", "试验站",     "DEPT_TEST", 4),
-        (2, "意见反馈", "申请部门",   "",           1),
-        (3, "二次评审", "评审委员会", "",           1),
-        (4, "领导意见", "领导审批",   "",           1),
-        (5, "合同盖章", "合同管理部", "",           1),
-    };
-
-    private static readonly string[] StageNames = { "", "意见评审", "意见反馈", "二次评审", "领导意见", "合同盖章" };
-
     public ContractReviewService(
         ISqlSugarClient db,
         SqlSugarRepository<ContractReview> reviewRep,
@@ -254,203 +239,6 @@ public class ContractReviewService : IDynamicApiController, ITransient
         return new { message = "删除成功" };
     }
 
-    // ══════════════════════════════════════════════════════════════
-    // 提交审批 POST /api/Order/contract/submit
-    // ══════════════════════════════════════════════════════════════
-    /// <summary>提交合同评审(初始化全部流程节点,启动第一个节点)📋</summary>
-    [DisplayName("提交合同评审")]
-    [ApiDescriptionSettings(Name = "SubmitContractReview"), HttpPost("contract/submit")]
-    public async Task<object> SubmitContractReview([FromBody] ContractReviewSubmitInput input)
-    {
-        var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.RecID)
-                     ?? throw Oops.Oh("合同评审记录不存在");
-
-        if (entity.FlowStatus == "reviewing")
-            throw Oops.Oh("该合同评审已在审批中");
-        if (entity.FlowStatus == "completed")
-            throw Oops.Oh("该合同评审已完成,无需再次提交");
-
-        var now = DateTime.Now;
-
-        // 清除旧流程节点(驳回后重新提交时重置所有节点)
-        await _flowRep.DeleteAsync(u => u.ReviewRecID == input.RecID);
-
-        // 初始化全部流程节点(共8个)
-        foreach (var t in FlowTemplate)
-        {
-            var flow = new ContractReviewFlow
-            {
-                ReviewRecID  = entity.RecID,
-                ReviewBillNo = entity.BillNo,
-                StageNo      = t.StageNo,
-                StageName    = t.StageName,
-                Department   = t.Department,
-                DeptNo       = t.DeptNo,
-                Seq          = t.Seq,
-                NodeStatus   = "pending",
-            };
-            await _flowRep.InsertAsync(flow);
-        }
-
-        // 启动第一个节点(意见评审-法律事务部)
-        var firstNode = await _flowRep.GetFirstAsync(u =>
-            u.ReviewRecID == entity.RecID && u.StageNo == 1 && u.Seq == 1);
-        if (firstNode != null)
-        {
-            firstNode.NodeStatus = "reviewing";
-            firstNode.StartTime  = now;
-            await _flowRep.UpdateAsync(firstNode);
-        }
-
-        entity.FlowStatus    = "reviewing";
-        entity.CurrentStage  = 1;
-        entity.CurrentDept   = FlowTemplate[0].Department;
-        entity.UpdateUser    = _userManager.Account ?? "system";
-        entity.UpdateTime    = now;
-        await _reviewRep.UpdateAsync(entity);
-
-        return new { message = "提交成功,流程已启动", currentDept = entity.CurrentDept };
-    }
-
-    // ══════════════════════════════════════════════════════════════
-    // 审批通过 POST /api/Order/contract/approve
-    // ══════════════════════════════════════════════════════════════
-    /// <summary>审批通过当前节点,自动推进到下一节点 📋</summary>
-    [DisplayName("合同评审审批通过")]
-    [ApiDescriptionSettings(Name = "ApproveContractReview"), HttpPost("contract/approve")]
-    public async Task<object> ApproveContractReview([FromBody] ContractReviewApproveInput input)
-    {
-        var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID)
-                     ?? throw Oops.Oh("合同评审记录不存在");
-
-        if (entity.FlowStatus != "reviewing")
-            throw Oops.Oh("当前合同评审不在审批中");
-
-        var now     = DateTime.Now;
-        var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount;
-        var name    = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName;
-
-        // 找当前审批中节点
-        var currentNode = await _flowRep.GetFirstAsync(u =>
-            u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing")
-            ?? throw Oops.Oh("未找到待审批节点,请刷新后重试");
-
-        // 完成当前节点 - 参照 SysJobService.UpdateJobDetail
-        currentNode.ReviewerAccount = account;
-        currentNode.ReviewerName    = name;
-        currentNode.Opinion         = input.Opinion;
-        currentNode.CompleteTime    = now;
-        currentNode.NodeStatus      = "approved";
-        if (currentNode.StartTime.HasValue)
-            currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2);
-        await _flowRep.UpdateAsync(currentNode);
-
-        // 查找下一节点:先找同一stage内的下一seq
-        var nextNode = await _flowRep.GetFirstAsync(u =>
-            u.ReviewRecID == input.ReviewRecID
-            && u.StageNo == currentNode.StageNo
-            && u.Seq == (currentNode.Seq ?? 1) + 1);
-
-        // 若同stage无下一节点,找下一stage的seq=1
-        if (nextNode == null)
-        {
-            nextNode = await _flowRep.GetFirstAsync(u =>
-                u.ReviewRecID == input.ReviewRecID
-                && u.StageNo == currentNode.StageNo + 1
-                && u.Seq == 1);
-        }
-
-        string message;
-        if (nextNode != null)
-        {
-            // 启动下一节点
-            nextNode.NodeStatus = "reviewing";
-            nextNode.StartTime  = now;
-            await _flowRep.UpdateAsync(nextNode);
-
-            entity.CurrentStage = nextNode.StageNo;
-            entity.CurrentDept  = nextNode.Department;
-            message = $"审批通过,已推进至【{nextNode.StageName} - {nextNode.Department}】";
-        }
-        else
-        {
-            // 所有节点均通过,流程完成
-            entity.FlowStatus   = "completed";
-            entity.CurrentStage = 5;
-            entity.CurrentDept  = "已完成";
-            message = "审批完成!所有环节均已通过";
-        }
-
-        entity.UpdateUser = account;
-        entity.UpdateTime = now;
-        await _reviewRep.UpdateAsync(entity);
-
-        return new { message };
-    }
-
-    // ══════════════════════════════════════════════════════════════
-    // 审批驳回 POST /api/Order/contract/reject
-    // ══════════════════════════════════════════════════════════════
-    /// <summary>驳回当前节点,合同评审退回草稿 📋</summary>
-    [DisplayName("合同评审审批驳回")]
-    [ApiDescriptionSettings(Name = "RejectContractReview"), HttpPost("contract/reject")]
-    public async Task<object> RejectContractReview([FromBody] ContractReviewRejectInput input)
-    {
-        var entity = await _reviewRep.GetFirstAsync(u => u.RecID == input.ReviewRecID)
-                     ?? throw Oops.Oh("合同评审记录不存在");
-
-        if (entity.FlowStatus != "reviewing")
-            throw Oops.Oh("当前合同评审不在审批中");
-
-        var now     = DateTime.Now;
-        var account = string.IsNullOrWhiteSpace(input.ReviewerAccount) ? (_userManager.Account ?? "system") : input.ReviewerAccount;
-        var name    = string.IsNullOrWhiteSpace(input.ReviewerName) ? account : input.ReviewerName;
-
-        // 完成当前节点(驳回状态)
-        var currentNode = await _flowRep.GetFirstAsync(u =>
-            u.ReviewRecID == input.ReviewRecID && u.NodeStatus == "reviewing");
-        if (currentNode != null)
-        {
-            currentNode.ReviewerAccount = account;
-            currentNode.ReviewerName    = name;
-            currentNode.Opinion         = input.Opinion;
-            currentNode.CompleteTime    = now;
-            currentNode.NodeStatus      = "rejected";
-            if (currentNode.StartTime.HasValue)
-                currentNode.ActualDays = Math.Round((decimal)(now - currentNode.StartTime.Value).TotalDays, 2);
-            await _flowRep.UpdateAsync(currentNode);
-        }
-
-        entity.FlowStatus  = "rejected";
-        entity.UpdateUser  = account;
-        entity.UpdateTime  = now;
-        await _reviewRep.UpdateAsync(entity);
-
-        return new { message = "已驳回,请修改后重新提交" };
-    }
-
-    // ══════════════════════════════════════════════════════════════
-    // 更新节点审批人 POST /api/Order/contract/flow/update
-    // ══════════════════════════════════════════════════════════════
-    /// <summary>更新流程节点的审批人账号/姓名(在提交前预填)📋</summary>
-    [DisplayName("更新合同评审节点审批人")]
-    [ApiDescriptionSettings(Name = "UpdateContractReviewFlowNode"), HttpPost("contract/flow/update")]
-    public async Task<object> UpdateFlowNode([FromBody] ContractReviewFlowUpdateInput input)
-    {
-        var node = await _flowRep.GetFirstAsync(u => u.RecID == input.FlowRecID)
-                   ?? throw Oops.Oh("流程节点不存在");
-
-        if (!string.IsNullOrWhiteSpace(input.ReviewerAccount))
-            node.ReviewerAccount = input.ReviewerAccount;
-        if (!string.IsNullOrWhiteSpace(input.ReviewerName))
-            node.ReviewerName = input.ReviewerName;
-        if (!string.IsNullOrWhiteSpace(input.Opinion))
-            node.Opinion = input.Opinion;
-
-        await _flowRep.UpdateAsync(node);
-        return new { message = "更新成功" };
-    }
-
     // ──────────────── 内部查询结果映射类 ────────────────
 
     private sealed class ContractReviewListRow

+ 0 - 61
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/ContractReviewDto.cs

@@ -67,64 +67,3 @@ public class ContractReviewDeleteInput
     [Required(ErrorMessage = "RecID不能为空")]
     public int RecID { get; set; }
 }
-
-// ─────────────────────────── 提交入参 ───────────────────────────
-
-/// <summary>提交审批入参</summary>
-public class ContractReviewSubmitInput
-{
-    [Required(ErrorMessage = "RecID不能为空")]
-    public int RecID { get; set; }
-}
-
-// ─────────────────────────── 审批入参 ───────────────────────────
-
-/// <summary>审批通过入参</summary>
-public class ContractReviewApproveInput
-{
-    [Required(ErrorMessage = "ReviewRecID不能为空")]
-    public int ReviewRecID { get; set; }
-
-    /// <summary>审批意见</summary>
-    public string? Opinion { get; set; }
-
-    /// <summary>审批人账号(不传则取当前登录账号)</summary>
-    public string? ReviewerAccount { get; set; }
-
-    /// <summary>审批人姓名(不传则取当前登录账号)</summary>
-    public string? ReviewerName { get; set; }
-}
-
-/// <summary>审批驳回入参</summary>
-public class ContractReviewRejectInput
-{
-    [Required(ErrorMessage = "ReviewRecID不能为空")]
-    public int ReviewRecID { get; set; }
-
-    /// <summary>驳回原因</summary>
-    public string? Opinion { get; set; }
-
-    /// <summary>审批人账号(不传则取当前登录账号)</summary>
-    public string? ReviewerAccount { get; set; }
-
-    /// <summary>审批人姓名(不传则取当前登录账号)</summary>
-    public string? ReviewerName { get; set; }
-}
-
-// ─────────────────────────── 流程节点更新入参 ───────────────────────────
-
-/// <summary>更新流程节点审批人信息入参</summary>
-public class ContractReviewFlowUpdateInput
-{
-    [Required(ErrorMessage = "FlowRecID不能为空")]
-    public int FlowRecID { get; set; }
-
-    /// <summary>审批人账号</summary>
-    public string? ReviewerAccount { get; set; }
-
-    /// <summary>审批人姓名</summary>
-    public string? ReviewerName { get; set; }
-
-    /// <summary>补填意见</summary>
-    public string? Opinion { get; set; }
-}

+ 2 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/OrderDeliveryDto.cs

@@ -214,6 +214,8 @@ public sealed class FlowNbrRow
 {
     public string? Line { get; set; }
     public string? Nbr { get; set; }
+    /// <summary>工单号</summary>
+    public string? WorkOrd { get; set; }
     public string? ItemNum { get; set; }
     public string? Descr { get; set; }
     public string? Descr1 { get; set; }

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

@@ -168,47 +168,34 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
     [HttpGet("delivery/flow/{entryId}")]
     public async Task<object> GetDeliveryFlow(long entryId)
     {
-        // ── 先取工单号 ──
-        const string monoSql = """
-            SELECT a.moentry_mono
-            FROM mes_moentry a
-            JOIN mes_morder b ON a.moentry_mono = b.morder_no
-            WHERE a.soentry_id = @Id
-            """;
-        var mono = await _db.Ado.GetStringAsync(monoSql, new { Id = entryId });
-
-        // ── 并发查询各 Tab ──
-        var basicTask = QueryBasic(entryId);
-        var orderInfoTask = QueryOrderInfo(entryId);
-        var reviewTask = QueryReview(entryId, mono);
-        var changesTask = QueryChanges(entryId);
-        var scheduleTask = QuerySchedule(mono);
-        var materialTask = QueryMaterials(entryId, mono);
-        var poTask = QueryPo(mono);
-        var prTask = QueryPr(mono);
-        var deliveryScheduleTask = QueryDeliverySchedule(mono);
-        var nbrTask = QueryNbr(mono);
-        var prodReportTask = QueryProdReport(mono);
-        var iqcTask = QueryIqc(mono);
-        var fqcTask = QueryFqc(mono);
-        var receiptTask = QueryReceipt(mono);
-        var shipmentTask = QueryShipment(entryId);
-
-        await Task.WhenAll(basicTask, orderInfoTask, reviewTask, changesTask,
-            scheduleTask, materialTask, poTask, prTask, deliveryScheduleTask,
-            nbrTask, prodReportTask, iqcTask, fqcTask, receiptTask, shipmentTask);
+        // 同一 ISqlSugarClient 底层共用一条连接,不能对 _db.Ado 并发执行多查询(MySqlConnector 会报 Cannot Open when State is Connecting)
+        var basic = await QueryBasic(entryId);
+        var orderInfo = await QueryOrderInfo(entryId);
+        var review = await QueryReview(entryId);
+        var changes = await QueryChanges(entryId);
+        var schedule = await QuerySchedule(entryId);
+        var material = await QueryMaterials(entryId);
+        var po = await QueryPo(entryId);
+        var pr = await QueryPr(entryId);
+        var deliverySchedule = await QueryDeliverySchedule(entryId);
+        var nbr = await QueryNbr(entryId);
+        var prodReport = await QueryProdReport(entryId);
+        var iqc = await QueryIqc(entryId);
+        var fqc = await QueryFqc(entryId);
+        var receipt = await QueryReceipt(entryId);
+        var shipment = await QueryShipment(entryId);
 
         return new
         {
-            basic = basicTask.Result,
-            tab1 = orderInfoTask.Result,
-            tab2 = new { review = reviewTask.Result, changes = changesTask.Result },
-            tab3 = scheduleTask.Result,
-            tab4 = materialTask.Result,
-            tab5 = new { po = poTask.Result, pr = prTask.Result, deliverySchedule = deliveryScheduleTask.Result },
-            tab6 = nbrTask.Result,
-            tab7 = new { prodReport = prodReportTask.Result, iqc = iqcTask.Result },
-            tab8 = new { fqc = fqcTask.Result, receipt = receiptTask.Result, shipment = shipmentTask.Result },
+            basic,
+            tab1 = orderInfo,
+            tab2 = new { review, changes },
+            tab3 = schedule,
+            tab4 = material,
+            tab5 = new { po, pr, deliverySchedule },
+            tab6 = nbr,
+            tab7 = new { prodReport, iqc },
+            tab8 = new { fqc, receipt, shipment },
         };
     }
 
@@ -257,284 +244,445 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
     }
 
     // ── Tab2 订单评审 ──
-    private async Task<FlowReviewRow?> QueryReview(long entryId, string? mono)
+    /// <remarks>
+    /// 查询最新评审记录(num=1 的首行物料)的预计交期评估信息。
+    /// </remarks>
+    private async Task<FlowReviewRow?> QueryReview(long entryId)
     {
+        //测试提交
         const string sql = """
             SELECT
-                entry.sys_capacity_date AS SysCapacityDate,
-                mo.moentry_mono         AS MorderNo,
-                entry.create_time       AS CreateTime,
-                mo.make_qty             AS MakeQty,
-                mo.use_qty              AS UseQty,
-                mo.mo_qty               AS MoQty
-            FROM crm_seorderentry entry
-            LEFT JOIN (
-                SELECT a.soentry_id, a.moentry_mono,
-                       b.make_qty, b.use_qty, b.mo_qty
-                FROM mes_moentry a
-                LEFT JOIN mes_morder b ON a.moentry_mono = b.morder_no
-            ) mo ON entry.id = mo.soentry_id
-            WHERE entry.Id = @Id AND entry.IsDeleted = 0
+                b.satisfy_time AS SysCapacityDate,
+                c.moentry_mono AS MorderNo,
+                c.create_time AS CreateTime,
+                IF(b.make_qty = 0 AND b.erp_cls = 1, b.lack_qty, b.make_qty) AS MakeQty,
+                b.use_qty AS UseQty,
+                b.mo_qty AS MoQty
+            FROM b_examine_result a
+            LEFT JOIN b_bom_child_examine b ON a.id = b.examine_id
+            LEFT JOIN mes_moentry c ON a.sentry_id = c.soentry_id
+            LEFT JOIN mes_morder d ON c.moentry_mono = d.morder_no
+            WHERE a.create_time = (
+                SELECT MAX(create_time)
+                FROM b_examine_result
+                WHERE sentry_id = @EntryId
+            )
+            AND b.num = 1
+            AND a.sentry_id = @EntryId
+            LIMIT 1
             """;
-        return (await _db.Ado.SqlQueryAsync<FlowReviewRow>(sql, new { Id = entryId })).FirstOrDefault();
+        return (await _db.Ado.SqlQueryAsync<FlowReviewRow>(sql, new { EntryId = entryId })).FirstOrDefault();
     }
 
-    // ── Tab2 变更记录 ──
+    // ── Tab2 变更记录(MySQL:按订单行关联变更单)──
     private async Task<List<FlowChangeRow>> QueryChanges(long entryId)
     {
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY c.update_time) AS Sno,
-                c.bill_no           AS BillNo,
-                c.change_Type       AS ChangeType,
-                c.change_Reason     AS ChangeReason,
-                c.change_content    AS ChangeContent,
-                c.update_time       AS UpdateTime
-            FROM crm_seorder_change c
-            JOIN crm_seorderentry entry ON c.bill_no = (
-                SELECT s.bill_no FROM crm_seorder s WHERE s.Id = entry.seorder_id
-            )
-            WHERE entry.Id = @Id
-            ORDER BY c.update_time
+                ROW_NUMBER() OVER (ORDER BY a.id) AS Sno,
+                a.bill_no AS BillNo,
+                a.change_Type AS ChangeType,
+                a.change_Reason AS ChangeReason,
+                a.change_content AS ChangeContent,
+                a.update_time AS UpdateTime
+            FROM crm_seorder_change a
+            LEFT JOIN crm_seorderentry b ON a.bill_no = b.bill_no
+            WHERE a.bill_no IS NOT NULL AND b.id = @Id
+            ORDER BY a.id
             """;
         return await _db.Ado.SqlQueryAsync<FlowChangeRow>(sql, new { Id = entryId });
     }
 
-    // ── Tab3 计划排程 ──
-    private async Task<List<FlowScheduleRow>> QuerySchedule(string? mono)
+    // ── Tab3 计划排程1(MySQL:按分录 Id 关联工单排程 + 物料主档)──
+    private async Task<List<FlowScheduleRow>> QuerySchedule(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY WorkDate) AS Sno,
-                WorkOrds, WorkDate, Line, Op, WorkQty,
-                ItemNum, Descr, Descr1, CreateTime
-            FROM PeriodSequenceDet
-            WHERE WorkOrds = @Mono
-            ORDER BY WorkDate
+                ROW_NUMBER() OVER (ORDER BY sch.PlanDate) AS Sno,
+                sch.WorkOrds,
+                DATE(sch.PlanDate) AS WorkDate,
+                sch.Line,
+                sch.Op,
+                CAST(sch.OrdQty AS DECIMAL(18, 2)) AS WorkQty,
+                sch.ItemNum,
+                im.Descr,
+                im.Descr1,
+                sch.CreateTime
+            FROM PeriodSequenceDet sch
+            LEFT JOIN ItemMaster im ON sch.ItemNum = im.ItemNum
+            LEFT JOIN mes_morder mo ON mo.morder_no = sch.WorkOrds
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid
+            LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
+            WHERE se.id = @EntryId AND mo.IsDeleted = 0
+            ORDER BY sch.PlanDate
             """;
-        return await _db.Ado.SqlQueryAsync<FlowScheduleRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowScheduleRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab4 物料需求 ──
-    private async Task<List<FlowMaterialRow>> QueryMaterials(long entryId, string? mono)
+    // ── Tab4 物料需求(MySQL:最新评审结果 + BOM 子件明细,同需求明细核验口径)──
+    private async Task<List<FlowMaterialRow>> QueryMaterials(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY num) AS Sno,
-                num, item_number AS ItemNumber, item_name AS ItemName,
-                model AS Model, unit AS Unit, bom_number AS BomNumber,
-                kitting_time AS KittingTime, erp_cls_name AS ErpClsName,
-                backflush AS Backflush, qty AS Qty, scrap AS Scrap,
-                wastage AS Wastage, needCount AS NeedCount,
-                self_lack_qty AS SelfLackQty, sqty AS Sqty,
-                use_qty AS UseQty, lack_qty AS LackQty,
-                mo_qty AS MoQty, make_qty AS MakeQty,
-                purchase_qty AS PurchaseQty,
-                purchase_occupy_qty AS PurchaseOccupyQty,
-                subcontracting_qty AS SubcontractingQty,
-                satisfy_time AS SatisfyTime,
-                type AS Type, haveicsubs AS HaveIcSubs,
-                substitute_code AS SubstituteCode
-            FROM b_kitting_detail
-            WHERE mono = @Mono
-            ORDER BY num
+                ROW_NUMBER() OVER (ORDER BY bce.id) AS Sno,
+                CAST(bce.num AS CHAR) AS Num,
+                bce.item_number AS ItemNumber,
+                bce.item_name AS ItemName,
+                bce.bom_number AS BomNumber,
+                bce.model AS Model,
+                DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
+                CASE WHEN bce.type = 1 THEN '替代件' ELSE '标准件' END AS Type,
+                bce.unit AS Unit,
+                CASE bce.erp_cls
+                    WHEN 0 THEN '配置类'
+                    WHEN 1 THEN '自制'
+                    WHEN 2 THEN '委外加工'
+                    WHEN 3 THEN '外购'
+                    WHEN 4 THEN '虚拟件'
+                END AS ErpClsName,
+                CAST(bce.qty AS DECIMAL(18, 2)) AS Qty,
+                CAST(bce.needCount AS DECIMAL(18, 2)) AS NeedCount,
+                CAST(bce.sqty AS DECIMAL(18, 2)) AS Sqty,
+                CAST(bce.use_qty AS DECIMAL(18, 2)) AS UseQty,
+                CAST(bce.self_lack_qty AS DECIMAL(18, 2)) AS SelfLackQty,
+                CAST(bce.lack_qty AS DECIMAL(18, 2)) AS LackQty,
+                CAST(bce.mo_qty AS DECIMAL(18, 2)) AS MoQty,
+                CAST(IF(bce.make_qty = 0 AND bce.erp_cls = 1, bce.lack_qty, bce.make_qty) AS DECIMAL(18, 2)) AS MakeQty,
+                CAST(bce.purchase_qty AS DECIMAL(18, 2)) AS PurchaseQty,
+                CAST(bce.purchase_occupy_qty AS DECIMAL(18, 2)) AS PurchaseOccupyQty,
+                CAST(bce.subcontracting_qty AS DECIMAL(18, 2)) AS SubcontractingQty,
+                CASE WHEN bce.backflush = 1 THEN '是' ELSE '否' END AS Backflush,
+                DATE_FORMAT(bce.satisfy_time, '%Y-%m-%d') AS SatisfyTime,
+                CASE WHEN bce.haveicsubs = 1 THEN '是' ELSE '否' END AS HaveIcSubs,
+                bce.substitute_code AS SubstituteCode
+            FROM crm_seorder se
+            INNER JOIN crm_seorderentry sen ON se.id = sen.seorder_id
+            LEFT JOIN b_examine_result ber ON ber.Id = (
+                SELECT br.Id
+                FROM b_examine_result br
+                WHERE br.sorderid = se.id AND br.sentry_id = sen.id
+                ORDER BY br.create_time DESC
+                LIMIT 1
+            )
+            INNER JOIN b_bom_child_examine bce ON ber.Id = bce.examine_id AND bce.is_use = 1
+            WHERE sen.id = @EntryId
+            ORDER BY bce.id ASC
             """;
-        return await _db.Ado.SqlQueryAsync<FlowMaterialRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowMaterialRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab5 采购订单 ──
-    private async Task<List<FlowPoRow>> QueryPo(string? mono)
+    // ── Tab5 采购订单(MySQL:经占用/工单关联到订单分录)──
+    private async Task<List<FlowPoRow>> QueryPo(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY po.create_time) AS Sno,
-                po.po_billno        AS PoBillNo,
-                po.supplier_no      AS SupplierNo,
-                po.supplier_name    AS SupplierName,
-                po.po_total         AS PoTotal,
-                po.po_purchaser     AS PoPurchaser,
-                po.state            AS State,
-                po.po_ssend_date    AS PoSsendDate,
-                po.create_time      AS CreateTime
-            FROM srm_po_main po
-            WHERE po.pr_mono = @Mono
-            ORDER BY po.create_time
+                ROW_NUMBER() OVER (ORDER BY t.PoBillNo) AS Sno,
+                t.PoBillNo,
+                t.SupplierNo,
+                t.SupplierName,
+                t.PoTotal,
+                t.PoPurchaser,
+                t.State,
+                t.PoSsendDate,
+                t.CreateTime
+            FROM (
+                SELECT DISTINCT
+                    pm.po_billno AS PoBillNo,
+                    pm.supplier_no AS SupplierNo,
+                    pm.supplier_name AS SupplierName,
+                    CAST(pm.po_total AS DECIMAL(18, 2)) AS PoTotal,
+                    pm.po_purchaser AS PoPurchaser,
+                    pm.create_time AS CreateTime,
+                    pm.po_ssend_date AS PoSsendDate,
+                    CASE pm.state
+                        WHEN 0 THEN '新增'
+                        WHEN 1 THEN '审核中'
+                        WHEN 2 THEN '同意'
+                        WHEN 3 THEN '关闭'
+                    END AS State
+                FROM srm_po_main pm
+                LEFT JOIN srm_po_list pl ON pm.id = pl.po_id
+                LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id AND pm.factory_id = occ.factory_id
+                LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
+                LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid
+                LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
+                WHERE se.id = @EntryId AND pm.IsDeleted = 0
+            ) AS t
+            ORDER BY t.PoBillNo
             """;
-        return await _db.Ado.SqlQueryAsync<FlowPoRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowPoRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab5 采购申请 ──
-    private async Task<List<FlowPrRow>> QueryPr(string? mono)
+    // ── Tab5 采购申请(MySQL)──
+    private async Task<List<FlowPrRow>> QueryPr(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY pr.create_time) AS Sno,
-                pr.pr_billno            AS PrBillNo,
-                pr.number               AS Number,
-                pr.icitem_name          AS IcItemName,
-                pr.model                AS Model,
-                pr.pr_purchasenumber    AS PrPurchaseNumber,
-                pr.pr_purchasename      AS PrPurchaseName,
-                pr.pr_aqty              AS PrAQty,
-                pr.pr_ssend_date        AS PrSsendDate,
-                pr.pr_sarrive_date      AS PrSarriveDate,
-                pr.pr_unit              AS PrUnit,
-                pr.pr_purchaser         AS PrPurchaser
-            FROM srm_pr_main pr
-            WHERE pr.pr_mono = @Mono AND pr.state > 1
-            ORDER BY pr.create_time
+                ROW_NUMBER() OVER (ORDER BY pm.pr_billno) AS Sno,
+                pm.pr_billno AS PrBillNo,
+                ic.number AS Number,
+                pm.icitem_name AS IcItemName,
+                ic.model AS Model,
+                pm.pr_purchasenumber AS PrPurchaseNumber,
+                pm.pr_purchasename AS PrPurchaseName,
+                CAST(pm.pr_aqty AS DECIMAL(18, 2)) AS PrAQty,
+                pm.pr_ssend_date AS PrSsendDate,
+                pm.pr_sarrive_date AS PrSarriveDate,
+                pm.pr_unit AS PrUnit,
+                pm.pr_purchaser AS PrPurchaser
+            FROM srm_pr_main pm
+            LEFT JOIN srm_po_occupy occ ON pm.Id = occ.polist_id AND pm.factory_id = occ.factory_id
+            LEFT JOIN ic_item ic ON pm.icitem_id = ic.Id
+            LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
+            LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid
+            LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
+            WHERE se.id = @EntryId AND pm.state <> 0 AND pm.IsDeleted = 0
+            ORDER BY pm.pr_billno
             """;
-        return await _db.Ado.SqlQueryAsync<FlowPrRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowPrRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab5 物料交货单 ──
-    private async Task<List<FlowDeliveryScheduleRow>> QueryDeliverySchedule(string? mono)
+    // ── Tab5 物料交货单(MySQL:srm_polist_ds)──
+    private async Task<List<FlowDeliveryScheduleRow>> QueryDeliverySchedule(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY d.requestdate) AS Sno,
-                d.ponumber      AS PoNumber,
-                d.dsnum         AS DsNum,
-                d.itemnum       AS ItemNum,
-                d.Descr         AS Descr,
-                d.suppliercode  AS SupplierCode,
-                d.supplier      AS Supplier,
-                d.requestdate   AS RequestDate,
-                d.needdate      AS NeedDate,
-                d.schedqty      AS SchedQty,
-                d.sentqty       AS SentQty,
-                d.restqty       AS RestQty
-            FROM srm_delivery_det d
-            WHERE d.mono = @Mono
-            ORDER BY d.requestdate
+                ROW_NUMBER() OVER (ORDER BY t.DsNum) AS Sno,
+                t.PoNumber,
+                t.DsNum,
+                t.ItemNum,
+                t.Descr,
+                t.SupplierCode,
+                t.Supplier,
+                t.RequestDate,
+                t.NeedDate,
+                t.SchedQty,
+                t.SentQty,
+                t.RestQty
+            FROM (
+                SELECT
+                    ds.dsnum AS DsNum,
+                    ds.ponumber AS PoNumber,
+                    ds.itemnum AS ItemNum,
+                    im.Descr AS Descr,
+                    ds.suppliercode AS SupplierCode,
+                    ds.supplier AS Supplier,
+                    ds.requestdate AS RequestDate,
+                    ds.needdate AS NeedDate,
+                    ds.schedqty AS SchedQty,
+                    ds.sentqty AS SentQty,
+                    ds.restqty AS RestQty
+                FROM srm_po_main pm
+                LEFT JOIN srm_po_list pl ON pm.id = pl.po_id
+                LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id AND pm.factory_id = occ.factory_id
+                LEFT JOIN mes_morder mo ON mo.id = occ.morder_id
+                LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid
+                LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
+                LEFT JOIN srm_polist_ds ds ON pl.po_billno = ds.ponumber AND pl.ItemNum = ds.itemnum
+                LEFT JOIN ItemMaster im ON ds.itemnum = im.ItemNum
+                WHERE se.id = @EntryId AND pm.IsDeleted = 0
+            ) AS t
+            ORDER BY t.DsNum
             """;
-        return await _db.Ado.SqlQueryAsync<FlowDeliveryScheduleRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowDeliveryScheduleRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab6 生产备料 ──
-    private async Task<List<FlowNbrRow>> QueryNbr(string? mono)
+    // ── Tab6 生产备料(MySQL:备料明细经工单关联分录)──
+    private async Task<List<FlowNbrRow>> QueryNbr(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                CAST(d.Line AS CHAR) AS Line,
-                n.Nbr, d.ItemNum, d.Descr, d.Descr1,
-                d.LocationFrom, d.LocationTo,
-                d.CurrQtyOpened, d.QtyOrd, d.QtyFrom, d.QtyRec,
-                d.unit, d.UpdateTime
-            FROM NbrMaster n
-            JOIN NbrDetail d ON n.Nbr = d.Nbr
-            WHERE n.WorkOrd = @Mono AND n.type = 'SM'
-            ORDER BY d.Line
+                CAST(nd.Line AS CHAR) AS Line,
+                nm.Nbr AS Nbr,
+                nd.ItemNum,
+                im.Descr AS Descr,
+                im.Descr1 AS Descr1,
+                CAST(nd.QtyFrom AS DECIMAL(18, 2)) AS QtyFrom,
+                CAST(nd.QtyOrd AS DECIMAL(18, 2)) AS QtyOrd,
+                CAST(nd.CurrQtyOpened AS DECIMAL(18, 2)) AS CurrQtyOpened,
+                CAST(nd.QtyRec AS DECIMAL(18, 2)) AS QtyRec,
+                nd.UM AS Unit,
+                nd.LocationFrom,
+                nd.LocationTo,
+                nd.UpdateTime AS UpdateTime,
+                nd.WorkOrd AS WorkOrd
+            FROM NbrDetail nd
+            LEFT JOIN NbrMaster nm ON nd.nbr = nm.nbr
+            LEFT JOIN mes_morder mo ON mo.morder_no = nm.WorkOrd
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid
+            LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id AND mo.product_code = se.item_number
+            LEFT JOIN ItemMaster im ON nd.ItemNum = im.ItemNum
+            WHERE se.id = @EntryId AND mo.IsDeleted = 0 AND nm.Type = 'SM'
+            ORDER BY nm.Nbr
             """;
-        return await _db.Ado.SqlQueryAsync<FlowNbrRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowNbrRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab7 生产报工 ──
-    private async Task<List<FlowProdReportRow>> QueryProdReport(string? mono)
+    // ── Tab7 生产报工(OpTransEmployee + 销售订单行)──
+    private async Task<List<FlowProdReportRow>> QueryProdReport(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY r.ProdDate) AS Sno,
-                r.WorkOrd, r.ItemNum, r.Descr, r.Descr1, r.Um,
-                r.QtyCompleted, r.QtyReject, r.QtyScrapped,
-                r.ProdDate, r.Op, r.Name
-            FROM mes_production_report r
-            WHERE r.WorkOrd = @Mono
-            ORDER BY r.ProdDate
+                ROW_NUMBER() OVER (ORDER BY ote.ProdDate DESC) AS Sno,
+                ote.WorkOrd,
+                us.Name AS Name,
+                ote.Op,
+                ote.ProdDate,
+                im.ItemNum,
+                im.Descr,
+                im.Descr1,
+                im.Um,
+                CAST(ote.QtyCompleted AS DECIMAL(18,2)) AS QtyCompleted,
+                CAST(ote.QtyReject AS DECIMAL(18,2)) AS QtyReject,
+                CAST(ote.QtyScrapped AS DECIMAL(18,2)) AS QtyScrapped
+            FROM OpTransEmployee ote
+            LEFT JOIN mes_morder mo ON mo.morder_no = ote.WorkOrd
+            INNER JOIN mes_moentry me ON mo.Id = me.moentry_moid
+            LEFT JOIN crm_seorderentry se ON se.Id = me.soentry_id
+            LEFT JOIN ItemMaster im ON ote.ItemNum = im.ItemNum
+            LEFT JOIN rf_user us ON ote.Name = us.account
+            WHERE se.id = @EntryId AND mo.IsDeleted = 0
+            ORDER BY ote.ProdDate DESC
             """;
-        return await _db.Ado.SqlQueryAsync<FlowProdReportRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowProdReportRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab7 IQC信息 ──
-    private async Task<List<FlowIqcRow>> QueryIqc(string? mono)
+    // ── Tab7 IQC信息(请购/采购/检验链路)──
+    private async Task<List<FlowIqcRow>> QueryIqc(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY i.FAPPLYTIME) AS Line,
-                i.po_bill       AS PoBill,
-                i.po_billline   AS PoBillLine,
-                i.FBILLNO       AS FBillNo,
-                i.FMATERIALCFG  AS FMaterialCfg,
-                i.wlmc          AS Wlmc,
-                i.ggxh          AS Ggxh,
-                i.FRINSQTY      AS FRinsQty,
-                i.FAPPLYTIME    AS FApplyTime,
-                i.FINSPESTARTDATE   AS FInspeStartDate,
-                i.FINSPECTSTATUS    AS FInspecStatus
-            FROM mes_iqc_bill i
-            WHERE i.mono = @Mono
-            ORDER BY i.FAPPLYTIME
+                ROW_NUMBER() OVER (ORDER BY iqcjy.FAPPLYTIME DESC) AS Line,
+                iqcjy.po_bill AS PoBill,
+                iqcjy.po_billline AS PoBillLine,
+                iqcjy.FBILLNO AS FBillNo,
+                iqcjy.FMATERIALCFG AS FMaterialCfg,
+                iqcjy.wlmc AS Wlmc,
+                iqcjy.ggxh AS Ggxh,
+                CAST(iqcjy.FRINSQTY AS DECIMAL(18,2)) AS FRinsQty,
+                iqcjy.FAPPLYTIME AS FApplyTime,
+                iqcjy.FINSPESTARTDATE AS FInspeStartDate,
+                iqcjy.FINSPECTSTATUS AS FInspecStatus
+            FROM srm_pr_main pr
+            LEFT JOIN PurOrdDetail pod ON pr.pr_billno = pod.Req
+            LEFT JOIN vscm_cgshrk sh ON pod.PurOrd = sh.OrdNbr AND pod.Line = sh.OrdLine
+            LEFT JOIN scm_jhjh_jq jq ON pod.PurOrd = jq.cgdd AND pod.Line = jq.ddhh
+            LEFT JOIN (
+                SELECT
+                    f.po_bill,
+                    f.po_billline,
+                    a.FINSPESTARTDATE,
+                    b.FAPPLYTIME,
+                    b.FBILLNO,
+                    a.FMATERIALCFG,
+                    a.wlmc,
+                    a.ggxh,
+                    a.FRINSQTY,
+                    c.FINSPECTSTATUS
+                FROM qms_qcp_inspbill a
+                LEFT JOIN qms_qcp_inspecapplyn b ON a.lydjbh = b.FBILLNO
+                LEFT JOIN qms_qcp_insappnentry c ON b.id = c.glid
+                LEFT JOIN scm_shd cd ON c.shdh = cd.shddh
+                LEFT JOIN scm_shdzb f ON f.shpc = c.FLOTNUMBER AND f.sh_material_code = c.FMATERIALCFG AND f.glid = cd.id
+            ) iqcjy ON pod.PurOrd = iqcjy.po_bill AND pod.Line = iqcjy.po_billline
+            WHERE jq.flag = '1'
+              AND pr.sentry_id IS NOT NULL
+              AND pr.sentry_id = @EntryId
+            ORDER BY iqcjy.FAPPLYTIME DESC
             """;
-        return await _db.Ado.SqlQueryAsync<FlowIqcRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowIqcRow>(sql, new { EntryId = entryId });
     }
 
     // ── Tab8 FQC信息 ──
-    private async Task<List<FlowFqcRow>> QueryFqc(string? mono)
+    private async Task<List<FlowFqcRow>> QueryFqc(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY f.FINSPEENDDATE) AS Sno,
-                f.WorkOrd, f.sczld, f.FBILLNO,
-                f.FMATERIALCFG  AS FMaterialCfg,
-                f.wlmc          AS Wlmc,
-                f.ggxh          AS Ggxh,
-                f.scph          AS Scph,
-                f.FRINSQTY      AS FRinsQty,
-                f.FINSPEENDDATE AS FInspeEndDate
-            FROM mes_fqc_bill f
-            WHERE f.WorkOrd = @Mono
-            ORDER BY f.FINSPEENDDATE
+                ROW_NUMBER() OVER (ORDER BY a.FBILLNO ASC) AS Sno,
+                b.WorkOrd,
+                a.sczld,
+                a.FBILLNO,
+                a.FMATERIALCFG AS FMaterialCfg,
+                a.wlmc,
+                a.ggxh,
+                a.scph,
+                CAST(a.FRINSQTY AS DECIMAL(18,2)) AS FRinsQty,
+                a.FINSPEENDDATE AS FInspeEndDate
+            FROM qms_qcpp_inspbill a
+            LEFT JOIN WorkOrdMaster b ON a.sczld = b.Batch
+            LEFT JOIN mes_moentry c ON b.WorkOrd = c.moentry_mono
+            WHERE c.soentry_id = @EntryId
+            ORDER BY a.FBILLNO
             """;
-        return await _db.Ado.SqlQueryAsync<FlowFqcRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowFqcRow>(sql, new { EntryId = entryId });
     }
 
     // ── Tab8 生产入库 ──
-    private async Task<List<FlowReceiptRow>> QueryReceipt(string? mono)
+    private async Task<List<FlowReceiptRow>> QueryReceipt(long entryId)
     {
-        if (string.IsNullOrWhiteSpace(mono)) return new();
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY r.CreateTime) AS Sno,
-                r.Status, r.WorkOrd, r.itemnum AS ItemNum, r.descr AS Descr,
-                r.UM, r.QtyOrded, r.CompQty,
-                r.inspection_number AS InspectionNumber,
-                r.qualified_number  AS QualifiedNumber,
-                r.QtyChangeAdvance,
-                r.moentry_prdname   AS MoentryPrdName,
-                r.moentry_wrkcname  AS MoentryWrkcName,
-                r.OrdDate, r.DueDate, r.CreateTime
-            FROM mes_production_receipt r
-            WHERE r.WorkOrd = @Mono
-            ORDER BY r.CreateTime
+                ROW_NUMBER() OVER (ORDER BY mo.morder_no) AS Sno,
+                CASE
+                    WHEN c.Status = 'r' THEN '下达'
+                    WHEN c.Status = 'c' THEN '关闭'
+                    WHEN c.Status = 'w' THEN '投产'
+                    ELSE '初始'
+                END AS Status,
+                c.WorkOrd,
+                im.Um,
+                c.QtyOrded,
+                IFNULL(compp.CompQty, 0) AS CompQty,
+                NULL AS InspectionNumber,
+                NULL AS QualifiedNumber,
+                CASE WHEN a.QtyChange > 0 THEN a.QtyChange ELSE 0 END AS QtyChangeAdvance,
+                mo.moentry_prdname,
+                mo.moentry_wrkcname,
+                c.OrdDate,
+                c.DueDate,
+                a.CreateTime,
+                c.itemnum AS ItemNum,
+                im.descr AS Descr
+            FROM WorkOrdMaster c
+            LEFT JOIN InvTransHist a ON a.WorkOrd = c.WorkOrd AND a.TransType = 'rct-wo' AND a.QtyChange > 0
+            LEFT JOIN GeneralizedCodeMaster b ON a.TransType = b.Val AND b.FldName = 'TransType' AND b.`Domain` = c.`Domain`
+            LEFT JOIN ItemMaster im ON c.ItemNum = im.ItemNum AND im.`Domain` = c.`Domain`
+            LEFT JOIN PeriodSequenceDet det ON det.ItemNum = im.ItemNum AND det.WorkOrds = c.WorkOrd AND det.`Domain` = c.`Domain`
+            LEFT JOIN (
+                SELECT WorkOrds, SUM(IFNULL(CompQty, 0)) AS CompQty
+                FROM PeriodSequenceDet
+                GROUP BY WorkOrds
+            ) compp ON c.WorkOrd = compp.WorkOrds
+            LEFT JOIN mes_morder mo ON mo.morder_no = c.WorkOrd AND mo.IsDeleted = 0
+            LEFT JOIN mes_moentry motry ON motry.moentry_moid = mo.Id AND motry.IsDeleted = 0
+            WHERE motry.soentry_id = @EntryId
+            ORDER BY mo.morder_no
             """;
-        return await _db.Ado.SqlQueryAsync<FlowReceiptRow>(sql, new SugarParameter("@Mono", mono));
+        return await _db.Ado.SqlQueryAsync<FlowReceiptRow>(sql, new { EntryId = entryId });
     }
 
-    // ── Tab8 成品发运 ──
+    // ── Tab8 成品发运(与订单行 OrdNbr + 行物料一致)──
     private async Task<List<FlowShipmentRow>> QueryShipment(long entryId)
     {
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY d.ShipDate) AS Sno,
-                d.Id, d.Location, d.ShipDate,
-                d.QtyToShip, d.RealQty,
-                d.Location AS LocationCode
-            FROM ASNBOLShipperDetail d
-            JOIN crm_seorderentry entry ON d.ContainerItem = entry.item_number
-            JOIN crm_seorder sorder     ON d.OrdNbr = sorder.bill_no
-                                       AND sorder.Id = entry.seorder_id
-            WHERE entry.Id = @Id AND d.IsActive = 1
-            ORDER BY d.ShipDate
+                ROW_NUMBER() OVER (ORDER BY ad.Id) AS Sno,
+                ad.Id,
+                ad.location AS Location,
+                ad.UpdateTime AS ShipDate,
+                CAST(ad.QtyToShip AS DECIMAL(18,2)) AS QtyToShip,
+                lm.LocationCode,
+                ad.RealQty
+            FROM ASNBOLShipperDetail ad
+            INNER JOIN ASNBOLShipperMaster am ON ad.ASNBOLShipperRecID = am.RecID
+            LEFT JOIN LocationMaster lm ON ad.location = lm.Location
+            INNER JOIN crm_seorderentry se ON se.id = @EntryId
+                AND ad.ContainerItem = se.item_number
+            INNER JOIN crm_seorder so ON so.Id = se.seorder_id AND ad.OrdNbr = so.bill_no
+            WHERE am.shtype = 'SH'
+              AND am.typed <> 'S'
+            ORDER BY ad.Id
             """;
-        return await _db.Ado.SqlQueryAsync<FlowShipmentRow>(sql, new { Id = entryId });
+        return await _db.Ado.SqlQueryAsync<FlowShipmentRow>(sql, new { EntryId = entryId });
     }
 }

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

@@ -201,7 +201,7 @@ public class ProductDesignService : IDynamicApiController, ITransient
     }
 
     private static ProductDesign MapMaster(ProductDesign entity, ProductDesignSaveInput input, string user, DateTime now, bool isNew)
-    {
+    { 
         entity.ContractNo = string.IsNullOrWhiteSpace(input.ContractNo) ? null : input.ContractNo.Trim();
         entity.ProductKind = input.ProductKind;
         entity.DesignLeadAccount = string.IsNullOrWhiteSpace(input.DesignLeadAccount) ? null : input.DesignLeadAccount.Trim();

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

@@ -467,25 +467,6 @@ public class SeOrderService : IDynamicApiController, ITransient
         return new { instanceId };
     }
 
-    /// <summary>提交合同评审审批流程</summary>
-    [DisplayName("提交合同评审审批")]
-    [HttpPost("seorder/submitContractReview")]
-    public async Task<object> SubmitContractReview([FromBody] SubmitReviewInput input)
-    {
-        var order = await _seOrderRep.GetByIdAsync(input.OrderId)
-            ?? throw Oops.Oh("订单不存在");
-        var engine = App.GetRequiredService<Admin.NET.Plugin.ApprovalFlow.Service.FlowEngineService>();
-        var instanceId = await engine.StartFlow(new Admin.NET.Plugin.ApprovalFlow.Service.StartFlowInput
-        {
-            BizType = "CONTRACT_REVIEW",
-            BizId = order.Id,
-            BizNo = order.BillNo,
-            Title = $"合同评审-{order.BillNo}",
-            Comment = input.Comment,
-        });
-        return new { instanceId };
-    }
-
     public class SubmitReviewInput
     {
         public long OrderId { get; set; }

+ 22 - 0
server/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/FlowEngine/FlowEngineService.cs

@@ -1,4 +1,5 @@
 using System.Text.Json;
+using Admin.NET.Core.Service;
 
 namespace Admin.NET.Plugin.ApprovalFlow.Service;
 
@@ -15,6 +16,7 @@ public class FlowEngineService : ITransient
     private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
     private readonly SqlSugarRepository<SysUser> _userRep;
     private readonly UserManager _userManager;
+    private readonly SysOrgService _sysOrgService;
     private readonly FlowNotifyService _notifyService;
 
     public FlowEngineService(
@@ -25,6 +27,7 @@ public class FlowEngineService : ITransient
         SqlSugarRepository<SysUserRole> userRoleRep,
         SqlSugarRepository<SysUser> userRep,
         UserManager userManager,
+        SysOrgService sysOrgService,
         FlowNotifyService notifyService)
     {
         _flowRep = flowRep;
@@ -34,9 +37,21 @@ public class FlowEngineService : ITransient
         _userRoleRep = userRoleRep;
         _userRep = userRep;
         _userManager = userManager;
+        _sysOrgService = sysOrgService;
         _notifyService = notifyService;
     }
 
+    /// <summary>
+    /// 机构数据权限依赖实体的 OrgId;插入时必须与当前用户机构一致,否则非超管在「我发起的/待办」中查不到(OrgId=0 会被过滤)。
+    /// </summary>
+    private async Task<long> ResolveOrgIdForNewFlowEntityAsync()
+    {
+        var oid = _userManager.OrgId;
+        if (oid > 0) return oid;
+        var list = await _sysOrgService.GetUserOrgIdList();
+        return list is { Count: > 0 } ? list[0] : 0;
+    }
+
     // ═══════════════════════════════════════════
     //  核心生命周期
     // ═══════════════════════════════════════════
@@ -57,6 +72,7 @@ public class FlowEngineService : ITransient
         var flowData = JsonSerializer.Deserialize<ApprovalFlowItem>(flow.FlowJson)
             ?? throw Oops.Oh("FlowJson 反序列化失败");
 
+        var orgId = await ResolveOrgIdForNewFlowEntityAsync();
         var instance = new ApprovalFlowInstance
         {
             FlowId = flow.Id,
@@ -70,6 +86,7 @@ public class FlowEngineService : ITransient
             Status = FlowInstanceStatusEnum.Running,
             FlowJsonSnapshot = flow.FlowJson,
             StartTime = DateTime.Now,
+            OrgId = orgId,
         };
 
         var startNode = flowData.Nodes.FirstOrDefault(n =>
@@ -158,6 +175,7 @@ public class FlowEngineService : ITransient
         await _taskRep.AsUpdateable(task).ExecuteCommandAsync();
 
         var targetUser = await _userRep.GetByIdAsync(targetUserId);
+        var instForOrg = await _instanceRep.GetByIdAsync(task.InstanceId);
         var newTask = new ApprovalFlowTask
         {
             InstanceId = task.InstanceId,
@@ -166,6 +184,7 @@ public class FlowEngineService : ITransient
             AssigneeId = targetUserId,
             AssigneeName = targetUser?.RealName,
             Status = FlowTaskStatusEnum.Pending,
+            OrgId = instForOrg?.OrgId ?? 0,
         };
         await _taskRep.InsertAsync(newTask);
 
@@ -261,6 +280,7 @@ public class FlowEngineService : ITransient
         var task = await GetPendingTask(taskId);
         var targetUser = await _userRep.GetByIdAsync(targetUserId);
 
+        var instForAddSign = await _instanceRep.GetByIdAsync(task.InstanceId);
         var newTask = new ApprovalFlowTask
         {
             InstanceId = task.InstanceId,
@@ -271,6 +291,7 @@ public class FlowEngineService : ITransient
             Status = FlowTaskStatusEnum.Pending,
             IsAddSign = true,
             AddSignById = _userManager.UserId,
+            OrgId = instForAddSign?.OrgId ?? 0,
         };
         await _taskRep.InsertAsync(newTask);
 
@@ -370,6 +391,7 @@ public class FlowEngineService : ITransient
             AssigneeId = a.userId,
             AssigneeName = a.userName,
             Status = FlowTaskStatusEnum.Pending,
+            OrgId = instance.OrgId,
         }).ToList();
 
         await _taskRep.AsInsertable(tasks).ExecuteCommandAsync();