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

feat(s5): add iqc sqe disposition flow

- 新增 ado_s5_iqc_sqe_disposition CodeFirst 实体(EntityBaseDel+ITenantIdFilter,无 SQL)
- 新增 ROLE_S5_SQE seed + superAdmin.NET UAT 绑定
- S5_IQC_INSPBILL flow v1→v2 升级:start→N1→N2→GW_PD→[pd==1:N3_SQE_DISPOSITION→end]/[default:end]
- GW_PD exclusiveGateway 按 GetBizData(pd) 分流;N3 绑 ROLE_S5_SQE;v1 下架、历史实例靠 FlowJsonSnapshot 兼容
- 新增 sqe-submit-disposition + state 扩展 canSqeSubmit/isWaitingSqe/hasSqeDisposition/sqeDisposition*
- drawer SQE 处置区(处置方式/数量/意见/供应商回复);不覆盖 qms.clfs,SQE 类型写独立表
- 不写 FBILLSTATUS/审核字段/leader、不写库存、不入库、不做仓储通知、不改 ApprovalFlow 核心、不新增 SQL/菜单/功能码
- 冒烟:不合格→网关→N3→SQE→CLOSED;合格→default→CLOSED 不进 N3;6 项校验幂等全400无500;SQE表仅+1;qms.clfs 未覆盖
- 版本 server 1.0.238 / Web 2.4.238
YY968XX 1 день назад
Родитель
Сommit
a7db543292

+ 1 - 1
Web/package.json

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

+ 23 - 0
Web/src/views/aidop/s5/api/iqcInspBillFlow.ts

@@ -15,10 +15,29 @@ export interface IqcFlowState {
 	canSupervisorApprove?: boolean;
 	canSupervisorReject?: boolean;
 	isCompleted?: boolean;
+	canSqeSubmit?: boolean;
+	isWaitingSqe?: boolean;
+	hasSqeDisposition?: boolean;
+	sqeDispositionId?: number | null;
+	sqeDispositionStatus?: number | null;
+	sqeDispositionType?: number | null;
 	updatedAt?: string | null;
 	message?: string | null;
 }
 
+export interface IqcSqeDispositionPayload {
+	id: number;
+	dispositionType: number;
+	dispositionQty?: number | null;
+	concessionQty?: number | null;
+	selectionQty?: number | null;
+	scrapQty?: number | null;
+	dispositionOpinion: string;
+	supplierResponse?: string;
+	attachmentJson?: string;
+	comment?: string;
+}
+
 export interface IqcSubmitResultPayload {
 	id: number;
 	pd: number;
@@ -45,3 +64,7 @@ export function approveIqcInspBill(id: number, comment?: string) {
 export function rejectIqcInspBill(id: number, comment: string) {
 	return service.post<IqcFlowState>('/api/S5IqcInspBillFlow/supervisor-reject', { id, comment }).then((r) => r.data);
 }
+
+export function sqeSubmitIqcInspBillDisposition(payload: IqcSqeDispositionPayload) {
+	return service.post<IqcFlowState>('/api/S5IqcInspBillFlow/sqe-submit-disposition', payload).then((r) => r.data);
+}

+ 89 - 1
Web/src/views/aidop/s5/iqc/components/IqcInspBillDetailDrawer.vue

@@ -116,6 +116,42 @@
 					<el-button type="success" :loading="submitting" @click="onApprove">通过</el-button>
 					<el-button type="warning" :loading="submitting" @click="onReject">退回</el-button>
 				</div>
+
+				<!-- SQE 处置(仅 canSqeSubmit 显示)-->
+				<div v-if="flowState?.canSqeSubmit" class="flow-op mt12">
+					<div class="flow-op-title">SQE 处置(不合格)</div>
+					<div class="sqe-hint">不合格数量 {{ dashNum(detail.unqualifiedQty) }} · 检验员处理方式(clfs) {{ dash(detail.processMethod) }}</div>
+					<el-form :model="sqeForm" label-width="96px" size="small" class="mt12">
+						<el-form-item label="处置方式">
+							<el-select v-model="sqeForm.dispositionType" style="width: 180px">
+								<el-option :value="0" label="让步接收" />
+								<el-option :value="1" label="挑选" />
+								<el-option :value="2" label="报废" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="处置数量">
+							<el-input-number v-model="sqeForm.dispositionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="让步数量">
+							<el-input-number v-model="sqeForm.concessionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="挑选数量">
+							<el-input-number v-model="sqeForm.selectionQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="报废数量">
+							<el-input-number v-model="sqeForm.scrapQty" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="处置意见">
+							<el-input v-model="sqeForm.dispositionOpinion" type="textarea" :rows="2" style="width: 320px" />
+						</el-form-item>
+						<el-form-item label="供应商回复">
+							<el-input v-model="sqeForm.supplierResponse" type="textarea" :rows="2" style="width: 320px" />
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" :loading="submitting" @click="onSqeSubmit">提交 SQE 处置</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
 			</template>
 			<el-empty v-else-if="!loading" description="暂无详情数据" />
 		</div>
@@ -126,7 +162,7 @@
 import { computed, reactive, watch } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import type { IqcInspBillDetail } from '../../api/iqcInspBill';
-import type { IqcFlowState, IqcSubmitResultPayload } from '../../api/iqcInspBillFlow';
+import type { IqcFlowState, IqcSubmitResultPayload, IqcSqeDispositionPayload } from '../../api/iqcInspBillFlow';
 
 const props = defineProps<{
 	modelValue: boolean;
@@ -142,6 +178,7 @@ const emit = defineEmits<{
 	(e: 'submit-result', payload: IqcSubmitResultPayload): void;
 	(e: 'approve'): void;
 	(e: 'reject', comment: string): void;
+	(e: 'sqe-submit', payload: IqcSqeDispositionPayload): void;
 }>();
 
 const innerVisible = computed({
@@ -158,6 +195,25 @@ const resultForm = reactive<{ pd: number; dhsl: number; bhgsl: number; clfs: num
 	comment: '',
 });
 
+// SQE 处置表单(仅 canSqeSubmit 时可见)
+const sqeForm = reactive<{
+	dispositionType: number;
+	dispositionQty: number | null;
+	concessionQty: number | null;
+	selectionQty: number | null;
+	scrapQty: number | null;
+	dispositionOpinion: string;
+	supplierResponse: string;
+}>({
+	dispositionType: 0,
+	dispositionQty: null,
+	concessionQty: null,
+	selectionQty: null,
+	scrapQty: null,
+	dispositionOpinion: '',
+	supplierResponse: '',
+});
+
 // 每次打开 drawer 重置录入表单
 watch(
 	() => props.modelValue,
@@ -168,10 +224,38 @@ watch(
 			resultForm.bhgsl = 0;
 			resultForm.clfs = null;
 			resultForm.comment = '';
+			sqeForm.dispositionType = 0;
+			sqeForm.dispositionQty = null;
+			sqeForm.concessionQty = null;
+			sqeForm.selectionQty = null;
+			sqeForm.scrapQty = null;
+			sqeForm.dispositionOpinion = '';
+			sqeForm.supplierResponse = '';
 		}
 	}
 );
 
+function onSqeSubmit() {
+	if (sqeForm.dispositionType !== 0 && sqeForm.dispositionType !== 1 && sqeForm.dispositionType !== 2)
+		return ElMessage.error('请选择处置方式');
+	if (!sqeForm.dispositionOpinion || !sqeForm.dispositionOpinion.trim()) return ElMessage.error('请填写处置意见');
+	for (const q of [sqeForm.dispositionQty, sqeForm.concessionQty, sqeForm.selectionQty, sqeForm.scrapQty]) {
+		if (q !== null && q !== undefined && q < 0) return ElMessage.error('处置数量不能为负');
+	}
+	const id = props.detail?.id;
+	if (!id) return ElMessage.error('检验单 id 缺失');
+	emit('sqe-submit', {
+		id,
+		dispositionType: sqeForm.dispositionType,
+		dispositionQty: sqeForm.dispositionQty,
+		concessionQty: sqeForm.concessionQty,
+		selectionQty: sqeForm.selectionQty,
+		scrapQty: sqeForm.scrapQty,
+		dispositionOpinion: sqeForm.dispositionOpinion.trim(),
+		supplierResponse: sqeForm.supplierResponse || undefined,
+	});
+}
+
 function onPdChange() {
 	if (resultForm.pd === 0) {
 		resultForm.bhgsl = 0;
@@ -267,4 +351,8 @@ function onClosed() {
 	font-weight: bold;
 	margin-bottom: 10px;
 }
+.sqe-hint {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
 </style>

+ 16 - 0
Web/src/views/aidop/s5/iqc/iqcTaskList.vue

@@ -81,6 +81,7 @@
 			@submit-result="onSubmitResult"
 			@approve="onApprove"
 			@reject="onReject"
+			@sqe-submit="onSqeSubmit"
 		/>
 
 		<div class="pager">
@@ -108,8 +109,10 @@ import {
 	submitIqcInspBillResult,
 	approveIqcInspBill,
 	rejectIqcInspBill,
+	sqeSubmitIqcInspBillDisposition,
 	type IqcFlowState,
 	type IqcSubmitResultPayload,
+	type IqcSqeDispositionPayload,
 } from '../api/iqcInspBillFlow';
 import IqcInspBillDetailDrawer from './components/IqcInspBillDetailDrawer.vue';
 
@@ -242,6 +245,19 @@ async function onReject(comment: string) {
 	}
 }
 
+async function onSqeSubmit(payload: IqcSqeDispositionPayload) {
+	flowSubmitting.value = true;
+	try {
+		await sqeSubmitIqcInspBillDisposition(payload);
+		ElMessage.success('SQE 处置已提交,流程完成');
+		await refreshAfterAction();
+	} catch (e: any) {
+		ElMessage.error(e?.message || 'SQE 处置提交失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
 async function loadList() {
 	loading.value = true;
 	try {

+ 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.237</AssemblyVersion>
-    <FileVersion>1.0.237</FileVersion>
-    <Version>1.0.237</Version>
+    <AssemblyVersion>1.0.238</AssemblyVersion>
+    <FileVersion>1.0.238</FileVersion>
+    <Version>1.0.238</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 107 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/AdoS5IqcSqeDisposition.cs

@@ -0,0 +1,107 @@
+using Admin.NET.Core;
+
+namespace Admin.NET.Plugin.AiDOP.Entity;
+
+/// <summary>
+/// S5 来料检验 SQE 处置记录。
+///
+/// 不合格来料检验单(qms_qcp_inspbill.pd=1)经检验主管通过后进入 N3_SQE_DISPOSITION,
+/// SQE 提交处置方案落此表。disposition_type 复用 clfs 编码(0 让步接收 / 1 挑选 / 2 报废),
+/// 不覆盖 qms_qcp_inspbill.clfs。本批不做仓储确认/库存/入库/附件上传(attachment_json 预留)。
+/// 一单一有效处置由 service 校验(status=1 且未软删)。
+/// </summary>
+[SugarTable("ado_s5_iqc_sqe_disposition", "S5 来料检验SQE处置")]
+[SugarIndex("idx_s5_sqe_disp_inspbill", nameof(InspbillId), OrderByType.Asc)]
+[SugarIndex("idx_s5_sqe_disp_flow", nameof(FlowInstanceId), OrderByType.Asc)]
+[SugarIndex("uk_s5_sqe_disp_no", nameof(DispositionNo), OrderByType.Asc, true)]
+[SugarIndex("idx_s5_sqe_disp_status", nameof(Status), OrderByType.Asc)]
+public class AdoS5IqcSqeDisposition : 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_qcp_inspbill.id)</summary>
+    [SugarColumn(ColumnName = "inspbill_id", ColumnDescription = "检验单id")]
+    public long InspbillId { get; set; }
+
+    /// <summary>检验单号(FBILLNO)</summary>
+    [SugarColumn(ColumnName = "inspbill_no", ColumnDescription = "检验单号", Length = 80)]
+    public string InspbillNo { get; set; } = string.Empty;
+
+    /// <summary>来源单号(lydjbh)</summary>
+    [SugarColumn(ColumnName = "source_bill_no", ColumnDescription = "来源单号", Length = 255, IsNullable = true)]
+    public string? SourceBillNo { get; set; }
+
+    /// <summary>业务类型</summary>
+    [SugarColumn(ColumnName = "biz_type", ColumnDescription = "业务类型", Length = 80, IsNullable = true)]
+    public string? BizType { get; set; } = "S5_IQC_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>处置方式:0 让步接收 / 1 挑选 / 2 报废</summary>
+    [SugarColumn(ColumnName = "disposition_type", ColumnDescription = "处置方式")]
+    public int DispositionType { get; set; }
+
+    /// <summary>处置数量</summary>
+    [SugarColumn(ColumnName = "disposition_qty", ColumnDescription = "处置数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? DispositionQty { get; set; }
+
+    /// <summary>让步接收数量</summary>
+    [SugarColumn(ColumnName = "concession_qty", ColumnDescription = "让步接收数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? ConcessionQty { get; set; }
+
+    /// <summary>挑选数量</summary>
+    [SugarColumn(ColumnName = "selection_qty", ColumnDescription = "挑选数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? SelectionQty { get; set; }
+
+    /// <summary>报废数量</summary>
+    [SugarColumn(ColumnName = "scrap_qty", ColumnDescription = "报废数量", Length = 18, DecimalDigits = 6, IsNullable = true)]
+    public decimal? ScrapQty { get; set; }
+
+    /// <summary>供应商回复</summary>
+    [SugarColumn(ColumnName = "supplier_response", ColumnDescription = "供应商回复", Length = 500, IsNullable = true)]
+    public string? SupplierResponse { get; set; }
+
+    /// <summary>处置意见</summary>
+    [SugarColumn(ColumnName = "disposition_opinion", ColumnDescription = "处置意见", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
+    public string? DispositionOpinion { get; set; }
+
+    /// <summary>附件 JSON(预留)</summary>
+    [SugarColumn(ColumnName = "attachment_json", ColumnDescription = "附件JSON", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
+    public string? AttachmentJson { get; set; }
+
+    /// <summary>SQE 处理人 id</summary>
+    [SugarColumn(ColumnName = "sqe_user_id", ColumnDescription = "SQE处理人id", IsNullable = true)]
+    public long? SqeUserId { get; set; }
+
+    /// <summary>SQE 处理人名</summary>
+    [SugarColumn(ColumnName = "sqe_user_name", ColumnDescription = "SQE处理人名", Length = 100, IsNullable = true)]
+    public string? SqeUserName { get; set; }
+
+    /// <summary>状态:1 已提交(本批最小)</summary>
+    [SugarColumn(ColumnName = "status", ColumnDescription = "状态")]
+    public int Status { get; set; }
+
+    /// <summary>提交时间</summary>
+    [SugarColumn(ColumnName = "submitted_at", ColumnDescription = "提交时间", IsNullable = true)]
+    public DateTime? SubmittedAt { get; set; }
+
+    /// <summary>确认人(预留)</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; }
+}

+ 72 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/IqcInspBillFlowSeed.cs

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

+ 52 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/IqcInspBillFlowDto.cs

@@ -82,9 +82,61 @@ public class IqcFlowStateOutput
     /// <summary>流程是否已完成/终态</summary>
     public bool IsCompleted { get; set; }
 
+    /// <summary>当前用户是否可提交 SQE 处置(当前节点 N3 且当前用户有 N3 待办)</summary>
+    public bool CanSqeSubmit { get; set; }
+
+    /// <summary>是否停留在 SQE 处置节点</summary>
+    public bool IsWaitingSqe { get; set; }
+
+    /// <summary>是否已有 SQE 处置记录</summary>
+    public bool HasSqeDisposition { get; set; }
+
+    /// <summary>SQE 处置记录 id</summary>
+    public long? SqeDispositionId { get; set; }
+
+    /// <summary>SQE 处置记录状态(1=已提交)</summary>
+    public int? SqeDispositionStatus { get; set; }
+
+    /// <summary>SQE 处置方式(0 让步接收 / 1 挑选 / 2 报废)</summary>
+    public int? SqeDispositionType { get; set; }
+
     /// <summary>更新时间</summary>
     public DateTime? UpdatedAt { get; set; }
 
     /// <summary>提示信息</summary>
     public string? Message { get; set; }
 }
+
+/// <summary>SQE 提交处置方案入参。写 ado_s5_iqc_sqe_disposition 并推进 N3→end。</summary>
+public class IqcSqeSubmitDispositionInput
+{
+    /// <summary>检验单 id</summary>
+    public long Id { get; set; }
+
+    /// <summary>处置方式:0 让步接收 / 1 挑选 / 2 报废</summary>
+    public int DispositionType { get; set; }
+
+    /// <summary>处置数量</summary>
+    public decimal? DispositionQty { get; set; }
+
+    /// <summary>让步接收数量</summary>
+    public decimal? ConcessionQty { get; set; }
+
+    /// <summary>挑选数量</summary>
+    public decimal? SelectionQty { get; set; }
+
+    /// <summary>报废数量</summary>
+    public decimal? ScrapQty { get; set; }
+
+    /// <summary>处置意见(必填)</summary>
+    public string? DispositionOpinion { get; set; }
+
+    /// <summary>供应商回复</summary>
+    public string? SupplierResponse { get; set; }
+
+    /// <summary>附件 JSON(预留,本批不做上传)</summary>
+    public string? AttachmentJson { get; set; }
+
+    /// <summary>流程意见(可选)</summary>
+    public string? Comment { get; set; }
+}

+ 15 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillFlowBizHandler.cs

@@ -17,18 +17,33 @@ public static class IqcInspBillFlowConst
     /// <summary>N2 检验主管审核节点</summary>
     public const string NodeSupervisor = "N2_SUP_REVIEW";
 
+    /// <summary>GW_PD 排他网关(按 pd 分流:pd=1→SQE,否则完成)</summary>
+    public const string GatewayPd = "GW_PD";
+
+    /// <summary>N3 SQE 处置节点</summary>
+    public const string NodeSqe = "N3_SQE_DISPOSITION";
+
     /// <summary>检验员角色编码</summary>
     public const string RoleInspectorCode = "ROLE_S5_IQC_INSPECTOR";
 
     /// <summary>检验主管角色编码</summary>
     public const string RoleSupervisorCode = "ROLE_S5_IQC_SUPERVISOR";
 
+    /// <summary>SQE 角色编码</summary>
+    public const string RoleSqeCode = "ROLE_S5_SQE";
+
     /// <summary>检验员角色固定 Id(种子与流程定义共用)</summary>
     public const long RoleInspectorId = 1329915010001L;
 
     /// <summary>检验主管角色固定 Id</summary>
     public const long RoleSupervisorId = 1329915010002L;
 
+    /// <summary>SQE 角色固定 Id</summary>
+    public const long RoleSqeId = 1329915010003L;
+
+    /// <summary>流程定义版本(升 v2 加入 GW_PD/N3_SQE;seed 幂等升级依据)</summary>
+    public const int FlowDefinitionVersion = 2;
+
     /// <summary>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审)</summary>
     public const string UatAccount = "superAdmin.NET";
 

+ 105 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillFlowService.cs

@@ -1,6 +1,8 @@
+using Admin.NET.Plugin.AiDOP.Entity;
 using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
 using Admin.NET.Plugin.ApprovalFlow;
 using Admin.NET.Plugin.ApprovalFlow.Service;
+using Yitter.IdGenerator;
 
 namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 
@@ -154,6 +156,83 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
         return await BuildStateAsync(input.Id);
     }
 
+    /// <summary>
+    /// SQE 提交处置方案(N3 → 完成)。写 ado_s5_iqc_sqe_disposition + 推进 N3→end。不写 qms.clfs/库存/仓储。
+    /// </summary>
+    [DisplayName("SQE提交处置方案")]
+    [HttpPost("sqe-submit-disposition")]
+    public async Task<IqcFlowStateOutput> SqeSubmitDisposition([FromBody] IqcSqeSubmitDispositionInput input)
+    {
+        var billNo = await EnsureBillExistsAsync(input.Id);
+
+        // 参数校验
+        if (input.DispositionType is not (0 or 1 or 2)) throw Oops.Oh("处置方式只能为 0(让步接收)/1(挑选)/2(报废)");
+        if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
+        foreach (var q in new[] { input.DispositionQty, input.ConcessionQty, input.SelectionQty, input.ScrapQty })
+            if (q.HasValue && q.Value < 0) throw Oops.Oh("处置数量不能为负");
+
+        // 检验单必须不合格(合格单无 SQE 节点)
+        var pdRows = await _db.Ado.SqlQueryAsync<int?>(
+            "SELECT pd FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", input.Id) });
+        if (pdRows.FirstOrDefault() != 1) throw Oops.Oh("仅不合格检验单(pd=1)可做 SQE 处置");
+
+        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("当前用户没有待处理的 SQE 处置任务(需 SQE 角色成员)");
+        if (myTask.NodeId != IqcInspBillFlowConst.NodeSqe) throw Oops.Oh("当前不在 SQE 处置节点");
+
+        // 一单一有效处置
+        var hasEffective = await _db.Queryable<AdoS5IqcSqeDisposition>()
+            .Where(x => x.InspbillId == input.Id && x.Status == 1)
+            .AnyAsync();
+        if (hasEffective) throw Oops.Oh("该检验单已有有效 SQE 处置,不可重复提交");
+
+        // 来源单号
+        var srcRows = await _db.Ado.SqlQueryAsync<string>(
+            "SELECT lydjbh FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", input.Id) });
+
+        var now = DateTime.Now;
+        var disposition = new AdoS5IqcSqeDisposition
+        {
+            Id = YitIdHelper.NextId(),
+            TenantId = _userManager.TenantId,
+            OrgId = inst.OrgId,
+            InspbillId = input.Id,
+            InspbillNo = billNo,
+            SourceBillNo = srcRows.FirstOrDefault(),
+            BizType = IqcInspBillFlowConst.BizType,
+            FlowInstanceId = inst.Id,
+            DispositionNo = $"SQE-{billNo}-{now:yyyyMMddHHmmss}",
+            DispositionType = input.DispositionType,
+            DispositionQty = input.DispositionQty,
+            ConcessionQty = input.ConcessionQty,
+            SelectionQty = input.SelectionQty,
+            ScrapQty = input.ScrapQty,
+            SupplierResponse = input.SupplierResponse,
+            DispositionOpinion = input.DispositionOpinion,
+            AttachmentJson = input.AttachmentJson,
+            SqeUserId = _userManager.UserId,
+            SqeUserName = _userManager.RealName ?? _userManager.Account,
+            Status = 1,
+            SubmittedAt = now,
+            CreateTime = 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>
@@ -226,6 +305,19 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
         output.FlowStatus = inst.Status.ToString();
         output.UpdatedAt = inst.EndTime ?? inst.StartTime;
 
+        // SQE 处置摘要(不合格单可能已有处置记录,任何状态都可展示)
+        var disp = await _db.Queryable<AdoS5IqcSqeDisposition>()
+            .Where(x => x.InspbillId == bizId)
+            .OrderByDescending(x => x.Id)
+            .FirstAsync();
+        if (disp != null)
+        {
+            output.HasSqeDisposition = true;
+            output.SqeDispositionId = disp.Id;
+            output.SqeDispositionStatus = disp.Status;
+            output.SqeDispositionType = disp.DispositionType;
+        }
+
         if (inst.Status != FlowInstanceStatusEnum.Running)
         {
             output.IsCompleted = true;
@@ -234,6 +326,7 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
         }
 
         output.CurrentNodeCode = inst.CurrentNodeId;
+        output.IsWaitingSqe = inst.CurrentNodeId == IqcInspBillFlowConst.NodeSqe;
         var myTask = await GetMyPendingTaskAsync(inst.Id);
         if (myTask != null)
         {
@@ -251,12 +344,21 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
                 output.CanSupervisorReject = true;
                 output.Message = "待当前用户主管审核";
             }
+            else if (myTask.NodeId == IqcInspBillFlowConst.NodeSqe)
+            {
+                output.CurrentAssigneeRole = IqcInspBillFlowConst.RoleSqeCode;
+                output.CanSqeSubmit = true;
+                output.Message = "待当前用户提交 SQE 处置";
+            }
         }
         else
         {
-            output.CurrentAssigneeRole = inst.CurrentNodeId == IqcInspBillFlowConst.NodeInspect
-                ? IqcInspBillFlowConst.RoleInspectorCode
-                : IqcInspBillFlowConst.RoleSupervisorCode;
+            output.CurrentAssigneeRole = inst.CurrentNodeId switch
+            {
+                IqcInspBillFlowConst.NodeInspect => IqcInspBillFlowConst.RoleInspectorCode,
+                IqcInspBillFlowConst.NodeSqe => IqcInspBillFlowConst.RoleSqeCode,
+                _ => IqcInspBillFlowConst.RoleSupervisorCode,
+            };
             output.Message = "流程进行中(当前用户无待办)";
         }
         return output;