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

feat(s6): add ipqc quality disposition (N3)

不合格质量处置最小闭环(v2 流程 + 网关分流):
- 主管 N2 整批判定驱动 GW_RESULT(disposition_required):合格→end / 不合格→N3_QUALITY_DISPOSITION
- 质量工程师提交处置(REWORK/CONCESSION/SCRAP/OTHER)→写 ado_s6_ipqc_quality_disposition→N3→end,一单一处置
- 两级判定:项目 pd(明细辅助)+整批 jgpd(主管确认,可空不硬编码枚举);分流权威=disposition_required
- 复用 ApprovalFlow 网关(GetBizData)未改核心;新增质量工程师角色;Seed v1→v2 幂等升级;仅记录方案不写库存/生产/仓储
bump version Web 2.4.250 / server 1.0.248
YY968XX 1 неделя назад
Родитель
Сommit
9cd63896d5

+ 1 - 1
Web/package.json

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

+ 48 - 0
Web/src/views/aidop/s6/api/ipqcInspectionDisposition.ts

@@ -0,0 +1,48 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+// ── S6 过程检验单 不合格质量处置(写接口,正常鉴权)──
+// 后端 api/IpqcInspectionDisposition。N3_QUALITY_DISPOSITION 质量工程师提交处置方案 → N3→end。
+// 仅记录处置结论,不触发库存/生产/仓储/返工/报废实际动作,不做附件上传。
+export interface IpqcDisposition {
+	id?: number;
+	inspectionId?: number;
+	inspectionNo?: string;
+	dispositionNo?: string;
+	dispositionType?: string;
+	dispositionQty?: number | null;
+	reworkQty?: number | null;
+	concessionQty?: number | null;
+	scrapQty?: number | null;
+	dispositionOpinion?: string;
+	responsibilityDept?: string;
+	responsibleUser?: string;
+	status?: number;
+	submittedByName?: string;
+	submittedAt?: string;
+}
+
+export interface IpqcSubmitDispositionPayload {
+	id: number;
+	/** 处置类型 REWORK/CONCESSION/SCRAP/OTHER */
+	dispositionType: string;
+	dispositionQty?: number | null;
+	reworkQty?: number | null;
+	concessionQty?: number | null;
+	scrapQty?: number | null;
+	/** 处置意见(必填) */
+	dispositionOpinion: string;
+	responsibilityDept?: string;
+	responsibleUser?: string;
+	comment?: string;
+}
+
+export function fetchIpqcDisposition(id: number) {
+	return service
+		.get<IpqcDisposition>('/api/IpqcInspectionDisposition/state', { params: withAidopTenantParams({ id }) })
+		.then((r) => r.data);
+}
+
+export function submitIpqcDisposition(payload: IpqcSubmitDispositionPayload) {
+	return service.post<IpqcDisposition>('/api/IpqcInspectionDisposition/submit', payload).then((r) => r.data);
+}

+ 15 - 5
Web/src/views/aidop/s6/api/ipqcInspectionFlow.ts

@@ -1,9 +1,10 @@
 import service from '/@/utils/request';
 import { withAidopTenantParams } from '../../api/aidopTenant';
 
-// ── S6 过程检验单 检验员+主管最小审批流(写接口,正常鉴权)──
-// 后端 api/IpqcInspectionFlow。BizType=IPQC_INSPECTION。N1_INSPECT→N2_SUP_REVIEW→end。
-// 不做不合格处置/SQE/生产/仓储/库存/外部推送。
+// ── S6 过程检验单 检验员+主管+质量处置审批流(写接口,正常鉴权)──
+// 后端 api/IpqcInspectionFlow。BizType=IPQC_INSPECTION。
+// v2:N1_INSPECT→N2_SUP_REVIEW→GW_RESULT(整批判定)→N3_QUALITY_DISPOSITION/end。
+// 整批判定驱动分流(disposition_required),不触发库存/生产/仓储/外部推送。
 export interface IpqcFlowState {
 	bizType?: string;
 	bizId?: number;
@@ -16,6 +17,12 @@ export interface IpqcFlowState {
 	canSubmitResult?: boolean;
 	canSupervisorApprove?: boolean;
 	canSupervisorReturn?: boolean;
+	/** 当前用户是否可提交质量处置(N3) */
+	canSubmitDisposition?: boolean;
+	/** 是否需质量处置(1=不合格转N3 / 0=合格结束) */
+	dispositionRequired?: number | null;
+	/** 是否已有质量处置记录 */
+	hasDisposition?: boolean;
 	isCompleted?: boolean;
 	inspectorName?: string | null;
 	supervisorName?: string | null;
@@ -49,8 +56,11 @@ 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);
+/** 主管整批审核:qualified=true 整批合格→end;false 整批不合格→N3 质量处置。jgpd 整批判定值(可空)。 */
+export function approveIpqcInspection(id: number, qualified: boolean, jgpd?: number | null, comment?: string) {
+	return service
+		.post<IpqcFlowState>('/api/IpqcInspectionFlow/supervisor-approve', { id, qualified, jgpd, comment })
+		.then((r) => r.data);
 }
 
 export function returnIpqcInspection(id: number, comment: string) {

+ 124 - 7
Web/src/views/aidop/s6/process-quality/processInspectionList.vue

@@ -88,9 +88,41 @@
 						<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>
+						<span class="flow-meta">整批判定 jgpd(可空):</span>
+						<el-input-number v-model="supervisorForm.jgpd" :controls="false" placeholder="可空" style="width: 110px; margin-right: 12px" />
+						<el-button type="success" :loading="flowActing" @click="doApprove(true)">整批合格·通过</el-button>
+						<el-button type="danger" :loading="flowActing" @click="doApprove(false)">整批不合格·转处置</el-button>
 						<el-button type="warning" :loading="flowActing" @click="doReturn">退回</el-button>
 					</div>
+					<el-form v-if="flowState.canSubmitDisposition" :inline="true" :model="dispForm" class="flow-form" @submit.prevent>
+						<el-form-item label="处置类型">
+							<el-select v-model="dispForm.dispositionType" placeholder="必选" style="width: 140px">
+								<el-option label="返工 REWORK" value="REWORK" />
+								<el-option label="让步 CONCESSION" value="CONCESSION" />
+								<el-option label="报废 SCRAP" value="SCRAP" />
+								<el-option label="其它 OTHER" value="OTHER" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="处置数量"><el-input-number v-model="dispForm.dispositionQty" :min="0" :controls="false" style="width: 100px" /></el-form-item>
+						<el-form-item label="返工"><el-input-number v-model="dispForm.reworkQty" :min="0" :controls="false" style="width: 90px" /></el-form-item>
+						<el-form-item label="让步"><el-input-number v-model="dispForm.concessionQty" :min="0" :controls="false" style="width: 90px" /></el-form-item>
+						<el-form-item label="报废"><el-input-number v-model="dispForm.scrapQty" :min="0" :controls="false" style="width: 90px" /></el-form-item>
+						<el-form-item label="责任部门"><el-input v-model="dispForm.responsibilityDept" clearable style="width: 130px" /></el-form-item>
+						<el-form-item label="责任人"><el-input v-model="dispForm.responsibleUser" clearable style="width: 110px" /></el-form-item>
+						<el-form-item label="处置意见"><el-input v-model="dispForm.dispositionOpinion" type="textarea" :rows="1" style="width: 220px" /></el-form-item>
+						<el-button type="primary" :loading="flowActing" @click="doSubmitDisposition">提交处置</el-button>
+					</el-form>
+					<div v-if="disposition && disposition.id" class="flow-form">
+						<el-descriptions :column="3" border size="small">
+							<el-descriptions-item label="处置类型">{{ dispTypeText(disposition.dispositionType) }}</el-descriptions-item>
+							<el-descriptions-item label="处置数量">{{ disposition.dispositionQty ?? '-' }}</el-descriptions-item>
+							<el-descriptions-item label="返工/让步/报废">{{ disposition.reworkQty ?? 0 }} / {{ disposition.concessionQty ?? 0 }} / {{ disposition.scrapQty ?? 0 }}</el-descriptions-item>
+							<el-descriptions-item label="责任部门">{{ disposition.responsibilityDept || '-' }}</el-descriptions-item>
+							<el-descriptions-item label="责任人">{{ disposition.responsibleUser || '-' }}</el-descriptions-item>
+							<el-descriptions-item label="处置人">{{ disposition.submittedByName || '-' }}</el-descriptions-item>
+							<el-descriptions-item label="处置意见" :span="3">{{ disposition.dispositionOpinion || '-' }}</el-descriptions-item>
+						</el-descriptions>
+					</div>
 				</div>
 
 				<el-descriptions :column="3" border size="small">
@@ -164,6 +196,7 @@ import {
 	returnIpqcInspection,
 	type IpqcFlowState,
 } from '../api/ipqcInspectionFlow';
+import { fetchIpqcDisposition, submitIpqcDisposition, type IpqcDisposition } from '../api/ipqcInspectionDisposition';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '过程检验单列表');
@@ -246,6 +279,8 @@ async function openDetail(row: IpqcInspectionRow) {
 	detail.value = {};
 	detailLines.value = [];
 	flowState.value = {};
+	disposition.value = {};
+	supervisorForm.jgpd = null;
 	try {
 		const data = await fetchIpqcInspectionDetail(row.id);
 		detail.value = data || {};
@@ -260,6 +295,7 @@ async function openDetail(row: IpqcInspectionRow) {
 	}
 	if (currentFlowBizId.value != null) {
 		await loadFlowState(currentFlowBizId.value);
+		await loadDisposition(currentFlowBizId.value);
 	}
 }
 
@@ -278,12 +314,48 @@ const submitForm = reactive<{ jgpd: number | null; jyhgsl: number; jybhgsl: numb
 const flowStatusText = computed(() => flowState.value.businessStatus || flowState.value.flowStatus || '未发起');
 const flowTagType = computed(() => {
 	const s = flowState.value.businessStatus;
-	if (s === 'APPROVED') return 'success';
+	if (s === 'APPROVED' || s === 'DISPOSED') return 'success';
 	if (s === 'RETURNED') return 'warning';
+	if (s === 'PENDING_DISPOSITION') return 'danger';
 	if (s === 'PENDING_REVIEW') return 'primary';
 	return 'info';
 });
 
+// 主管整批判定表单 + N3 质量处置表单 + 已有处置
+const supervisorForm = reactive<{ jgpd: number | null }>({ jgpd: null });
+const disposition = ref<IpqcDisposition>({});
+const dispForm = reactive<{
+	dispositionType: string;
+	dispositionQty: number | null;
+	reworkQty: number | null;
+	concessionQty: number | null;
+	scrapQty: number | null;
+	responsibilityDept: string;
+	responsibleUser: string;
+	dispositionOpinion: string;
+}>({
+	dispositionType: '',
+	dispositionQty: null,
+	reworkQty: null,
+	concessionQty: null,
+	scrapQty: null,
+	responsibilityDept: '',
+	responsibleUser: '',
+	dispositionOpinion: '',
+});
+
+function dispTypeText(t?: string) {
+	return { REWORK: '返工', CONCESSION: '让步', SCRAP: '报废', OTHER: '其它' }[t || ''] || t || '-';
+}
+
+async function loadDisposition(id: number) {
+	try {
+		disposition.value = (await fetchIpqcDisposition(id)) || {};
+	} catch {
+		disposition.value = {};
+	}
+}
+
 async function loadFlowState(id: number) {
 	flowLoading.value = true;
 	try {
@@ -298,6 +370,7 @@ async function loadFlowState(id: number) {
 
 async function refreshAfterFlow(id: number) {
 	await loadFlowState(id);
+	await loadDisposition(id);
 	await loadList();
 }
 
@@ -336,21 +409,65 @@ async function doSubmitInspection() {
 	}
 }
 
-async function doApprove() {
+async function doApprove(qualified: boolean) {
+	const id = currentFlowBizId.value;
+	if (!id) return;
+	const tip = qualified ? '确认整批合格并结束流程?' : '确认整批不合格并转质量处置?';
+	try {
+		await ElMessageBox.confirm(tip, '整批判定', { type: 'warning' });
+	} catch {
+		return;
+	}
+	flowActing.value = true;
+	try {
+		await approveIpqcInspection(id, qualified, supervisorForm.jgpd);
+		ElMessage.success(qualified ? '整批合格,流程完成' : '整批不合格,已转质量处置');
+		await refreshAfterFlow(id);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '审核失败');
+	} finally {
+		flowActing.value = false;
+	}
+}
+
+async function doSubmitDisposition() {
 	const id = currentFlowBizId.value;
 	if (!id) return;
+	if (!dispForm.dispositionType) {
+		ElMessage.warning('请选择处置类型');
+		return;
+	}
+	if (!dispForm.dispositionOpinion || !dispForm.dispositionOpinion.trim()) {
+		ElMessage.warning('处置意见必填');
+		return;
+	}
+	const sub = (dispForm.reworkQty ?? 0) + (dispForm.concessionQty ?? 0) + (dispForm.scrapQty ?? 0);
+	if (dispForm.dispositionQty != null && sub > dispForm.dispositionQty) {
+		ElMessage.warning('返工+让步+报废之和不能大于处置数量');
+		return;
+	}
 	try {
-		await ElMessageBox.confirm('确认审核通过并结束流程?', '审核通过', { type: 'warning' });
+		await ElMessageBox.confirm('确认提交质量处置并结束流程?', '提交处置', { type: 'warning' });
 	} catch {
 		return;
 	}
 	flowActing.value = true;
 	try {
-		await approveIpqcInspection(id);
-		ElMessage.success('审核通过,流程完成');
+		await submitIpqcDisposition({
+			id,
+			dispositionType: dispForm.dispositionType,
+			dispositionQty: dispForm.dispositionQty,
+			reworkQty: dispForm.reworkQty,
+			concessionQty: dispForm.concessionQty,
+			scrapQty: dispForm.scrapQty,
+			dispositionOpinion: dispForm.dispositionOpinion,
+			responsibilityDept: dispForm.responsibilityDept || undefined,
+			responsibleUser: dispForm.responsibleUser || undefined,
+		});
+		ElMessage.success('处置已提交,流程完成');
 		await refreshAfterFlow(id);
 	} catch (e: any) {
-		ElMessage.error(e?.message || '审核通过失败');
+		ElMessage.error(e?.message || '提交处置失败');
 	} finally {
 		flowActing.value = false;
 	}

+ 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.247</AssemblyVersion>
-    <FileVersion>1.0.247</FileVersion>
-    <Version>1.0.247</Version>
+    <AssemblyVersion>1.0.248</AssemblyVersion>
+    <FileVersion>1.0.248</FileVersion>
+    <Version>1.0.248</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -79,6 +79,10 @@ public class AdoS6IpqcInspectionFlowState : EntityBaseDel, ITenantIdFilter
     [SugarColumn(ColumnName = "return_reason", ColumnDescription = "退回原因", Length = 500, IsNullable = true)]
     public string? ReturnReason { get; set; }
 
+    /// <summary>是否需要质量处置(DOP 流程控制字段,主管 N2 整批判定驱动:1=不合格转N3 / 0=合格结束;与 qms_gcjyd.jgpd 整批判定一致)。网关 GW_RESULT 判据。</summary>
+    [SugarColumn(ColumnName = "disposition_required", ColumnDescription = "是否需质量处置(1需/0否)", IsNullable = true)]
+    public int? DispositionRequired { get; set; }
+
     /// <summary>乐观锁版本</summary>
     [SugarColumn(ColumnName = "row_version", ColumnDescription = "乐观锁版本", IsNullable = true)]
     public long? RowVersion { get; set; }

+ 110 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoS6IpqcQualityDisposition.cs

@@ -0,0 +1,110 @@
+using Admin.NET.Core;
+
+namespace Admin.NET.Plugin.AiDOP.Entity;
+
+/// <summary>
+/// S6 过程检验(IPQC)不合格质量处置记录。
+///
+/// 整批不合格过程检验单(主管 N2 判定不合格)进入 N3_QUALITY_DISPOSITION,质量工程师提交处置方案落此表。
+/// 仅"记录处置结论"(disposition_type=REWORK/CONCESSION/SCRAP/OTHER);
+/// 本批不做仓储确认/库存/入库/出库/返工单/报废单/附件上传(attachment_json 预留)。
+/// 一单一有效处置由 service 校验(status=1 且未软删)+ inspection_id 唯一约束。
+/// </summary>
+[SugarTable("ado_s6_ipqc_quality_disposition", "S6过程检验质量处置")]
+[SugarIndex("uk_s6_ipqc_disp_insp", nameof(InspectionId), OrderByType.Asc, true)]
+[SugarIndex("idx_s6_ipqc_disp_flow", nameof(FlowInstanceId), OrderByType.Asc)]
+[SugarIndex("uk_s6_ipqc_disp_no", nameof(DispositionNo), OrderByType.Asc, true)]
+public class AdoS6IpqcQualityDisposition : 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>处置单号(唯一)</summary>
+    [SugarColumn(ColumnName = "disposition_no", ColumnDescription = "处置单号", Length = 80)]
+    public string DispositionNo { get; set; } = string.Empty;
+
+    /// <summary>处置类型:REWORK 返工 / CONCESSION 让步 / SCRAP 报废 / OTHER 其它</summary>
+    [SugarColumn(ColumnName = "disposition_type", ColumnDescription = "处置类型", Length = 32)]
+    public string DispositionType { get; set; } = string.Empty;
+
+    /// <summary>处置数量</summary>
+    [SugarColumn(ColumnName = "disposition_qty", ColumnDescription = "处置数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? DispositionQty { get; set; }
+
+    /// <summary>返工数量</summary>
+    [SugarColumn(ColumnName = "rework_qty", ColumnDescription = "返工数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? ReworkQty { get; set; }
+
+    /// <summary>让步数量</summary>
+    [SugarColumn(ColumnName = "concession_qty", ColumnDescription = "让步数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? ConcessionQty { get; set; }
+
+    /// <summary>报废数量</summary>
+    [SugarColumn(ColumnName = "scrap_qty", ColumnDescription = "报废数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? ScrapQty { get; set; }
+
+    /// <summary>处置意见(必填)</summary>
+    [SugarColumn(ColumnName = "disposition_opinion", ColumnDescription = "处置意见", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
+    public string? DispositionOpinion { get; set; }
+
+    /// <summary>责任部门</summary>
+    [SugarColumn(ColumnName = "responsibility_dept", ColumnDescription = "责任部门", Length = 255, IsNullable = true)]
+    public string? ResponsibilityDept { get; set; }
+
+    /// <summary>责任人</summary>
+    [SugarColumn(ColumnName = "responsible_user", ColumnDescription = "责任人", Length = 100, IsNullable = true)]
+    public string? ResponsibleUser { get; set; }
+
+    /// <summary>附件 JSON(预留,本批不做上传)</summary>
+    [SugarColumn(ColumnName = "attachment_json", ColumnDescription = "附件JSON", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
+    public string? AttachmentJson { get; set; }
+
+    /// <summary>状态:1 已提交(本批最小)</summary>
+    [SugarColumn(ColumnName = "status", ColumnDescription = "状态")]
+    public int Status { get; set; }
+
+    /// <summary>提交人 id(质量工程师)</summary>
+    [SugarColumn(ColumnName = "submitted_by", ColumnDescription = "提交人id", IsNullable = true)]
+    public long? SubmittedBy { get; set; }
+
+    /// <summary>提交人姓名</summary>
+    [SugarColumn(ColumnName = "submitted_by_name", ColumnDescription = "提交人姓名", Length = 100, IsNullable = true)]
+    public string? SubmittedByName { get; set; }
+
+    /// <summary>提交时间</summary>
+    [SugarColumn(ColumnName = "submitted_at", ColumnDescription = "提交时间", IsNullable = true)]
+    public DateTime? SubmittedAt { get; set; }
+
+    /// <summary>确认人(预留,本批 N3 提交即完成,不做独立确认)</summary>
+    [SugarColumn(ColumnName = "confirmed_by", ColumnDescription = "确认人", IsNullable = true)]
+    public long? ConfirmedBy { get; set; }
+
+    /// <summary>确认时间(预留)</summary>
+    [SugarColumn(ColumnName = "confirmed_at", ColumnDescription = "确认时间", IsNullable = true)]
+    public DateTime? ConfirmedAt { get; set; }
+
+    /// <summary>乐观锁版本</summary>
+    [SugarColumn(ColumnName = "row_version", ColumnDescription = "乐观锁版本", IsNullable = true)]
+    public long? RowVersion { get; set; }
+}

+ 77 - 13
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/IpqcInspectionFlowSeed.cs

@@ -12,11 +12,12 @@ 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/不合格处置。
+/// 顺序:① 扩展表 ado_s6_ipqc_inspection_flow_state + 处置表 ado_s6_ipqc_quality_disposition(CodeFirst 建表,无 SQL)
+/// ② 角色 ROLE_S6_IPQC_INSPECTOR / SUPERVISOR / QUALITY_ENGINEER ③ UAT 成员绑定(superAdmin.NET 同绑三角色,仅 UAT 自审)
+/// ④ ApprovalBizType(IPQC_INSPECTION) ⑤ ApprovalFlow v2 已发布定义
+///   (start→N1_INSPECT→N2_SUP_REVIEW→GW_RESULT(disposition_required==1→N3 / 否则→end)→N3_QUALITY_DISPOSITION→end)。
+/// v1→v2 幂等升级:已发布定义含 N3 则跳过;仍为 v1 则下架旧版发布 v2;历史 Running 实例用各自 FlowJsonSnapshot 冻结不受影响。
+/// superAdmin.NET 不存在时仅告警不崩溃。
 /// </summary>
 public static class IpqcInspectionFlowSeed
 {
@@ -28,8 +29,9 @@ public static class IpqcInspectionFlowSeed
                 typeof(ApprovalBizType),
                 typeof(ApprovalFlowEntity),
                 typeof(ApprovalFlowVersion));
-            // 流程状态轻量扩展表(CodeFirst 建表,无 SQL)
+            // 流程状态轻量扩展表 + 质量处置表(CodeFirst 建表,无 SQL)
             db.CodeFirst.InitTables(typeof(AdoS6IpqcInspectionFlowState));
+            db.CodeFirst.InitTables(typeof(AdoS6IpqcQualityDisposition));
             EnsureRoles(db);
             EnsureUatMembers(db);
             EnsureBizType(db);
@@ -45,6 +47,7 @@ public static class IpqcInspectionFlowSeed
     {
         EnsureRole(db, IpqcInspectionFlowConst.RoleInspectorId, IpqcInspectionFlowConst.RoleInspectorCode, "过程检验员", 820);
         EnsureRole(db, IpqcInspectionFlowConst.RoleSupervisorId, IpqcInspectionFlowConst.RoleSupervisorCode, "过程检验主管", 821);
+        EnsureRole(db, IpqcInspectionFlowConst.RoleQualityEngineerId, IpqcInspectionFlowConst.RoleQualityEngineerCode, "过程检验质量工程师", 822);
     }
 
     private static void EnsureRole(ISqlSugarClient db, long id, string code, string name, int orderNo)
@@ -77,6 +80,7 @@ public static class IpqcInspectionFlowSeed
         }
         EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleInspectorId);
         EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleSupervisorId);
+        EnsureUserRole(db, user.Id, IpqcInspectionFlowConst.RoleQualityEngineerId);
     }
 
     private static void EnsureUserRole(ISqlSugarClient db, long userId, long roleId)
@@ -125,9 +129,22 @@ public static class IpqcInspectionFlowSeed
                 .Where(x => x.BizType == IpqcInspectionFlowConst.BizType && x.OrgId == orgId && x.IsPublished && !x.IsDelete)
                 .OrderByDescending(x => x.Version)
                 .First();
-            // 已有已发布定义则跳过(幂等,不覆盖)。历史实例用各自 FlowJsonSnapshot。
-            if (existing != null) continue;
 
+            // v1→v2 幂等升级:已发布定义若已含 N3/GW_RESULT(v2)则跳过;若仍为 v1(无 N3)则下架旧版、发布 v2。
+            // 历史 Running/Approved 实例使用各自 FlowJsonSnapshot 冻结,不受本次 republish 影响。
+            if (existing != null)
+            {
+                var isV2 = (existing.FlowJson ?? "").Contains(IpqcInspectionFlowConst.NodeDisposition)
+                    && (existing.FlowJson ?? "").Contains(IpqcInspectionFlowConst.GatewayResult);
+                if (isV2) continue;
+                db.Updateable<ApprovalFlowEntity>()
+                    .SetColumns(x => x.IsPublished == false)
+                    .SetColumns(x => x.UpdateTime == now)
+                    .Where(x => x.Id == existing.Id)
+                    .ExecuteCommand();
+            }
+
+            var nextVersion = (existing?.Version ?? 0) + 1;
             var flow = new ApprovalFlowEntity
             {
                 Id = YitIdHelper.NextId(),
@@ -137,8 +154,8 @@ public static class IpqcInspectionFlowSeed
                 FormJson = "{}",
                 FlowJson = flowJson,
                 Status = 1,
-                Remark = "系统默认流程 v1:开始-检验员检验-检验主管审核-结束",
-                Version = 1,
+                Remark = "系统默认流程 v2:开始-检验员检验-检验主管审核-网关(整批判定)-质量处置-结束",
+                Version = nextVersion,
                 IsPublished = true,
                 OrgId = orgId,
                 CreateTime = now,
@@ -208,20 +225,67 @@ public static class IpqcInspectionFlowSeed
                     Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" },
                 },
                 new()
+                {
+                    Id = IpqcInspectionFlowConst.GatewayResult,
+                    Type = "bpmn:exclusiveGateway",
+                    X = 660,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "整批判定分流",
+                        Conditions = new List<GatewayCondition>
+                        {
+                            new()
+                            {
+                                Expression = "disposition_required == 1",
+                                TargetNodeId = IpqcInspectionFlowConst.NodeDisposition,
+                                IsDefault = false,
+                                Label = "整批不合格转处置",
+                            },
+                            new()
+                            {
+                                TargetNodeId = "end",
+                                IsDefault = true,
+                                Label = "整批合格完成",
+                            },
+                        },
+                    },
+                    Text = new FlowTextItem { X = 660, Y = 154, Value = "整批判定分流" },
+                },
+                new()
+                {
+                    Id = IpqcInspectionFlowConst.NodeDisposition,
+                    Type = "bpmn:userTask",
+                    X = 820,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "质量处置",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = IpqcInspectionFlowConst.RoleQualityEngineerId.ToString(),
+                        ApproverNames = "过程检验质量工程师",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 820, Y = 154, Value = "质量处置" },
+                },
+                new()
                 {
                     Id = "end",
                     Type = "bpmn:endEvent",
-                    X = 700,
+                    X = 980,
                     Y = 120,
                     Properties = new FlowProperties { NodeName = "结束" },
-                    Text = new FlowTextItem { X = 700, Y = 154, Value = "结束" },
+                    Text = new FlowTextItem { X = 980, 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),
+                BuildEdge("edge-n2-gw", IpqcInspectionFlowConst.NodeSupervisor, IpqcInspectionFlowConst.GatewayResult, 540, 120, 640, 120),
+                BuildEdge("edge-gw-n3", IpqcInspectionFlowConst.GatewayResult, IpqcInspectionFlowConst.NodeDisposition, 680, 120, 780, 120),
+                BuildEdge("edge-gw-end", IpqcInspectionFlowConst.GatewayResult, "end", 680, 120, 960, 120),
+                BuildEdge("edge-n3-end", IpqcInspectionFlowConst.NodeDisposition, "end", 860, 120, 960, 120),
             }
         };
         return JsonSerializer.Serialize(item);

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

@@ -0,0 +1,60 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+
+/// <summary>
+/// 质量工程师提交不合格处置入参。写 ado_s6_ipqc_quality_disposition 并推进 ApprovalFlow N3→end。
+/// 仅记录处置方案,不触发库存/生产/仓储/返工/报废实际动作。
+/// </summary>
+public class IpqcSubmitDispositionInput
+{
+    /// <summary>检验单 id(qms_gcjyd.id,BizId)</summary>
+    public long Id { get; set; }
+
+    /// <summary>处置类型:REWORK 返工 / CONCESSION 让步 / SCRAP 报废 / OTHER 其它</summary>
+    public string? DispositionType { get; set; }
+
+    /// <summary>处置数量(不得为负)</summary>
+    public decimal? DispositionQty { get; set; }
+
+    /// <summary>返工数量(不得为负)</summary>
+    public decimal? ReworkQty { get; set; }
+
+    /// <summary>让步数量(不得为负)</summary>
+    public decimal? ConcessionQty { get; set; }
+
+    /// <summary>报废数量(不得为负)</summary>
+    public decimal? ScrapQty { get; set; }
+
+    /// <summary>处置意见(必填)</summary>
+    public string? DispositionOpinion { get; set; }
+
+    /// <summary>责任部门</summary>
+    public string? ResponsibilityDept { get; set; }
+
+    /// <summary>责任人</summary>
+    public string? ResponsibleUser { get; set; }
+
+    /// <summary>流程意见(可选)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>
+/// 质量处置详情输出(只读,供前端展示已提交处置)。
+/// </summary>
+public class IpqcDispositionOutput
+{
+    public long Id { get; set; }
+    public long InspectionId { get; set; }
+    public string? InspectionNo { get; set; }
+    public string? DispositionNo { get; set; }
+    public string? DispositionType { get; set; }
+    public decimal? DispositionQty { get; set; }
+    public decimal? ReworkQty { get; set; }
+    public decimal? ConcessionQty { get; set; }
+    public decimal? ScrapQty { get; set; }
+    public string? DispositionOpinion { get; set; }
+    public string? ResponsibilityDept { get; set; }
+    public string? ResponsibleUser { get; set; }
+    public int Status { get; set; }
+    public string? SubmittedByName { get; set; }
+    public DateTime? SubmittedAt { get; set; }
+}

+ 20 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/IpqcInspectionFlowDto.cs

@@ -28,12 +28,22 @@ public class IpqcSubmitInspectionInput
     public string? Comment { get; set; }
 }
 
-/// <summary>检验主管通过入参。</summary>
+/// <summary>
+/// 检验主管审核通过入参(整批判定驱动流程分流)。
+/// Qualified=true 整批合格→end;Qualified=false 整批不合格→N3 质量处置。
+/// Jgpd 为整批判定值(写 qms_gcjyd.jgpd,可空,枚举未确认不硬编码、由前端传测试/已确认合法值)。
+/// </summary>
 public class IpqcSupervisorApproveInput
 {
     /// <summary>检验单 id</summary>
     public long Id { get; set; }
 
+    /// <summary>整批是否合格(true=合格结束 / false=不合格转质量处置)</summary>
+    public bool Qualified { get; set; } = true;
+
+    /// <summary>整批判定 jgpd(写 qms_gcjyd.jgpd,可空,不硬编码枚举)</summary>
+    public int? Jgpd { get; set; }
+
     /// <summary>审批意见(可选)</summary>
     public string? Comment { get; set; }
 }
@@ -86,6 +96,15 @@ public class IpqcFlowStateOutput
     /// <summary>当前用户是否可主管退回</summary>
     public bool CanSupervisorReturn { get; set; }
 
+    /// <summary>当前用户是否可提交质量处置(当前节点 N3 且本人 pending)</summary>
+    public bool CanSubmitDisposition { get; set; }
+
+    /// <summary>是否需质量处置(1=不合格转N3 / 0=合格结束;主管 N2 整批判定驱动)</summary>
+    public int? DispositionRequired { get; set; }
+
+    /// <summary>是否已有质量处置记录</summary>
+    public bool HasDisposition { get; set; }
+
     /// <summary>流程是否已完成/终态</summary>
     public bool IsCompleted { get; set; }
 

+ 186 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionDispositionService.cs

@@ -0,0 +1,186 @@
+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)不合格质量处置服务(N3_QUALITY_DISPOSITION)。
+///
+/// 整批不合格检验单经主管 N2 判定后进入 N3;质量工程师提交处置方案 → 写 ado_s6_ipqc_quality_disposition + 推进 N3→end。
+/// 仅"记录处置结论"(disposition_type=REWORK/CONCESSION/SCRAP/OTHER);
+/// 不写 qms_gcjyd/qms_gcjydzb、不写库存/生产/仓储、不产生返工/报废/入出库实际动作、不做附件上传。
+/// 一单一有效处置(inspection_id 唯一 + status=1 校验);正常鉴权,权限以当前用户 N3 pending task 归属为准。
+/// 职责分离:本服务管处置数据;N1/N2 流程见 IpqcInspectionFlowService。
+/// </summary>
+[ApiDescriptionSettings(Order = 313, Description = "过程检验单质量处置")]
+[Route("api/IpqcInspectionDisposition")]
+[NonUnify]
+public class IpqcInspectionDispositionService : IDynamicApiController, ITransient
+{
+    private static readonly string[] DispositionTypes =
+    {
+        IpqcInspectionFlowConst.DispTypeRework,
+        IpqcInspectionFlowConst.DispTypeConcession,
+        IpqcInspectionFlowConst.DispTypeScrap,
+        IpqcInspectionFlowConst.DispTypeOther,
+    };
+
+    private readonly ISqlSugarClient _db;
+    private readonly FlowEngineService _flowEngine;
+    private readonly UserManager _userManager;
+
+    public IpqcInspectionDispositionService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
+    {
+        _db = db;
+        _flowEngine = flowEngine;
+        _userManager = userManager;
+    }
+
+    /// <summary>
+    /// 质量处置详情(只读,供前端展示已提交处置)。查不到返回 null。
+    /// </summary>
+    [DisplayName("过程检验单质量处置详情")]
+    [HttpGet("state")]
+    public async Task<IpqcDispositionOutput?> GetState([FromQuery] long id)
+    {
+        if (id <= 0) return null;
+        return await _db.Queryable<AdoS6IpqcQualityDisposition>()
+            .ClearFilter()
+            .Where(x => x.InspectionId == id && x.Status == 1)
+            .OrderByDescending(x => x.Id)
+            .Select(x => new IpqcDispositionOutput
+            {
+                Id = x.Id,
+                InspectionId = x.InspectionId,
+                InspectionNo = x.InspectionNo,
+                DispositionNo = x.DispositionNo,
+                DispositionType = x.DispositionType,
+                DispositionQty = x.DispositionQty,
+                ReworkQty = x.ReworkQty,
+                ConcessionQty = x.ConcessionQty,
+                ScrapQty = x.ScrapQty,
+                DispositionOpinion = x.DispositionOpinion,
+                ResponsibilityDept = x.ResponsibilityDept,
+                ResponsibleUser = x.ResponsibleUser,
+                Status = x.Status,
+                SubmittedByName = x.SubmittedByName,
+                SubmittedAt = x.SubmittedAt,
+            })
+            .FirstAsync();
+    }
+
+    /// <summary>
+    /// 质量工程师提交处置方案(写处置表 + 推进 N3→end)。仅记录方案,不触发库存/生产/仓储动作。
+    /// </summary>
+    [DisplayName("质量工程师提交处置")]
+    [HttpPost("submit")]
+    public async Task<IpqcDispositionOutput?> Submit([FromBody] IpqcSubmitDispositionInput input)
+    {
+        var billNo = await EnsureBillExistsAsync(input.Id);
+
+        // 参数校验
+        var type = (input.DispositionType ?? string.Empty).Trim().ToUpperInvariant();
+        if (!DispositionTypes.Contains(type))
+            throw Oops.Oh("处置类型只能为 REWORK(返工)/CONCESSION(让步)/SCRAP(报废)/OTHER(其它)");
+        if (string.IsNullOrWhiteSpace(input.DispositionOpinion))
+            throw Oops.Oh("处置意见必填");
+        foreach (var q in new[] { input.DispositionQty, input.ReworkQty, input.ConcessionQty, input.ScrapQty })
+            if (q is < 0) throw Oops.Oh("处置数量不能为负");
+        var sub = (input.ReworkQty ?? 0) + (input.ConcessionQty ?? 0) + (input.ScrapQty ?? 0);
+        if (input.DispositionQty.HasValue && sub > input.DispositionQty.Value)
+            throw Oops.Oh("返工+让步+报废数量之和不能大于处置数量");
+
+        var inst = await GetLatestInstanceAsync(input.Id);
+        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.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
+
+        // 一单一有效处置
+        var hasEffective = await _db.Queryable<AdoS6IpqcQualityDisposition>()
+            .ClearFilter()
+            .AnyAsync(x => x.InspectionId == input.Id && x.Status == 1);
+        if (hasEffective) throw Oops.Oh("该检验单已有有效质量处置,不可重复提交");
+
+        var now = DateTime.Now;
+        var userName = _userManager.RealName ?? _userManager.Account;
+        var disposition = new AdoS6IpqcQualityDisposition
+        {
+            Id = YitIdHelper.NextId(),
+            TenantId = _userManager.TenantId,
+            OrgId = inst.OrgId,
+            InspectionId = input.Id,
+            InspectionNo = billNo,
+            BizType = IpqcInspectionFlowConst.BizType,
+            FlowInstanceId = inst.Id,
+            DispositionNo = $"IPQCDISP-{billNo}-{now:yyyyMMddHHmmss}",
+            DispositionType = type,
+            DispositionQty = input.DispositionQty,
+            ReworkQty = input.ReworkQty,
+            ConcessionQty = input.ConcessionQty,
+            ScrapQty = input.ScrapQty,
+            DispositionOpinion = input.DispositionOpinion,
+            ResponsibilityDept = input.ResponsibilityDept,
+            ResponsibleUser = input.ResponsibleUser,
+            Status = 1,
+            SubmittedBy = _userManager.UserId,
+            SubmittedByName = userName,
+            SubmittedAt = now,
+            RowVersion = 1,
+            CreateTime = now,
+        };
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _db.Insertable(disposition).ExecuteCommandAsync();
+            // 更新扩展表业务状态 DISPOSED(不回写 qms_gcjyd.status)
+            await _db.Ado.ExecuteCommandAsync(
+                "UPDATE ado_s6_ipqc_inspection_flow_state SET business_status=@st, current_node=NULL, UpdateTime=@now WHERE inspection_id=@id AND IsDelete=0",
+                new List<SugarParameter>
+                {
+                    new("@st", IpqcInspectionFlowConst.StatusDisposed),
+                    new("@now", now),
+                    new("@id", input.Id),
+                });
+            // 推进 N3 → end
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await GetState(input.Id);
+    }
+
+    // ───────────────────────── helpers ─────────────────────────
+
+    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();
+    }
+
+    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();
+    }
+
+    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();
+    }
+}

+ 14 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowBizHandler.cs

@@ -6,10 +6,11 @@ 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 仅返回基础信息(本批线性流程无网关条件)。
+/// Handler 回调全部 no-op:不回写 qms_gcjyd、不写库存/生产/仓储、不发额外通知。
+/// 检验结果字段由 IpqcInspectionFlowService/IpqcInspectionDispositionService 在事务内写入 qms_gcjyd 白名单列 / 处置表;
+/// 流程状态由 ApprovalFlowInstance 承载 + 轻量扩展表 ado_s6_ipqc_inspection_flow_state 落审核信息,不回写 qms_gcjyd.status。
+/// GetBizData 返回 GW_RESULT 网关判据 disposition_required(主管 N2 整批判定驱动,源自扩展表),
+/// 不用 jgpd 枚举做流程分流(枚举未确认)。
 /// </summary>
 public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
 {
@@ -31,14 +32,20 @@ public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
     /// <summary>流程结束回调:no-op(不写 qms_gcjyd.status/库存/生产/仓储)。审核结果由 ApprovalFlowInstance + 扩展表承载。</summary>
     public Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId) => Task.CompletedTask;
 
-    /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
+    /// <summary>返回检验单基础信息 + 网关判据 disposition_required(GW_RESULT 按此分流:==1→N3,否则→end)。</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>();
+        // 网关判据:主管 N2 整批判定写入扩展表 disposition_required(1=不合格转处置 / 0=合格结束)。
+        var dispRows = await _db.Ado.SqlQueryAsync<int?>(
+            "SELECT disposition_required FROM ado_s6_ipqc_inspection_flow_state WHERE inspection_id=@id AND IsDelete=0 ORDER BY id DESC LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId) });
+        var dispositionRequired = dispRows.FirstOrDefault() ?? 0;
+        if (r == null)
+            return new Dictionary<string, object> { ["disposition_required"] = dispositionRequired };
         return new Dictionary<string, object>
         {
             ["billNo"] = r.BillNo ?? "",
@@ -46,6 +53,7 @@ public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
             ["materialCode"] = r.MaterialCode ?? "",
             ["materialName"] = r.MaterialName ?? "",
             ["jgpd"] = r.Jgpd ?? -1,
+            ["disposition_required"] = dispositionRequired,
         };
     }
 

+ 28 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowConst.cs

@@ -1,10 +1,12 @@
 namespace Admin.NET.Plugin.AiDOP.Manufacturing;
 
 /// <summary>
-/// S6 过程检验单(IPQC)检验员+主管最小流程常量(BizType / 节点码 / 角色码 / 角色固定 Id)。
+/// S6 过程检验单(IPQC)检验员+主管+质量处置流程常量(BizType / 节点码 / 网关 / 角色码 / 角色固定 Id)。
 ///
-/// 最小闭环:N1_INSPECT 检验员录入并提交 → N2_SUP_REVIEW 检验主管审核(通过=完成 / 退回=回 N1)。
-/// 本批不含网关 / SQE / 不合格处置 / 生产 / 仓储 / 库存节点。
+/// v2 流程:N1_INSPECT 检验员录入 → N2_SUP_REVIEW 检验主管审核(整批判定)
+///   → GW_RESULT 网关(disposition_required==1→N3 / 否则→end)→ N3_QUALITY_DISPOSITION 质量处置 → end。
+/// 退回:N2→N1。整批不合格进入 N3 由主管 N2 显式决策驱动(disposition_required),与整批判定 jgpd 一致;
+/// 不猜 jgpd/pd 枚举、不硬编码。处置只"记录方案",不触发库存/生产/仓储实际动作。
 /// 角色 Id 取号避撞 S5 IQC(S5 用 132991501000x;S6 用 132991601000x)。
 /// </summary>
 public static class IpqcInspectionFlowConst
@@ -18,22 +20,34 @@ public static class IpqcInspectionFlowConst
     /// <summary>N2 检验主管审核节点</summary>
     public const string NodeSupervisor = "N2_SUP_REVIEW";
 
+    /// <summary>GW_RESULT 排他网关(按 disposition_required 分流:==1→N3,否则→end)</summary>
+    public const string GatewayResult = "GW_RESULT";
+
+    /// <summary>N3 质量处置节点</summary>
+    public const string NodeDisposition = "N3_QUALITY_DISPOSITION";
+
     /// <summary>检验员角色编码</summary>
     public const string RoleInspectorCode = "ROLE_S6_IPQC_INSPECTOR";
 
     /// <summary>检验主管角色编码</summary>
     public const string RoleSupervisorCode = "ROLE_S6_IPQC_SUPERVISOR";
 
+    /// <summary>质量工程师角色编码</summary>
+    public const string RoleQualityEngineerCode = "ROLE_S6_IPQC_QUALITY_ENGINEER";
+
     /// <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>质量工程师角色固定 Id</summary>
+    public const long RoleQualityEngineerId = 1329916010003L;
 
-    /// <summary>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审;职责隔离须用两独立账号验证)</summary>
+    /// <summary>流程定义版本(v2 加入 GW_RESULT/N3;seed 幂等升级依据)</summary>
+    public const int FlowDefinitionVersion = 2;
+
+    /// <summary>UAT 冒烟绑定账号(同绑三角色,仅 UAT 自审;职责隔离须用三独立账号验证)</summary>
     public const string UatAccount = "superAdmin.NET";
 
     // 业务状态(落轻量扩展表 business_status,不回写 qms_gcjyd.status)
@@ -41,4 +55,12 @@ public static class IpqcInspectionFlowConst
     public const string StatusPendingReview = "PENDING_REVIEW";
     public const string StatusApproved = "APPROVED";
     public const string StatusReturned = "RETURNED";
+    public const string StatusPendingDisposition = "PENDING_DISPOSITION";
+    public const string StatusDisposed = "DISPOSED";
+
+    // 处置类型(IPQC 自定义,非复用 S5 数值枚举;仅记录方案,不触发实际动作)
+    public const string DispTypeRework = "REWORK";
+    public const string DispTypeConcession = "CONCESSION";
+    public const string DispTypeScrap = "SCRAP";
+    public const string DispTypeOther = "OTHER";
 }

+ 41 - 14
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowService.cs

@@ -7,13 +7,14 @@ using Yitter.IdGenerator;
 namespace Admin.NET.Plugin.AiDOP.Manufacturing;
 
 /// <summary>
-/// S6 过程检验单(IPQC)检验员+主管最小审批流服务(IPQC-INSPECTOR-SUPERVISOR-FLOW-1)。
+/// S6 过程检验单(IPQC)检验员+主管流程服务(v2:含整批判定驱动的质量处置分流)。
 ///
-/// 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/通知。
+/// N1_INSPECT 检验员录入 → 提交 → N2_SUP_REVIEW 主管整批审核:
+///   整批合格(Qualified=true) → GW_RESULT(disposition_required=0) → end;
+///   整批不合格(Qualified=false) → GW_RESULT(disposition_required=1) → N3_QUALITY_DISPOSITION(见 IpqcInspectionDispositionService)。
+/// 退回:N2→N1。复用 ApprovalFlow 引擎(StartFlow/Approve/ReturnToPrev + IFlowBizHandler),不改核心。
+/// 整批判定 jgpd 写 qms_gcjyd 白名单列(可空、不硬编码枚举);disposition_required 落扩展表作网关判据(与 jgpd 整批判定一致)。
+/// 项目级 pd(qms_gcjydzb)仅明细展示辅助,不驱动整批流程。不回写 qms_gcjyd.status;不写库存/生产/仓储。
 /// 正常鉴权(非 AllowAnonymous);权限以当前用户 pending task 归属为准。
 /// </summary>
 [ApiDescriptionSettings(Order = 312, Description = "过程检验单流程")]
@@ -119,7 +120,8 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 检验主管通过(N2 → 完成)。
+    /// 检验主管通过(整批判定):Qualified=true 整批合格→end;Qualified=false 整批不合格→N3 质量处置。
+    /// 写整批 jgpd(可空、不硬编码枚举)+ disposition_required(网关判据),再 Approve→GW_RESULT 分流。
     /// </summary>
     [DisplayName("检验主管通过")]
     [HttpPost("supervisor-approve")]
@@ -129,11 +131,22 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
         var (inst, myTask) = await RequireSupervisorTaskAsync(input.Id);
         var now = DateTime.Now;
         var supervisorName = _userManager.RealName ?? _userManager.Account;
+        var dispositionRequired = input.Qualified ? 0 : 1;
+        var businessStatus = input.Qualified ? IpqcInspectionFlowConst.StatusApproved : IpqcInspectionFlowConst.StatusPendingDisposition;
+        var nextNode = input.Qualified ? null : IpqcInspectionFlowConst.NodeDisposition;
 
         var tran = await _db.AsTenant().UseTranAsync(async () =>
         {
-            await UpsertFlowStateAsync(input.Id, billNo, inst.Id, IpqcInspectionFlowConst.StatusApproved,
-                null, now, isSubmit: false, reviewResult: "APPROVED", supervisorName: supervisorName);
+            // 整批判定 jgpd 写 qms_gcjyd 白名单列(可空,不硬编码枚举;由前端传测试/已确认合法值)
+            if (input.Jgpd.HasValue)
+                await _db.Ado.ExecuteCommandAsync(
+                    "UPDATE qms_gcjyd SET jgpd=@jgpd WHERE id=@id",
+                    new List<SugarParameter> { new("@jgpd", input.Jgpd.Value), new("@id", input.Id) });
+
+            // 先写 disposition_required(GW_RESULT 网关判据,Approve 时 GetBizData 读取)再推进
+            await UpsertFlowStateAsync(input.Id, billNo, inst.Id, businessStatus, nextNode, now,
+                isSubmit: false, reviewResult: input.Qualified ? "APPROVED" : "TO_DISPOSITION",
+                supervisorName: supervisorName, dispositionRequired: dispositionRequired);
             await _flowEngine.Approve(myTask.Id, input.Comment);
         });
         if (!tran.IsSuccess) throw tran.ErrorException;
@@ -222,10 +235,10 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
             .AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
     }
 
-    /// <summary>Upsert 流程状态扩展表(不回写 qms_gcjyd.status)。</summary>
+    /// <summary>Upsert 流程状态扩展表(不回写 qms_gcjyd.status)。dispositionRequired 非空时更新网关判据。</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)
+        string? reviewResult = null, string? returnReason = null, int? dispositionRequired = null)
     {
         var existing = await _db.Queryable<AdoS6IpqcInspectionFlowState>()
             .ClearFilter()
@@ -244,6 +257,7 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
                 FlowInstanceId = instanceId,
                 BusinessStatus = businessStatus,
                 CurrentNode = currentNode,
+                DispositionRequired = dispositionRequired,
                 RowVersion = 1,
                 CreateTime = now,
             };
@@ -268,6 +282,7 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
         existing.FlowInstanceId = instanceId;
         existing.BusinessStatus = businessStatus;
         existing.CurrentNode = currentNode;
+        if (dispositionRequired.HasValue) existing.DispositionRequired = dispositionRequired;
         existing.RowVersion = (existing.RowVersion ?? 0) + 1;
         existing.UpdateTime = now;
         if (isSubmit)
@@ -302,7 +317,10 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
             output.BusinessStatus = state.BusinessStatus;
             output.InspectorName = state.InspectorName;
             output.SupervisorName = state.SupervisorName;
+            output.DispositionRequired = state.DispositionRequired;
         }
+        output.HasDisposition = await _db.Queryable<AdoS6IpqcQualityDisposition>()
+            .ClearFilter().AnyAsync(x => x.InspectionId == bizId && x.Status == 1);
 
         if (inst == null)
         {
@@ -341,12 +359,21 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
                 output.CanSupervisorReturn = true;
                 output.Message = "待当前用户主管审核";
             }
+            else if (myTask.NodeId == IpqcInspectionFlowConst.NodeDisposition)
+            {
+                output.CurrentAssigneeRole = IpqcInspectionFlowConst.RoleQualityEngineerCode;
+                output.CanSubmitDisposition = true;
+                output.Message = "待当前用户提交质量处置";
+            }
         }
         else
         {
-            output.CurrentAssigneeRole = inst.CurrentNodeId == IpqcInspectionFlowConst.NodeInspect
-                ? IpqcInspectionFlowConst.RoleInspectorCode
-                : IpqcInspectionFlowConst.RoleSupervisorCode;
+            output.CurrentAssigneeRole = inst.CurrentNodeId switch
+            {
+                IpqcInspectionFlowConst.NodeInspect => IpqcInspectionFlowConst.RoleInspectorCode,
+                IpqcInspectionFlowConst.NodeDisposition => IpqcInspectionFlowConst.RoleQualityEngineerCode,
+                _ => IpqcInspectionFlowConst.RoleSupervisorCode,
+            };
             output.Message = "流程进行中(当前用户无待办)";
         }
         return output;