Pārlūkot izejas kodu

feat(s6): add ipqc inspector supervisor flow

检验员提交→主管审核/退回最小闭环(复用 ApprovalFlow,BizType=IPQC_INSPECTION,BizId=qms_gcjyd.id):
- 流程服务/常量/BizHandler(no-op)/DTO/轻量扩展表 AdoS6IpqcInspectionFlowState/Seed(角色+流程定义,无SQL)
- 仅写 qms_gcjyd 头白名单列,不回写 status,审核信息落扩展表,不写库存/生产/仓储
- 接线修复:列表/详情暴露 sourceRowId(qms_gcjyd.id)供流程 BizId
bump version Web 2.4.248 / server 1.0.246
YY968XX 2 nedēļas atpakaļ
vecāks
revīzija
f2331d4b16

+ 1 - 1
Web/package.json

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

+ 4 - 0
Web/src/views/aidop/s6/api/ipqcInspection.ts

@@ -49,6 +49,8 @@ export interface IpqcInspectionRow {
 	inspecStandardVersion?: string;
 	inspecStandardVersion?: string;
 	/** 检规编号 inspec_standard_code */
 	/** 检规编号 inspec_standard_code */
 	inspecStandardCode?: string;
 	inspecStandardCode?: string;
+	/** 源检验单 id(qms_gcjyd.id;流程 BizId 用此) */
+	sourceRowId?: number;
 }
 }
 
 
 export function fetchIpqcInspectionList(params: any) {
 export function fetchIpqcInspectionList(params: any) {
@@ -123,6 +125,8 @@ export interface IpqcInspectionDetail {
 	inspecStandardCode?: string;
 	inspecStandardCode?: string;
 	/** 检验状态 status */
 	/** 检验状态 status */
 	inspectionStatus?: string;
 	inspectionStatus?: string;
+	/** 源检验单 id(qms_gcjyd.id;流程 BizId 用此) */
+	sourceRowId?: number;
 	/** 明细行(确定子集列;j1~j70 后置不返回) */
 	/** 明细行(确定子集列;j1~j70 后置不返回) */
 	lines?: IpqcInspectionDetailLine[];
 	lines?: IpqcInspectionDetailLine[];
 }
 }

+ 58 - 0
Web/src/views/aidop/s6/api/ipqcInspectionFlow.ts

@@ -0,0 +1,58 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+// ── S6 过程检验单 检验员+主管最小审批流(写接口,正常鉴权)──
+// 后端 api/IpqcInspectionFlow。BizType=IPQC_INSPECTION。N1_INSPECT→N2_SUP_REVIEW→end。
+// 不做不合格处置/SQE/生产/仓储/库存/外部推送。
+export interface IpqcFlowState {
+	bizType?: string;
+	bizId?: number;
+	instanceId?: number | null;
+	flowStatus?: string;
+	businessStatus?: string | null;
+	currentNodeCode?: string | null;
+	currentNodeName?: string | null;
+	currentAssigneeRole?: string | null;
+	canSubmitResult?: boolean;
+	canSupervisorApprove?: boolean;
+	canSupervisorReturn?: boolean;
+	isCompleted?: boolean;
+	inspectorName?: string | null;
+	supervisorName?: string | null;
+	updatedAt?: string | null;
+	message?: string | null;
+}
+
+export interface IpqcSubmitInspectionPayload {
+	id: number;
+	/** 结果判定 jgpd(可空,枚举未确认不强写) */
+	jgpd?: number | null;
+	/** 检验合格数量 jyhgsl */
+	jyhgsl: number;
+	/** 检验不合格数量 jybhgsl */
+	jybhgsl: number;
+	/** 样本量 ybl(检验数) */
+	ybl?: number | null;
+	/** 备注 bz */
+	bz?: string;
+	/** 提交说明 */
+	comment?: string;
+}
+
+export function fetchIpqcInspectionFlowState(id: number) {
+	return service
+		.get<IpqcFlowState>('/api/IpqcInspectionFlow/state', { params: withAidopTenantParams({ id }) })
+		.then((r) => r.data);
+}
+
+export function submitIpqcInspection(payload: IpqcSubmitInspectionPayload) {
+	return service.post<IpqcFlowState>('/api/IpqcInspectionFlow/submit-inspection', payload).then((r) => r.data);
+}
+
+export function approveIpqcInspection(id: number, comment?: string) {
+	return service.post<IpqcFlowState>('/api/IpqcInspectionFlow/supervisor-approve', { id, comment }).then((r) => r.data);
+}
+
+export function returnIpqcInspection(id: number, comment: string) {
+	return service.post<IpqcFlowState>('/api/IpqcInspectionFlow/supervisor-return', { id, comment }).then((r) => r.data);
+}

+ 196 - 2
Web/src/views/aidop/s6/process-quality/processInspectionList.vue

@@ -59,6 +59,40 @@
 
 
 		<el-drawer v-model="detailVisible" title="过程检验单详情" size="60%" :destroy-on-close="true">
 		<el-drawer v-model="detailVisible" title="过程检验单详情" size="60%" :destroy-on-close="true">
 			<div v-loading="detailLoading">
 			<div v-loading="detailLoading">
+				<div class="flow-panel" v-loading="flowLoading">
+					<div class="flow-status-line">
+						<el-tag size="small" :type="flowTagType">{{ flowStatusText }}</el-tag>
+						<span class="flow-msg">{{ flowState.message }}</span>
+						<span v-if="flowState.inspectorName" class="flow-meta">检验员:{{ flowState.inspectorName }}</span>
+						<span v-if="flowState.supervisorName" class="flow-meta">主管:{{ flowState.supervisorName }}</span>
+					</div>
+					<el-form v-if="flowState.canSubmitResult" :inline="true" :model="submitForm" class="flow-form" @submit.prevent>
+						<el-form-item label="判定">
+							<el-select v-model="submitForm.jgpd" clearable placeholder="可空" style="width: 120px">
+								<el-option label="合格(0)" :value="0" />
+								<el-option label="不合格(1)" :value="1" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="合格数">
+							<el-input-number v-model="submitForm.jyhgsl" :min="0" :controls="false" style="width: 110px" />
+						</el-form-item>
+						<el-form-item label="不合格数">
+							<el-input-number v-model="submitForm.jybhgsl" :min="0" :controls="false" style="width: 110px" />
+						</el-form-item>
+						<el-form-item label="样本量">
+							<el-input-number v-model="submitForm.ybl" :min="0" :controls="false" style="width: 110px" />
+						</el-form-item>
+						<el-form-item label="备注">
+							<el-input v-model="submitForm.bz" clearable style="width: 160px" />
+						</el-form-item>
+						<el-button type="primary" :loading="flowActing" @click="doSubmitInspection">提交检验结果</el-button>
+					</el-form>
+					<div v-if="flowState.canSupervisorApprove || flowState.canSupervisorReturn" class="flow-form">
+						<el-button type="success" :loading="flowActing" @click="doApprove">审核通过</el-button>
+						<el-button type="warning" :loading="flowActing" @click="doReturn">退回</el-button>
+					</div>
+				</div>
+
 				<el-descriptions :column="3" border size="small">
 				<el-descriptions :column="3" border size="small">
 					<el-descriptions-item label="单据编号">{{ detail.billNo || '-' }}</el-descriptions-item>
 					<el-descriptions-item label="单据编号">{{ detail.billNo || '-' }}</el-descriptions-item>
 					<el-descriptions-item label="产品型号">{{ detail.productModel || '-' }}</el-descriptions-item>
 					<el-descriptions-item label="产品型号">{{ detail.productModel || '-' }}</el-descriptions-item>
@@ -114,7 +148,7 @@
 <script setup lang="ts" name="aidopS6ProcessQualityInspectionList">
 <script setup lang="ts" name="aidopS6ProcessQualityInspectionList">
 import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { useRoute } from 'vue-router';
-import { ElMessage } from 'element-plus';
+import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import {
 import {
 	fetchIpqcInspectionList,
 	fetchIpqcInspectionList,
@@ -123,6 +157,13 @@ import {
 	type IpqcInspectionDetail,
 	type IpqcInspectionDetail,
 	type IpqcInspectionDetailLine,
 	type IpqcInspectionDetailLine,
 } from '../api/ipqcInspection';
 } from '../api/ipqcInspection';
+import {
+	fetchIpqcInspectionFlowState,
+	submitIpqcInspection,
+	approveIpqcInspection,
+	returnIpqcInspection,
+	type IpqcFlowState,
+} from '../api/ipqcInspectionFlow';
 
 
 const route = useRoute();
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '过程检验单列表');
 const pageTitle = computed(() => (route.meta?.title as string) || '过程检验单列表');
@@ -187,27 +228,157 @@ function fmtDateTime(v?: string) {
 	return v.length > 19 ? v.slice(0, 19).replace('T', ' ') : v.replace('T', ' ');
 	return v.length > 19 ? v.slice(0, 19).replace('T', ' ') : v.replace('T', ' ');
 }
 }
 
 
-// 只读详情 drawer:头 + 明细(确定子集列)。无写按钮
+// 详情 drawer:头 + 明细(确定子集列)+ 检验员/主管流程区
 const detailVisible = ref(false);
 const detailVisible = ref(false);
 const detailLoading = ref(false);
 const detailLoading = ref(false);
 const detail = ref<IpqcInspectionDetail>({});
 const detail = ref<IpqcInspectionDetail>({});
 const detailLines = ref<IpqcInspectionDetailLine[]>([]);
 const detailLines = ref<IpqcInspectionDetailLine[]>([]);
+const currentDetailId = ref<number | null>(null);
+// 流程 BizId = 源检验单 id(qms_gcjyd.id = mdp_std.source_row_id),与 mdp 展示 id 不同空间。
+const currentFlowBizId = ref<number | null>(null);
 
 
 async function openDetail(row: IpqcInspectionRow) {
 async function openDetail(row: IpqcInspectionRow) {
 	if (!row?.id) return;
 	if (!row?.id) return;
+	currentDetailId.value = row.id;
+	currentFlowBizId.value = row.sourceRowId ?? null;
 	detailVisible.value = true;
 	detailVisible.value = true;
 	detailLoading.value = true;
 	detailLoading.value = true;
 	detail.value = {};
 	detail.value = {};
 	detailLines.value = [];
 	detailLines.value = [];
+	flowState.value = {};
 	try {
 	try {
 		const data = await fetchIpqcInspectionDetail(row.id);
 		const data = await fetchIpqcInspectionDetail(row.id);
 		detail.value = data || {};
 		detail.value = data || {};
 		detailLines.value = data?.lines || [];
 		detailLines.value = data?.lines || [];
+		if (currentFlowBizId.value == null && data?.sourceRowId != null) {
+			currentFlowBizId.value = data.sourceRowId;
+		}
 	} catch (e: any) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '加载过程检验单详情失败');
 		ElMessage.error(e?.message || '加载过程检验单详情失败');
 	} finally {
 	} finally {
 		detailLoading.value = false;
 		detailLoading.value = false;
 	}
 	}
+	if (currentFlowBizId.value != null) {
+		await loadFlowState(currentFlowBizId.value);
+	}
+}
+
+// ── 流程区(检验员提交 / 主管审核·退回)──
+const flowLoading = ref(false);
+const flowActing = ref(false);
+const flowState = ref<IpqcFlowState>({});
+const submitForm = reactive<{ jgpd: number | null; jyhgsl: number; jybhgsl: number; ybl: number | null; bz: string }>({
+	jgpd: null,
+	jyhgsl: 0,
+	jybhgsl: 0,
+	ybl: null,
+	bz: '',
+});
+
+const flowStatusText = computed(() => flowState.value.businessStatus || flowState.value.flowStatus || '未发起');
+const flowTagType = computed(() => {
+	const s = flowState.value.businessStatus;
+	if (s === 'APPROVED') return 'success';
+	if (s === 'RETURNED') return 'warning';
+	if (s === 'PENDING_REVIEW') return 'primary';
+	return 'info';
+});
+
+async function loadFlowState(id: number) {
+	flowLoading.value = true;
+	try {
+		flowState.value = (await fetchIpqcInspectionFlowState(id)) || {};
+	} catch (e: any) {
+		ElMessage.error(e?.message || '加载流程状态失败');
+		flowState.value = {};
+	} finally {
+		flowLoading.value = false;
+	}
+}
+
+async function refreshAfterFlow(id: number) {
+	await loadFlowState(id);
+	await loadList();
+}
+
+async function doSubmitInspection() {
+	const id = currentFlowBizId.value;
+	if (!id) return;
+	if (submitForm.jybhgsl < 0 || submitForm.jyhgsl < 0) {
+		ElMessage.warning('数量不能为负');
+		return;
+	}
+	if (submitForm.ybl != null && submitForm.ybl > 0 && submitForm.jybhgsl > submitForm.ybl) {
+		ElMessage.warning('不合格数量不能大于样本量');
+		return;
+	}
+	try {
+		await ElMessageBox.confirm('确认提交检验结果并进入主管审核?', '提交确认', { type: 'warning' });
+	} catch {
+		return;
+	}
+	flowActing.value = true;
+	try {
+		await submitIpqcInspection({
+			id,
+			jgpd: submitForm.jgpd,
+			jyhgsl: submitForm.jyhgsl,
+			jybhgsl: submitForm.jybhgsl,
+			ybl: submitForm.ybl,
+			bz: submitForm.bz || undefined,
+		});
+		ElMessage.success('已提交,进入主管审核');
+		await refreshAfterFlow(id);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交失败');
+	} finally {
+		flowActing.value = false;
+	}
+}
+
+async function doApprove() {
+	const id = currentFlowBizId.value;
+	if (!id) return;
+	try {
+		await ElMessageBox.confirm('确认审核通过并结束流程?', '审核通过', { type: 'warning' });
+	} catch {
+		return;
+	}
+	flowActing.value = true;
+	try {
+		await approveIpqcInspection(id);
+		ElMessage.success('审核通过,流程完成');
+		await refreshAfterFlow(id);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '审核通过失败');
+	} finally {
+		flowActing.value = false;
+	}
+}
+
+async function doReturn() {
+	const id = currentFlowBizId.value;
+	if (!id) return;
+	let reason = '';
+	try {
+		const { value } = await ElMessageBox.prompt('请填写退回意见(必填)', '退回', {
+			type: 'warning',
+			inputValidator: (v: string) => (v && v.trim() ? true : '退回意见必填'),
+		});
+		reason = (value || '').trim();
+	} catch {
+		return;
+	}
+	flowActing.value = true;
+	try {
+		await returnIpqcInspection(id, reason);
+		ElMessage.success('已退回检验员');
+		await refreshAfterFlow(id);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '退回失败');
+	} finally {
+		flowActing.value = false;
+	}
 }
 }
 
 
 onMounted(() => loadList());
 onMounted(() => loadList());
@@ -227,4 +398,27 @@ onActivated(() => loadList());
 	margin: 16px 0 8px;
 	margin: 16px 0 8px;
 	font-weight: 600;
 	font-weight: 600;
 }
 }
+.flow-panel {
+	margin-bottom: 16px;
+	padding: 12px;
+	background: var(--el-fill-color-lighter);
+	border-radius: 4px;
+}
+.flow-status-line {
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	margin-bottom: 8px;
+	flex-wrap: wrap;
+}
+.flow-msg {
+	color: var(--el-text-color-regular);
+}
+.flow-meta {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+.flow-form {
+	margin-top: 8px;
+}
 </style>
 </style>

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.245</AssemblyVersion>
-    <FileVersion>1.0.245</FileVersion>
-    <Version>1.0.245</Version>
+    <AssemblyVersion>1.0.246</AssemblyVersion>
+    <FileVersion>1.0.246</FileVersion>
+    <Version>1.0.246</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 85 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoS6IpqcInspectionFlowState.cs

@@ -0,0 +1,85 @@
+using Admin.NET.Core;
+
+namespace Admin.NET.Plugin.AiDOP.Entity;
+
+/// <summary>
+/// S6 过程检验单(IPQC)流程状态轻量扩展表。
+///
+/// 源表 qms_gcjyd 无审核人/审核时间/并发/租户列,故本表承载流程态与审核信息,
+/// 不给归口表 qms_gcjyd 加列、不回写 qms_gcjyd.status。
+/// 一检验单一行(inspection_id 唯一);流程权威状态仍由 ApprovalFlowInstance/Task 承载,
+/// 本表为业务侧展示与审核信息落点(business_status / 审核人 / 审核时间 / 退回原因 / row_version 乐观锁)。
+/// </summary>
+[SugarTable("ado_s6_ipqc_inspection_flow_state", "S6过程检验单流程状态")]
+[SugarIndex("uk_s6_ipqc_flow_state_insp", nameof(InspectionId), OrderByType.Asc, true)]
+[SugarIndex("idx_s6_ipqc_flow_state_flow", nameof(FlowInstanceId), OrderByType.Asc)]
+public class AdoS6IpqcInspectionFlowState : EntityBaseDel, ITenantIdFilter
+{
+    /// <summary>租户</summary>
+    [SugarColumn(ColumnName = "tenant_id", ColumnDescription = "租户", IsNullable = true)]
+    public long? TenantId { get; set; }
+
+    /// <summary>工厂/组织</summary>
+    [SugarColumn(ColumnName = "org_id", ColumnDescription = "工厂/组织", IsNullable = true)]
+    public long? OrgId { get; set; }
+
+    /// <summary>检验单 id(qms_gcjyd.id,BizId)</summary>
+    [SugarColumn(ColumnName = "inspection_id", ColumnDescription = "检验单id")]
+    public long InspectionId { get; set; }
+
+    /// <summary>检验单号(qms_gcjyd.djbh)</summary>
+    [SugarColumn(ColumnName = "inspection_no", ColumnDescription = "检验单号", Length = 255, IsNullable = true)]
+    public string? InspectionNo { get; set; }
+
+    /// <summary>业务类型(IPQC_INSPECTION)</summary>
+    [SugarColumn(ColumnName = "biz_type", ColumnDescription = "业务类型", Length = 80, IsNullable = true)]
+    public string? BizType { get; set; } = "IPQC_INSPECTION";
+
+    /// <summary>流程实例 id</summary>
+    [SugarColumn(ColumnName = "flow_instance_id", ColumnDescription = "流程实例id", IsNullable = true)]
+    public long? FlowInstanceId { get; set; }
+
+    /// <summary>业务状态(INSPECTING / PENDING_REVIEW / APPROVED / RETURNED)</summary>
+    [SugarColumn(ColumnName = "business_status", ColumnDescription = "业务状态", Length = 32, IsNullable = true)]
+    public string? BusinessStatus { get; set; }
+
+    /// <summary>当前节点码(N1_INSPECT / N2_SUP_REVIEW,完成为空)</summary>
+    [SugarColumn(ColumnName = "current_node", ColumnDescription = "当前节点", Length = 32, IsNullable = true)]
+    public string? CurrentNode { get; set; }
+
+    /// <summary>检验员用户 id</summary>
+    [SugarColumn(ColumnName = "inspector_user_id", ColumnDescription = "检验员用户id", IsNullable = true)]
+    public long? InspectorUserId { get; set; }
+
+    /// <summary>检验员姓名</summary>
+    [SugarColumn(ColumnName = "inspector_name", ColumnDescription = "检验员姓名", Length = 100, IsNullable = true)]
+    public string? InspectorName { get; set; }
+
+    /// <summary>检验主管用户 id</summary>
+    [SugarColumn(ColumnName = "supervisor_user_id", ColumnDescription = "检验主管用户id", IsNullable = true)]
+    public long? SupervisorUserId { get; set; }
+
+    /// <summary>检验主管姓名</summary>
+    [SugarColumn(ColumnName = "supervisor_name", ColumnDescription = "检验主管姓名", Length = 100, IsNullable = true)]
+    public string? SupervisorName { get; set; }
+
+    /// <summary>提交时间(检验员)</summary>
+    [SugarColumn(ColumnName = "submitted_at", ColumnDescription = "提交时间", IsNullable = true)]
+    public DateTime? SubmittedAt { get; set; }
+
+    /// <summary>审核时间(主管)</summary>
+    [SugarColumn(ColumnName = "reviewed_at", ColumnDescription = "审核时间", IsNullable = true)]
+    public DateTime? ReviewedAt { get; set; }
+
+    /// <summary>审核结果(APPROVED / RETURNED)</summary>
+    [SugarColumn(ColumnName = "review_result", ColumnDescription = "审核结果", Length = 32, IsNullable = true)]
+    public string? ReviewResult { get; set; }
+
+    /// <summary>退回原因</summary>
+    [SugarColumn(ColumnName = "return_reason", ColumnDescription = "退回原因", Length = 500, IsNullable = true)]
+    public string? ReturnReason { get; set; }
+
+    /// <summary>乐观锁版本</summary>
+    [SugarColumn(ColumnName = "row_version", ColumnDescription = "乐观锁版本", IsNullable = true)]
+    public long? RowVersion { get; set; }
+}

+ 248 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/IpqcInspectionFlowSeed.cs

@@ -0,0 +1,248 @@
+using System.Diagnostics;
+using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.Entity;
+using Admin.NET.Plugin.AiDOP.Manufacturing;
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+using Yitter.IdGenerator;
+using ApprovalFlowEntity = Admin.NET.Plugin.ApprovalFlow.ApprovalFlow;
+
+namespace Admin.NET.Plugin.AiDOP.Infrastructure;
+
+/// <summary>
+/// S6 过程检验单(IPQC)检验员+主管流程种子(幂等)。
+///
+/// 顺序:① 扩展表 ado_s6_ipqc_inspection_flow_state(CodeFirst 建表,无 SQL)
+/// ② 角色 ROLE_S6_IPQC_INSPECTOR / ROLE_S6_IPQC_SUPERVISOR ③ UAT 成员绑定(superAdmin.NET 同绑两角色,仅 UAT 自审)
+/// ④ ApprovalBizType(IPQC_INSPECTION) ⑤ ApprovalFlow 已发布定义(start→N1_INSPECT→N2_SUP_REVIEW→end,节点绑角色)。
+/// 全部幂等:存在则跳过,不覆盖、不删除既有角色/流程。superAdmin.NET 不存在时仅告警不崩溃。
+/// 本批不含网关/SQE/不合格处置。
+/// </summary>
+public static class IpqcInspectionFlowSeed
+{
+    public static void EnsureSeed(ISqlSugarClient db)
+    {
+        try
+        {
+            db.CodeFirst.InitTables(
+                typeof(ApprovalBizType),
+                typeof(ApprovalFlowEntity),
+                typeof(ApprovalFlowVersion));
+            // 流程状态轻量扩展表(CodeFirst 建表,无 SQL)
+            db.CodeFirst.InitTables(typeof(AdoS6IpqcInspectionFlowState));
+            EnsureRoles(db);
+            EnsureUatMembers(db);
+            EnsureBizType(db);
+            EnsurePublishedFlow(db);
+        }
+        catch (Exception ex)
+        {
+            Trace.TraceWarning("S6 IPQC inspection flow seed: " + ex);
+        }
+    }
+
+    private static void EnsureRoles(ISqlSugarClient db)
+    {
+        EnsureRole(db, IpqcInspectionFlowConst.RoleInspectorId, IpqcInspectionFlowConst.RoleInspectorCode, "过程检验员", 820);
+        EnsureRole(db, IpqcInspectionFlowConst.RoleSupervisorId, IpqcInspectionFlowConst.RoleSupervisorCode, "过程检验主管", 821);
+    }
+
+    private static void EnsureRole(ISqlSugarClient db, long id, string code, string name, int orderNo)
+    {
+        var exists = db.Queryable<SysRole>().ClearFilter().Any(x => x.Code == code);
+        if (exists) return;
+        db.Insertable(new SysRole
+        {
+            Id = id,
+            TenantId = SqlSugarConst.DefaultTenantId,
+            Name = name,
+            Code = code,
+            OrderNo = orderNo,
+            DataScope = DataScopeEnum.Self,
+            Status = StatusEnum.Enable,
+            Remark = "S6 过程检验流程角色",
+            CreateTime = DateTime.Now,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsureUatMembers(ISqlSugarClient db)
+    {
+        var user = db.Queryable<SysUser>().ClearFilter()
+            .Where(x => x.Account == IpqcInspectionFlowConst.UatAccount)
+            .First();
+        if (user == null)
+        {
+            Trace.TraceWarning($"S6 IPQC flow seed: UAT 账号 {IpqcInspectionFlowConst.UatAccount} 不存在,跳过成员绑定(阶段2 冒烟前须指定成员)");
+            return;
+        }
+        EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleInspectorId);
+        EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleSupervisorId);
+    }
+
+    private static void EnsureUserRole(ISqlSugarClient db, long userId, long roleId)
+    {
+        var exists = db.Queryable<SysUserRole>().ClearFilter().Any(x => x.UserId == userId && x.RoleId == roleId);
+        if (exists) return;
+        db.Insertable(new SysUserRole
+        {
+            Id = YitIdHelper.NextId(),
+            UserId = userId,
+            RoleId = roleId,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsureBizType(ISqlSugarClient db)
+    {
+        var exists = db.Queryable<ApprovalBizType>().Any(x => x.Code == IpqcInspectionFlowConst.BizType);
+        if (exists) return;
+        db.Insertable(new ApprovalBizType
+        {
+            Id = YitIdHelper.NextId(),
+            Code = IpqcInspectionFlowConst.BizType,
+            Name = "过程检验单审批",
+            Remark = "S6 过程检验单 检验员→主管 审批流",
+            IsEnabled = true,
+            CreateTime = DateTime.Now,
+            UpdateTime = DateTime.Now,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsurePublishedFlow(ISqlSugarClient db)
+    {
+        var orgIds = db.Queryable<SysOrg>()
+            .Select(x => x.Id)
+            .ToList()
+            .Where(x => x > 0)
+            .Distinct()
+            .ToList();
+        if (orgIds.Count == 0) orgIds.Add(0);
+
+        var now = DateTime.Now;
+        var flowJson = BuildFlowJson();
+        foreach (var orgId in orgIds)
+        {
+            var existing = db.Queryable<ApprovalFlowEntity>()
+                .Where(x => x.BizType == IpqcInspectionFlowConst.BizType && x.OrgId == orgId && x.IsPublished && !x.IsDelete)
+                .OrderByDescending(x => x.Version)
+                .First();
+            // 已有已发布定义则跳过(幂等,不覆盖)。历史实例用各自 FlowJsonSnapshot。
+            if (existing != null) continue;
+
+            var flow = new ApprovalFlowEntity
+            {
+                Id = YitIdHelper.NextId(),
+                Code = $"S6IPQC_{orgId}",
+                Name = "过程检验单审批",
+                BizType = IpqcInspectionFlowConst.BizType,
+                FormJson = "{}",
+                FlowJson = flowJson,
+                Status = 1,
+                Remark = "系统默认流程 v1:开始-检验员检验-检验主管审核-结束",
+                Version = 1,
+                IsPublished = true,
+                OrgId = orgId,
+                CreateTime = now,
+                UpdateTime = now,
+            };
+            db.Insertable(flow).ExecuteCommand();
+            db.Insertable(new ApprovalFlowVersion
+            {
+                Id = YitIdHelper.NextId(),
+                FlowId = flow.Id,
+                Version = flow.Version,
+                FlowJson = flow.FlowJson,
+                FormJson = flow.FormJson,
+                PublishTime = now,
+                PublisherName = "系统初始化",
+                CreateTime = now,
+                UpdateTime = now,
+            }).ExecuteCommand();
+        }
+    }
+
+    private static string BuildFlowJson()
+    {
+        var item = new ApprovalFlowItem
+        {
+            Nodes = new List<ApprovalFlowNodeItem>
+            {
+                new()
+                {
+                    Id = "start",
+                    Type = "bpmn:startEvent",
+                    X = 100,
+                    Y = 120,
+                    Properties = new FlowProperties { NodeName = "开始" },
+                    Text = new FlowTextItem { X = 100, Y = 154, Value = "开始" },
+                },
+                new()
+                {
+                    Id = IpqcInspectionFlowConst.NodeInspect,
+                    Type = "bpmn:userTask",
+                    X = 300,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "检验员检验",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = IpqcInspectionFlowConst.RoleInspectorId.ToString(),
+                        ApproverNames = "过程检验员",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 300, Y = 154, Value = "检验员检验" },
+                },
+                new()
+                {
+                    Id = IpqcInspectionFlowConst.NodeSupervisor,
+                    Type = "bpmn:userTask",
+                    X = 500,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "检验主管审核",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = IpqcInspectionFlowConst.RoleSupervisorId.ToString(),
+                        ApproverNames = "过程检验主管",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" },
+                },
+                new()
+                {
+                    Id = "end",
+                    Type = "bpmn:endEvent",
+                    X = 700,
+                    Y = 120,
+                    Properties = new FlowProperties { NodeName = "结束" },
+                    Text = new FlowTextItem { X = 700, Y = 154, Value = "结束" },
+                },
+            },
+            Edges = new List<ApprovalFlowEdgeItem>
+            {
+                BuildEdge("edge-start-n1", "start", IpqcInspectionFlowConst.NodeInspect, 140, 120, 260, 120),
+                BuildEdge("edge-n1-n2", IpqcInspectionFlowConst.NodeInspect, IpqcInspectionFlowConst.NodeSupervisor, 340, 120, 460, 120),
+                BuildEdge("edge-n2-end", IpqcInspectionFlowConst.NodeSupervisor, "end", 540, 120, 660, 120),
+            }
+        };
+        return JsonSerializer.Serialize(item);
+    }
+
+    private static ApprovalFlowEdgeItem BuildEdge(string id, string source, string target, float sx, float sy, float ex, float ey)
+    {
+        return new ApprovalFlowEdgeItem
+        {
+            Id = id,
+            Type = "bpmn:sequenceFlow",
+            SourceNodeId = source,
+            TargetNodeId = target,
+            StartPoint = new FlowEdgePointItem { X = sx, Y = sy },
+            EndPoint = new FlowEdgePointItem { X = ex, Y = ey },
+            Properties = new FlowProperties(),
+            PointsList = new List<FlowEdgePointItem>
+            {
+                new() { X = sx, Y = sy },
+                new() { X = ex, Y = ey },
+            },
+        };
+    }
+}

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/IpqcInspectionDto.cs

@@ -93,6 +93,9 @@ public class IpqcInspectionListRow
 
 
     /// <summary>检规编号(inspec_standard_code ← jgbh)</summary>
     /// <summary>检规编号(inspec_standard_code ← jgbh)</summary>
     public string? InspecStandardCode { get; set; }
     public string? InspecStandardCode { get; set; }
+
+    /// <summary>源检验单 id(qms_gcjyd.id = mdp_std.source_row_id;流程 BizId 用此)</summary>
+    public long SourceRowId { get; set; }
 }
 }
 
 
 /// <summary>
 /// <summary>
@@ -125,6 +128,9 @@ public class IpqcInspectionDetailDto
     /// <summary>检验状态(inspection_status ← status)</summary>
     /// <summary>检验状态(inspection_status ← status)</summary>
     public string? InspectionStatus { get; set; }
     public string? InspectionStatus { get; set; }
 
 
+    /// <summary>源检验单 id(qms_gcjyd.id = mdp_std.source_row_id;流程 BizId 用此)</summary>
+    public long SourceRowId { get; set; }
+
     /// <summary>明细行(确定子集列;j1~j70 动态检验值列后置不返回)</summary>
     /// <summary>明细行(确定子集列;j1~j70 动态检验值列后置不返回)</summary>
     public List<IpqcInspectionDetailLineDto> Lines { get; set; } = new();
     public List<IpqcInspectionDetailLineDto> Lines { get; set; } = new();
 }
 }

+ 103 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/IpqcInspectionFlowDto.cs

@@ -0,0 +1,103 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+
+/// <summary>
+/// 检验员提交检验结果入参。写 qms_gcjyd 白名单列(jgpd/jyhgsl/jybhgsl/ybl/jyr/bz),并推进 ApprovalFlow N1→N2。
+/// 判定 jgpd 可空(枚举未确认时不阻塞技术链路,不强写未知值)。
+/// </summary>
+public class IpqcSubmitInspectionInput
+{
+    /// <summary>检验单 id(qms_gcjyd.id,BizId)</summary>
+    public long Id { get; set; }
+
+    /// <summary>结果判定(jgpd,字典码;枚举未确认,可空,不强写)</summary>
+    public int? Jgpd { get; set; }
+
+    /// <summary>检验合格数量(jyhgsl,不得为负)</summary>
+    public decimal Jyhgsl { get; set; }
+
+    /// <summary>检验不合格数量(jybhgsl,不得为负,不得大于样本量)</summary>
+    public decimal Jybhgsl { get; set; }
+
+    /// <summary>样本量(ybl,检验数,不得为负)</summary>
+    public decimal? Ybl { get; set; }
+
+    /// <summary>备注(bz,可选)</summary>
+    public string? Bz { get; set; }
+
+    /// <summary>提交说明(流程意见,可选)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>检验主管通过入参。</summary>
+public class IpqcSupervisorApproveInput
+{
+    /// <summary>检验单 id</summary>
+    public long Id { get; set; }
+
+    /// <summary>审批意见(可选)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>检验主管退回入参(回 N1,意见必填)。</summary>
+public class IpqcSupervisorReturnInput
+{
+    /// <summary>检验单 id</summary>
+    public long Id { get; set; }
+
+    /// <summary>退回意见(必填)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>
+/// 过程检验单流程状态输出(只读,供前端按钮控制)。
+/// </summary>
+public class IpqcFlowStateOutput
+{
+    /// <summary>BizType(IPQC_INSPECTION)</summary>
+    public string BizType { get; set; } = IpqcInspectionFlowConst.BizType;
+
+    /// <summary>BizId(=检验单 id)</summary>
+    public long BizId { get; set; }
+
+    /// <summary>流程实例 id(未启动为 null)</summary>
+    public long? InstanceId { get; set; }
+
+    /// <summary>流程状态(NotStarted / Running / Approved / Rejected / Cancelled / Terminated)</summary>
+    public string FlowStatus { get; set; } = "NotStarted";
+
+    /// <summary>业务状态(INSPECTING / PENDING_REVIEW / APPROVED / RETURNED)</summary>
+    public string? BusinessStatus { get; set; }
+
+    /// <summary>当前节点码(N1_INSPECT / N2_SUP_REVIEW,未启动/已结束为 null)</summary>
+    public string? CurrentNodeCode { get; set; }
+
+    /// <summary>当前节点名</summary>
+    public string? CurrentNodeName { get; set; }
+
+    /// <summary>当前节点角色</summary>
+    public string? CurrentAssigneeRole { get; set; }
+
+    /// <summary>当前用户是否可提交检验结果</summary>
+    public bool CanSubmitResult { get; set; }
+
+    /// <summary>当前用户是否可主管通过</summary>
+    public bool CanSupervisorApprove { get; set; }
+
+    /// <summary>当前用户是否可主管退回</summary>
+    public bool CanSupervisorReturn { get; set; }
+
+    /// <summary>流程是否已完成/终态</summary>
+    public bool IsCompleted { get; set; }
+
+    /// <summary>检验员姓名</summary>
+    public string? InspectorName { get; set; }
+
+    /// <summary>检验主管姓名</summary>
+    public string? SupervisorName { get; set; }
+
+    /// <summary>更新时间</summary>
+    public DateTime? UpdatedAt { get; set; }
+
+    /// <summary>提示信息</summary>
+    public string? Message { get; set; }
+}

+ 60 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowBizHandler.cs

@@ -0,0 +1,60 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 过程检验单审批流回调处理器(BizType=IPQC_INSPECTION)。
+///
+/// 本批(检验员+主管最小闭环)Handler 全部 no-op:不回写 qms_gcjyd、不写库存/生产/仓储、不发额外通知。
+/// 检验结果字段由 IpqcInspectionFlowService.submit-inspection 在事务内写入 qms_gcjyd 白名单列;
+/// 流程状态由 ApprovalFlowInstance 承载 + 轻量扩展表 ado_s6_ipqc_inspection_flow_state 落审核信息,
+/// 不回写 qms_gcjyd.status。GetBizData 仅返回基础信息(本批线性流程无网关条件)。
+/// </summary>
+public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => IpqcInspectionFlowConst.BizType;
+
+    private readonly ISqlSugarClient _db;
+
+    public IpqcInspectionFlowBizHandler(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>流程发起后回调:no-op(不回写 qms_gcjyd)。</summary>
+    public Task OnFlowStarted(long bizId, long instanceId) => Task.CompletedTask;
+
+    /// <summary>单节点完成回调:no-op。</summary>
+    public Task OnNodeCompleted(long bizId, long instanceId, string nodeId, string nodeName, long? approverUserId) => Task.CompletedTask;
+
+    /// <summary>流程结束回调:no-op(不写 qms_gcjyd.status/库存/生产/仓储)。审核结果由 ApprovalFlowInstance + 扩展表承载。</summary>
+    public Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId) => Task.CompletedTask;
+
+    /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<IpqcFlowBizData>(
+            "SELECT djbh AS BillNo, cplx AS ProductModel, wlbm AS MaterialCode, wlmc AS MaterialName, jgpd AS Jgpd FROM qms_gcjyd WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId) });
+        var r = rows.FirstOrDefault();
+        if (r == null) return new Dictionary<string, object>();
+        return new Dictionary<string, object>
+        {
+            ["billNo"] = r.BillNo ?? "",
+            ["productModel"] = r.ProductModel ?? "",
+            ["materialCode"] = r.MaterialCode ?? "",
+            ["materialName"] = r.MaterialName ?? "",
+            ["jgpd"] = r.Jgpd ?? -1,
+        };
+    }
+
+    private class IpqcFlowBizData
+    {
+        public string? BillNo { get; set; }
+        public string? ProductModel { get; set; }
+        public string? MaterialCode { get; set; }
+        public string? MaterialName { get; set; }
+        public long? Jgpd { get; set; }
+    }
+}

+ 44 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowConst.cs

@@ -0,0 +1,44 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 过程检验单(IPQC)检验员+主管最小流程常量(BizType / 节点码 / 角色码 / 角色固定 Id)。
+///
+/// 最小闭环:N1_INSPECT 检验员录入并提交 → N2_SUP_REVIEW 检验主管审核(通过=完成 / 退回=回 N1)。
+/// 本批不含网关 / SQE / 不合格处置 / 生产 / 仓储 / 库存节点。
+/// 角色 Id 取号避撞 S5 IQC(S5 用 132991501000x;S6 用 132991601000x)。
+/// </summary>
+public static class IpqcInspectionFlowConst
+{
+    /// <summary>流程业务类型编码</summary>
+    public const string BizType = "IPQC_INSPECTION";
+
+    /// <summary>N1 检验员检验节点</summary>
+    public const string NodeInspect = "N1_INSPECT";
+
+    /// <summary>N2 检验主管审核节点</summary>
+    public const string NodeSupervisor = "N2_SUP_REVIEW";
+
+    /// <summary>检验员角色编码</summary>
+    public const string RoleInspectorCode = "ROLE_S6_IPQC_INSPECTOR";
+
+    /// <summary>检验主管角色编码</summary>
+    public const string RoleSupervisorCode = "ROLE_S6_IPQC_SUPERVISOR";
+
+    /// <summary>检验员角色固定 Id(种子与流程定义共用)</summary>
+    public const long RoleInspectorId = 1329916010001L;
+
+    /// <summary>检验主管角色固定 Id</summary>
+    public const long RoleSupervisorId = 1329916010002L;
+
+    /// <summary>流程定义版本(seed 幂等升级依据)</summary>
+    public const int FlowDefinitionVersion = 1;
+
+    /// <summary>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审;职责隔离须用两独立账号验证)</summary>
+    public const string UatAccount = "superAdmin.NET";
+
+    // 业务状态(落轻量扩展表 business_status,不回写 qms_gcjyd.status)
+    public const string StatusInspecting = "INSPECTING";
+    public const string StatusPendingReview = "PENDING_REVIEW";
+    public const string StatusApproved = "APPROVED";
+    public const string StatusReturned = "RETURNED";
+}

+ 354 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowService.cs

@@ -0,0 +1,354 @@
+using Admin.NET.Plugin.AiDOP.Entity;
+using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+using Yitter.IdGenerator;
+
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 过程检验单(IPQC)检验员+主管最小审批流服务(IPQC-INSPECTOR-SUPERVISOR-FLOW-1)。
+///
+/// N1_INSPECT 检验员录入 jgpd/jyhgsl/jybhgsl/ybl/jyr/bz → 提交 → N2_SUP_REVIEW 检验主管审核(通过=完成 / 退回=回 N1)。
+/// 复用 ApprovalFlow 引擎扩展点(StartFlow/Approve/ReturnToPrev + IFlowBizHandler),不改 ApprovalFlow 核心。
+/// BizType=IPQC_INSPECTION,BizId=qms_gcjyd.id,BizCode=djbh。反查用 ApprovalFlowInstance(BizType,BizId),qms_gcjyd 不加列。
+/// submit-inspection 仅写 qms_gcjyd 白名单列;审核信息落轻量扩展表 ado_s6_ipqc_inspection_flow_state;
+/// 不回写 qms_gcjyd.status;不写库存/生产/仓储;不做不合格处置/SQE/通知。
+/// 正常鉴权(非 AllowAnonymous);权限以当前用户 pending task 归属为准。
+/// </summary>
+[ApiDescriptionSettings(Order = 312, Description = "过程检验单流程")]
+[Route("api/IpqcInspectionFlow")]
+[NonUnify]
+public class IpqcInspectionFlowService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly FlowEngineService _flowEngine;
+    private readonly UserManager _userManager;
+
+    public IpqcInspectionFlowService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
+    {
+        _db = db;
+        _flowEngine = flowEngine;
+        _userManager = userManager;
+    }
+
+    /// <summary>
+    /// 检验单流程状态(只读,供前端按钮控制)。
+    /// </summary>
+    [DisplayName("过程检验单流程状态")]
+    [HttpGet("state")]
+    public async Task<IpqcFlowStateOutput> GetState([FromQuery] long id)
+    {
+        await EnsureBillExistsAsync(id);
+        return await BuildStateAsync(id);
+    }
+
+    /// <summary>
+    /// 检验员提交检验结果(写 qms_gcjyd 白名单列 + 推进 N1→N2)。
+    /// </summary>
+    [DisplayName("检验员提交检验结果")]
+    [HttpPost("submit-inspection")]
+    public async Task<IpqcFlowStateOutput> SubmitInspection([FromBody] IpqcSubmitInspectionInput input)
+    {
+        var billNo = await EnsureBillExistsAsync(input.Id);
+
+        // 参数校验(不合格数不得为负、不得大于样本量;合格数不得为负)
+        if (input.Jyhgsl < 0) throw Oops.Oh("检验合格数量不能为负");
+        if (input.Jybhgsl < 0) throw Oops.Oh("检验不合格数量不能为负");
+        if (input.Ybl is < 0) throw Oops.Oh("样本量不能为负");
+        if (input.Ybl is > 0 && input.Jybhgsl > input.Ybl.Value)
+            throw Oops.Oh("不合格数量不能大于样本量(检验数)");
+
+        var inst = await GetLatestInstanceAsync(input.Id);
+        if (inst != null && inst.Status != FlowInstanceStatusEnum.Running)
+            throw Oops.Oh("该检验单流程已结束,无法再提交检验结果");
+
+        var now = DateTime.Now;
+        var inspectorName = _userManager.RealName ?? _userManager.Account;
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            // 无运行中实例 → 起流程(自动生成 N1 任务给检验员角色成员)
+            long instanceId;
+            if (inst == null)
+            {
+                instanceId = await _flowEngine.StartFlow(new StartFlowInput
+                {
+                    BizType = IpqcInspectionFlowConst.BizType,
+                    BizId = input.Id,
+                    BizNo = billNo,
+                    Title = $"过程检验单 {billNo}",
+                    Comment = input.Comment,
+                });
+            }
+            else
+            {
+                instanceId = inst.Id;
+            }
+
+            var myTask = await GetMyPendingTaskAsync(instanceId);
+            if (myTask == null)
+                throw Oops.Oh("当前用户没有待处理的检验任务(需检验员角色成员)");
+            if (myTask.NodeId != IpqcInspectionFlowConst.NodeInspect)
+                throw Oops.Oh("当前不在检验录入节点,无法提交检验结果");
+
+            // 写检验结果(仅 qms_gcjyd 白名单列 jgpd/jyhgsl/jybhgsl/ybl/jyr/bz;jgpd 可空不强写)
+            await _db.Ado.ExecuteCommandAsync(
+                "UPDATE qms_gcjyd SET jgpd=@jgpd, jyhgsl=@jyhgsl, jybhgsl=@jybhgsl, ybl=@ybl, jyr=@jyr, bz=@bz WHERE id=@id",
+                new List<SugarParameter>
+                {
+                    new("@jgpd", input.Jgpd.HasValue ? input.Jgpd.Value : (object?)null),
+                    new("@jyhgsl", input.Jyhgsl),
+                    new("@jybhgsl", input.Jybhgsl),
+                    new("@ybl", input.Ybl.HasValue ? input.Ybl.Value : (object?)null),
+                    new("@jyr", inspectorName),
+                    new("@bz", input.Bz),
+                    new("@id", input.Id),
+                });
+
+            // 落轻量扩展表(提交人/提交时间/业务状态,不回写 qms_gcjyd.status)
+            await UpsertFlowStateAsync(input.Id, billNo, instanceId, IpqcInspectionFlowConst.StatusPendingReview,
+                IpqcInspectionFlowConst.NodeSupervisor, now, isSubmit: true, inspectorName: inspectorName);
+
+            // 推进 N1 → N2
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    /// <summary>
+    /// 检验主管通过(N2 → 完成)。
+    /// </summary>
+    [DisplayName("检验主管通过")]
+    [HttpPost("supervisor-approve")]
+    public async Task<IpqcFlowStateOutput> SupervisorApprove([FromBody] IpqcSupervisorApproveInput input)
+    {
+        var billNo = await EnsureBillExistsAsync(input.Id);
+        var (inst, myTask) = await RequireSupervisorTaskAsync(input.Id);
+        var now = DateTime.Now;
+        var supervisorName = _userManager.RealName ?? _userManager.Account;
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await UpsertFlowStateAsync(input.Id, billNo, inst.Id, IpqcInspectionFlowConst.StatusApproved,
+                null, now, isSubmit: false, reviewResult: "APPROVED", supervisorName: supervisorName);
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    /// <summary>
+    /// 检验主管退回(N2 → 回 N1,意见必填)。
+    /// </summary>
+    [DisplayName("检验主管退回")]
+    [HttpPost("supervisor-return")]
+    public async Task<IpqcFlowStateOutput> SupervisorReturn([FromBody] IpqcSupervisorReturnInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.Comment))
+            throw Oops.Oh("退回必须填写意见");
+
+        var billNo = await EnsureBillExistsAsync(input.Id);
+        var (inst, myTask) = await RequireSupervisorTaskAsync(input.Id);
+        var now = DateTime.Now;
+        var supervisorName = _userManager.RealName ?? _userManager.Account;
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await UpsertFlowStateAsync(input.Id, billNo, inst.Id, IpqcInspectionFlowConst.StatusReturned,
+                IpqcInspectionFlowConst.NodeInspect, now, isSubmit: false, reviewResult: "RETURNED",
+                supervisorName: supervisorName, returnReason: input.Comment);
+            await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    // ───────────────────────── helpers ─────────────────────────
+
+    /// <summary>校验检验单存在并返回 djbh(无 djbh 用 id 兜底)。</summary>
+    private async Task<string> EnsureBillExistsAsync(long id)
+    {
+        if (id <= 0) throw Oops.Oh("检验单 id 非法");
+        var rows = await _db.Ado.SqlQueryAsync<string>(
+            "SELECT djbh FROM qms_gcjyd WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", id) });
+        if (rows.Count == 0) throw Oops.Oh("检验单不存在");
+        return rows[0] ?? id.ToString();
+    }
+
+    /// <summary>取该检验单最新流程实例(BizType+BizId,跨数据范围)。</summary>
+    private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
+    {
+        return await _db.Queryable<ApprovalFlowInstance>()
+            .ClearFilter()
+            .Where(x => x.BizType == IpqcInspectionFlowConst.BizType && x.BizId == bizId)
+            .OrderByDescending(x => x.Id)
+            .FirstAsync();
+    }
+
+    /// <summary>取当前用户在指定实例下的待办任务。</summary>
+    private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
+    {
+        var userId = _userManager.UserId;
+        return await _db.Queryable<ApprovalFlowTask>()
+            .ClearFilter()
+            .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
+            .FirstAsync();
+    }
+
+    /// <summary>校验并返回主管 N2 待办任务。</summary>
+    private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
+    {
+        var inst = await GetLatestInstanceAsync(bizId);
+        if (inst == null) throw Oops.Oh("该检验单尚未发起流程");
+        if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单流程已结束");
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
+        if (myTask.NodeId != IpqcInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
+        return (inst, myTask);
+    }
+
+    /// <summary>当前用户是否具备指定角色(跨数据范围)。</summary>
+    private async Task<bool> HasRoleAsync(long roleId)
+    {
+        var userId = _userManager.UserId;
+        return await _db.Queryable<SysUserRole>()
+            .ClearFilter()
+            .AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
+    }
+
+    /// <summary>Upsert 流程状态扩展表(不回写 qms_gcjyd.status)。</summary>
+    private async Task UpsertFlowStateAsync(long inspectionId, string billNo, long instanceId, string businessStatus,
+        string? currentNode, DateTime now, bool isSubmit, string? inspectorName = null, string? supervisorName = null,
+        string? reviewResult = null, string? returnReason = null)
+    {
+        var existing = await _db.Queryable<AdoS6IpqcInspectionFlowState>()
+            .ClearFilter()
+            .Where(x => x.InspectionId == inspectionId)
+            .FirstAsync();
+
+        if (existing == null)
+        {
+            var row = new AdoS6IpqcInspectionFlowState
+            {
+                Id = YitIdHelper.NextId(),
+                TenantId = _userManager.TenantId,
+                InspectionId = inspectionId,
+                InspectionNo = billNo,
+                BizType = IpqcInspectionFlowConst.BizType,
+                FlowInstanceId = instanceId,
+                BusinessStatus = businessStatus,
+                CurrentNode = currentNode,
+                RowVersion = 1,
+                CreateTime = now,
+            };
+            if (isSubmit)
+            {
+                row.InspectorUserId = _userManager.UserId;
+                row.InspectorName = inspectorName;
+                row.SubmittedAt = now;
+            }
+            else
+            {
+                row.SupervisorUserId = _userManager.UserId;
+                row.SupervisorName = supervisorName;
+                row.ReviewedAt = now;
+                row.ReviewResult = reviewResult;
+                row.ReturnReason = returnReason;
+            }
+            await _db.Insertable(row).ExecuteCommandAsync();
+            return;
+        }
+
+        existing.FlowInstanceId = instanceId;
+        existing.BusinessStatus = businessStatus;
+        existing.CurrentNode = currentNode;
+        existing.RowVersion = (existing.RowVersion ?? 0) + 1;
+        existing.UpdateTime = now;
+        if (isSubmit)
+        {
+            existing.InspectorUserId = _userManager.UserId;
+            existing.InspectorName = inspectorName;
+            existing.SubmittedAt = now;
+        }
+        else
+        {
+            existing.SupervisorUserId = _userManager.UserId;
+            existing.SupervisorName = supervisorName;
+            existing.ReviewedAt = now;
+            existing.ReviewResult = reviewResult;
+            existing.ReturnReason = returnReason;
+        }
+        await _db.Updateable(existing).ExecuteCommandAsync();
+    }
+
+    /// <summary>组装流程状态输出。</summary>
+    private async Task<IpqcFlowStateOutput> BuildStateAsync(long bizId)
+    {
+        var output = new IpqcFlowStateOutput { BizId = bizId };
+        var inst = await GetLatestInstanceAsync(bizId);
+
+        var state = await _db.Queryable<AdoS6IpqcInspectionFlowState>()
+            .ClearFilter()
+            .Where(x => x.InspectionId == bizId)
+            .FirstAsync();
+        if (state != null)
+        {
+            output.BusinessStatus = state.BusinessStatus;
+            output.InspectorName = state.InspectorName;
+            output.SupervisorName = state.SupervisorName;
+        }
+
+        if (inst == null)
+        {
+            output.FlowStatus = "NotStarted";
+            output.CanSubmitResult = await HasRoleAsync(IpqcInspectionFlowConst.RoleInspectorId);
+            output.Message = output.CanSubmitResult ? "待检验员录入结果" : "尚未发起流程";
+            return output;
+        }
+
+        output.InstanceId = inst.Id;
+        output.FlowStatus = inst.Status.ToString();
+        output.UpdatedAt = inst.EndTime ?? inst.StartTime;
+
+        if (inst.Status != FlowInstanceStatusEnum.Running)
+        {
+            output.IsCompleted = true;
+            output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "流程已完成(主管审核通过)" : $"流程已结束({inst.Status})";
+            return output;
+        }
+
+        output.CurrentNodeCode = inst.CurrentNodeId;
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask != null)
+        {
+            output.CurrentNodeName = myTask.NodeName;
+            if (myTask.NodeId == IpqcInspectionFlowConst.NodeInspect)
+            {
+                output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleInspectorCode;
+                output.CanSubmitResult = true;
+                output.Message = "待当前用户录入检验结果";
+            }
+            else if (myTask.NodeId == IpqcInspectionFlowConst.NodeSupervisor)
+            {
+                output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleSupervisorCode;
+                output.CanSupervisorApprove = true;
+                output.CanSupervisorReturn = true;
+                output.Message = "待当前用户主管审核";
+            }
+        }
+        else
+        {
+            output.CurrentAssigneeRole = inst.CurrentNodeId == IpqcInspectionFlowConst.NodeInspect
+                ? IpqcInspectionFlowConst.RoleInspectorCode
+                : IpqcInspectionFlowConst.RoleSupervisorCode;
+            output.Message = "流程进行中(当前用户无待办)";
+        }
+        return output;
+    }
+}

+ 4 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionService.cs

@@ -82,7 +82,8 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
                 m.material_code            AS MaterialCode,
                 m.material_code            AS MaterialCode,
                 m.material_name            AS MaterialName,
                 m.material_name            AS MaterialName,
                 m.inspec_standard_version  AS InspecStandardVersion,
                 m.inspec_standard_version  AS InspecStandardVersion,
-                m.inspec_standard_code     AS InspecStandardCode
+                m.inspec_standard_code     AS InspecStandardCode,
+                CAST(m.source_row_id AS SIGNED) AS SourceRowId
             FROM mdp_std_ipqc_inspection m
             FROM mdp_std_ipqc_inspection m
             WHERE {whereSql}
             WHERE {whereSql}
             ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)}
             ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)}
@@ -133,7 +134,8 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
                 m.material_name            AS MaterialName,
                 m.material_name            AS MaterialName,
                 m.inspec_standard_version  AS InspecStandardVersion,
                 m.inspec_standard_version  AS InspecStandardVersion,
                 m.inspec_standard_code     AS InspecStandardCode,
                 m.inspec_standard_code     AS InspecStandardCode,
-                m.inspection_status        AS InspectionStatus
+                m.inspection_status        AS InspectionStatus,
+                CAST(m.source_row_id AS SIGNED) AS SourceRowId
             FROM mdp_std_ipqc_inspection m
             FROM mdp_std_ipqc_inspection m
             WHERE {headWhere}
             WHERE {headWhere}
             LIMIT 1
             LIMIT 1

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs

@@ -79,6 +79,7 @@ public class Startup : AppStartup
                 AidopKpiMasterSeed.EnsureSeed(dbS4, tenantId!.Value);
                 AidopKpiMasterSeed.EnsureSeed(dbS4, tenantId!.Value);
             AidopImprovementApprovalSeed.EnsureSeed(dbS4);
             AidopImprovementApprovalSeed.EnsureSeed(dbS4);
             Admin.NET.Plugin.AiDOP.Infrastructure.IqcInspBillFlowSeed.EnsureSeed(dbS4);
             Admin.NET.Plugin.AiDOP.Infrastructure.IqcInspBillFlowSeed.EnsureSeed(dbS4);
+            Admin.NET.Plugin.AiDOP.Infrastructure.IpqcInspectionFlowSeed.EnsureSeed(dbS4);
         }
         }
         catch (Exception ex)
         catch (Exception ex)
         {
         {