Parcourir la source

feat(s7): FQC 成品检验单 检验员+主管审批流(工程链路)

- 新增 FqcInspBillFlowService(api/S7FqcInspBillFlow)state/submit-result/supervisor-approve/supervisor-reject
- BizType=S7_FQC_INSPBILL/BizId=qms_qcpp_inspbill.id/BizCode=FBILLNO;节点 N1_INSPECT→N2_SUP_REVIEW,退回 ReturnToPrev
- N1 事务内写 bill(pd/hgsl/bhgsl/clfs)+经 lydjbh=FBILLNO 关联写 qms_fqcbj(jyfzr/jykssj空补/jywcsj/FINSPECTSTATUS='检验完成')
- 关联安全约束:0跳过/1更新/>1抛错整体回滚,杜绝误批量更新
- 状态严格分离:业务结果→inspbill,任务进度→qms_fqcbj.FINSPECTSTATUS,审批状态→ApprovalFlow(不写FINSPECTSTATUS)
- FqcInspBillFlowSeed 幂等建 2 角色(ROLE_S7_FQC_INSPECTOR/SUPERVISOR)+UAT绑+BizType+发布流程,Startup+1行注册;复用ApprovalFlow未改核心
- 前端 fqcInspBillFlow.ts + drawer 加流程操作区(检验员录入/主管通过退回)按 state 驱动
- 纯审批流,业务表零写入(空态);bump version Web 2.4.249 / server 1.0.247
YY968XX il y a 1 semaine
Parent
commit
b2edb86e4d

+ 1 - 1
Web/package.json

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

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

@@ -0,0 +1,53 @@
+import service from '/@/utils/request';
+
+// ── S7 FQC 成品检验单 检验员+主管审批流(正常鉴权,非匿名)──
+
+export interface FqcFlowState {
+	bizType?: string;
+	bizId?: number;
+	instanceId?: number | null;
+	/** NotStarted / Running / Approved / Rejected / Cancelled / Terminated */
+	flowStatus?: string;
+	currentNodeCode?: string | null;
+	currentNodeName?: string | null;
+	currentAssigneeRole?: string | null;
+	canSubmitResult?: boolean;
+	canSupervisorApprove?: boolean;
+	canSupervisorReject?: boolean;
+	isCompleted?: boolean;
+	updatedAt?: string | null;
+	message?: string | null;
+}
+
+export interface FqcSubmitResultPayload {
+	id: number;
+	/** 0=合格 1=不合格 */
+	pd: number;
+	/** 合格数量 hgsl */
+	hgsl: number;
+	/** 不合格数量 bhgsl */
+	bhgsl: number;
+	/** 处理方式 0=让步接收 1=挑选 2=报废(pd=1 必填) */
+	clfs?: number | null;
+	comment?: string;
+}
+
+/** 流程状态(只读,供前端按钮控制) */
+export function fetchFqcFlowState(id: number) {
+	return service.get<FqcFlowState>('/api/S7FqcInspBillFlow/state', { params: { id } }).then((r) => r.data);
+}
+
+/** 检验员提交检验结果(写 pd/hgsl/bhgsl/clfs + 任务进度 + 推进 N1→N2) */
+export function submitFqcResult(payload: FqcSubmitResultPayload) {
+	return service.post<FqcFlowState>('/api/S7FqcInspBillFlow/submit-result', payload).then((r) => r.data);
+}
+
+/** 检验主管通过(N2→完成) */
+export function fqcSupervisorApprove(id: number, comment?: string) {
+	return service.post<FqcFlowState>('/api/S7FqcInspBillFlow/supervisor-approve', { id, comment }).then((r) => r.data);
+}
+
+/** 检验主管退回(回 N1,意见必填) */
+export function fqcSupervisorReject(id: number, comment: string) {
+	return service.post<FqcFlowState>('/api/S7FqcInspBillFlow/supervisor-reject', { id, comment }).then((r) => r.data);
+}

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

@@ -78,6 +78,52 @@
 						<el-empty description="暂无明细" :image-size="60" />
 					</template>
 				</el-table>
+
+				<el-descriptions title="流程信息" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="流程状态">{{ dash(flowState?.flowStatus) }}</el-descriptions-item>
+					<el-descriptions-item label="当前节点">{{ dash(flowState?.currentNodeName) }}</el-descriptions-item>
+					<el-descriptions-item label="当前角色">{{ dash(flowState?.currentAssigneeRole) }}</el-descriptions-item>
+					<el-descriptions-item label="说明">{{ dash(flowState?.message) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<!-- 检验员录入结果(仅 canSubmitResult 显示)-->
+				<div v-if="flowState?.canSubmitResult" class="flow-op mt12">
+					<div class="flow-op-title">检验员录入检验结果</div>
+					<el-form :model="resultForm" label-width="96px" size="small">
+						<el-form-item label="判定">
+							<el-select v-model="resultForm.pd" style="width: 180px" @change="onPdChange">
+								<el-option :value="0" label="合格" />
+								<el-option :value="1" label="不合格" />
+							</el-select>
+						</el-form-item>
+						<el-form-item label="合格数量">
+							<el-input-number v-model="resultForm.hgsl" :min="0" :controls="false" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="不合格数量">
+							<el-input-number v-model="resultForm.bhgsl" :min="0" :controls="false" :disabled="resultForm.pd === 0" style="width: 180px" />
+						</el-form-item>
+						<el-form-item label="处理方式" v-if="resultForm.pd === 1">
+							<el-select v-model="resultForm.clfs" style="width: 180px" clearable>
+								<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 v-model="resultForm.comment" type="textarea" :rows="2" style="width: 320px" />
+						</el-form-item>
+						<el-form-item>
+							<el-button type="primary" :loading="submitting" @click="onSubmitResult">提交检验结果</el-button>
+						</el-form-item>
+					</el-form>
+				</div>
+
+				<!-- 检验主管审核(仅 canSupervisorApprove/Reject 显示)-->
+				<div v-if="flowState?.canSupervisorApprove || flowState?.canSupervisorReject" class="flow-op mt12">
+					<div class="flow-op-title">检验主管审核</div>
+					<el-button type="success" :loading="submitting" @click="onApprove">通过</el-button>
+					<el-button type="warning" :loading="submitting" @click="onReject">退回</el-button>
+				</div>
 			</template>
 			<el-empty v-else-if="!loading" description="暂无详情数据" />
 		</div>
@@ -85,17 +131,24 @@
 </template>
 
 <script setup lang="ts" name="FqcInspBillDetailDrawer">
-import { computed } from 'vue';
+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';
 
 const props = defineProps<{
 	modelValue: boolean;
 	detail: FqcDetail | null;
 	loading?: boolean;
+	flowState?: FqcFlowState | null;
+	submitting?: boolean;
 }>();
 const emit = defineEmits<{
 	(e: 'update:modelValue', v: boolean): void;
 	(e: 'closed'): void;
+	(e: 'submit-result', payload: FqcSubmitResultPayload): void;
+	(e: 'approve'): void;
+	(e: 'reject', comment: string): void;
 }>();
 
 const innerVisible = computed({
@@ -103,6 +156,73 @@ const innerVisible = computed({
 	set: (v: boolean) => emit('update:modelValue', v),
 });
 
+// 检验员录入表单(仅 canSubmitResult 时可见)
+const resultForm = reactive<{ pd: number; hgsl: number; bhgsl: number; clfs: number | null; comment: string }>({
+	pd: 0,
+	hgsl: 0,
+	bhgsl: 0,
+	clfs: null,
+	comment: '',
+});
+
+// 每次打开 drawer 重置录入表单
+watch(
+	() => props.modelValue,
+	(v) => {
+		if (v) {
+			resultForm.pd = 0;
+			resultForm.hgsl = 0;
+			resultForm.bhgsl = 0;
+			resultForm.clfs = null;
+			resultForm.comment = '';
+		}
+	}
+);
+
+function onPdChange() {
+	if (resultForm.pd === 0) {
+		resultForm.bhgsl = 0;
+		resultForm.clfs = null;
+	}
+}
+
+function onSubmitResult() {
+	if (resultForm.pd !== 0 && resultForm.pd !== 1) return ElMessage.error('请选择判定');
+	if (resultForm.hgsl < 0) return ElMessage.error('合格数量不能为负');
+	if (resultForm.bhgsl < 0) return ElMessage.error('不合格数量不能为负');
+	if (resultForm.pd === 0 && resultForm.bhgsl !== 0) return ElMessage.error('判定合格时不合格数量必须为 0');
+	if (resultForm.pd === 1 && resultForm.bhgsl <= 0) return ElMessage.error('判定不合格时不合格数量必须大于 0');
+	if (resultForm.pd === 1 && (resultForm.clfs === null || resultForm.clfs === undefined)) return ElMessage.error('判定不合格时请选择处理方式');
+	const id = props.detail?.id;
+	if (!id) return ElMessage.error('检验单 id 缺失');
+	emit('submit-result', {
+		id,
+		pd: resultForm.pd,
+		hgsl: resultForm.hgsl,
+		bhgsl: resultForm.bhgsl,
+		clfs: resultForm.pd === 1 ? resultForm.clfs : null,
+		comment: resultForm.comment,
+	});
+}
+
+function onApprove() {
+	emit('approve');
+}
+
+async function onReject() {
+	try {
+		const { value } = await ElMessageBox.prompt('请填写退回意见', '退回', {
+			confirmButtonText: '确定退回',
+			cancelButtonText: '取消',
+			inputType: 'textarea',
+			inputValidator: (v: string) => (v && v.trim() ? true : '退回意见不能为空'),
+		});
+		emit('reject', value.trim());
+	} catch {
+		/* 用户取消 */
+	}
+}
+
 // 仅渲染非空样品,保留样品序号(j1→样品1)
 function nonEmptySamples(row: FqcDetailLine) {
 	const arr = row?.samples || [];
@@ -174,4 +294,14 @@ function onClosed() {
 	color: var(--el-text-color-secondary);
 	font-size: 12px;
 }
+.flow-op {
+	padding: 12px;
+	border: 1px solid var(--el-border-color-lighter);
+	border-radius: 4px;
+	background: var(--el-fill-color-light);
+}
+.flow-op-title {
+	font-weight: bold;
+	margin-bottom: 10px;
+}
 </style>

+ 79 - 6
Web/src/views/aidop/s7/fqc/fqcInspectionResultList.vue

@@ -80,7 +80,16 @@
 			/>
 		</div>
 
-		<FqcInspBillDetailDrawer v-model="detailVisible" :detail="detail" :loading="detailLoading" />
+		<FqcInspBillDetailDrawer
+			v-model="detailVisible"
+			:detail="detail"
+			:loading="detailLoading"
+			:flow-state="flowState"
+			:submitting="flowSubmitting"
+			@submit-result="onFlowSubmit"
+			@approve="onFlowApprove"
+			@reject="onFlowReject"
+		/>
 	</AidopDemoShell>
 </template>
 
@@ -91,6 +100,14 @@ import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
 import { fetchFqcResultList, fetchFqcDetail, type FqcResultRow, type FqcDetail } from '../api/fqcInspBill';
+import {
+	fetchFqcFlowState,
+	submitFqcResult,
+	fqcSupervisorApprove,
+	fqcSupervisorReject,
+	type FqcFlowState,
+	type FqcSubmitResultPayload,
+} from '../api/fqcInspBillFlow';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
@@ -116,22 +133,78 @@ const total = ref(0);
 const detailVisible = ref(false);
 const detail = ref<FqcDetail | null>(null);
 const detailLoading = ref(false);
+const flowState = ref<FqcFlowState | null>(null);
+const flowSubmitting = ref(false);
+const currentDetailId = ref<number | null>(null);
 
-async function openDetail(row: FqcResultRow) {
-	if (!row?.id) return;
-	detail.value = null;
-	detailVisible.value = true;
+async function loadDetailAndState(id: number) {
 	detailLoading.value = true;
 	try {
-		detail.value = await fetchFqcDetail(row.id);
+		const [d, s] = await Promise.all([fetchFqcDetail(id), fetchFqcFlowState(id)]);
+		detail.value = d;
+		flowState.value = s;
 	} catch (e: any) {
 		detail.value = null;
+		flowState.value = null;
 		ElMessage.error(e?.message || '加载FQC检验单详情失败');
 	} finally {
 		detailLoading.value = false;
 	}
 }
 
+async function openDetail(row: FqcResultRow) {
+	if (!row?.id) return;
+	currentDetailId.value = row.id;
+	detail.value = null;
+	flowState.value = null;
+	detailVisible.value = true;
+	await loadDetailAndState(row.id);
+}
+
+async function onFlowSubmit(payload: FqcSubmitResultPayload) {
+	flowSubmitting.value = true;
+	try {
+		await submitFqcResult(payload);
+		ElMessage.success('检验结果已提交');
+		if (currentDetailId.value) await loadDetailAndState(currentDetailId.value);
+		loadList();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '提交检验结果失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+async function onFlowApprove() {
+	if (!currentDetailId.value) return;
+	flowSubmitting.value = true;
+	try {
+		await fqcSupervisorApprove(currentDetailId.value);
+		ElMessage.success('已通过');
+		await loadDetailAndState(currentDetailId.value);
+		loadList();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '审核通过失败');
+	} finally {
+		flowSubmitting.value = false;
+	}
+}
+
+async function onFlowReject(comment: string) {
+	if (!currentDetailId.value) return;
+	flowSubmitting.value = true;
+	try {
+		await fqcSupervisorReject(currentDetailId.value, comment);
+		ElMessage.success('已退回');
+		await loadDetailAndState(currentDetailId.value);
+		loadList();
+	} catch (e: any) {
+		ElMessage.error(e?.message || '退回失败');
+	} 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.246</AssemblyVersion>
-    <FileVersion>1.0.246</FileVersion>
-    <Version>1.0.246</Version>
+    <AssemblyVersion>1.0.247</AssemblyVersion>
+    <FileVersion>1.0.247</FileVersion>
+    <Version>1.0.247</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -0,0 +1,93 @@
+using Admin.NET.Plugin.AiDOP.FinishedWarehouse;
+
+namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
+
+/// <summary>
+/// 检验员提交检验结果入参。写 qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs(业务结果)
+/// + qms_fqcbj.jyfzr/jykssj/jywcsj/FINSPECTSTATUS(检验任务执行进度,经 lydjbh=FBILLNO 关联),并推进 N1→N2。
+/// </summary>
+public class FqcSubmitResultInput
+{
+    /// <summary>检验单 id(qms_qcpp_inspbill.id,BizId)</summary>
+    public long Id { get; set; }
+
+    /// <summary>判定:0=合格 1=不合格</summary>
+    public int Pd { get; set; }
+
+    /// <summary>合格数量(qms_qcpp_inspbill.hgsl)</summary>
+    public decimal Hgsl { get; set; }
+
+    /// <summary>不合格数量(qms_qcpp_inspbill.bhgsl)</summary>
+    public decimal Bhgsl { get; set; }
+
+    /// <summary>处理方式:0=让步接收 1=挑选 2=报废(pd=1 时必填;本批仅保存,不触发返工/报废/放行/复检/库存/生产动作)</summary>
+    public int? Clfs { get; set; }
+
+    /// <summary>提交说明(可选)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>检验主管通过入参(N2→end,不覆盖检验员业务结果)。</summary>
+public class FqcSupervisorApproveInput
+{
+    /// <summary>检验单 id</summary>
+    public long Id { get; set; }
+
+    /// <summary>审批意见(可选)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>检验主管退回入参(ReturnToPrev 回 N1,意见必填;保留 pd/hgsl/bhgsl/clfs/FINSPECTSTATUS/jywcsj)。</summary>
+public class FqcSupervisorRejectInput
+{
+    /// <summary>检验单 id</summary>
+    public long Id { get; set; }
+
+    /// <summary>退回意见(必填)</summary>
+    public string? Comment { get; set; }
+}
+
+/// <summary>
+/// FQC 检验单流程状态输出(只读,供前端按钮控制)。
+/// </summary>
+public class FqcFlowStateOutput
+{
+    /// <summary>BizType(S7_FQC_INSPBILL)</summary>
+    public string BizType { get; set; } = FqcInspBillFlowConst.BizType;
+
+    /// <summary>BizId(=检验单 id)</summary>
+    public long BizId { get; set; }
+
+    /// <summary>流程实例 id(未启动为 null)</summary>
+    public long? InstanceId { get; set; }
+
+    /// <summary>流程状态(NotStarted / Running / Approved / Rejected / Cancelled / Terminated)</summary>
+    public string FlowStatus { get; set; } = "NotStarted";
+
+    /// <summary>当前节点码(N1_INSPECT / N2_SUP_REVIEW,未启动/已结束为 null)</summary>
+    public string? CurrentNodeCode { get; set; }
+
+    /// <summary>当前节点名</summary>
+    public string? CurrentNodeName { get; set; }
+
+    /// <summary>当前节点角色码</summary>
+    public string? CurrentAssigneeRole { get; set; }
+
+    /// <summary>当前用户是否可提交检验结果(N1 待办)</summary>
+    public bool CanSubmitResult { get; set; }
+
+    /// <summary>当前用户是否可主管通过(N2 待办)</summary>
+    public bool CanSupervisorApprove { get; set; }
+
+    /// <summary>当前用户是否可主管退回(N2 待办)</summary>
+    public bool CanSupervisorReject { get; set; }
+
+    /// <summary>流程是否已完成/终态</summary>
+    public bool IsCompleted { get; set; }
+
+    /// <summary>更新时间</summary>
+    public DateTime? UpdatedAt { get; set; }
+
+    /// <summary>提示信息</summary>
+    public string? Message { get; set; }
+}

+ 99 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowBizHandler.cs

@@ -0,0 +1,99 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
+
+/// <summary>
+/// S7 FQC 成品检验单流程常量(BizType / 节点码 / 角色码 / 角色固定 Id / UAT)。集中避免散落字符串。
+/// 最小线性流程:start → N1_INSPECT(检验员) → N2_SUP_REVIEW(检验主管) → end(无网关、无 SQE)。
+/// </summary>
+public static class FqcInspBillFlowConst
+{
+    /// <summary>流程业务类型编码</summary>
+    public const string BizType = "S7_FQC_INSPBILL";
+
+    /// <summary>N1 检验员检验节点</summary>
+    public const string NodeInspect = "N1_INSPECT";
+
+    /// <summary>N2 检验主管审核节点</summary>
+    public const string NodeSupervisor = "N2_SUP_REVIEW";
+
+    /// <summary>检验员角色编码</summary>
+    public const string RoleInspectorCode = "ROLE_S7_FQC_INSPECTOR";
+
+    /// <summary>检验主管角色编码</summary>
+    public const string RoleSupervisorCode = "ROLE_S7_FQC_SUPERVISOR";
+
+    /// <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>UAT 冒烟绑定账号(同绑两角色,仅 UAT 自审)</summary>
+    public const string UatAccount = "superAdmin.NET";
+
+    // 检验任务执行状态 FINSPECTSTATUS:未检验 / 检验中 / 检验完成
+    public const string InspectStatusDone = "检验完成";
+
+    // pd 判定:0=合格 1=不合格 | clfs 处理方式:0=让步接收 1=挑选 2=报废
+}
+
+/// <summary>
+/// S7 FQC 成品检验单审批流回调处理器(BizType=S7_FQC_INSPBILL)。
+///
+/// 本批(检验员+主管最小闭环)Handler 全部 no-op:不写 qms、不写库存/仓储/生产状态、不发额外通知。
+/// 检验业务结果 pd/hgsl/bhgsl/clfs 与检验任务 qms_fqcbj 字段由 FqcInspBillFlowService.submit-result 在事务内写入;
+/// 审批节点/待办/审核状态由 ApprovalFlowInstance 承载,绝不写入 qms_qcpp_inspbill.FBILLSTATUS 或 qms_fqcbj.FINSPECTSTATUS。
+/// GetBizData 仅返回基础信息供流程日志/展示(本批线性流程无网关条件)。
+/// </summary>
+public class FqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => FqcInspBillFlowConst.BizType;
+
+    private readonly ISqlSugarClient _db;
+
+    public FqcInspBillFlowBizHandler(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>流程发起后回调:本批 no-op。</summary>
+    public Task OnFlowStarted(long bizId, long instanceId) => Task.CompletedTask;
+
+    /// <summary>单节点完成回调:本批 no-op。</summary>
+    public Task OnNodeCompleted(long bizId, long instanceId, string nodeId, string nodeName, long? approverUserId) => Task.CompletedTask;
+
+    /// <summary>流程结束回调:本批 no-op(不写 qms/库存/仓储/生产;审核结果由 ApprovalFlowInstance 状态承载)。</summary>
+    public Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId) => Task.CompletedTask;
+
+    /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        var rows = await _db.Ado.SqlQueryAsync<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>();
+        return new Dictionary<string, object>
+        {
+            ["billNo"] = r.BillNo ?? "",
+            ["materialCode"] = r.MaterialCode ?? "",
+            ["materialName"] = r.MaterialName ?? "",
+            ["pd"] = r.Pd ?? "",
+            ["clfs"] = r.Clfs ?? -1,
+        };
+    }
+
+    private class FqcInspBillFlowBizData
+    {
+        public string? BillNo { get; set; }
+        public string? MaterialCode { get; set; }
+        public string? MaterialName { get; set; }
+        public string? Pd { get; set; }
+        public int? Clfs { get; set; }
+    }
+}

+ 298 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowService.cs

@@ -0,0 +1,298 @@
+using Admin.NET.Plugin.AiDOP.Entity;
+using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
+
+/// <summary>
+/// S7 FQC 成品检验单 检验员+主管最小审批流服务(S7-FQC-INSPBILL-INSPECTOR-SUPERVISOR-FLOW-1)。
+///
+/// N1_INSPECT 检验员录入 pd/hgsl/bhgsl/clfs → 提交 → N2_SUP_REVIEW 检验主管审核(通过=Approved / 退回=回 N1)。
+/// 复用 ApprovalFlow 引擎扩展点(StartFlow/Approve/ReturnToPrev + IFlowBizHandler),不改 ApprovalFlow 核心。
+/// BizType=S7_FQC_INSPBILL,BizId=qms_qcpp_inspbill.id,BizCode=FBILLNO。反查用 ApprovalFlowInstance(BizType,BizId),inspbill 不加列。
+///
+/// 状态归属(严格分离):
+///   - 检验业务结果 → qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs(仅 submit-result 写)
+///   - 检验任务执行进度 → qms_fqcbj.jyfzr/jykssj/jywcsj/FINSPECTSTATUS(经 lydjbh=FBILLNO 关联;仅执行状态未检验/检验中/检验完成)
+///   - 审批节点/待办/审核状态 → ApprovalFlowInstance(绝不写入 FINSPECTSTATUS)
+/// 不写 FBILLSTATUS/库存/仓储/生产状态;clfs 仅保存不触发任何后续动作。正常鉴权(非匿名)。
+/// </summary>
+[ApiDescriptionSettings(Order = 331, Description = "FQC检验单流程")]
+[Route("api/S7FqcInspBillFlow")]
+[NonUnify]
+public class FqcInspBillFlowService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly FlowEngineService _flowEngine;
+    private readonly UserManager _userManager;
+
+    public FqcInspBillFlowService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
+    {
+        _db = db;
+        _flowEngine = flowEngine;
+        _userManager = userManager;
+    }
+
+    /// <summary>检验单流程状态(只读,供前端按钮控制)。</summary>
+    [DisplayName("FQC检验单流程状态")]
+    [HttpGet("state")]
+    public async Task<FqcFlowStateOutput> GetState([FromQuery] long id)
+    {
+        await EnsureBillExistsAsync(id);
+        return await BuildStateAsync(id);
+    }
+
+    /// <summary>
+    /// 检验员提交检验结果(写 qms_qcpp_inspbill.pd/hgsl/bhgsl/clfs + qms_fqcbj 任务进度 + 推进 N1→N2)。
+    /// </summary>
+    [DisplayName("检验员提交检验结果")]
+    [HttpPost("submit-result")]
+    public async Task<FqcFlowStateOutput> SubmitResult([FromBody] FqcSubmitResultInput input)
+    {
+        var (billNo, sourceBillNo) = await EnsureBillAsync(input.Id);
+
+        // 参数校验
+        if (input.Pd != 0 && input.Pd != 1) throw Oops.Oh("判定 pd 只能为 0(合格) 或 1(不合格)");
+        if (input.Hgsl < 0) throw Oops.Oh("合格数量不能为负");
+        if (input.Bhgsl < 0) throw Oops.Oh("不合格数量不能为负");
+        if (input.Pd == 0)
+        {
+            if (input.Bhgsl != 0) throw Oops.Oh("判定合格时不合格数量必须为 0");
+        }
+        else // pd == 1
+        {
+            if (input.Bhgsl <= 0) throw Oops.Oh("判定不合格时不合格数量必须大于 0");
+            if (input.Clfs is not (0 or 1 or 2)) throw Oops.Oh("判定不合格时处理方式 clfs 必填且只能为 0(让步接收)/1(挑选)/2(报废)");
+        }
+
+        var inst = await GetLatestInstanceAsync(input.Id);
+        if (inst != null && inst.Status != FlowInstanceStatusEnum.Running)
+            throw Oops.Oh("该检验单流程已结束,无法再提交检验结果");
+
+        var inspectorName = _userManager.RealName ?? _userManager.Account;
+        var now = DateTime.Now;
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            // 无运行中实例 → 起流程(自动生成 N1 任务给检验员角色成员)
+            long instanceId = inst?.Id ?? await _flowEngine.StartFlow(new StartFlowInput
+            {
+                BizType = FqcInspBillFlowConst.BizType,
+                BizId = input.Id,
+                BizNo = billNo,
+                Title = $"FQC检验单 {billNo}",
+                Comment = input.Comment,
+            });
+
+            var myTask = await GetMyPendingTaskAsync(instanceId);
+            if (myTask == null)
+                throw Oops.Oh("当前用户没有待处理的检验任务(需检验员角色成员)");
+            if (myTask.NodeId != FqcInspBillFlowConst.NodeInspect)
+                throw Oops.Oh("当前不在检验录入节点,无法提交检验结果");
+
+            // ① 检验业务结果 → qms_qcpp_inspbill(仅 pd/hgsl/bhgsl/clfs,不覆盖继承字段)
+            await _db.Ado.ExecuteCommandAsync(
+                "UPDATE qms_qcpp_inspbill SET pd=@pd, hgsl=@hgsl, bhgsl=@bhgsl, clfs=@clfs WHERE id=@id",
+                new List<SugarParameter>
+                {
+                    new("@pd", input.Pd),
+                    new("@hgsl", input.Hgsl),
+                    new("@bhgsl", input.Bhgsl),
+                    new("@clfs", input.Pd == 1 ? input.Clfs : null),
+                    new("@id", input.Id),
+                });
+
+            // ② 检验任务执行进度 → qms_fqcbj(经 lydjbh=FBILLNO 关联;lydjbh 为空则跳过,防误伤)
+            //    安全约束(§二):先查关联数量 —— 0 条跳过、1 条更新、>1 条视为数据异常抛错整体回滚,
+            //    杜绝无保护批量 UPDATE 一次误更新多条任务(即使未来真实数据出现重复 FBILLNO 也安全)。
+            //    jykssj 若为空则补首次开始时间(IFNULL 保留既有);jywcsj=本次提交时间;FINSPECTSTATUS=检验完成
+            if (!string.IsNullOrWhiteSpace(sourceBillNo))
+            {
+                var taskCount = await _db.Ado.GetIntAsync(
+                    "SELECT COUNT(1) FROM qms_fqcbj WHERE FBILLNO=@bjbh",
+                    new List<SugarParameter> { new("@bjbh", sourceBillNo) });
+                if (taskCount > 1)
+                    throw Oops.Oh($"检验任务数据异常:来源单号 {sourceBillNo} 匹配到 {taskCount} 条报检任务(qms_fqcbj),无法安全更新,已整体回滚");
+                if (taskCount == 1)
+                {
+                    await _db.Ado.ExecuteCommandAsync(
+                        "UPDATE qms_fqcbj SET jyfzr=@jyfzr, jykssj=IFNULL(jykssj,@now), jywcsj=@now, FINSPECTSTATUS=@status WHERE FBILLNO=@bjbh",
+                        new List<SugarParameter>
+                        {
+                            new("@jyfzr", inspectorName),
+                            new("@now", now),
+                            new("@status", FqcInspBillFlowConst.InspectStatusDone),
+                            new("@bjbh", sourceBillNo),
+                        });
+                }
+            }
+
+            // ③ 推进 N1 → N2
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    /// <summary>检验主管通过(N2 → 完成,不覆盖检验员业务结果)。</summary>
+    [DisplayName("检验主管通过")]
+    [HttpPost("supervisor-approve")]
+    public async Task<FqcFlowStateOutput> SupervisorApprove([FromBody] FqcSupervisorApproveInput input)
+    {
+        await EnsureBillAsync(input.Id);
+        var (_, myTask) = await RequireSupervisorTaskAsync(input.Id);
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            await _flowEngine.Approve(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    /// <summary>检验主管退回(N2 → 回 N1,意见必填;保留检验员业务结果与任务进度)。</summary>
+    [DisplayName("检验主管退回")]
+    [HttpPost("supervisor-reject")]
+    public async Task<FqcFlowStateOutput> SupervisorReject([FromBody] FqcSupervisorRejectInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.Comment))
+            throw Oops.Oh("退回必须填写意见");
+
+        await EnsureBillAsync(input.Id);
+        var (_, myTask) = await RequireSupervisorTaskAsync(input.Id);
+
+        var tran = await _db.AsTenant().UseTranAsync(async () =>
+        {
+            // ReturnToPrev 回 N1;不清 pd/hgsl/bhgsl/clfs、不清 FINSPECTSTATUS/jywcsj,检验员改后重提
+            await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
+        });
+        if (!tran.IsSuccess) throw tran.ErrorException;
+
+        return await BuildStateAsync(input.Id);
+    }
+
+    // ───────────────────────── helpers ─────────────────────────
+
+    /// <summary>校验检验单存在并返回 FBILLNO。</summary>
+    private async Task<string> EnsureBillExistsAsync(long id)
+    {
+        var (billNo, _) = await EnsureBillAsync(id);
+        return billNo;
+    }
+
+    /// <summary>校验检验单存在并返回 (FBILLNO, lydjbh来源单号)。</summary>
+    private async Task<(string billNo, string? sourceBillNo)> EnsureBillAsync(long id)
+    {
+        if (id <= 0) throw Oops.Oh("检验单 id 非法");
+        var rows = await _db.Ado.SqlQueryAsync<FqcBillKey>(
+            "SELECT FBILLNO AS BillNo, lydjbh AS SourceBillNo FROM qms_qcpp_inspbill WHERE id=@id LIMIT 1",
+            new List<SugarParameter> { new("@id", id) });
+        var r = rows.FirstOrDefault();
+        if (r == null) throw Oops.Oh("检验单不存在");
+        return (r.BillNo ?? id.ToString(), r.SourceBillNo);
+    }
+
+    /// <summary>取该检验单最新流程实例(BizType+BizId,跨数据范围)。</summary>
+    private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
+    {
+        return await _db.Queryable<ApprovalFlowInstance>()
+            .ClearFilter()
+            .Where(x => x.BizType == FqcInspBillFlowConst.BizType && x.BizId == bizId)
+            .OrderByDescending(x => x.Id)
+            .FirstAsync();
+    }
+
+    /// <summary>取当前用户在指定实例下的待办任务。</summary>
+    private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
+    {
+        var userId = _userManager.UserId;
+        return await _db.Queryable<ApprovalFlowTask>()
+            .ClearFilter()
+            .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
+            .FirstAsync();
+    }
+
+    /// <summary>校验并返回主管 N2 待办任务。</summary>
+    private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
+    {
+        var inst = await GetLatestInstanceAsync(bizId);
+        if (inst == null) throw Oops.Oh("该检验单尚未发起流程");
+        if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单流程已结束");
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
+        if (myTask.NodeId != FqcInspBillFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
+        return (inst, myTask);
+    }
+
+    /// <summary>当前用户是否具备指定角色(跨数据范围)。</summary>
+    private async Task<bool> HasRoleAsync(long roleId)
+    {
+        var userId = _userManager.UserId;
+        return await _db.Queryable<SysUserRole>()
+            .ClearFilter()
+            .AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
+    }
+
+    /// <summary>组装流程状态输出。</summary>
+    private async Task<FqcFlowStateOutput> BuildStateAsync(long bizId)
+    {
+        var output = new FqcFlowStateOutput { BizId = bizId };
+        var inst = await GetLatestInstanceAsync(bizId);
+
+        if (inst == null)
+        {
+            output.FlowStatus = "NotStarted";
+            output.CanSubmitResult = await HasRoleAsync(FqcInspBillFlowConst.RoleInspectorId);
+            output.Message = output.CanSubmitResult ? "待检验员录入结果" : "尚未发起流程";
+            return output;
+        }
+
+        output.InstanceId = inst.Id;
+        output.FlowStatus = inst.Status.ToString();
+        output.UpdatedAt = inst.EndTime ?? inst.StartTime;
+
+        if (inst.Status != FlowInstanceStatusEnum.Running)
+        {
+            output.IsCompleted = true;
+            output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "流程已完成(主管审核通过)" : $"流程已结束({inst.Status})";
+            return output;
+        }
+
+        output.CurrentNodeCode = inst.CurrentNodeId;
+        var myTask = await GetMyPendingTaskAsync(inst.Id);
+        if (myTask != null)
+        {
+            output.CurrentNodeName = myTask.NodeName;
+            if (myTask.NodeId == FqcInspBillFlowConst.NodeInspect)
+            {
+                output.CurrentAssigneeRole = FqcInspBillFlowConst.RoleInspectorCode;
+                output.CanSubmitResult = true;
+                output.Message = "待当前用户录入检验结果";
+            }
+            else if (myTask.NodeId == FqcInspBillFlowConst.NodeSupervisor)
+            {
+                output.CurrentAssigneeRole = FqcInspBillFlowConst.RoleSupervisorCode;
+                output.CanSupervisorApprove = true;
+                output.CanSupervisorReject = true;
+                output.Message = "待当前用户主管审核";
+            }
+        }
+        else
+        {
+            output.CurrentAssigneeRole = inst.CurrentNodeId == FqcInspBillFlowConst.NodeInspect
+                ? FqcInspBillFlowConst.RoleInspectorCode
+                : FqcInspBillFlowConst.RoleSupervisorCode;
+            output.Message = "流程进行中(当前用户无待办)";
+        }
+        return output;
+    }
+
+    private class FqcBillKey
+    {
+        public string? BillNo { get; set; }
+        public string? SourceBillNo { get; set; }
+    }
+}

+ 244 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/FqcInspBillFlowSeed.cs

@@ -0,0 +1,244 @@
+using System.Diagnostics;
+using System.Text.Json;
+using Admin.NET.Plugin.AiDOP.FinishedWarehouse;
+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>
+/// S7 FQC 成品检验单 检验员+主管流程种子(幂等)。
+///
+/// 顺序:① 角色 ROLE_S7_FQC_INSPECTOR / ROLE_S7_FQC_SUPERVISOR ② UAT 成员绑定(superAdmin.NET 同绑两角色)
+/// ③ ApprovalBizType(S7_FQC_INSPBILL) ④ ApprovalFlow 已发布定义(start→N1_INSPECT→N2_SUP_REVIEW→end,节点绑角色,线性无网关)。
+/// 全部幂等:存在则跳过,不覆盖、不删除既有角色/流程。superAdmin.NET 不存在时仅告警不崩溃。
+/// </summary>
+public static class FqcInspBillFlowSeed
+{
+    public static void EnsureSeed(ISqlSugarClient db)
+    {
+        try
+        {
+            db.CodeFirst.InitTables(
+                typeof(ApprovalBizType),
+                typeof(ApprovalFlowEntity),
+                typeof(ApprovalFlowVersion));
+            EnsureRoles(db);
+            EnsureUatMembers(db);
+            EnsureBizType(db);
+            EnsurePublishedFlow(db);
+        }
+        catch (Exception ex)
+        {
+            Trace.TraceWarning("S7 FQC inspbill flow seed: " + ex);
+        }
+    }
+
+    private static void EnsureRoles(ISqlSugarClient db)
+    {
+        EnsureRole(db, FqcInspBillFlowConst.RoleInspectorId, FqcInspBillFlowConst.RoleInspectorCode, "成品检验员", 820);
+        EnsureRole(db, FqcInspBillFlowConst.RoleSupervisorId, FqcInspBillFlowConst.RoleSupervisorCode, "成品检验主管", 821);
+    }
+
+    private static void EnsureRole(ISqlSugarClient db, long id, string code, string name, int orderNo)
+    {
+        var exists = db.Queryable<SysRole>().ClearFilter().Any(x => x.Code == code);
+        if (exists) return;
+        db.Insertable(new SysRole
+        {
+            Id = id,
+            TenantId = SqlSugarConst.DefaultTenantId,
+            Name = name,
+            Code = code,
+            OrderNo = orderNo,
+            DataScope = DataScopeEnum.Self,
+            Status = StatusEnum.Enable,
+            Remark = "S7 成品检验流程角色",
+            CreateTime = DateTime.Now,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsureUatMembers(ISqlSugarClient db)
+    {
+        var user = db.Queryable<SysUser>().ClearFilter()
+            .Where(x => x.Account == FqcInspBillFlowConst.UatAccount)
+            .First();
+        if (user == null)
+        {
+            Trace.TraceWarning($"S7 FQC flow seed: UAT 账号 {FqcInspBillFlowConst.UatAccount} 不存在,跳过成员绑定(阶段2 冒烟前须指定成员)");
+            return;
+        }
+        EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleInspectorId);
+        EnsureUserRole(db, user.Id, FqcInspBillFlowConst.RoleSupervisorId);
+    }
+
+    private static void EnsureUserRole(ISqlSugarClient db, long userId, long roleId)
+    {
+        var exists = db.Queryable<SysUserRole>().ClearFilter().Any(x => x.UserId == userId && x.RoleId == roleId);
+        if (exists) return;
+        db.Insertable(new SysUserRole
+        {
+            Id = YitIdHelper.NextId(),
+            UserId = userId,
+            RoleId = roleId,
+        }).ExecuteCommand();
+    }
+
+    private static void EnsureBizType(ISqlSugarClient db)
+    {
+        var exists = db.Queryable<ApprovalBizType>().Any(x => x.Code == FqcInspBillFlowConst.BizType);
+        if (exists) return;
+        db.Insertable(new ApprovalBizType
+        {
+            Id = YitIdHelper.NextId(),
+            Code = FqcInspBillFlowConst.BizType,
+            Name = "成品检验单审批",
+            Remark = "S7 成品检验单 检验员→主管 审批流",
+            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 == FqcInspBillFlowConst.BizType && x.OrgId == orgId && x.IsPublished && !x.IsDelete)
+                .OrderByDescending(x => x.Version)
+                .First();
+
+            // 已发布定义存在即跳过(线性 N1→N2 首版,不做版本升级)。
+            if (existing != null) continue;
+
+            var flow = new ApprovalFlowEntity
+            {
+                Id = YitIdHelper.NextId(),
+                Code = $"S7FQC_{orgId}",
+                Name = "成品检验单审批",
+                BizType = FqcInspBillFlowConst.BizType,
+                FormJson = "{}",
+                FlowJson = flowJson,
+                Status = 1,
+                Remark = "系统默认流程 v1:开始-检验员检验-检验主管审核-结束(线性)",
+                Version = FqcInspBillFlowConst.FlowDefinitionVersion,
+                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 = FqcInspBillFlowConst.NodeInspect,
+                    Type = "bpmn:userTask",
+                    X = 300,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "检验员检验",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = FqcInspBillFlowConst.RoleInspectorId.ToString(),
+                        ApproverNames = "成品检验员",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 300, Y = 154, Value = "检验员检验" },
+                },
+                new()
+                {
+                    Id = FqcInspBillFlowConst.NodeSupervisor,
+                    Type = "bpmn:userTask",
+                    X = 500,
+                    Y = 120,
+                    Properties = new FlowProperties
+                    {
+                        NodeName = "检验主管审核",
+                        ApproverType = nameof(ApproverTypeEnum.Role),
+                        ApproverIds = FqcInspBillFlowConst.RoleSupervisorId.ToString(),
+                        ApproverNames = "成品检验主管",
+                        MultiApproveMode = "Any",
+                    },
+                    Text = new FlowTextItem { X = 500, Y = 154, Value = "检验主管审核" },
+                },
+                new()
+                {
+                    Id = "end",
+                    Type = "bpmn:endEvent",
+                    X = 700,
+                    Y = 120,
+                    Properties = new FlowProperties { NodeName = "结束" },
+                    Text = new FlowTextItem { X = 700, Y = 154, Value = "结束" },
+                },
+            },
+            Edges = new List<ApprovalFlowEdgeItem>
+            {
+                BuildEdge("edge-start-n1", "start", 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),
+            }
+        };
+        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 },
+            },
+        };
+    }
+}

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

@@ -80,6 +80,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.FqcInspBillFlowSeed.EnsureSeed(dbS4);
         }
         catch (Exception ex)
         {