ソースを参照

feat(s6): bridge phase1c inspection bill into approval review flow

将 Phase 1C 过程检验单(ado_s6_process_inspection_bill)真正接入 ApprovalFlow(Phase 1D 集成桥接)。
Complete → 提交审核 → N1(发起人)→N2(主管)→GW(质量结果)→N3(QE处置)→CLOSED,复用引擎/角色/处置模型,不改 IPQC/qms_gcjyd。

- 新 BizType=S6_PROCESS_INSPECTION,business_id=process_inspection_bill.id(DIRECT 集成,非 qms_gcjyd 投影)
- 四件套:Const / BizHandler(GetBizData 读 overall_result 定 disposition_required) / ReviewService(submit/approve/return/disposition) / FlowSeed(N1=Initiator,复用 ROLE_S6_IPQC_SUPERVISOR/QE)
- 质量结果 overall_result 全程只读,审批/处置不改写(§20 硬门);处置复用 ado_s6_ipqc_quality_disposition(biz_type 区分)
- 提交门槛 COMPLETED、一单一 active flow 幂等、事务、租户隔离、audit 走 ApprovalFlow
- 前端 Phase 1C 抽屉加审核面板(提交审核/主管通过/退回/质量处置,状态由后端拥有)
- 契约测试 6 例

版本:server 1.0.457 / Web 2.4.357
YY968XX 6 日 前
コミット
2ad8156765

+ 1 - 1
Web/package.json

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

+ 47 - 0
Web/src/views/aidop/s6/api/productionInstruction.ts

@@ -156,3 +156,50 @@ export function saveProcessInspectionResult(billId: number, samples: SaveResultS
 export function completeProcessInspection(billId: number) {
 	return service.post<SaveResultOutput>('/api/S6ProcessInspection/complete', { billId }).then((r) => r.data);
 }
+
+// ── Phase 1D 审核流(复用 ApprovalFlow;business_id=process_inspection_bill.id)──
+// 前端只触发动作,不回传任何状态/判定;审批状态与去向由后端拥有。
+export interface ReviewState {
+	billId?: number;
+	instanceId?: number | null;
+	overallResult?: string; // 质量结果只读回显,审批不改写
+	resultStatus?: string;
+	flowStatus?: string; // NotStarted/Running/Approved/Rejected/Cancelled/Terminated
+	currentNodeCode?: string;
+	currentNodeName?: string;
+	currentAssigneeRole?: string;
+	isCompleted?: boolean;
+	hasDisposition?: boolean;
+	dispositionRequired?: number;
+	canSubmitReview?: boolean;
+	canSupervisorApprove?: boolean;
+	canSupervisorReturn?: boolean;
+	canSubmitDisposition?: boolean;
+	message?: string;
+}
+
+export interface QeDispositionInput {
+	billId: number;
+	dispositionType: string; // REWORK/CONCESSION/SCRAP/OTHER
+	dispositionQty?: number | null;
+	dispositionOpinion?: string;
+	responsibilityDept?: string;
+	responsibleUser?: string;
+	comment?: string;
+}
+
+export function fetchReviewState(billId: number) {
+	return service.get<ReviewState>('/api/S6ProcessInspectionReview/state', { params: { billId } }).then((r) => r.data);
+}
+export function submitReview(billId: number) {
+	return service.post<ReviewState>('/api/S6ProcessInspectionReview/submit-review', { billId }).then((r) => r.data);
+}
+export function reviewSupervisorApprove(billId: number, comment?: string) {
+	return service.post<ReviewState>('/api/S6ProcessInspectionReview/supervisor-approve', { billId, comment }).then((r) => r.data);
+}
+export function reviewSupervisorReturn(billId: number, comment: string) {
+	return service.post<ReviewState>('/api/S6ProcessInspectionReview/supervisor-return', { billId, comment }).then((r) => r.data);
+}
+export function reviewQeDisposition(input: QeDispositionInput) {
+	return service.post<ReviewState>('/api/S6ProcessInspectionReview/qe-disposition', input).then((r) => r.data);
+}

+ 113 - 0
Web/src/views/aidop/s6/production-record/productionInstructionList.vue

@@ -145,6 +145,32 @@
 					<el-button :loading="saving" @click="onSave">保存</el-button>
 					<el-button type="primary" :loading="completing" @click="onComplete">完成检验</el-button>
 				</div>
+
+				<div class="review-panel" v-if="detail">
+					<div class="snap-title">审核流程 · 复用 ApprovalFlow(质量结果只读,审批不改写)</div>
+					<div class="review-row">
+						审核状态:<b>{{ review?.flowStatus || '—' }}</b>
+						<span v-if="review?.currentNodeName"> 当前节点:{{ review.currentNodeName }}</span>
+						<span v-if="review?.message" class="review-msg"> {{ review.message }}</span>
+						<el-tag v-if="review?.hasDisposition" type="warning" size="small" style="margin-left: 8px">已处置</el-tag>
+					</div>
+					<div class="review-actions">
+						<el-button v-if="review?.canSubmitReview" type="primary" :loading="reviewing" @click="onSubmitReview">提交审核</el-button>
+						<el-button v-if="review?.canSupervisorApprove" type="success" :loading="reviewing" @click="onSupervisorApprove">主管通过</el-button>
+						<el-button v-if="review?.canSupervisorReturn" :loading="reviewing" @click="onSupervisorReturn">退回</el-button>
+						<template v-if="review?.canSubmitDisposition">
+							<el-select v-model="dispType" size="small" style="width: 150px" placeholder="处置类型">
+								<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-input v-model="dispOpinion" size="small" style="width: 220px" placeholder="处置意见(必填)" />
+							<el-button type="warning" :loading="reviewing" @click="onQeDisposition">提交处置</el-button>
+						</template>
+						<el-tag v-if="review?.isCompleted" :type="review?.flowStatus === 'Approved' ? 'success' : 'info'" size="small" disable-transitions>{{ review?.flowStatus === 'Approved' ? '审核已闭环 (CLOSED)' : review?.flowStatus }}</el-tag>
+					</div>
+				</div>
 			</div>
 		</el-drawer>
 	</AidopDemoShell>
@@ -160,10 +186,16 @@ import {
 	fetchProcessInspectionDetail,
 	saveProcessInspectionResult,
 	completeProcessInspection,
+	fetchReviewState,
+	submitReview,
+	reviewSupervisorApprove,
+	reviewSupervisorReturn,
+	reviewQeDisposition,
 	type ProductionInstructionRow,
 	type ProcessInspectionDetail,
 	type ProcessInspectionSnapshotLine,
 	type SaveResultSample,
+	type ReviewState,
 } from '../api/productionInstruction';
 
 const loading = ref(false);
@@ -181,6 +213,85 @@ const entry = reactive<Record<string, string>>({});
 const saving = ref(false);
 const completing = ref(false);
 
+// Phase 1D 审核流状态(后端拥有;前端只触发动作、不回传状态/判定)
+const review = ref<ReviewState | null>(null);
+const reviewing = ref(false);
+const dispType = ref('');
+const dispOpinion = ref('');
+
+async function loadReview(billId: number) {
+	try {
+		review.value = await fetchReviewState(billId);
+	} catch {
+		review.value = null;
+	}
+}
+
+async function onSubmitReview() {
+	const billId = detail.value?.billId;
+	if (!billId || reviewing.value) return;
+	reviewing.value = true;
+	try {
+		review.value = await submitReview(billId);
+		ElMessage.success('已提交审核');
+	} catch (e: any) {
+		const msg = e?.response?.data?.message || e?.message || '';
+		ElMessage.error(String(msg).includes('INSPECTION_NOT_COMPLETED') ? '检验未完成,无法提交审核' : '提交审核失败');
+		await loadReview(billId);
+	} finally {
+		reviewing.value = false;
+	}
+}
+
+async function onSupervisorApprove() {
+	const billId = detail.value?.billId;
+	if (!billId || reviewing.value) return;
+	reviewing.value = true;
+	try {
+		review.value = await reviewSupervisorApprove(billId);
+		ElMessage.success('主管审核通过');
+	} catch {
+		ElMessage.error('主管审核失败');
+		await loadReview(billId);
+	} finally {
+		reviewing.value = false;
+	}
+}
+
+async function onSupervisorReturn() {
+	const billId = detail.value?.billId;
+	if (!billId || reviewing.value) return;
+	try {
+		const { value } = await ElMessageBox.prompt('退回意见(必填)', '退回', { inputPattern: /\S+/, inputErrorMessage: '意见必填' });
+		reviewing.value = true;
+		review.value = await reviewSupervisorReturn(billId, value);
+		ElMessage.success('已退回');
+	} catch (e) {
+		if (e !== 'cancel') ElMessage.error('退回失败');
+	} finally {
+		reviewing.value = false;
+	}
+}
+
+async function onQeDisposition() {
+	const billId = detail.value?.billId;
+	if (!billId || reviewing.value) return;
+	if (!dispType.value) return ElMessage.warning('请选择处置类型');
+	if (!dispOpinion.value?.trim()) return ElMessage.warning('处置意见必填');
+	reviewing.value = true;
+	try {
+		review.value = await reviewQeDisposition({ billId, dispositionType: dispType.value, dispositionOpinion: dispOpinion.value.trim() });
+		ElMessage.success('已提交质量处置');
+		dispType.value = '';
+		dispOpinion.value = '';
+	} catch {
+		ElMessage.error('提交处置失败');
+		await loadReview(billId);
+	} finally {
+		reviewing.value = false;
+	}
+}
+
 const query = reactive({
 	page: 1,
 	pageSize: 10,
@@ -345,6 +456,7 @@ async function openDetail(billId: number) {
 	try {
 		detail.value = await fetchProcessInspectionDetail(billId);
 		initEntry();
+		await loadReview(billId);
 	} catch {
 		ElMessage.error('检验单详情加载失败');
 	} finally {
@@ -357,6 +469,7 @@ async function reloadDetail() {
 	if (!billId) return;
 	detail.value = await fetchProcessInspectionDetail(billId);
 	initEntry();
+	await loadReview(billId);
 }
 
 async function onSave() {

+ 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.456</AssemblyVersion>
-    <FileVersion>1.0.456</FileVersion>
-    <Version>1.0.456</Version>
+    <AssemblyVersion>1.0.457</AssemblyVersion>
+    <FileVersion>1.0.457</FileVersion>
+    <Version>1.0.457</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 89 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S6/Manufacturing/S6ProcessInspectionReviewContractTests.cs

@@ -0,0 +1,89 @@
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing;
+
+/// <summary>
+/// S6 Phase 1D 桥接契约测试:Phase 1C bill 接入 ApprovalFlow(BizType=S6_PROCESS_INSPECTION)。
+/// 覆盖:独立 BizType 绑 bill.id、提交门槛 COMPLETED、FAIL→disposition_required、质量结果不改写、tenant/幂等/N1=Initiator。
+/// </summary>
+public class S6ProcessInspectionReviewContractTests
+{
+    private static string Svc() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionReviewService.cs"));
+    private static string Handler() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionFlowBizHandler.cs"));
+    private static string Const() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionFlowConst.cs"));
+    private static string Seed() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Infrastructure", "S6ProcessInspectionFlowSeed.cs"));
+
+    [Fact]
+    public void BizType_IsIndependent_BoundToProcessInspectionBill()
+    {
+        Assert.Contains("\"S6_PROCESS_INSPECTION\"", Const());
+        var s = Svc();
+        // business_id = process_inspection_bill.id(StartFlow BizId=input.BillId),不绑 qms_gcjyd
+        Assert.Contains("BizId = input.BillId", s);
+        Assert.Contains("FROM ado_s6_process_inspection_bill", s);
+        Assert.DoesNotContain("qms_gcjyd", s);
+    }
+
+    [Fact]
+    public void Submit_Gate_RequiresCompleted()
+    {
+        var s = Svc();
+        Assert.Contains("INSPECTION_NOT_COMPLETED", s);
+        Assert.Contains("StatusCompleted", s);
+    }
+
+    [Fact]
+    public void Gateway_FailDrivesDisposition_FromOverallResult()
+    {
+        // 网关判据 disposition_required 由 overall_result=FAIL 决定,只读、不改写质量结果
+        var h = Handler();
+        Assert.Contains("overall_result", h);
+        Assert.Contains("ResultFail", h);
+        Assert.Contains("[\"disposition_required\"]", h);
+        // Handler 不 UPDATE 质量结果
+        Assert.DoesNotContain("UPDATE ado_s6_process_inspection_result", h);
+    }
+
+    [Fact]
+    public void Disposition_DoesNotOverwrite_QualityResult()
+    {
+        var s = Svc();
+        // 处置落 ado_s6_ipqc_quality_disposition,绝不 UPDATE overall_result(§20 硬门)
+        Assert.Contains("AdoS6IpqcQualityDisposition", s);
+        Assert.DoesNotContain("UPDATE ado_s6_process_inspection_result SET overall_result", s);
+        Assert.DoesNotContain("SET overall_result", s);
+    }
+
+    [Fact]
+    public void Idempotent_And_TenantSafe_And_Transactional()
+    {
+        var s = Svc();
+        Assert.Contains("已在审核中,请勿重复提交", s);            // 一单一 active flow
+        Assert.Contains("ResolveTenantOrThrow", s);
+        Assert.Contains("UseTranAsync", s);
+        Assert.DoesNotContain("tenant_id IS NULL", s);
+    }
+
+    [Fact]
+    public void N1_IsInitiator_ReuseSupervisorAndQeRoles()
+    {
+        var seed = Seed();
+        Assert.Contains("nameof(ApproverTypeEnum.Initiator)", seed);      // N1=发起人
+        Assert.Contains("RoleSupervisorId", seed);                        // N2 复用 IPQC 主管角色
+        Assert.Contains("RoleQualityEngineerId", seed);                   // N3 复用 IPQC QE 角色
+        // 复用 IPQC 角色 Id(不新造角色)
+        Assert.Contains("IpqcInspectionFlowConst.RoleSupervisorId", Const());
+    }
+
+    private static string FindFile(params string[] parts)
+    {
+        var dir = new DirectoryInfo(AppContext.BaseDirectory);
+        while (dir != null)
+        {
+            var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
+            if (File.Exists(candidate)) return candidate;
+            dir = dir.Parent;
+        }
+        throw new FileNotFoundException(string.Join("/", parts));
+    }
+}

+ 213 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S6ProcessInspectionFlowSeed.cs

@@ -0,0 +1,213 @@
+using System.Diagnostics;
+using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.Entity;
+using Admin.NET.Plugin.AiDOP.Manufacturing;
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+using Yitter.IdGenerator;
+using ApprovalFlowEntity = Admin.NET.Plugin.ApprovalFlow.ApprovalFlow;
+
+namespace Admin.NET.Plugin.AiDOP.Infrastructure;
+
+/// <summary>
+/// S6 Phase 1D 过程检验单(Phase 1C bill)审核流程种子(幂等)。
+///
+/// 桥接:让 ado_s6_process_inspection_bill 接入 ApprovalFlow(BizType=S6_PROCESS_INSPECTION)。
+/// 复用 IpqcInspectionFlowSeed 已建的角色(ROLE_S6_IPQC_SUPERVISOR/QUALITY_ENGINEER)与处置表 ado_s6_ipqc_quality_disposition,
+/// 本种子只:① 确保引擎/处置表存在(CodeFirst 幂等)② 注册 BizType ③ 发布流程定义
+///   (start→N1_SUBMIT(Initiator 发起人)→N2_SUP_REVIEW(主管)→GW_RESULT(disposition_required==1→N3 / 否则→end)→N3_QE_DISPOSITION(QE)→end)。
+/// N1=发起人本人(提交审核人),无需检验员角色成员;N2/N3 复用 IPQC 主管/QE 角色。已发布则跳过(幂等)。
+/// </summary>
+public static class S6ProcessInspectionFlowSeed
+{
+    public static void EnsureSeed(ISqlSugarClient db)
+    {
+        try
+        {
+            db.CodeFirst.InitTables(
+                typeof(ApprovalBizType),
+                typeof(ApprovalFlowEntity),
+                typeof(ApprovalFlowVersion));
+            db.CodeFirst.InitTables(typeof(AdoS6IpqcQualityDisposition));
+            EnsureBizType(db);
+            EnsurePublishedFlow(db);
+        }
+        catch (Exception ex)
+        {
+            Trace.TraceWarning("S6 process inspection review flow seed: " + ex);
+        }
+    }
+
+    private static void EnsureBizType(ISqlSugarClient db)
+    {
+        var exists = db.Queryable<ApprovalBizType>().Any(x => x.Code == S6ProcessInspectionFlowConst.BizType);
+        if (exists) return;
+        db.Insertable(new ApprovalBizType
+        {
+            Id = YitIdHelper.NextId(),
+            Code = S6ProcessInspectionFlowConst.BizType,
+            Name = "过程检验单(Phase1C)审核",
+            Remark = "S6 过程检验单(ado_s6_process_inspection_bill) 提交审核→主管→质量处置 审批流",
+            IsEnabled = true,
+            CreateTime = DateTime.Now,
+            UpdateTime = DateTime.Now,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsurePublishedFlow(ISqlSugarClient db)
+    {
+        var orgIds = db.Queryable<SysOrg>()
+            .Select(x => x.Id).ToList()
+            .Where(x => x > 0).Distinct().ToList();
+        if (orgIds.Count == 0) orgIds.Add(0);
+
+        var now = DateTime.Now;
+        var flowJson = BuildFlowJson();
+        foreach (var orgId in orgIds)
+        {
+            var existing = db.Queryable<ApprovalFlowEntity>()
+                .Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.OrgId == orgId && x.IsPublished && !x.IsDelete)
+                .OrderByDescending(x => x.Version)
+                .First();
+            if (existing != null) continue; // 已发布则跳过(幂等)
+
+            var flow = new ApprovalFlowEntity
+            {
+                Id = YitIdHelper.NextId(),
+                Code = $"S6PI_{orgId}",
+                Name = "过程检验单(Phase1C)审核",
+                BizType = S6ProcessInspectionFlowConst.BizType,
+                FormJson = "{}",
+                FlowJson = flowJson,
+                Status = 1,
+                Remark = "系统默认流程 v1:开始-提交审核(发起人)-检验主管审核-网关(质量结果)-质量处置-结束",
+                Version = 1,
+                IsPublished = true,
+                OrgId = orgId,
+                CreateTime = now,
+                UpdateTime = now,
+            };
+            db.Insertable(flow).ExecuteCommand();
+            db.Insertable(new ApprovalFlowVersion
+            {
+                Id = YitIdHelper.NextId(),
+                FlowId = flow.Id,
+                Version = flow.Version,
+                FlowJson = flow.FlowJson,
+                FormJson = flow.FormJson,
+                PublishTime = now,
+                PublisherName = "系统初始化",
+                CreateTime = now,
+                UpdateTime = now,
+            }).ExecuteCommand();
+        }
+    }
+
+    private static string BuildFlowJson()
+    {
+        var item = new ApprovalFlowItem
+        {
+            Nodes = new List<ApprovalFlowNodeItem>
+            {
+                new()
+                {
+                    Id = "start", Type = "bpmn:startEvent", X = 100, Y = 120,
+                    Properties = new FlowProperties { NodeName = "开始" },
+                    Text = new FlowTextItem { X = 100, Y = 154, Value = "开始" },
+                },
+                new()
+                {
+                    Id = S6ProcessInspectionFlowConst.NodeSubmit, Type = "bpmn:userTask", X = 300, Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "提交审核",
+                        ApproverType = nameof(ApproverTypeEnum.Initiator),
+                        ApproverIds = "",
+                        ApproverNames = "发起人",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 300, Y = 154, Value = "提交审核" },
+                },
+                new()
+                {
+                    Id = S6ProcessInspectionFlowConst.NodeSupervisor, Type = "bpmn:userTask", X = 500, Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "检验主管审核",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = S6ProcessInspectionFlowConst.RoleSupervisorId.ToString(),
+                        ApproverNames = "过程检验主管",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" },
+                },
+                new()
+                {
+                    Id = S6ProcessInspectionFlowConst.GatewayResult, Type = "bpmn:exclusiveGateway", X = 660, Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "质量结果分流",
+                        Conditions = new List<GatewayCondition>
+                        {
+                            new()
+                            {
+                                Expression = "disposition_required == 1",
+                                TargetNodeId = S6ProcessInspectionFlowConst.NodeDisposition,
+                                IsDefault = false,
+                                Label = "不合格转质量处置",
+                            },
+                            new()
+                            {
+                                TargetNodeId = "end",
+                                IsDefault = true,
+                                Label = "合格完成",
+                            },
+                        },
+                    },
+                    Text = new FlowTextItem { X = 660, Y = 154, Value = "质量结果分流" },
+                },
+                new()
+                {
+                    Id = S6ProcessInspectionFlowConst.NodeDisposition, Type = "bpmn:userTask", X = 820, Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "质量处置",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = S6ProcessInspectionFlowConst.RoleQualityEngineerId.ToString(),
+                        ApproverNames = "过程检验质量工程师",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 820, Y = 154, Value = "质量处置" },
+                },
+                new()
+                {
+                    Id = "end", Type = "bpmn:endEvent", X = 980, Y = 120,
+                    Properties = new FlowProperties { NodeName = "结束" },
+                    Text = new FlowTextItem { X = 980, Y = 154, Value = "结束" },
+                },
+            },
+            Edges = new List<ApprovalFlowEdgeItem>
+            {
+                BuildEdge("edge-start-n1", "start", S6ProcessInspectionFlowConst.NodeSubmit, 140, 120, 260, 120),
+                BuildEdge("edge-n1-n2", S6ProcessInspectionFlowConst.NodeSubmit, S6ProcessInspectionFlowConst.NodeSupervisor, 340, 120, 460, 120),
+                BuildEdge("edge-n2-gw", S6ProcessInspectionFlowConst.NodeSupervisor, S6ProcessInspectionFlowConst.GatewayResult, 540, 120, 640, 120),
+                BuildEdge("edge-gw-n3", S6ProcessInspectionFlowConst.GatewayResult, S6ProcessInspectionFlowConst.NodeDisposition, 680, 120, 780, 120),
+                BuildEdge("edge-gw-end", S6ProcessInspectionFlowConst.GatewayResult, "end", 680, 120, 960, 120),
+                BuildEdge("edge-n3-end", S6ProcessInspectionFlowConst.NodeDisposition, "end", 860, 120, 960, 120),
+            }
+        };
+        return JsonSerializer.Serialize(item);
+    }
+
+    private static ApprovalFlowEdgeItem BuildEdge(string id, string source, string target, float sx, float sy, float ex, float ey)
+    {
+        return new ApprovalFlowEdgeItem
+        {
+            Id = id, Type = "bpmn:sequenceFlow", SourceNodeId = source, TargetNodeId = target,
+            StartPoint = new FlowEdgePointItem { X = sx, Y = sy },
+            EndPoint = new FlowEdgePointItem { X = ex, Y = ey },
+            Properties = new FlowProperties(),
+            PointsList = new List<FlowEdgePointItem> { new() { X = sx, Y = sy }, new() { X = ex, Y = ey } },
+        };
+    }
+}

+ 64 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/S6ProcessInspectionReviewDto.cs

@@ -0,0 +1,64 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+
+/// <summary>Phase 1D 审核流状态(只读,供前端按钮控制)。所有判定/状态由后端拥有,前端不得回传状态。</summary>
+public class S6ReviewStateOutput
+{
+    public long BillId { get; set; }
+    public long? InstanceId { get; set; }
+    /// <summary>Phase 1C 质量结果(PASS/FAIL/PENDING/INVALID)—— 只读回显,审批不改写。</summary>
+    public string? OverallResult { get; set; }
+    /// <summary>Phase 1C 结果状态(NOT_STARTED/IN_PROGRESS/COMPLETED)。</summary>
+    public string? ResultStatus { get; set; }
+    /// <summary>流程状态:NotStarted / Running / Approved / Rejected / Cancelled / Terminated。</summary>
+    public string? FlowStatus { get; set; }
+    public string? CurrentNodeCode { get; set; }
+    public string? CurrentNodeName { get; set; }
+    public string? CurrentAssigneeRole { get; set; }
+    public bool IsCompleted { get; set; }
+    public bool HasDisposition { get; set; }
+    /// <summary>网关判据(FAIL→1)。</summary>
+    public int DispositionRequired { get; set; }
+    // 按钮门控(后端计算)
+    public bool CanSubmitReview { get; set; }
+    public bool CanSupervisorApprove { get; set; }
+    public bool CanSupervisorReturn { get; set; }
+    public bool CanSubmitDisposition { get; set; }
+    public string? Message { get; set; }
+}
+
+/// <summary>提交审核入参:仅单据 id;租户/发起人后端自解析。</summary>
+public class S6SubmitReviewInput
+{
+    public long BillId { get; set; }
+    public string? Comment { get; set; }
+}
+
+/// <summary>主管审核通过入参(路由自动按 overall_result 决定去向)。</summary>
+public class S6ReviewApproveInput
+{
+    public long BillId { get; set; }
+    public string? Comment { get; set; }
+}
+
+/// <summary>主管退回入参(意见必填)。</summary>
+public class S6ReviewReturnInput
+{
+    public long BillId { get; set; }
+    public string? Comment { get; set; }
+}
+
+/// <summary>QE 质量处置入参(不改写 Phase 1C 质量结果)。</summary>
+public class S6QeDispositionInput
+{
+    public long BillId { get; set; }
+    /// <summary>REWORK / CONCESSION / SCRAP / OTHER。</summary>
+    public string DispositionType { get; set; } = string.Empty;
+    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 string? Comment { get; set; }
+}

+ 68 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6ProcessInspectionFlowBizHandler.cs

@@ -0,0 +1,68 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 Phase 1C 过程检验单审批流回调处理器(BizType=S6_PROCESS_INSPECTION)。
+///
+/// Handler 回调全部 no-op:不改写 Phase 1C 检验事实(ado_s6_process_inspection_result 仍是质量真源),
+/// 不写 qms_gcjyd、不写库存/生产/仓储。审批状态由 ApprovalFlowInstance 承载。
+/// GetBizData 返回 GW_RESULT 网关判据 disposition_required —— 直接由 Phase 1C overall_result 决定
+/// (FAIL→1 进 N3 质量处置 / PASS→0 结束),**只读、不改写**质量结果(§20 质量结果与处置分离硬门)。
+/// </summary>
+public class S6ProcessInspectionFlowBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => S6ProcessInspectionFlowConst.BizType;
+
+    private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
+
+    public S6ProcessInspectionFlowBizHandler(ISqlSugarClient db, UserManager userManager)
+    {
+        _db = db;
+        _userManager = userManager;
+    }
+
+    public Task OnFlowStarted(long bizId, long instanceId) => Task.CompletedTask;
+    public Task OnNodeCompleted(long bizId, long instanceId, string nodeId, string nodeName, long? approverUserId) => Task.CompletedTask;
+    public Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId) => Task.CompletedTask;
+
+    /// <summary>返回单据基础信息 + 网关判据 disposition_required(GW_RESULT 分流:FAIL==1→N3,PASS→end)。</summary>
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        // 强制租户隔离:GetBizData 在流程审批 HTTP 请求上下文内被调用,裸 SQL 显式按当前登录租户过滤。
+        var tid = _userManager.TenantId;
+        var rows = await _db.Ado.SqlQueryAsync<S6PiFlowBizData>(
+            """
+            SELECT b.work_order_no AS WorkOrderNo, b.item_code AS ItemCode, b.item_name AS ItemName,
+                   r.overall_result AS OverallResult, r.status AS ResultStatus
+            FROM ado_s6_process_inspection_bill b
+            LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
+            WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
+            """,
+            new List<SugarParameter> { new("@id", bizId), new("@tid", tid) });
+        var r = rows.FirstOrDefault();
+        // 网关判据:overall_result=FAIL → 需质量处置(1);否则(PASS/其它) → 0 直接结束。只读,不回写。
+        var dispositionRequired = string.Equals(r?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
+        if (r == null)
+            return new Dictionary<string, object> { ["disposition_required"] = dispositionRequired };
+        return new Dictionary<string, object>
+        {
+            ["workOrderNo"] = r.WorkOrderNo ?? "",
+            ["itemCode"] = r.ItemCode ?? "",
+            ["itemName"] = r.ItemName ?? "",
+            ["overallResult"] = r.OverallResult ?? "",
+            ["disposition_required"] = dispositionRequired,
+        };
+    }
+
+    private class S6PiFlowBizData
+    {
+        public string? WorkOrderNo { get; set; }
+        public string? ItemCode { get; set; }
+        public string? ItemName { get; set; }
+        public string? OverallResult { get; set; }
+        public string? ResultStatus { get; set; }
+    }
+}

+ 53 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6ProcessInspectionFlowConst.cs

@@ -0,0 +1,53 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 Phase 1C 过程检验单(ado_s6_process_inspection_bill)审核流程常量(Phase 1D 桥接)。
+///
+/// 桥接目标:让 Phase 1C 快照驱动的过程检验单(Complete 后 overall_result=PASS/FAIL)真正接入 ApprovalFlow。
+/// 不重造引擎,不改 IPQC(qms_gcjyd) 流程。BizType 独立 = S6_PROCESS_INSPECTION,business_id = process_inspection_bill.id。
+///
+/// 拓扑:start → N1_SUBMIT(发起人=提交审核人,自动通过) → N2_SUP_REVIEW(过程检验主管)
+///   → GW_RESULT(disposition_required==1→N3 / 否则→end) → N3_QE_DISPOSITION(过程检验质量工程师) → end。
+/// 网关判据 disposition_required 由 Phase 1C 结果 overall_result 决定(FAIL→1 进处置 / PASS→0 结束),
+/// 由 BizHandler.GetBizData 从 ado_s6_process_inspection_result 直读,**不改写** Phase 1C 检验事实(质量结果与审批分离)。
+/// 复用既有 ROLE_S6_IPQC_SUPERVISOR / QUALITY_ENGINEER 角色(IpqcInspectionFlowConst)与 ado_s6_ipqc_quality_disposition 处置表。
+/// </summary>
+public static class S6ProcessInspectionFlowConst
+{
+    /// <summary>流程业务类型编码(独立于 IPQC_INSPECTION,绑 Phase 1C bill)</summary>
+    public const string BizType = "S6_PROCESS_INSPECTION";
+
+    /// <summary>N1 提交审核节点(发起人本人=提交审核人,提交即自动通过推进 N2)</summary>
+    public const string NodeSubmit = "N1_SUBMIT";
+
+    /// <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 质量处置节点(不合格 FAIL 进入)</summary>
+    public const string NodeDisposition = "N3_QE_DISPOSITION";
+
+    /// <summary>复用 IPQC 检验主管角色(编码/固定 Id 见 IpqcInspectionFlowConst)</summary>
+    public const string RoleSupervisorCode = IpqcInspectionFlowConst.RoleSupervisorCode;
+    public const long RoleSupervisorId = IpqcInspectionFlowConst.RoleSupervisorId;
+
+    /// <summary>复用 IPQC 质量工程师角色</summary>
+    public const string RoleQualityEngineerCode = IpqcInspectionFlowConst.RoleQualityEngineerCode;
+    public const long RoleQualityEngineerId = IpqcInspectionFlowConst.RoleQualityEngineerId;
+
+    /// <summary>流程定义版本</summary>
+    public const int FlowDefinitionVersion = 1;
+
+    // 处置类型(复用 IPQC 枚举)
+    public const string DispTypeRework = IpqcInspectionFlowConst.DispTypeRework;
+    public const string DispTypeConcession = IpqcInspectionFlowConst.DispTypeConcession;
+    public const string DispTypeScrap = IpqcInspectionFlowConst.DispTypeScrap;
+    public const string DispTypeOther = IpqcInspectionFlowConst.DispTypeOther;
+
+    // Phase 1C 结果口径
+    public const string ResultPass = "PASS";
+    public const string ResultFail = "FAIL";
+    public const string StatusCompleted = "COMPLETED";
+}

+ 309 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/S6ProcessInspectionReviewService.cs

@@ -0,0 +1,309 @@
+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 Phase 1D 过程检验单(Phase 1C bill)审核流程服务(桥接:Phase 1C Complete → ApprovalFlow)。
+///
+/// business_id = ado_s6_process_inspection_bill.id(BizType=S6_PROCESS_INSPECTION)。复用 ApprovalFlow 引擎,不改核心。
+/// 提交门槛:结果 status=COMPLETED 才能提交审核;幂等:一单最多一个 active flow。
+/// N1_SUBMIT=发起人本人(提交即自动通过)→ N2_SUP_REVIEW 主管审核 → GW_RESULT(overall_result=FAIL→N3 / PASS→end)
+///   → N3_QE_DISPOSITION 质量工程师处置 → end。质量结果 overall_result 全程只读、审批不改写(§20 硬门)。
+/// 处置落 ado_s6_ipqc_quality_disposition(biz_type 区分,复用不建重复表)。严格租户隔离(ResolveTenantOrThrow)。
+/// </summary>
+[ApiDescriptionSettings(Order = 305, Description = "过程检验单审核")]
+[Route("api/S6ProcessInspectionReview")]
+[NonUnify]
+public class S6ProcessInspectionReviewService : IDynamicApiController, ITransient
+{
+    private const long MainTenantId = 1300000000001L;
+
+    private readonly ISqlSugarClient _db;
+    private readonly FlowEngineService _flowEngine;
+    private readonly UserManager _userManager;
+
+    public S6ProcessInspectionReviewService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
+    {
+        _db = db;
+        _flowEngine = flowEngine;
+        _userManager = userManager;
+    }
+
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId) throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
+    }
+
+    /// <summary>审核流状态(只读,供前端按钮控制)。</summary>
+    [DisplayName("过程检验单审核状态")]
+    [HttpGet("state")]
+    public async Task<S6ReviewStateOutput> GetState([FromQuery] long billId)
+    {
+        await LoadBillAsync(billId);
+        return await BuildStateAsync(billId);
+    }
+
+    /// <summary>提交审核(仅 COMPLETED 可提交;幂等;发起流程并自动通过 N1→N2)。</summary>
+    [DisplayName("提交审核")]
+    [HttpPost("submit-review")]
+    public async Task<S6ReviewStateOutput> SubmitReview([FromBody] S6SubmitReviewInput input)
+    {
+        var bill = await LoadBillAsync(input.BillId);
+        if (!string.Equals(bill.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase))
+            throw Oops.Oh("INSPECTION_NOT_COMPLETED:检验未完成,无法提交审核");
+
+        var inst = await GetLatestInstanceAsync(input.BillId);
+        if (inst != null && inst.Status == FlowInstanceStatusEnum.Running)
+            throw Oops.Oh("该检验单已在审核中,请勿重复提交");
+        if (inst != null && inst.Status != FlowInstanceStatusEnum.Running && inst.Status != FlowInstanceStatusEnum.Rejected && inst.Status != FlowInstanceStatusEnum.Cancelled)
+            throw Oops.Oh("该检验单审核流程已结束");
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            var instanceId = await _flowEngine.StartFlow(new StartFlowInput
+            {
+                BizType = S6ProcessInspectionFlowConst.BizType,
+                BizId = input.BillId,
+                BizNo = bill.WorkOrderNo,
+                Title = $"过程检验单 {bill.WorkOrderNo}",
+                Comment = input.Comment,
+            });
+            // N1_SUBMIT=发起人本人,取自己的 N1 待办并自动通过 → N2
+            var myTask = await GetMyPendingTaskAsync(instanceId);
+            if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
+                throw Oops.Oh("提交审核节点异常,未取到发起人任务");
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.BillId);
+    }
+
+    /// <summary>主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。</summary>
+    [DisplayName("主管审核通过")]
+    [HttpPost("supervisor-approve")]
+    public async Task<S6ReviewStateOutput> SupervisorApprove([FromBody] S6ReviewApproveInput input)
+    {
+        await LoadBillAsync(input.BillId);
+        var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+        return await BuildStateAsync(input.BillId);
+    }
+
+    /// <summary>主管退回(N2→N1,意见必填)。质量结果不改写。</summary>
+    [DisplayName("主管退回")]
+    [HttpPost("supervisor-return")]
+    public async Task<S6ReviewStateOutput> SupervisorReturn([FromBody] S6ReviewReturnInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见");
+        await LoadBillAsync(input.BillId);
+        var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+        return await BuildStateAsync(input.BillId);
+    }
+
+    /// <summary>QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。</summary>
+    [DisplayName("质量处置")]
+    [HttpPost("qe-disposition")]
+    public async Task<S6ReviewStateOutput> QeDisposition([FromBody] S6QeDispositionInput input)
+    {
+        var bill = await LoadBillAsync(input.BillId);
+        var type = (input.DispositionType ?? "").Trim().ToUpperInvariant();
+        if (type != S6ProcessInspectionFlowConst.DispTypeRework && type != S6ProcessInspectionFlowConst.DispTypeConcession
+            && type != S6ProcessInspectionFlowConst.DispTypeScrap && type != S6ProcessInspectionFlowConst.DispTypeOther)
+            throw Oops.Oh("处置类型非法(REWORK/CONCESSION/SCRAP/OTHER)");
+        if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
+
+        var inst = await GetLatestInstanceAsync(input.BillId);
+        if (inst == null || inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask == null) throw Oops.Oh("当前用户没有待处理的处置任务(需质量工程师角色成员)");
+        if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
+
+        var tid = ResolveTenantOrThrow();
+        // 一单一有效处置(status=1 且未软删)+ InspectionId 唯一约束
+        var existed = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
+            .AnyAsync(x => x.InspectionId == input.BillId && x.TenantId == tid && x.Status == 1);
+        if (existed) throw Oops.Oh("该检验单已存在有效质量处置记录");
+
+        var now = DateTime.Now;
+        var opName = _userManager.RealName ?? _userManager.Account;
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _db.Insertable(new AdoS6IpqcQualityDisposition
+            {
+                Id = YitIdHelper.NextId(),
+                TenantId = tid,
+                InspectionId = input.BillId,
+                InspectionNo = bill.WorkOrderNo,
+                BizType = S6ProcessInspectionFlowConst.BizType,
+                FlowInstanceId = inst.Id,
+                DispositionNo = $"S6PIDISP-{bill.WorkOrderNo}-{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 = opName,
+                SubmittedAt = now,
+                RowVersion = 1,
+                CreateTime = now,
+            }).ExecuteCommandAsync();
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+        return await BuildStateAsync(input.BillId);
+    }
+
+    // ───────────────────────── helpers ─────────────────────────
+
+    private sealed class BillRow
+    {
+        public long Id { get; set; }
+        public string? WorkOrderNo { get; set; }
+        public string? OverallResult { get; set; }
+        public string? ResultStatus { get; set; }
+    }
+
+    /// <summary>加载 Phase 1C 单据 + 结果(严格租户隔离)。</summary>
+    private async Task<BillRow> LoadBillAsync(long billId)
+    {
+        if (billId <= 0) throw Oops.Oh("检验单 id 非法");
+        var tid = ResolveTenantOrThrow();
+        var row = await _db.Ado.SqlQuerySingleAsync<BillRow>(
+            """
+            SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
+            FROM ado_s6_process_inspection_bill b
+            LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
+            WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
+            """,
+            new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
+        if (row == null) throw Oops.Oh("检验单不存在");
+        return row;
+    }
+
+    private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
+        => await _db.Queryable<ApprovalFlowInstance>().ClearFilter()
+            .Where(x => x.BizType == S6ProcessInspectionFlowConst.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();
+    }
+
+    private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
+    {
+        var inst = await GetLatestInstanceAsync(bizId);
+        if (inst == null) throw Oops.Oh("该检验单尚未发起审核");
+        if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
+        if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
+        return (inst, myTask);
+    }
+
+    private async Task<bool> HasRoleAsync(long roleId)
+    {
+        var userId = _userManager.UserId;
+        return await _db.Queryable<SysUserRole>().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
+    }
+
+    private async Task<S6ReviewStateOutput> BuildStateAsync(long billId)
+    {
+        var tid = ResolveTenantOrThrow();
+        var output = new S6ReviewStateOutput { BillId = billId };
+        var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
+            """
+            SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
+            FROM ado_s6_process_inspection_bill b
+            LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
+            WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
+            """,
+            new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
+        output.OverallResult = bill?.OverallResult;
+        output.ResultStatus = bill?.ResultStatus;
+        output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
+        output.HasDisposition = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
+            .AnyAsync(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1);
+
+        var inst = await GetLatestInstanceAsync(billId);
+        if (inst == null)
+        {
+            output.FlowStatus = "NotStarted";
+            output.CanSubmitReview = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
+            output.Message = output.CanSubmitReview ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
+            return output;
+        }
+
+        output.InstanceId = inst.Id;
+        output.FlowStatus = inst.Status.ToString();
+        if (inst.Status != FlowInstanceStatusEnum.Running)
+        {
+            output.IsCompleted = true;
+            output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
+            // 已结束且非通过(驳回/撤回)→ 允许重新提交
+            output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled
+                && string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
+            return output;
+        }
+
+        output.CurrentNodeCode = inst.CurrentNodeId;
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask != null)
+        {
+            output.CurrentNodeName = myTask.NodeName;
+            if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
+            {
+                output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
+                output.CanSupervisorApprove = true;
+                output.CanSupervisorReturn = true;
+                output.Message = "待当前用户主管审核";
+            }
+            else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeDisposition)
+            {
+                output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleQualityEngineerCode;
+                output.CanSubmitDisposition = true;
+                output.Message = "待当前用户提交质量处置";
+            }
+            else
+            {
+                output.Message = "流程进行中";
+            }
+        }
+        else
+        {
+            output.CurrentAssigneeRole = inst.CurrentNodeId switch
+            {
+                S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
+                S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode,
+                _ => null,
+            };
+            output.Message = "流程进行中(当前用户无待办)";
+        }
+        return output;
+    }
+}

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

@@ -112,6 +112,7 @@ public class Startup : AppStartup
             AidopImprovementApprovalSeed.EnsureSeed(dbS4);
             Admin.NET.Plugin.AiDOP.Infrastructure.IqcInspBillFlowSeed.EnsureSeed(dbS4);
             Admin.NET.Plugin.AiDOP.Infrastructure.IpqcInspectionFlowSeed.EnsureSeed(dbS4);
+            Admin.NET.Plugin.AiDOP.Infrastructure.S6ProcessInspectionFlowSeed.EnsureSeed(dbS4);
             Admin.NET.Plugin.AiDOP.Infrastructure.FqcInspBillFlowSeed.EnsureSeed(dbS4);
         }
         catch (Exception ex)