Эх сурвалжийг харах

feat(s7): FQC 检验单只读详情 drawer(头+品检项目明细)

- FqcInspBillService 新增 GET detail?id:头 qms_qcpp_inspbill + 明细 qms_qcpp_inspbillst(glid=id)
- 明细子表运行时 EnsureFqcDetailTableAsync 归口 aidopdev 建 82 列空表(方案A;13列utf8mb3避免行超65535)
- SAP工单号沿用 MIN(WorkOrdMaster.WorkOrd) sczld=Batch;样本 j1..j50 收敛 Samples[] 前端非空展开
- 契约对齐 S6:扁平头+lines[],id<=0/未命中返回 null(204),非法 id 400;明细 ORDER BY xh,id
- 新增只读 FqcInspBillDetailDrawer.vue(无写表单),结果列表加详情入口
- 纯只读,业务表零写入;bump version Web 2.4.247 / server 1.0.245
YY968XX 10 цаг өмнө
parent
commit
4aa325e745

+ 1 - 1
Web/package.json

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

+ 64 - 0
Web/src/views/aidop/s7/api/fqcInspBill.ts

@@ -97,3 +97,67 @@ export function fetchFqcResultList(params: any) {
 		.get<FqcPaged<FqcResultRow>>('/api/S7FqcInspBill/result-list', { params: withAidopTenantParams(params) })
 		.get<FqcPaged<FqcResultRow>>('/api/S7FqcInspBill/result-list', { params: withAidopTenantParams(params) })
 		.then((r) => r.data);
 		.then((r) => r.data);
 }
 }
+
+// ── S7 FQC 检验单只读详情(头 qms_qcpp_inspbill + 明细 qms_qcpp_inspbillst,glid=id)──
+export interface FqcDetailLine {
+	id?: number;
+	/** 序号 xh */
+	seqNo?: number;
+	/** 检验项目 jyxm */
+	inspectionItem?: string;
+	/** 接收标准 jsbz */
+	acceptStandard?: string;
+	/** 技术要求 jsyq */
+	techRequirement?: string;
+	/** 抽样方案 cyfa */
+	samplingPlan?: string;
+	/** 样本量 ybl */
+	sampleQty?: string;
+	/** 上限 sx */
+	upperLimit?: string;
+	/** 下限 xx */
+	lowerLimit?: string;
+	/** 规格标准 SizeSpecifications */
+	sizeSpecification?: string;
+	/** 判定 pd(原样) */
+	judgment?: string;
+	/** 非数值型 OK */
+	nonNumericOk?: number;
+	/** 非数值型 NG */
+	nonNumericNg?: number;
+	/** 备注 bz */
+	remark?: string;
+	/** 样品值(样品1..样品N,按序 j1..j50,末尾空值已裁剪) */
+	samples?: (string | null)[];
+}
+
+export interface FqcDetail {
+	id?: number;
+	billNo?: string;
+	inspectTime?: string;
+	sourceBillNo?: string;
+	inspector?: string;
+	judgment?: string;
+	materialCode?: string;
+	materialName?: string;
+	spec?: string;
+	productionBatchNo?: string;
+	productionOrderNo?: string;
+	orderQty?: number;
+	sapWorkOrderNo?: string;
+	inspectQty?: number;
+	qualifiedQty?: number;
+	unqualifiedQty?: number;
+	fireNo?: string;
+	inspectSpecNo?: string;
+	inspectSpecVersion?: string;
+	/** 品检项目明细(按 xh,id 排序;样本 j1~j50 收敛为 samples[]) */
+	lines?: FqcDetailLine[];
+}
+
+/** FQC 检验单只读详情(id<=0/未命中返回 null) */
+export function fetchFqcDetail(id: number) {
+	return service
+		.get<FqcDetail>('/api/S7FqcInspBill/detail', { params: withAidopTenantParams({ id }) })
+		.then((r) => r.data);
+}

+ 177 - 0
Web/src/views/aidop/s7/fqc/components/FqcInspBillDetailDrawer.vue

@@ -0,0 +1,177 @@
+<template>
+	<el-drawer v-model="innerVisible" :size="720" direction="rtl" @closed="onClosed">
+		<template #header>
+			<div class="drawer-title">
+				<span class="t1">FQC检验单详情</span>
+				<span class="t2">{{ detail?.billNo || '-' }}</span>
+			</div>
+		</template>
+
+		<div v-loading="loading" class="detail-body">
+			<template v-if="detail">
+				<el-descriptions title="基本信息" :column="2" border size="small">
+					<el-descriptions-item label="单据编号">{{ dash(detail.billNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检验时间">{{ fmtDateTime(detail.inspectTime) }}</el-descriptions-item>
+					<el-descriptions-item label="来源单据编号">{{ dash(detail.sourceBillNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检验人">{{ dash(detail.inspector) }}</el-descriptions-item>
+					<el-descriptions-item label="判定">{{ dash(detail.judgment) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="成品信息" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="物料编码">{{ dash(detail.materialCode) }}</el-descriptions-item>
+					<el-descriptions-item label="物料名称">{{ dash(detail.materialName) }}</el-descriptions-item>
+					<el-descriptions-item label="规格型号">{{ dash(detail.spec) }}</el-descriptions-item>
+					<el-descriptions-item label="生产批号">{{ dash(detail.productionBatchNo) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="生产信息" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="生产指令单">{{ dash(detail.productionOrderNo) }}</el-descriptions-item>
+					<el-descriptions-item label="指令单数量">{{ dashNum(detail.orderQty) }}</el-descriptions-item>
+					<el-descriptions-item label="SAP工单号">{{ dash(detail.sapWorkOrderNo) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="检验数量" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="检验数量">{{ dashNum(detail.inspectQty) }}</el-descriptions-item>
+					<el-descriptions-item label="合格数量">{{ dashNum(detail.qualifiedQty) }}</el-descriptions-item>
+					<el-descriptions-item label="不合格数量">{{ dashNum(detail.unqualifiedQty) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<el-descriptions title="检规信息" :column="2" border size="small" class="mt12">
+					<el-descriptions-item label="击发编号">{{ dash(detail.fireNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检规编号">{{ dash(detail.inspectSpecNo) }}</el-descriptions-item>
+					<el-descriptions-item label="检规版本">{{ dash(detail.inspectSpecVersion) }}</el-descriptions-item>
+				</el-descriptions>
+
+				<div class="items-title mt12">品检项目明细</div>
+				<el-table :data="detail.lines || []" border stripe size="small" row-key="id">
+					<el-table-column type="expand">
+						<template #default="{ row }">
+							<div class="samples-wrap">
+								<template v-if="nonEmptySamples(row).length">
+									<span class="samples-label">样品值:</span>
+									<el-tag
+										v-for="s in nonEmptySamples(row)"
+										:key="s.idx"
+										class="sample-tag"
+										type="info"
+										disable-transitions
+									>样品{{ s.idx }}: {{ s.val }}</el-tag>
+								</template>
+								<span v-else class="samples-empty">无样品数据</span>
+							</div>
+						</template>
+					</el-table-column>
+					<el-table-column prop="seqNo" label="序号" min-width="70" resizable />
+					<el-table-column prop="inspectionItem" label="检验项目" min-width="140" show-overflow-tooltip resizable />
+					<el-table-column prop="acceptStandard" label="接收标准" min-width="140" show-overflow-tooltip resizable />
+					<el-table-column prop="techRequirement" label="技术要求" min-width="140" show-overflow-tooltip resizable />
+					<el-table-column prop="samplingPlan" label="抽样方案" min-width="120" show-overflow-tooltip resizable />
+					<el-table-column prop="sampleQty" label="样本量" min-width="90" resizable />
+					<el-table-column prop="upperLimit" label="上限" min-width="90" resizable />
+					<el-table-column prop="lowerLimit" label="下限" min-width="90" resizable />
+					<el-table-column prop="sizeSpecification" label="规格标准" min-width="120" show-overflow-tooltip resizable />
+					<el-table-column prop="judgment" label="判定" min-width="90" resizable />
+					<el-table-column prop="nonNumericOk" label="非数值OK" min-width="100" resizable />
+					<el-table-column prop="nonNumericNg" label="非数值NG" min-width="100" resizable />
+					<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip resizable />
+					<template #empty>
+						<el-empty description="暂无明细" :image-size="60" />
+					</template>
+				</el-table>
+			</template>
+			<el-empty v-else-if="!loading" description="暂无详情数据" />
+		</div>
+	</el-drawer>
+</template>
+
+<script setup lang="ts" name="FqcInspBillDetailDrawer">
+import { computed } from 'vue';
+import type { FqcDetail, FqcDetailLine } from '../../api/fqcInspBill';
+
+const props = defineProps<{
+	modelValue: boolean;
+	detail: FqcDetail | null;
+	loading?: boolean;
+}>();
+const emit = defineEmits<{
+	(e: 'update:modelValue', v: boolean): void;
+	(e: 'closed'): void;
+}>();
+
+const innerVisible = computed({
+	get: () => props.modelValue,
+	set: (v: boolean) => emit('update:modelValue', v),
+});
+
+// 仅渲染非空样品,保留样品序号(j1→样品1)
+function nonEmptySamples(row: FqcDetailLine) {
+	const arr = row?.samples || [];
+	const out: { idx: number; val: string }[] = [];
+	for (let i = 0; i < arr.length; i++) {
+		const v = arr[i];
+		if (v !== null && v !== undefined && String(v).trim() !== '') out.push({ idx: i + 1, val: String(v) });
+	}
+	return out;
+}
+
+function dash(v: unknown) {
+	if (v === null || v === undefined || v === '') return '-';
+	return String(v);
+}
+function dashNum(v?: number | null) {
+	if (v === null || v === undefined) return '-';
+	return String(v);
+}
+function fmtDateTime(v?: string | null) {
+	if (!v) return '-';
+	return String(v).replace('T', ' ').slice(0, 19);
+}
+function onClosed() {
+	emit('closed');
+}
+</script>
+
+<style scoped lang="scss">
+.drawer-title {
+	display: flex;
+	align-items: baseline;
+	gap: 10px;
+	.t1 {
+		font-weight: bold;
+		font-size: 16px;
+	}
+	.t2 {
+		color: var(--el-text-color-secondary);
+		font-size: 13px;
+	}
+}
+.detail-body {
+	min-height: 120px;
+}
+.mt12 {
+	margin-top: 12px;
+}
+.items-title {
+	font-weight: bold;
+	font-size: 14px;
+	margin-bottom: 8px;
+}
+.samples-wrap {
+	padding: 6px 12px;
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
+	gap: 6px;
+}
+.samples-label {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+.sample-tag {
+	margin: 0;
+}
+.samples-empty {
+	color: var(--el-text-color-secondary);
+	font-size: 12px;
+}
+</style>

+ 28 - 1
Web/src/views/aidop/s7/fqc/fqcInspectionResultList.vue

@@ -58,6 +58,11 @@
 			<el-table-column prop="judgment" label="判定" min-width="120" show-overflow-tooltip resizable />
 			<el-table-column prop="judgment" label="判定" min-width="120" show-overflow-tooltip resizable />
 			<el-table-column prop="qualifiedQty" label="合格数量" min-width="110" resizable />
 			<el-table-column prop="qualifiedQty" label="合格数量" min-width="110" resizable />
 			<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" resizable />
 			<el-table-column prop="unqualifiedQty" label="不合格数量" min-width="110" resizable />
+			<el-table-column label="操作" min-width="90" fixed="right">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openDetail(row)">详情</el-button>
+				</template>
+			</el-table-column>
 			<template #empty>
 			<template #empty>
 				<el-empty description="暂无数据" />
 				<el-empty description="暂无数据" />
 			</template>
 			</template>
@@ -74,6 +79,8 @@
 				@size-change="loadList"
 				@size-change="loadList"
 			/>
 			/>
 		</div>
 		</div>
+
+		<FqcInspBillDetailDrawer v-model="detailVisible" :detail="detail" :loading="detailLoading" />
 	</AidopDemoShell>
 	</AidopDemoShell>
 </template>
 </template>
 
 
@@ -82,7 +89,8 @@ import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { useRoute } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import { fetchFqcResultList, type FqcResultRow } from '../api/fqcInspBill';
+import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
+import { fetchFqcResultList, fetchFqcDetail, type FqcResultRow, type FqcDetail } from '../api/fqcInspBill';
 
 
 const route = useRoute();
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
 const pageTitle = computed(() => (route.meta?.title as string) || 'FQC检验单列表');
@@ -105,6 +113,25 @@ const loading = ref(false);
 const rows = ref<FqcResultRow[]>([]);
 const rows = ref<FqcResultRow[]>([]);
 const total = ref(0);
 const total = ref(0);
 
 
+const detailVisible = ref(false);
+const detail = ref<FqcDetail | null>(null);
+const detailLoading = ref(false);
+
+async function openDetail(row: FqcResultRow) {
+	if (!row?.id) return;
+	detail.value = null;
+	detailVisible.value = true;
+	detailLoading.value = true;
+	try {
+		detail.value = await fetchFqcDetail(row.id);
+	} catch (e: any) {
+		detail.value = null;
+		ElMessage.error(e?.message || '加载FQC检验单详情失败');
+	} finally {
+		detailLoading.value = false;
+	}
+}
+
 function fmtDateTime(v?: string | null) {
 function fmtDateTime(v?: string | null) {
 	if (!v) return '';
 	if (!v) return '';
 	const s = String(v);
 	const s = String(v);

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.244</AssemblyVersion>
-    <FileVersion>1.0.244</FileVersion>
-    <Version>1.0.244</Version>
+    <AssemblyVersion>1.0.245</AssemblyVersion>
+    <FileVersion>1.0.245</FileVersion>
+    <Version>1.0.245</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 149 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/FqcInspBillDto.cs

@@ -211,3 +211,152 @@ public class FqcResultListRow
     /// <summary>不合格数量(qms_qcpp_inspbill.bhgsl)</summary>
     /// <summary>不合格数量(qms_qcpp_inspbill.bhgsl)</summary>
     public decimal? UnqualifiedQty { get; set; }
     public decimal? UnqualifiedQty { get; set; }
 }
 }
+
+// ══════════════════════════════════════════════════════════════════════════
+// FQC 检验单只读详情:头(qms_qcpp_inspbill + SAP工单号)+ 品检项目明细(qms_qcpp_inspbillst,glid=id)
+// 契约对齐 S6 IPQC:扁平头字段 + Lines[];id<=0/未命中 return null。样本 j1..j50 收敛为 Samples[](样品序号值)。
+// ══════════════════════════════════════════════════════════════════════════
+
+/// <summary>FQC 检验单详情(只读):扁平头 + 明细行。</summary>
+public class FqcInspBillDetailDto
+{
+    // —— 基本信息 ——
+    /// <summary>主键(qms_qcpp_inspbill.id)</summary>
+    public long Id { get; set; }
+    /// <summary>单据编号 FBILLNO</summary>
+    public string? BillNo { get; set; }
+    /// <summary>检验时间 FINSPEENDDATE</summary>
+    public DateTime? InspectTime { get; set; }
+    /// <summary>来源单据编号 lydjbh</summary>
+    public string? SourceBillNo { get; set; }
+    /// <summary>检验人 jyr</summary>
+    public string? Inspector { get; set; }
+    /// <summary>判定 pd(原样,字典未接)</summary>
+    public string? Judgment { get; set; }
+
+    // —— 成品信息 ——
+    /// <summary>物料编码 FMATERIALCFG</summary>
+    public string? MaterialCode { get; set; }
+    /// <summary>物料名称 wlmc</summary>
+    public string? MaterialName { get; set; }
+    /// <summary>规格型号 ggxh</summary>
+    public string? Spec { get; set; }
+    /// <summary>生产批号 scph</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    // —— 生产信息 ——
+    /// <summary>生产指令单 sczld</summary>
+    public string? ProductionOrderNo { get; set; }
+    /// <summary>指令单数量 sczldsl</summary>
+    public decimal? OrderQty { get; set; }
+    /// <summary>SAP 工单号 WorkOrdMaster.WorkOrd(sczld=Batch, MIN)</summary>
+    public string? SapWorkOrderNo { get; set; }
+
+    // —— 检验数量 ——
+    /// <summary>检验数量 jysj</summary>
+    public decimal? InspectQty { get; set; }
+    /// <summary>合格数量 hgsl</summary>
+    public decimal? QualifiedQty { get; set; }
+    /// <summary>不合格数量 bhgsl</summary>
+    public decimal? UnqualifiedQty { get; set; }
+
+    // —— 检规信息 ——
+    /// <summary>击发编号 jfbh</summary>
+    public string? FireNo { get; set; }
+    /// <summary>检规编号 jgbh</summary>
+    public string? InspectSpecNo { get; set; }
+    /// <summary>检规版本 jgbb</summary>
+    public string? InspectSpecVersion { get; set; }
+
+    /// <summary>品检项目明细(qms_qcpp_inspbillst,按 xh,id 排序)</summary>
+    public List<FqcInspBillDetailLineDto> Lines { get; set; } = new();
+}
+
+/// <summary>FQC 品检项目明细行(只读,仅已确认列 + 样本数组;jlhb/jyyqsbj/vbl 语义未确认后置)。</summary>
+public class FqcInspBillDetailLineDto
+{
+    /// <summary>明细主键(qms_qcpp_inspbillst.id)</summary>
+    public long Id { get; set; }
+    /// <summary>序号 xh</summary>
+    public int? SeqNo { get; set; }
+    /// <summary>检验项目 jyxm</summary>
+    public string? InspectionItem { get; set; }
+    /// <summary>接收标准 jsbz</summary>
+    public string? AcceptStandard { get; set; }
+    /// <summary>技术要求 jsyq</summary>
+    public string? TechRequirement { get; set; }
+    /// <summary>抽样方案 cyfa</summary>
+    public string? SamplingPlan { get; set; }
+    /// <summary>样本量 ybl</summary>
+    public string? SampleQty { get; set; }
+    /// <summary>上限 sx</summary>
+    public string? UpperLimit { get; set; }
+    /// <summary>下限 xx</summary>
+    public string? LowerLimit { get; set; }
+    /// <summary>规格标准 SizeSpecifications</summary>
+    public string? SizeSpecification { get; set; }
+    /// <summary>判定 pd(原样)</summary>
+    public string? Judgment { get; set; }
+    /// <summary>非数值型 OK NonNumericalTypeOK</summary>
+    public long? NonNumericOk { get; set; }
+    /// <summary>非数值型 NG NonNumericalTypeNG</summary>
+    public long? NonNumericNg { get; set; }
+    /// <summary>备注 bz</summary>
+    public string? Remark { get; set; }
+    /// <summary>样品值(样品1..样品N,按序 j1..j50,末尾空值已裁剪;前端按非空动态展开)</summary>
+    public List<string?> Samples { get; set; } = new();
+}
+
+/// <summary>明细查询行(内部):已确认列 + j1..j50 原始样本列,ToLine() 收敛为 Samples[]。禁止直接出前端。</summary>
+public class FqcInspBillDetailLineRow
+{
+    public long Id { get; set; }
+    public int? SeqNo { get; set; }
+    public string? InspectionItem { get; set; }
+    public string? AcceptStandard { get; set; }
+    public string? TechRequirement { get; set; }
+    public string? SamplingPlan { get; set; }
+    public string? SampleQty { get; set; }
+    public string? UpperLimit { get; set; }
+    public string? LowerLimit { get; set; }
+    public string? SizeSpecification { get; set; }
+    public string? Judgment { get; set; }
+    public long? NonNumericOk { get; set; }
+    public long? NonNumericNg { get; set; }
+    public string? Remark { get; set; }
+
+    public string? J1 { get; set; } public string? J2 { get; set; } public string? J3 { get; set; } public string? J4 { get; set; } public string? J5 { get; set; }
+    public string? J6 { get; set; } public string? J7 { get; set; } public string? J8 { get; set; } public string? J9 { get; set; } public string? J10 { get; set; }
+    public string? J11 { get; set; } public string? J12 { get; set; } public string? J13 { get; set; } public string? J14 { get; set; } public string? J15 { get; set; }
+    public string? J16 { get; set; } public string? J17 { get; set; } public string? J18 { get; set; } public string? J19 { get; set; } public string? J20 { get; set; }
+    public string? J21 { get; set; } public string? J22 { get; set; } public string? J23 { get; set; } public string? J24 { get; set; } public string? J25 { get; set; }
+    public string? J26 { get; set; } public string? J27 { get; set; } public string? J28 { get; set; } public string? J29 { get; set; } public string? J30 { get; set; }
+    public string? J31 { get; set; } public string? J32 { get; set; } public string? J33 { get; set; } public string? J34 { get; set; } public string? J35 { get; set; }
+    public string? J36 { get; set; } public string? J37 { get; set; } public string? J38 { get; set; } public string? J39 { get; set; } public string? J40 { get; set; }
+    public string? J41 { get; set; } public string? J42 { get; set; } public string? J43 { get; set; } public string? J44 { get; set; } public string? J45 { get; set; }
+    public string? J46 { get; set; } public string? J47 { get; set; } public string? J48 { get; set; } public string? J49 { get; set; } public string? J50 { get; set; }
+
+    /// <summary>收敛为明细 DTO:j1..j50 → Samples[](裁剪末尾空值,保留样品序号位置)。</summary>
+    public FqcInspBillDetailLineDto ToLine()
+    {
+        var raw = new[]
+        {
+            J1, J2, J3, J4, J5, J6, J7, J8, J9, J10, J11, J12, J13, J14, J15, J16, J17, J18, J19, J20,
+            J21, J22, J23, J24, J25, J26, J27, J28, J29, J30, J31, J32, J33, J34, J35, J36, J37, J38, J39, J40,
+            J41, J42, J43, J44, J45, J46, J47, J48, J49, J50,
+        };
+        var last = -1;
+        for (var i = 0; i < raw.Length; i++)
+            if (!string.IsNullOrWhiteSpace(raw[i])) last = i;
+        var samples = last < 0 ? new List<string?>() : raw.Take(last + 1).ToList();
+
+        return new FqcInspBillDetailLineDto
+        {
+            Id = Id, SeqNo = SeqNo, InspectionItem = InspectionItem, AcceptStandard = AcceptStandard,
+            TechRequirement = TechRequirement, SamplingPlan = SamplingPlan, SampleQty = SampleQty,
+            UpperLimit = UpperLimit, LowerLimit = LowerLimit, SizeSpecification = SizeSpecification,
+            Judgment = Judgment, NonNumericOk = NonNumericOk, NonNumericNg = NonNumericNg, Remark = Remark,
+            Samples = samples,
+        };
+    }
+}

+ 147 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillService.cs

@@ -31,6 +31,9 @@ public class FqcInspBillService : IDynamicApiController, ITransient
     /// <summary>归口空表建表守卫:每进程仅尝试一次 CREATE TABLE IF NOT EXISTS。</summary>
     /// <summary>归口空表建表守卫:每进程仅尝试一次 CREATE TABLE IF NOT EXISTS。</summary>
     private static int _taskTableEnsured;
     private static int _taskTableEnsured;
 
 
+    /// <summary>检验单明细子表归口空表建表守卫:每进程仅尝试一次。</summary>
+    private static int _detailTableEnsured;
+
     public FqcInspBillService(ISqlSugarClient db)
     public FqcInspBillService(ISqlSugarClient db)
     {
     {
         _db = db;
         _db = db;
@@ -223,6 +226,82 @@ public class FqcInspBillService : IDynamicApiController, ITransient
         return new { total, page, pageSize, list };
         return new { total, page, pageSize, list };
     }
     }
 
 
+    /// <summary>
+    /// FQC 检验单只读详情(源 qms_qcpp_inspbill 头 + qms_qcpp_inspbillst 明细,glid=id)。
+    ///
+    /// 契约对齐 S6 IPQC:id&lt;=0 或未命中返回 null(HTTP 200/204);扁平头 + Lines[]。
+    /// 明细首查前 EnsureFqcDetailTableAsync 幂等建归口空表;样本 j1..j50 收敛为 Samples[](末尾空值裁剪)。
+    /// 仅 SELECT:无任何写入。两表无 tenant_id,TenantId 接收即忽略。
+    /// </summary>
+    [DisplayName("FQC检验单详情")]
+    [HttpGet("detail")]
+    public async Task<FqcInspBillDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
+    {
+        if (id <= 0) return null;
+        await EnsureFqcDetailTableAsync();
+
+        var head = (await _db.Ado.SqlQueryAsync<FqcInspBillDetailDto>(
+            """
+            SELECT
+                a.id            AS Id,
+                a.FBILLNO       AS BillNo,
+                a.FINSPEENDDATE AS InspectTime,
+                a.lydjbh        AS SourceBillNo,
+                a.jyr           AS Inspector,
+                a.pd            AS Judgment,
+                a.FMATERIALCFG  AS MaterialCode,
+                a.wlmc          AS MaterialName,
+                a.ggxh          AS Spec,
+                a.scph          AS ProductionBatchNo,
+                a.sczld         AS ProductionOrderNo,
+                a.sczldsl       AS OrderQty,
+                (SELECT MIN(wo.WorkOrd) FROM WorkOrdMaster wo WHERE wo.Batch = a.sczld) AS SapWorkOrderNo,
+                a.jysj          AS InspectQty,
+                a.hgsl          AS QualifiedQty,
+                a.bhgsl         AS UnqualifiedQty,
+                a.jfbh          AS FireNo,
+                a.jgbh          AS InspectSpecNo,
+                a.jgbb          AS InspectSpecVersion
+            FROM qms_qcpp_inspbill a
+            WHERE a.id = @id
+            LIMIT 1
+            """,
+            new List<SugarParameter> { new("@id", id) })).FirstOrDefault();
+
+        if (head == null || head.Id <= 0) return null;
+
+        var lineRows = await _db.Ado.SqlQueryAsync<FqcInspBillDetailLineRow>(
+            """
+            SELECT
+                st.id   AS Id,
+                st.xh   AS SeqNo,
+                st.jyxm AS InspectionItem,
+                st.jsbz AS AcceptStandard,
+                st.jsyq AS TechRequirement,
+                st.cyfa AS SamplingPlan,
+                st.ybl  AS SampleQty,
+                st.sx   AS UpperLimit,
+                st.xx   AS LowerLimit,
+                st.SizeSpecifications AS SizeSpecification,
+                st.pd   AS Judgment,
+                st.NonNumericalTypeOK AS NonNumericOk,
+                st.NonNumericalTypeNG AS NonNumericNg,
+                st.bz   AS Remark,
+                st.j1,st.j2,st.j3,st.j4,st.j5,st.j6,st.j7,st.j8,st.j9,st.j10,
+                st.j11,st.j12,st.j13,st.j14,st.j15,st.j16,st.j17,st.j18,st.j19,st.j20,
+                st.j21,st.j22,st.j23,st.j24,st.j25,st.j26,st.j27,st.j28,st.j29,st.j30,
+                st.j31,st.j32,st.j33,st.j34,st.j35,st.j36,st.j37,st.j38,st.j39,st.j40,
+                st.j41,st.j42,st.j43,st.j44,st.j45,st.j46,st.j47,st.j48,st.j49,st.j50
+            FROM qms_qcpp_inspbillst st
+            WHERE st.glid = @id
+            ORDER BY st.xh, st.id
+            """,
+            new List<SugarParameter> { new("@id", id) });
+
+        head.Lines = lineRows.Select(r => r.ToLine()).ToList();
+        return head;
+    }
+
     /// <summary>
     /// <summary>
     /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
     /// 排序解析:白名单命中则 "col dir, tie",否则回落默认;方向仅 asc/desc,非法回落 desc。
     /// </summary>
     /// </summary>
@@ -296,4 +375,72 @@ public class FqcInspBillService : IDynamicApiController, ITransient
             throw;
             throw;
         }
         }
     }
     }
+
+    /// <summary>
+    /// 检验单明细子表归口空表建表(方案 A,DDL 例外、每进程一次、零 DML):
+    /// aidopdev 无 qms_qcpp_inspbillst 时 CREATE TABLE IF NOT EXISTS 建同构 dopdemo 的 82 列空表(glid→主表 id),
+    /// 待上游抽数填充。样本列 j1..j50 一并建。仅建表,不写任何数据行。
+    /// 字符集忠实复刻 dopdemo:13 个列用 utf8mb3(3 字节),其余表默认 utf8mb4;否则 ~68 个 VARCHAR(255) 全 utf8mb4
+    /// 会超 MySQL 单行 65535 字节上限(Row size too large)。join 键 glid/id 为 bigint,字符集不影响。
+    /// </summary>
+    private async Task EnsureFqcDetailTableAsync()
+    {
+        if (Interlocked.Exchange(ref _detailTableEnsured, 1) == 1) return;
+        try
+        {
+            await _db.Ado.ExecuteCommandAsync(
+                """
+                CREATE TABLE IF NOT EXISTS qms_qcpp_inspbillst (
+                    id BIGINT NOT NULL PRIMARY KEY COMMENT '子表id',
+                    glid BIGINT NULL COMMENT '关联id(=qms_qcpp_inspbill.id)',
+                    jyxm VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '检验项目',
+                    sx VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '上限',
+                    xx VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '下限',
+                    bz VARCHAR(255) NULL COMMENT '备注',
+                    czf VARCHAR(255) NULL,
+                    j1 VARCHAR(255) NULL, j2 VARCHAR(255) NULL, j3 VARCHAR(255) NULL, j4 VARCHAR(255) NULL, j5 VARCHAR(255) NULL,
+                    j6 VARCHAR(255) NULL, j7 VARCHAR(255) NULL, j8 VARCHAR(255) NULL, j9 VARCHAR(255) NULL, j10 VARCHAR(255) NULL,
+                    j11 VARCHAR(255) NULL, j12 VARCHAR(255) NULL, j13 VARCHAR(255) NULL, j14 VARCHAR(255) NULL, j15 VARCHAR(255) NULL,
+                    j16 VARCHAR(255) NULL, j17 VARCHAR(255) NULL, j18 VARCHAR(255) NULL, j19 VARCHAR(255) NULL, j20 VARCHAR(255) NULL,
+                    j21 VARCHAR(255) NULL, j22 VARCHAR(255) NULL, j23 VARCHAR(255) NULL, j24 VARCHAR(255) NULL, j25 VARCHAR(255) NULL,
+                    j26 VARCHAR(255) NULL, j27 VARCHAR(255) NULL, j28 VARCHAR(255) NULL, j29 VARCHAR(255) NULL, j30 VARCHAR(255) NULL,
+                    j31 VARCHAR(255) NULL, j32 VARCHAR(255) NULL, j33 VARCHAR(255) NULL, j34 VARCHAR(255) NULL, j35 VARCHAR(255) NULL,
+                    j36 VARCHAR(255) NULL, j37 VARCHAR(255) NULL, j38 VARCHAR(255) NULL, j39 VARCHAR(255) NULL, j40 VARCHAR(255) NULL,
+                    j41 VARCHAR(255) NULL, j42 VARCHAR(255) NULL, j43 VARCHAR(255) NULL, j44 VARCHAR(255) NULL, j45 VARCHAR(255) NULL,
+                    j46 VARCHAR(255) NULL, j47 VARCHAR(255) NULL, j48 VARCHAR(255) NULL, j49 VARCHAR(255) NULL, j50 VARCHAR(255) NULL,
+                    pd VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '判定',
+                    ybl VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '样本量',
+                    txfl BIGINT NULL COMMENT '指标分类',
+                    bztk TEXT NULL,
+                    jsyq TEXT NULL COMMENT '技术要求',
+                    jyyqsbgj TEXT NULL COMMENT '检验仪器设备工具',
+                    jyff TEXT NULL,
+                    jytk TEXT NULL,
+                    jsbz TEXT NULL COMMENT '接收标准',
+                    cyfa TEXT NULL COMMENT '抽样方案',
+                    jl TEXT NULL,
+                    jlqjbh VARCHAR(255) NULL COMMENT '计量器具编号',
+                    jlbh VARCHAR(255) NULL COMMENT '计量器具编号',
+                    jf1 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '击发1',
+                    jf2 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '击发2',
+                    jf3 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '击发3',
+                    jf4 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '击发4',
+                    jf5 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '击发5',
+                    NonNumericalTypeOK BIGINT NULL COMMENT '非数值OK',
+                    NonNumericalTypeNG BIGINT NULL COMMENT '非数值NG',
+                    ThresholdSwitch VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '自动判定按钮',
+                    ThresholdSwitchInt BIGINT NULL COMMENT '自动判定按钮',
+                    SizeSpecifications VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL COMMENT '规范标准',
+                    ybl1 VARCHAR(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL,
+                    xh INT NULL COMMENT '序号',
+                    KEY idx_qms_qcpp_inspbillst_glid (glid)
+                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='FQC检验单明细(品检项目) 归口自旧系统dopdemo,空表待上游抽数'
+                """);
+        }
+        catch
+        {
+            Interlocked.Exchange(ref _detailTableEnsured, 0);
+            throw;
+        }
+    }
 }
 }