Преглед изворни кода

feat(s7): FQC 不合格 QE 处置节点(N3+网关,工程链路)

- 流程 v1→v2 升级:N1→N2→GW_RESULT(disposition_required==1→N3/否则→end)→N3_QE_DISPOSITION→end
- 网关判据 disposition_required 由 BizHandler.GetBizData 从 pd 计算(pd varchar 去空=="1"),避开定义写脆弱 pd==1
- 新增 FqcInspBillFlowService.qe-submit-disposition:事务内 insert ado_s7_fqc_qe_disposition + Approve(N3→end)
- 一单一有效处置:inspbill_id 唯一索引 + status==1 + pending-task 三重防重;仅记录方案不触发库存/仓储/生产
- 新增角色 ROLE_S7_FQC_QE(1329915020003);Seed v1→v2 幂等升级 + InitTables 处置表(CodeFirst 无 SQL)
- state 扩 CanSubmitDisposition/DispositionRequired/HasDisposition;drawer 加 QE 处置操作区
- Startup 未改;纯审批流业务表零写入(空态);bump version Web 2.4.252 / server 1.0.250
YY968XX пре 2 недеља
родитељ
комит
aaa4a96297

+ 1 - 1
Web/package.json

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

+ 33 - 0
Web/src/views/aidop/s7/api/fqcInspBillFlow.ts

@@ -14,11 +14,39 @@ export interface FqcFlowState {
 	canSubmitResult?: boolean;
 	canSupervisorApprove?: boolean;
 	canSupervisorReject?: boolean;
+	/** 当前用户是否可提交 QE 处置(N3 待办) */
+	canSubmitDisposition?: boolean;
+	/** 网关判据:是否需要 QE 处置(1=需要/pd=1) */
+	dispositionRequired?: number;
+	/** 是否停留在 QE 处置节点 */
+	isWaitingDisposition?: boolean;
+	/** 是否已有有效 QE 处置记录 */
+	hasDisposition?: boolean;
+	/** QE 处置类型(已提交时展示) */
+	dispositionType?: string | null;
+	/** QE 处置意见(已提交时展示) */
+	dispositionOpinion?: string | null;
 	isCompleted?: boolean;
 	updatedAt?: string | null;
 	message?: string | null;
 }
 
+export interface FqcQeDispositionPayload {
+	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;
+	attachmentJson?: string;
+	comment?: string;
+}
+
 export interface FqcSubmitResultPayload {
 	id: number;
 	/** 0=合格 1=不合格 */
@@ -51,3 +79,8 @@ export function fqcSupervisorApprove(id: number, comment?: string) {
 export function fqcSupervisorReject(id: number, comment: string) {
 	return service.post<FqcFlowState>('/api/S7FqcInspBillFlow/supervisor-reject', { id, comment }).then((r) => r.data);
 }
+
+/** QE 提交处置方案(N3→end) */
+export function submitFqcQeDisposition(payload: FqcQeDispositionPayload) {
+	return service.post<FqcFlowState>('/api/S7FqcInspBillFlow/qe-submit-disposition', payload).then((r) => r.data);
+}

+ 103 - 1
Web/src/views/aidop/s7/fqc/components/FqcInspBillDetailDrawer.vue

@@ -124,6 +124,53 @@
 					<el-button type="success" :loading="submitting" @click="onApprove">通过</el-button>
 					<el-button type="warning" :loading="submitting" @click="onReject">退回</el-button>
 				</div>
+
+				<!-- QE 处置(仅 canSubmitDisposition 显示;不合格 pd=1 经主管通过后进入)-->
+				<div v-if="flowState?.canSubmitDisposition" class="flow-op mt12">
+					<div class="flow-op-title">QE 质量处置(不合格)</div>
+					<el-form :model="qeForm" label-width="96px" size="small">
+						<el-form-item label="处置类型">
+							<el-select v-model="qeForm.dispositionType" style="width: 180px">
+								<el-option value="REWORK" label="返工" />
+								<el-option value="CONCESSION" label="让步放行" />
+								<el-option value="SCRAP" label="报废" />
+								<el-option value="OTHER" label="其它" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="处置数量">
+							<el-input-number v-model="qeForm.dispositionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="返工数量">
+							<el-input-number v-model="qeForm.reworkQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="让步数量">
+							<el-input-number v-model="qeForm.concessionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="报废数量">
+							<el-input-number v-model="qeForm.scrapQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="责任部门">
+							<el-input v-model="qeForm.responsibilityDept" style="width: 240px" />
+						</el-form-item>
+						<el-form-item label="责任人">
+							<el-input v-model="qeForm.responsibleUser" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="处置意见">
+							<el-input v-model="qeForm.dispositionOpinion" type="textarea" :rows="2" style="width: 320px" />
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" :loading="submitting" @click="onSubmitQe">提交 QE 处置</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+
+				<!-- 已提交 QE 处置(只读展示)-->
+				<el-descriptions v-if="flowState?.hasDisposition" title="QE 处置结论(仅展示)" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="处置类型">{{ dash(flowState?.dispositionType) }}</el-descriptions-item>
+					<el-descriptions-item label="处置意见">
+						<span class="pre">{{ dash(flowState?.dispositionOpinion) }}</span>
+					</el-descriptions-item>
+				</el-descriptions>
 			</template>
 			<el-empty v-else-if="!loading" description="暂无详情数据" />
 		</div>
@@ -134,7 +181,7 @@
 import { computed, reactive, watch } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import type { FqcDetail, FqcDetailLine } from '../../api/fqcInspBill';
-import type { FqcFlowState, FqcSubmitResultPayload } from '../../api/fqcInspBillFlow';
+import type { FqcFlowState, FqcSubmitResultPayload, FqcQeDispositionPayload } from '../../api/fqcInspBillFlow';
 
 const props = defineProps<{
 	modelValue: boolean;
@@ -149,6 +196,7 @@ const emit = defineEmits<{
 	(e: 'submit-result', payload: FqcSubmitResultPayload): void;
 	(e: 'approve'): void;
 	(e: 'reject', comment: string): void;
+	(e: 'qe-submit', payload: FqcQeDispositionPayload): void;
 }>();
 
 const innerVisible = computed({
@@ -165,6 +213,27 @@ const resultForm = reactive<{ pd: number; hgsl: number; bhgsl: number; clfs: num
 	comment: '',
 });
 
+// QE 处置表单(仅 canSubmitDisposition 时可见)
+const qeForm = reactive<{
+	dispositionType: string;
+	dispositionQty: number | null;
+	reworkQty: number | null;
+	concessionQty: number | null;
+	scrapQty: number | null;
+	dispositionOpinion: string;
+	responsibilityDept: string;
+	responsibleUser: string;
+}>({
+	dispositionType: 'REWORK',
+	dispositionQty: null,
+	reworkQty: null,
+	concessionQty: null,
+	scrapQty: null,
+	dispositionOpinion: '',
+	responsibilityDept: '',
+	responsibleUser: '',
+});
+
 // 每次打开 drawer 重置录入表单
 watch(
 	() => props.modelValue,
@@ -175,10 +244,39 @@ watch(
 			resultForm.bhgsl = 0;
 			resultForm.clfs = null;
 			resultForm.comment = '';
+			qeForm.dispositionType = 'REWORK';
+			qeForm.dispositionQty = null;
+			qeForm.reworkQty = null;
+			qeForm.concessionQty = null;
+			qeForm.scrapQty = null;
+			qeForm.dispositionOpinion = '';
+			qeForm.responsibilityDept = '';
+			qeForm.responsibleUser = '';
 		}
 	}
 );
 
+function onSubmitQe() {
+	if (!['REWORK', 'CONCESSION', 'SCRAP', 'OTHER'].includes(qeForm.dispositionType)) return ElMessage.error('请选择处置类型');
+	if (!qeForm.dispositionOpinion || !qeForm.dispositionOpinion.trim()) return ElMessage.error('处置意见必填');
+	for (const q of [qeForm.dispositionQty, qeForm.reworkQty, qeForm.concessionQty, qeForm.scrapQty]) {
+		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
+	}
+	const id = props.detail?.id;
+	if (!id) return ElMessage.error('检验单 id 缺失');
+	emit('qe-submit', {
+		id,
+		dispositionType: qeForm.dispositionType,
+		dispositionQty: qeForm.dispositionQty,
+		reworkQty: qeForm.reworkQty,
+		concessionQty: qeForm.concessionQty,
+		scrapQty: qeForm.scrapQty,
+		dispositionOpinion: qeForm.dispositionOpinion.trim(),
+		responsibilityDept: qeForm.responsibilityDept || undefined,
+		responsibleUser: qeForm.responsibleUser || undefined,
+	});
+}
+
 function onPdChange() {
 	if (resultForm.pd === 0) {
 		resultForm.bhgsl = 0;
@@ -304,4 +402,8 @@ function onClosed() {
 	font-weight: bold;
 	margin-bottom: 10px;
 }
+.pre {
+	white-space: pre-wrap;
+	word-break: break-word;
+}
 </style>

+ 17 - 0
Web/src/views/aidop/s7/fqc/fqcInspectionResultList.vue

@@ -89,6 +89,7 @@
 			@submit-result="onFlowSubmit"
 			@approve="onFlowApprove"
 			@reject="onFlowReject"
+			@qe-submit="onFlowQe"
 		/>
 	</AidopDemoShell>
 </template>
@@ -105,8 +106,10 @@ import {
 	submitFqcResult,
 	fqcSupervisorApprove,
 	fqcSupervisorReject,
+	submitFqcQeDisposition,
 	type FqcFlowState,
 	type FqcSubmitResultPayload,
+	type FqcQeDispositionPayload,
 } from '../api/fqcInspBillFlow';
 
 const route = useRoute();
@@ -205,6 +208,20 @@ async function onFlowReject(comment: string) {
 	}
 }
 
+async function onFlowQe(payload: FqcQeDispositionPayload) {
+	flowSubmitting.value = true;
+	try {
+		await submitFqcQeDisposition(payload);
+		ElMessage.success('QE 处置已提交');
+		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
+		loadList();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交 QE 处置失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
 function fmtDateTime(v?: string | null) {
 	if (!v) return '';
 	const s = String(v);

+ 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.249</AssemblyVersion>
-    <FileVersion>1.0.249</FileVersion>
-    <Version>1.0.249</Version>
+    <AssemblyVersion>1.0.250</AssemblyVersion>
+    <FileVersion>1.0.250</FileVersion>
+    <Version>1.0.250</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 116 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoS7FqcQeDisposition.cs

@@ -0,0 +1,116 @@
+using Admin.NET.Core;
+
+namespace Admin.NET.Plugin.AiDOP.Entity;
+
+/// <summary>
+/// S7 成品检验(FQC)不合格 QE 处置记录。
+///
+/// 不合格成品检验单(qms_qcpp_inspbill.pd="1")经主管 N2 通过后,GW_RESULT 网关(disposition_required==1)
+/// 分流进入 N3_QE_DISPOSITION,质量工程师提交处置方案落此表。
+/// 仅"记录处置结论"(disposition_type=REWORK/CONCESSION/SCRAP/OTHER);
+/// 本批不做仓储确认/库存/入库/出库/返工单/报废单/隔离单/附件上传(attachment_json 预留)。
+/// 一单一有效处置由 service 校验(status=1 且未软删)+ inspbill_id 唯一约束。
+/// 不覆盖 qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs,不写 qms_fqcbj。
+/// </summary>
+[SugarTable("ado_s7_fqc_qe_disposition", "S7成品检验QE处置")]
+[SugarIndex("uk_s7_fqc_disp_inspbill", nameof(InspbillId), OrderByType.Asc, true)]
+[SugarIndex("idx_s7_fqc_disp_flow", nameof(FlowInstanceId), OrderByType.Asc)]
+[SugarIndex("uk_s7_fqc_disp_no", nameof(DispositionNo), OrderByType.Asc, true)]
+public class AdoS7FqcQeDisposition : 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_qcpp_inspbill.id,BizId)</summary>
+    [SugarColumn(ColumnName = "inspbill_id", ColumnDescription = "检验单id")]
+    public long InspbillId { get; set; }
+
+    /// <summary>检验单号(FBILLNO)</summary>
+    [SugarColumn(ColumnName = "inspbill_no", ColumnDescription = "检验单号", Length = 80, IsNullable = true)]
+    public string? InspbillNo { get; set; }
+
+    /// <summary>来源单号(lydjbh)</summary>
+    [SugarColumn(ColumnName = "source_bill_no", ColumnDescription = "来源单号", Length = 255, IsNullable = true)]
+    public string? SourceBillNo { get; set; }
+
+    /// <summary>业务类型(S7_FQC_INSPBILL)</summary>
+    [SugarColumn(ColumnName = "biz_type", ColumnDescription = "业务类型", Length = 80, IsNullable = true)]
+    public string? BizType { get; set; } = "S7_FQC_INSPBILL";
+
+    /// <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>处置意见(必填,service 校验)</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; }
+}

+ 58 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/FqcInspBillFlowDto.cs

@@ -82,6 +82,24 @@ public class FqcFlowStateOutput
     /// <summary>当前用户是否可主管退回(N2 待办)</summary>
     public bool CanSupervisorReject { get; set; }
 
+    /// <summary>当前用户是否可提交 QE 处置(N3 待办)</summary>
+    public bool CanSubmitDisposition { get; set; }
+
+    /// <summary>网关判据:是否需要 QE 处置(1=需要/pd=1,0=不需要);供前端展示/调试</summary>
+    public int DispositionRequired { get; set; }
+
+    /// <summary>是否停留在 QE 处置节点</summary>
+    public bool IsWaitingDisposition { get; set; }
+
+    /// <summary>是否已有有效 QE 处置记录</summary>
+    public bool HasDisposition { get; set; }
+
+    /// <summary>QE 处置类型(已提交时展示,REWORK/CONCESSION/SCRAP/OTHER)</summary>
+    public string? DispositionType { get; set; }
+
+    /// <summary>QE 处置意见(已提交时展示)</summary>
+    public string? DispositionOpinion { get; set; }
+
     /// <summary>流程是否已完成/终态</summary>
     public bool IsCompleted { get; set; }
 
@@ -91,3 +109,43 @@ public class FqcFlowStateOutput
     /// <summary>提示信息</summary>
     public string? Message { get; set; }
 }
+
+/// <summary>
+/// QE 提交处置方案入参(N3_QE_DISPOSITION)。写 ado_s7_fqc_qe_disposition 并推进 N3→end。
+/// 仅记录处置结论,不覆盖 qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs、不写 qms_fqcbj/库存/仓储/生产。
+/// </summary>
+public class FqcQeSubmitDispositionInput
+{
+    /// <summary>检验单 id(qms_qcpp_inspbill.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>附件 JSON(预留,本批不做上传)</summary>
+    public string? AttachmentJson { get; set; }
+
+    /// <summary>流程意见(可选)</summary>
+    public string? Comment { get; set; }
+}

+ 29 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowBizHandler.cs

@@ -18,28 +18,45 @@ public static class FqcInspBillFlowConst
     /// <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 QE 处置节点(不合格 pd=1 进入)</summary>
+    public const string NodeDisposition = "N3_QE_DISPOSITION";
+
     /// <summary>检验员角色编码</summary>
     public const string RoleInspectorCode = "ROLE_S7_FQC_INSPECTOR";
 
     /// <summary>检验主管角色编码</summary>
     public const string RoleSupervisorCode = "ROLE_S7_FQC_SUPERVISOR";
 
+    /// <summary>质量工程师(QE)角色编码</summary>
+    public const string RoleQeCode = "ROLE_S7_FQC_QE";
+
     /// <summary>检验员角色固定 Id(种子与流程定义共用;S7 段 1329915020xxx,避开 S5 的 1329915010xxx)</summary>
     public const long RoleInspectorId = 1329915020001L;
 
     /// <summary>检验主管角色固定 Id</summary>
     public const long RoleSupervisorId = 1329915020002L;
 
-    /// <summary>流程定义版本(线性 N1→N2,首版=1)</summary>
-    public const int FlowDefinitionVersion = 1;
+    /// <summary>QE 角色固定 Id</summary>
+    public const long RoleQeId = 1329915020003L;
+
+    /// <summary>流程定义版本(v2 加入 GW_RESULT/N3_QE_DISPOSITION;seed 幂等升级依据)</summary>
+    public const int FlowDefinitionVersion = 2;
 
-    /// <summary>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审)</summary>
+    /// <summary>UAT 冒烟绑定账号(同绑角色,仅 UAT 自审)</summary>
     public const string UatAccount = "superAdmin.NET";
 
     // 检验任务执行状态 FINSPECTSTATUS:未检验 / 检验中 / 检验完成
     public const string InspectStatusDone = "检验完成";
 
     // pd 判定:0=合格 1=不合格 | clfs 处理方式:0=让步接收 1=挑选 2=报废
+    // QE 处置类型(string,仅记录方案,不触发实际动作)
+    public const string DispTypeRework = "REWORK";
+    public const string DispTypeConcession = "CONCESSION";
+    public const string DispTypeScrap = "SCRAP";
+    public const string DispTypeOther = "OTHER";
 }
 
 /// <summary>
@@ -70,14 +87,20 @@ public class FqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
     /// <summary>流程结束回调:本批 no-op(不写 qms/库存/仓储/生产;审核结果由 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)。
+    ///
+    /// disposition_required 由 pd 在 C# 侧计算(pd 为 varchar,直接在流程定义写 "pd == 1" 会 string==int 脆弱):
+    /// pd 去空后 == "1"(不合格) → 1(进 QE 处置);null/"0"/其它值 → 0(合格结束)。检验员 N1 已写 pd,主管 N2 通过后网关读取。
+    /// </summary>
     public async Task<Dictionary<string, object>> GetBizData(long bizId)
     {
         var rows = await _db.Ado.SqlQueryAsync<FqcInspBillFlowBizData>(
             "SELECT FBILLNO AS BillNo, FMATERIALCFG AS MaterialCode, wlmc AS MaterialName, pd AS Pd, clfs AS Clfs FROM qms_qcpp_inspbill WHERE id=@id LIMIT 1",
             new List<SugarParameter> { new("@id", bizId) });
         var r = rows.FirstOrDefault();
-        if (r == null) return new Dictionary<string, object>();
+        var dispositionRequired = (r?.Pd?.Trim() == "1") ? 1 : 0;
+        if (r == null) return new Dictionary<string, object> { ["disposition_required"] = dispositionRequired };
         return new Dictionary<string, object>
         {
             ["billNo"] = r.BillNo ?? "",
@@ -85,6 +108,7 @@ public class FqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
             ["materialName"] = r.MaterialName ?? "",
             ["pd"] = r.Pd ?? "",
             ["clfs"] = r.Clfs ?? -1,
+            ["disposition_required"] = dispositionRequired,
         };
     }
 

+ 101 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowService.cs

@@ -2,6 +2,7 @@ using Admin.NET.Plugin.AiDOP.Entity;
 using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
 using Admin.NET.Plugin.ApprovalFlow;
 using Admin.NET.Plugin.ApprovalFlow.Service;
+using Yitter.IdGenerator;
 
 namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 
@@ -174,6 +175,76 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
         return await BuildStateAsync(input.Id);
     }
 
+    /// <summary>
+    /// QE 提交处置方案(N3_QE_DISPOSITION → end)。写 ado_s7_fqc_qe_disposition + 推进 N3→end,同事务。
+    /// 仅记录处置结论;不覆盖 qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs、不写 qms_fqcbj/库存/仓储/生产。一单一有效处置。
+    /// </summary>
+    [DisplayName("QE提交处置方案")]
+    [HttpPost("qe-submit-disposition")]
+    public async Task<FqcFlowStateOutput> QeSubmitDisposition([FromBody] FqcQeSubmitDispositionInput input)
+    {
+        var (billNo, sourceBillNo) = await EnsureBillAsync(input.Id);
+
+        // 参数校验
+        var dt = input.DispositionType?.Trim().ToUpperInvariant();
+        if (dt is not (FqcInspBillFlowConst.DispTypeRework or FqcInspBillFlowConst.DispTypeConcession
+            or FqcInspBillFlowConst.DispTypeScrap or FqcInspBillFlowConst.DispTypeOther))
+            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 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("当前用户没有待处理的 QE 处置任务(需质量工程师角色成员)");
+        if (myTask.NodeId != FqcInspBillFlowConst.NodeDisposition) throw Oops.Oh("当前不在 QE 处置节点");
+
+        // 一单一有效处置(唯一索引 inspbill_id + status==1 双保险)
+        var hasEffective = await _db.Queryable<AdoS7FqcQeDisposition>()
+            .Where(x => x.InspbillId == input.Id && x.Status == 1)
+            .AnyAsync();
+        if (hasEffective) throw Oops.Oh("该检验单已有有效 QE 处置,不可重复提交");
+
+        var now = DateTime.Now;
+        var disposition = new AdoS7FqcQeDisposition
+        {
+            Id = YitIdHelper.NextId(),
+            TenantId = _userManager.TenantId,
+            OrgId = inst.OrgId,
+            InspbillId = input.Id,
+            InspbillNo = billNo,
+            SourceBillNo = sourceBillNo,
+            BizType = FqcInspBillFlowConst.BizType,
+            FlowInstanceId = inst.Id,
+            DispositionNo = $"FQCQE-{billNo}-{now:yyyyMMddHHmmss}",
+            DispositionType = dt!,
+            DispositionQty = input.DispositionQty,
+            ReworkQty = input.ReworkQty,
+            ConcessionQty = input.ConcessionQty,
+            ScrapQty = input.ScrapQty,
+            DispositionOpinion = input.DispositionOpinion!.Trim(),
+            ResponsibilityDept = input.ResponsibilityDept,
+            ResponsibleUser = input.ResponsibleUser,
+            AttachmentJson = input.AttachmentJson,
+            SubmittedBy = _userManager.UserId,
+            SubmittedByName = _userManager.RealName ?? _userManager.Account,
+            Status = 1,
+            SubmittedAt = now,
+        };
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _db.Insertable(disposition).ExecuteCommandAsync();
+            // 推进 N3 → end
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
     // ───────────────────────── helpers ─────────────────────────
 
     /// <summary>校验检验单存在并返回 FBILLNO。</summary>
@@ -254,14 +325,31 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
         output.FlowStatus = inst.Status.ToString();
         output.UpdatedAt = inst.EndTime ?? inst.StartTime;
 
+        // QE 处置摘要(任何状态都展示已提交处置)+ 网关判据 disposition_required(由 pd 计算,供展示/调试)
+        var disp = await _db.Queryable<AdoS7FqcQeDisposition>()
+            .Where(x => x.InspbillId == bizId && x.Status == 1)
+            .OrderByDescending(x => x.Id)
+            .FirstAsync();
+        if (disp != null)
+        {
+            output.HasDisposition = true;
+            output.DispositionType = disp.DispositionType;
+            output.DispositionOpinion = disp.DispositionOpinion;
+        }
+        var pdRows = await _db.Ado.SqlQueryAsync<string>(
+            "SELECT pd FROM qms_qcpp_inspbill WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId) });
+        output.DispositionRequired = (pdRows.FirstOrDefault()?.Trim() == "1") ? 1 : 0;
+
         if (inst.Status != FlowInstanceStatusEnum.Running)
         {
             output.IsCompleted = true;
-            output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "流程已完成(主管审核通过)" : $"流程已结束({inst.Status})";
+            output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "流程已完成(审批通过)" : $"流程已结束({inst.Status})";
             return output;
         }
 
         output.CurrentNodeCode = inst.CurrentNodeId;
+        output.IsWaitingDisposition = inst.CurrentNodeId == FqcInspBillFlowConst.NodeDisposition;
         var myTask = await GetMyPendingTaskAsync(inst.Id);
         if (myTask != null)
         {
@@ -279,12 +367,21 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
                 output.CanSupervisorReject = true;
                 output.Message = "待当前用户主管审核";
             }
+            else if (myTask.NodeId == FqcInspBillFlowConst.NodeDisposition)
+            {
+                output.CurrentAssigneeRole = FqcInspBillFlowConst.RoleQeCode;
+                output.CanSubmitDisposition = true;
+                output.Message = "待当前用户提交 QE 处置";
+            }
         }
         else
         {
-            output.CurrentAssigneeRole = inst.CurrentNodeId == FqcInspBillFlowConst.NodeInspect
-                ? FqcInspBillFlowConst.RoleInspectorCode
-                : FqcInspBillFlowConst.RoleSupervisorCode;
+            output.CurrentAssigneeRole = inst.CurrentNodeId switch
+            {
+                FqcInspBillFlowConst.NodeInspect => FqcInspBillFlowConst.RoleInspectorCode,
+                FqcInspBillFlowConst.NodeDisposition => FqcInspBillFlowConst.RoleQeCode,
+                _ => FqcInspBillFlowConst.RoleSupervisorCode,
+            };
             output.Message = "流程进行中(当前用户无待办)";
         }
         return output;

+ 71 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FqcInspBillFlowSeed.cs

@@ -1,5 +1,6 @@
 using System.Diagnostics;
 using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.Entity;
 using Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 using Admin.NET.Plugin.ApprovalFlow;
 using Admin.NET.Plugin.ApprovalFlow.Service;
@@ -25,6 +26,8 @@ public static class FqcInspBillFlowSeed
                 typeof(ApprovalBizType),
                 typeof(ApprovalFlowEntity),
                 typeof(ApprovalFlowVersion));
+            // QE 处置表(CodeFirst 建表,无 SQL)
+            db.CodeFirst.InitTables(typeof(AdoS7FqcQeDisposition));
             EnsureRoles(db);
             EnsureUatMembers(db);
             EnsureBizType(db);
@@ -40,6 +43,7 @@ public static class FqcInspBillFlowSeed
     {
         EnsureRole(db, FqcInspBillFlowConst.RoleInspectorId, FqcInspBillFlowConst.RoleInspectorCode, "成品检验员", 820);
         EnsureRole(db, FqcInspBillFlowConst.RoleSupervisorId, FqcInspBillFlowConst.RoleSupervisorCode, "成品检验主管", 821);
+        EnsureRole(db, FqcInspBillFlowConst.RoleQeId, FqcInspBillFlowConst.RoleQeCode, "成品检验QE", 822);
     }
 
     private static void EnsureRole(ISqlSugarClient db, long id, string code, string name, int orderNo)
@@ -72,6 +76,7 @@ public static class FqcInspBillFlowSeed
         }
         EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleInspectorId);
         EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleSupervisorId);
+        EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleQeId);
     }
 
     private static void EnsureUserRole(ISqlSugarClient db, long userId, long roleId)
@@ -121,9 +126,21 @@ public static class FqcInspBillFlowSeed
                 .OrderByDescending(x => x.Version)
                 .First();
 
-            // 已发布定义存在即跳过(线性 N1→N2 首版,不做版本升级)。
-            if (existing != null) continue;
+            // v1→v2 幂等升级:已发布定义若已含 N3_QE_DISPOSITION+GW_RESULT(v2)则跳过;
+            // 若仍为 v1(无 N3/网关)则下架旧版、发布 v2。历史 Running/Approved 实例用各自 FlowJsonSnapshot,不受影响。
+            if (existing != null)
+            {
+                var isV2 = (existing.FlowJson ?? "").Contains(FqcInspBillFlowConst.NodeDisposition)
+                    && (existing.FlowJson ?? "").Contains(FqcInspBillFlowConst.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(),
@@ -133,8 +150,8 @@ public static class FqcInspBillFlowSeed
                 FormJson = "{}",
                 FlowJson = flowJson,
                 Status = 1,
-                Remark = "系统默认流程 v1:开始-检验员检验-检验主管审核-结束(线性)",
-                Version = FqcInspBillFlowConst.FlowDefinitionVersion,
+                Remark = "系统默认流程 v2:开始-检验员检验-检验主管审核-网关(pd=1)-QE处置-结束",
+                Version = nextVersion,
                 IsPublished = true,
                 OrgId = orgId,
                 CreateTime = now,
@@ -204,20 +221,67 @@ public static class FqcInspBillFlowSeed
                     Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" },
                 },
                 new()
+                {
+                    Id = FqcInspBillFlowConst.GatewayResult,
+                    Type = "bpmn:exclusiveGateway",
+                    X = 660,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "判定分流",
+                        Conditions = new List<GatewayCondition>
+                        {
+                            new()
+                            {
+                                Expression = "disposition_required == 1",
+                                TargetNodeId = FqcInspBillFlowConst.NodeDisposition,
+                                IsDefault = false,
+                                Label = "不合格转QE",
+                            },
+                            new()
+                            {
+                                TargetNodeId = "end",
+                                IsDefault = true,
+                                Label = "合格完成",
+                            },
+                        },
+                    },
+                    Text = new FlowTextItem { X = 660, Y = 154, Value = "判定分流" },
+                },
+                new()
+                {
+                    Id = FqcInspBillFlowConst.NodeDisposition,
+                    Type = "bpmn:userTask",
+                    X = 820,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "QE处置",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = FqcInspBillFlowConst.RoleQeId.ToString(),
+                        ApproverNames = "成品检验QE",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 820, Y = 154, Value = "QE处置" },
+                },
+                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", FqcInspBillFlowConst.NodeInspect, 140, 120, 260, 120),
                 BuildEdge("edge-n1-n2", FqcInspBillFlowConst.NodeInspect, FqcInspBillFlowConst.NodeSupervisor, 340, 120, 460, 120),
-                BuildEdge("edge-n2-end", FqcInspBillFlowConst.NodeSupervisor, "end", 540, 120, 660, 120),
+                BuildEdge("edge-n2-gw", FqcInspBillFlowConst.NodeSupervisor, FqcInspBillFlowConst.GatewayResult, 540, 120, 640, 120),
+                BuildEdge("edge-gw-n3", FqcInspBillFlowConst.GatewayResult, FqcInspBillFlowConst.NodeDisposition, 680, 120, 780, 120),
+                BuildEdge("edge-gw-end", FqcInspBillFlowConst.GatewayResult, "end", 680, 120, 960, 120),
+                BuildEdge("edge-n3-end", FqcInspBillFlowConst.NodeDisposition, "end", 860, 120, 960, 120),
             }
         };
         return JsonSerializer.Serialize(item);