Przeglądaj źródła

feat(s6): add ipqc inspection readonly detail drawer

头+明细只读详情:归口 qms_gcjydzb → mdp_stg/std 明细中台层 + GET /api/IpqcInspection/detail + 前端查看按钮/只读 drawer(确定子集列,j1-j70 后置)。
bump version Web 2.4.244 / server 1.0.242
YY968XX 21 godzin temu
rodzic
commit
8a5137d2c2

+ 1 - 1
Web/package.json

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

+ 76 - 0
Web/src/views/aidop/s6/api/ipqcInspection.ts

@@ -56,3 +56,79 @@ export function fetchIpqcInspectionList(params: any) {
 		.get<Paged<IpqcInspectionRow>>('/api/IpqcInspection/list', { params: withAidopTenantParams(params) })
 		.then((r) => r.data);
 }
+
+// ── S6 过程检验单 详情(只读:头 + 明细)──
+export interface IpqcInspectionDetailLine {
+	id?: number;
+	/** 检验项目 jyxm */
+	inspectionItem?: string;
+	/** 检验依据 jyyj */
+	inspectionBasis?: string;
+	/** 技术标准 jsbz */
+	technicalStandard?: string;
+	/** 检验方法 jyff */
+	inspectionMethod?: string;
+	/** 上限 sx */
+	upperLimit?: string;
+	/** 下限 xx */
+	lowerLimit?: string;
+	/** 工序编码 ProcessCode */
+	processCode?: string;
+	/** 工序名称 ProcessName */
+	processName?: string;
+	/** 测量仪器 MeasurementInstrument */
+	measurementInstrument?: string;
+	/** 检验人 Inspector */
+	inspector?: string;
+	/** 检验时间 InspectionTime */
+	inspectionTime?: string;
+	/** 样本量 ybl */
+	sampleQty?: number;
+	/** 检验数量 jysl */
+	inspectionQty?: number;
+	/** 不合格数量 bhgsl */
+	unqualifiedQty?: number;
+	/** 结果判定 pd(字典码原样透出) */
+	resultJudgement?: string;
+	/** 非数值型OK */
+	nonNumericOk?: number;
+	/** 非数值型NG */
+	nonNumericNg?: number;
+	/** 备注 bz */
+	remark?: string;
+	/** 附件 fj */
+	attachment?: string;
+}
+
+export interface IpqcInspectionDetail {
+	id?: number;
+	billNo?: string;
+	productModel?: string;
+	productionBatchNo?: string;
+	productionWorkOrder?: string;
+	resultJudgement?: string;
+	attachment?: string;
+	remark?: string;
+	inspector?: string;
+	processCode?: string;
+	processName?: string;
+	productionPerson?: string;
+	sampleQty?: number;
+	formNo?: string;
+	versionNo?: string;
+	effectiveDate?: string;
+	materialCode?: string;
+	materialName?: string;
+	inspecStandardVersion?: string;
+	inspecStandardCode?: string;
+	/** 检验状态 status */
+	inspectionStatus?: string;
+	/** 明细行(确定子集列;j1~j70 后置不返回) */
+	lines?: IpqcInspectionDetailLine[];
+}
+
+export function fetchIpqcInspectionDetail(id: number) {
+	return service
+		.get<IpqcInspectionDetail>('/api/IpqcInspection/detail', { params: withAidopTenantParams({ id }) })
+		.then((r) => r.data);
+}

+ 96 - 1
Web/src/views/aidop/s6/process-quality/processInspectionList.vue

@@ -35,6 +35,11 @@
 			<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip resizable />
 			<el-table-column prop="inspecStandardVersion" label="检规版本" min-width="110" resizable />
 			<el-table-column prop="inspecStandardCode" label="检规编号" min-width="130" show-overflow-tooltip resizable />
+			<el-table-column label="操作" width="90" fixed="right">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openDetail(row)">查看</el-button>
+				</template>
+			</el-table-column>
 			<template #empty>
 				<el-empty description="暂无数据" />
 			</template>
@@ -51,6 +56,58 @@
 				@size-change="onSizeChange"
 			/>
 		</div>
+
+		<el-drawer v-model="detailVisible" title="过程检验单详情" size="60%" :destroy-on-close="true">
+			<div v-loading="detailLoading">
+				<el-descriptions :column="3" border size="small">
+					<el-descriptions-item label="单据编号">{{ detail.billNo || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="产品型号">{{ detail.productModel || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="生产批号">{{ detail.productionBatchNo || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="生产工单">{{ detail.productionWorkOrder || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="结果判定">{{ detail.resultJudgement || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="检验状态">{{ detail.inspectionStatus || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="检验人">{{ detail.inspector || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="工序编码">{{ detail.processCode || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="工序名称">{{ detail.processName || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="生产人员">{{ detail.productionPerson || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="样本量">{{ detail.sampleQty ?? '-' }}</el-descriptions-item>
+					<el-descriptions-item label="生效日期">{{ fmtDate(detail.effectiveDate) || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="物料编码">{{ detail.materialCode || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="物料名称">{{ detail.materialName || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="表单编号">{{ detail.formNo || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="版本号">{{ detail.versionNo || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="检规版本">{{ detail.inspecStandardVersion || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="检规编号">{{ detail.inspecStandardCode || '-' }}</el-descriptions-item>
+					<el-descriptions-item label="备注" :span="3">{{ detail.remark || '-' }}</el-descriptions-item>
+				</el-descriptions>
+
+				<div class="detail-lines-title">检验明细</div>
+				<el-table :data="detailLines" border stripe size="small">
+					<el-table-column type="index" label="#" width="50" />
+					<el-table-column prop="inspectionItem" label="检验项目" min-width="140" show-overflow-tooltip />
+					<el-table-column prop="inspectionBasis" label="检验依据" min-width="140" show-overflow-tooltip />
+					<el-table-column prop="processCode" label="工序编码" min-width="110" show-overflow-tooltip />
+					<el-table-column prop="processName" label="工序名称" min-width="120" show-overflow-tooltip />
+					<el-table-column prop="inspectionMethod" label="检验方法" min-width="120" show-overflow-tooltip />
+					<el-table-column prop="technicalStandard" label="技术标准" min-width="120" show-overflow-tooltip />
+					<el-table-column prop="upperLimit" label="上限" min-width="90" />
+					<el-table-column prop="lowerLimit" label="下限" min-width="90" />
+					<el-table-column prop="measurementInstrument" label="测量仪器" min-width="120" show-overflow-tooltip />
+					<el-table-column prop="sampleQty" label="样本量" min-width="80" />
+					<el-table-column prop="inspectionQty" label="检验数量" min-width="90" />
+					<el-table-column prop="unqualifiedQty" label="不合格数" min-width="90" />
+					<el-table-column prop="resultJudgement" label="判定" min-width="80" />
+					<el-table-column prop="inspector" label="检验人" min-width="100" show-overflow-tooltip />
+					<el-table-column prop="inspectionTime" label="检验时间" min-width="150">
+						<template #default="{ row }">{{ fmtDateTime(row.inspectionTime) }}</template>
+					</el-table-column>
+					<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
+					<template #empty>
+						<el-empty description="暂无明细" />
+					</template>
+				</el-table>
+			</div>
+		</el-drawer>
 	</AidopDemoShell>
 </template>
 
@@ -59,7 +116,13 @@ import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import { fetchIpqcInspectionList, type IpqcInspectionRow } from '../api/ipqcInspection';
+import {
+	fetchIpqcInspectionList,
+	fetchIpqcInspectionDetail,
+	type IpqcInspectionRow,
+	type IpqcInspectionDetail,
+	type IpqcInspectionDetailLine,
+} from '../api/ipqcInspection';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '过程检验单列表');
@@ -119,6 +182,34 @@ function fmtDate(v?: string) {
 	return v.length >= 10 ? v.slice(0, 10) : v;
 }
 
+function fmtDateTime(v?: string) {
+	if (!v) return '';
+	return v.length > 19 ? v.slice(0, 19).replace('T', ' ') : v.replace('T', ' ');
+}
+
+// 只读详情 drawer:头 + 明细(确定子集列)。无写按钮。
+const detailVisible = ref(false);
+const detailLoading = ref(false);
+const detail = ref<IpqcInspectionDetail>({});
+const detailLines = ref<IpqcInspectionDetailLine[]>([]);
+
+async function openDetail(row: IpqcInspectionRow) {
+	if (!row?.id) return;
+	detailVisible.value = true;
+	detailLoading.value = true;
+	detail.value = {};
+	detailLines.value = [];
+	try {
+		const data = await fetchIpqcInspectionDetail(row.id);
+		detail.value = data || {};
+		detailLines.value = data?.lines || [];
+	} catch (e: any) {
+		ElMessage.error(e?.message || '加载过程检验单详情失败');
+	} finally {
+		detailLoading.value = false;
+	}
+}
+
 onMounted(() => loadList());
 onActivated(() => loadList());
 </script>
@@ -132,4 +223,8 @@ onActivated(() => loadList());
 	display: flex;
 	justify-content: flex-end;
 }
+.detail-lines-title {
+	margin: 16px 0 8px;
+	font-weight: 600;
+}
 </style>

+ 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.241</AssemblyVersion>
-    <FileVersion>1.0.241</FileVersion>
-    <Version>1.0.241</Version>
+    <AssemblyVersion>1.0.242</AssemblyVersion>
+    <FileVersion>1.0.242</FileVersion>
+    <Version>1.0.242</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 80 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/Dto/IpqcInspectionDto.cs

@@ -94,3 +94,83 @@ public class IpqcInspectionListRow
     /// <summary>检规编号(inspec_standard_code ← jgbh)</summary>
     public string? InspecStandardCode { get; set; }
 }
+
+/// <summary>
+/// S6 过程检验单(IPQC)详情(只读):头(mdp_std_ipqc_inspection)+ 明细(mdp_std_ipqc_inspection_detail 确定子集列)。
+/// 查不到返回 null。明细为空返回空数组(前端"暂无明细")。
+/// </summary>
+public class IpqcInspectionDetailDto
+{
+    /// <summary>主键 id(mdp_std_ipqc_inspection.id)</summary>
+    public long Id { get; set; }
+    public string? BillNo { get; set; }
+    public string? ProductModel { get; set; }
+    public string? ProductionBatchNo { get; set; }
+    public string? ProductionWorkOrder { get; set; }
+    public string? ResultJudgement { get; set; }
+    public string? Attachment { get; set; }
+    public string? Remark { get; set; }
+    public string? Inspector { get; set; }
+    public string? ProcessCode { get; set; }
+    public string? ProcessName { get; set; }
+    public string? ProductionPerson { get; set; }
+    public decimal? SampleQty { get; set; }
+    public string? FormNo { get; set; }
+    public string? VersionNo { get; set; }
+    public DateTime? EffectiveDate { get; set; }
+    public string? MaterialCode { get; set; }
+    public string? MaterialName { get; set; }
+    public string? InspecStandardVersion { get; set; }
+    public string? InspecStandardCode { get; set; }
+    /// <summary>检验状态(inspection_status ← status)</summary>
+    public string? InspectionStatus { get; set; }
+
+    /// <summary>明细行(确定子集列;j1~j70 动态检验值列后置不返回)</summary>
+    public List<IpqcInspectionDetailLineDto> Lines { get; set; } = new();
+}
+
+/// <summary>
+/// S6 过程检验单明细行(只读,确定子集列)。源自 qms_gcjydzb → mdp_std_ipqc_inspection_detail。
+/// </summary>
+public class IpqcInspectionDetailLineDto
+{
+    public long Id { get; set; }
+    /// <summary>检验项目(inspection_item ← jyxm)</summary>
+    public string? InspectionItem { get; set; }
+    /// <summary>检验依据(inspection_basis ← jyyj)</summary>
+    public string? InspectionBasis { get; set; }
+    /// <summary>技术标准(technical_standard ← jsbz)</summary>
+    public string? TechnicalStandard { get; set; }
+    /// <summary>检验方法(inspection_method ← jyff)</summary>
+    public string? InspectionMethod { get; set; }
+    /// <summary>上限(upper_limit ← sx)</summary>
+    public string? UpperLimit { get; set; }
+    /// <summary>下限(lower_limit ← xx)</summary>
+    public string? LowerLimit { get; set; }
+    /// <summary>工序编码(process_code ← ProcessCode)</summary>
+    public string? ProcessCode { get; set; }
+    /// <summary>工序名称(process_name ← ProcessName)</summary>
+    public string? ProcessName { get; set; }
+    /// <summary>测量仪器(measurement_instrument ← MeasurementInstrument)</summary>
+    public string? MeasurementInstrument { get; set; }
+    /// <summary>检验人(inspector ← Inspector)</summary>
+    public string? Inspector { get; set; }
+    /// <summary>检验时间(inspection_time ← InspectionTime)</summary>
+    public DateTime? InspectionTime { get; set; }
+    /// <summary>样本量(sample_qty ← ybl)</summary>
+    public int? SampleQty { get; set; }
+    /// <summary>检验数量(inspection_qty ← jysl)</summary>
+    public decimal? InspectionQty { get; set; }
+    /// <summary>不合格数量(unqualified_qty ← bhgsl)</summary>
+    public decimal? UnqualifiedQty { get; set; }
+    /// <summary>结果判定(result_judgement ← pd,字典码原样透出)</summary>
+    public string? ResultJudgement { get; set; }
+    /// <summary>非数值型OK(non_numeric_ok ← NonNumericalTypeOK)</summary>
+    public int? NonNumericOk { get; set; }
+    /// <summary>非数值型NG(non_numeric_ng ← NonNumericalTypeNG)</summary>
+    public int? NonNumericNg { get; set; }
+    /// <summary>备注(remark ← bz)</summary>
+    public string? Remark { get; set; }
+    /// <summary>附件(attachment ← fj)</summary>
+    public string? Attachment { get; set; }
+}

+ 175 - 13
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionMdpSyncService.cs

@@ -3,19 +3,21 @@ namespace Admin.NET.Plugin.AiDOP.Manufacturing;
 /// <summary>
 /// S6 过程检验单(IPQC)数据中台只读同步转换服务(DOP 内部,独立于 S6MdpSyncTransformService 的 KPI 管线)。
 ///
-/// 源:aidopdev.qms_gcjyd(过程检验单头,归口自旧系统 dopdemo,31 列)
-/// 链路:qms_gcjyd 只读 → mdp_stg_ipqc_inspection(raw_data JSON) → mdp_std_ipqc_inspection(typed)。
+/// 源:aidopdev.qms_gcjyd(过程检验单头,31 列)+ aidopdev.qms_gcjydzb(明细,100 列),归口自旧系统 dopdemo。
+/// 链路:qms_gcjyd/qms_gcjydzb 只读 → mdp_stg_ipqc_inspection(_detail)(raw_data JSON) → mdp_std_ipqc_inspection(_detail)(typed)。
 ///
 /// 归口说明(CTO 拍板方案 A):
-///   qms_gcjyd 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
-///   本服务 EnsureTablesAsync 在 aidopdev 建同构空表 qms_gcjyd(IF NOT EXISTS),
-///   作为归口源;后续由上游一次性抽数填充,届时无需改代码即随刷新流通。
+///   qms_gcjyd/qms_gcjydzb 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
+///   本服务 EnsureTablesAsync 在 aidopdev 建同构空表(IF NOT EXISTS)作为归口源;
+///   后续由上游一次性抽数填充,届时无需改代码即随刷新流通。
 ///
 /// 约束:
-///   - 只读 qms_gcjyd,仅写 mdp_stg_*/mdp_std_*/run_log;绝不 INSERT/UPDATE/DELETE 业务源单据表。
-///   - qms_gcjyd 无 tenant/org/factory 列 → 标准层按常量 tenant_id=0, factory_id=1 落库。
-///   - 本批只读列表:只做头表转换,不碰明细 qms_gcjydzb(留待 DETAIL 批)。
-///   - result_judgement(jgpd)为字典码,原样透出,语义解码后置。
+///   - 只读 qms_gcjyd/qms_gcjydzb,仅写 mdp_stg_*/mdp_std_*/run_log;绝不 INSERT/UPDATE/DELETE 业务源单据表。
+///   - 源表无 tenant/org/factory 列 → 标准层按常量 tenant_id=0, factory_id=1 落库。
+///   - 明细头关联:qms_gcjydzb.glid -> qms_gcjyd.id;标准层经 std_head_id 回填 mdp_std_ipqc_inspection.id。
+///   - 明细只转换已确认子集列(检验项目/依据/工序/方法/技术标准/上下限/测量仪器/检验人/检验时间/样本量/检验数量/不合格数量/判定/备注等);
+///     j1~j70 动态检验值列语义不明、后置不转换、不落 typed 字段。
+///   - result_judgement(jgpd/pd)为字典码,原样透出,语义解码后置。
 ///   - 源为 0 行时转换成功完成、处理数为 0,不报错。
 /// </summary>
 public class IpqcInspectionMdpSyncService : ITransient
@@ -30,7 +32,7 @@ public class IpqcInspectionMdpSyncService : ITransient
     }
 
     /// <summary>
-    /// 全量同步转换:贴源(头) → 标准化(头)。
+    /// 全量同步转换:贴源(头/明细) → 标准化(头/明细)。
     /// </summary>
     public async Task<IpqcInspectionMdpSyncResult> RunFullAsync(CancellationToken cancellationToken = default, string triggerType = "AUTO")
     {
@@ -45,7 +47,9 @@ public class IpqcInspectionMdpSyncService : ITransient
         try
         {
             result.HeadStageRows = await SyncHeadStagingAsync(batchId, now);
+            result.DetailStageRows = await SyncDetailStagingAsync(batchId, now);
             result.HeadStandardRows = await TransformHeadStandardAsync(batchId, now);
+            result.DetailStandardRows = await TransformDetailStandardAsync(batchId, now);
             await MarkRunSuccessAsync(runLogId, now, result);
             return result;
         }
@@ -56,7 +60,7 @@ public class IpqcInspectionMdpSyncService : ITransient
         }
     }
 
-    /// <summary>防御式建表(幂等):归口源空表 qms_gcjyd + 贴源层 + 标准层。</summary>
+    /// <summary>防御式建表(幂等):归口源空表 qms_gcjyd/qms_gcjydzb + 贴源层(头/明细) + 标准层(头/明细)。</summary>
     private async Task EnsureTablesAsync()
     {
         // 归口源表(方案 A):同构自旧系统 dopdemo.qms_gcjyd 头表,空表,待上游抽数填充。
@@ -152,6 +156,97 @@ public class IpqcInspectionMdpSyncService : ITransient
                 KEY idx_mdp_std_ipqc_insp_effdate (tenant_id, effective_date)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单头标准层'
             """);
+        // 归口源明细表(方案 A):同构自旧系统 dopdemo.qms_gcjydzb(100 列),空表,待上游抽数填充。
+        // 关联键 glid -> qms_gcjyd.id。id 不设 AUTO_INCREMENT:抽数将携带旧系统主键。
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            CREATE TABLE IF NOT EXISTS qms_gcjydzb (
+                id BIGINT NOT NULL PRIMARY KEY COMMENT '子表id',
+                glid BIGINT NULL COMMENT '关联id',
+                dwsj TEXT NULL, jyxm TEXT NULL, jybz TEXT NULL, jyyj TEXT NULL,
+                j1 TEXT NULL, j2 TEXT NULL, j3 TEXT NULL, j4 TEXT NULL, j5 TEXT NULL,
+                ljbh TEXT NULL, sczyry TEXT NULL, fj TEXT NULL, bz TEXT NULL,
+                j6 TEXT NULL, j7 TEXT NULL, j8 TEXT NULL, j9 TEXT NULL, j10 TEXT NULL,
+                j11 TEXT NULL, j12 TEXT NULL, j13 TEXT NULL, j14 TEXT NULL, j15 TEXT NULL,
+                j16 TEXT NULL, j17 TEXT NULL, j18 TEXT NULL, j19 TEXT NULL, j20 TEXT NULL,
+                j21 TEXT NULL, j22 TEXT NULL, j23 TEXT NULL, j24 TEXT NULL, j25 TEXT NULL,
+                j26 TEXT NULL, j27 TEXT NULL, j28 TEXT NULL, j29 TEXT NULL, j30 TEXT NULL,
+                j31 TEXT NULL, j32 TEXT NULL, j33 TEXT NULL, j34 TEXT NULL, j35 TEXT NULL,
+                j36 TEXT NULL, j37 TEXT NULL, j38 TEXT NULL, j39 TEXT NULL, j40 TEXT NULL,
+                pd BIGINT NULL COMMENT '结果判定',
+                jysl DECIMAL(10,0) NULL COMMENT '检验数量',
+                bhgsl DECIMAL(10,0) NULL COMMENT '不合格数量',
+                j41 TEXT NULL, j42 TEXT NULL, j43 TEXT NULL, j44 TEXT NULL, j45 TEXT NULL,
+                j46 TEXT NULL, j47 TEXT NULL, j48 TEXT NULL, j49 TEXT NULL, j50 TEXT NULL,
+                j51 TEXT NULL, j52 TEXT NULL, j53 TEXT NULL, j54 TEXT NULL, j55 TEXT NULL,
+                j56 TEXT NULL, j57 TEXT NULL, j58 TEXT NULL, j59 TEXT NULL, j60 TEXT NULL,
+                j61 TEXT NULL, j62 TEXT NULL, j63 TEXT NULL, j64 TEXT NULL, j65 TEXT NULL,
+                j66 TEXT NULL, j67 TEXT NULL, j68 TEXT NULL, j69 TEXT NULL, j70 TEXT NULL,
+                nature TEXT NULL, jsbz TEXT NULL, jypc TEXT NULL, ggbh TEXT NULL,
+                ProcessCode TEXT NULL, ProcessName TEXT NULL, jyff TEXT NULL,
+                ybl INT NULL COMMENT '样本量',
+                NonNumericalTypeOK INT NULL COMMENT '非数值型OK',
+                NonNumericalTypeNG INT NULL COMMENT '非数值型NG',
+                Inspector TEXT NULL,
+                InspectionTime DATETIME NULL COMMENT '检验时间',
+                jgid BIGINT NULL,
+                AbnormalNumber TEXT NULL, MeasurementInstrument TEXT NULL, sx TEXT NULL, xx TEXT NULL,
+                KEY idx_qms_gcjydzb_glid (glid)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='过程检验单(明细) 归口自旧系统dopdemo,空表待上游抽数'
+            """);
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            CREATE TABLE IF NOT EXISTS mdp_stg_ipqc_inspection_detail (
+                id BIGINT AUTO_INCREMENT PRIMARY KEY,
+                tenant_id BIGINT NOT NULL DEFAULT 0,
+                factory_id BIGINT NULL DEFAULT 1,
+                source_system VARCHAR(50) NOT NULL DEFAULT 'AIDOP',
+                source_table VARCHAR(100) NOT NULL,
+                source_row_id VARCHAR(100) NOT NULL,
+                source_biz_key VARCHAR(200) NULL,
+                sync_batch_id VARCHAR(100) NOT NULL,
+                sync_time DATETIME NOT NULL,
+                process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+                raw_data JSON NOT NULL,
+                update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+                UNIQUE KEY uk_mdp_stg_ipqc_insp_dtl (tenant_id, source_table, source_row_id)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单明细贴源层'
+            """);
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            CREATE TABLE IF NOT EXISTS mdp_std_ipqc_inspection_detail (
+                id BIGINT AUTO_INCREMENT PRIMARY KEY,
+                tenant_id BIGINT NOT NULL DEFAULT 0,
+                std_head_id BIGINT NULL,
+                source_head_id VARCHAR(100) NULL,
+                inspection_item TEXT NULL,
+                inspection_basis TEXT NULL,
+                technical_standard TEXT NULL,
+                inspection_method TEXT NULL,
+                upper_limit VARCHAR(255) NULL,
+                lower_limit VARCHAR(255) NULL,
+                process_code VARCHAR(255) NULL,
+                process_name VARCHAR(255) NULL,
+                measurement_instrument VARCHAR(255) NULL,
+                inspector VARCHAR(255) NULL,
+                inspection_time DATETIME NULL,
+                sample_qty INT NULL,
+                inspection_qty DECIMAL(18,6) NULL,
+                unqualified_qty DECIMAL(18,6) NULL,
+                result_judgement VARCHAR(32) NULL,
+                non_numeric_ok INT NULL,
+                non_numeric_ng INT NULL,
+                remark TEXT NULL,
+                attachment TEXT NULL,
+                source_row_id VARCHAR(100) NOT NULL,
+                source_biz_key VARCHAR(200) NULL,
+                sync_batch_id VARCHAR(100) NOT NULL,
+                sync_time DATETIME NOT NULL,
+                update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+                UNIQUE KEY uk_mdp_std_ipqc_insp_dtl (tenant_id, source_row_id),
+                KEY idx_mdp_std_ipqc_insp_dtl_head (std_head_id)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单明细标准层(确定子集列;j1-j70后置)'
+            """);
     }
 
     /// <summary>贴源头:qms_gcjyd -> mdp_stg_ipqc_inspection(raw_data JSON 快照)。返回源头行数。</summary>
@@ -217,6 +312,71 @@ public class IpqcInspectionMdpSyncService : ITransient
         return rows;
     }
 
+    /// <summary>贴源明细:qms_gcjydzb -> mdp_stg_ipqc_inspection_detail(raw_data JSON 快照,确定子集列)。返回源明细行数。</summary>
+    private async Task<int> SyncDetailStagingAsync(string batchId, DateTime now)
+    {
+        var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjydzb");
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            INSERT INTO mdp_stg_ipqc_inspection_detail
+            (tenant_id, factory_id, source_system, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time, process_status, raw_data)
+            SELECT
+                0, 1, 'AIDOP', 'qms_gcjydzb',
+                CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now, 'PENDING',
+                JSON_OBJECT(
+                    'id', d.id, 'glid', d.glid, 'jyxm', CAST(d.jyxm AS CHAR), 'jyyj', CAST(d.jyyj AS CHAR),
+                    'jsbz', CAST(d.jsbz AS CHAR), 'jyff', CAST(d.jyff AS CHAR), 'sx', CAST(d.sx AS CHAR), 'xx', CAST(d.xx AS CHAR),
+                    'ProcessCode', CAST(d.ProcessCode AS CHAR), 'ProcessName', CAST(d.ProcessName AS CHAR),
+                    'MeasurementInstrument', CAST(d.MeasurementInstrument AS CHAR), 'Inspector', CAST(d.Inspector AS CHAR),
+                    'InspectionTime', d.InspectionTime, 'ybl', d.ybl, 'jysl', d.jysl, 'bhgsl', d.bhgsl,
+                    'pd', d.pd, 'NonNumericalTypeOK', d.NonNumericalTypeOK, 'NonNumericalTypeNG', d.NonNumericalTypeNG,
+                    'bz', CAST(d.bz AS CHAR), 'fj', CAST(d.fj AS CHAR)
+                )
+            FROM qms_gcjydzb d
+            ON DUPLICATE KEY UPDATE
+                source_biz_key=VALUES(source_biz_key), sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time),
+                process_status=VALUES(process_status), raw_data=VALUES(raw_data), update_time=CURRENT_TIMESTAMP
+            """,
+            new SugarParameter("@BatchId", batchId),
+            new SugarParameter("@Now", now));
+        return rows;
+    }
+
+    /// <summary>标准化明细:qms_gcjydzb -> mdp_std_ipqc_inspection_detail(确定子集列;经 glid 回填 std_head_id)。返回处理明细行数。</summary>
+    private async Task<int> TransformDetailStandardAsync(string batchId, DateTime now)
+    {
+        var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjydzb");
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            INSERT INTO mdp_std_ipqc_inspection_detail
+            (tenant_id, std_head_id, source_head_id, inspection_item, inspection_basis, technical_standard, inspection_method,
+             upper_limit, lower_limit, process_code, process_name, measurement_instrument, inspector, inspection_time,
+             sample_qty, inspection_qty, unqualified_qty, result_judgement, non_numeric_ok, non_numeric_ng, remark, attachment,
+             source_row_id, source_biz_key, sync_batch_id, sync_time)
+            SELECT
+                0, h.id, CAST(d.glid AS CHAR), CAST(d.jyxm AS CHAR), CAST(d.jyyj AS CHAR), CAST(d.jsbz AS CHAR), CAST(d.jyff AS CHAR),
+                CAST(d.sx AS CHAR), CAST(d.xx AS CHAR), CAST(d.ProcessCode AS CHAR), CAST(d.ProcessName AS CHAR),
+                CAST(d.MeasurementInstrument AS CHAR), CAST(d.Inspector AS CHAR), d.InspectionTime,
+                d.ybl, d.jysl, d.bhgsl, CAST(d.pd AS CHAR), d.NonNumericalTypeOK, d.NonNumericalTypeNG,
+                CAST(d.bz AS CHAR), CAST(d.fj AS CHAR),
+                CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now
+            FROM qms_gcjydzb d
+            LEFT JOIN mdp_std_ipqc_inspection h ON h.tenant_id = 0 AND h.source_row_id = CAST(d.glid AS CHAR)
+            ON DUPLICATE KEY UPDATE
+                std_head_id=VALUES(std_head_id), source_head_id=VALUES(source_head_id), inspection_item=VALUES(inspection_item),
+                inspection_basis=VALUES(inspection_basis), technical_standard=VALUES(technical_standard), inspection_method=VALUES(inspection_method),
+                upper_limit=VALUES(upper_limit), lower_limit=VALUES(lower_limit), process_code=VALUES(process_code), process_name=VALUES(process_name),
+                measurement_instrument=VALUES(measurement_instrument), inspector=VALUES(inspector), inspection_time=VALUES(inspection_time),
+                sample_qty=VALUES(sample_qty), inspection_qty=VALUES(inspection_qty), unqualified_qty=VALUES(unqualified_qty),
+                result_judgement=VALUES(result_judgement), non_numeric_ok=VALUES(non_numeric_ok), non_numeric_ng=VALUES(non_numeric_ng),
+                remark=VALUES(remark), attachment=VALUES(attachment),
+                sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time), update_time=CURRENT_TIMESTAMP
+            """,
+            new SugarParameter("@BatchId", batchId),
+            new SugarParameter("@Now", now));
+        return rows;
+    }
+
     private async Task<long> InsertRunLogAsync(string batchId, DateTime startedAt, string triggerType)
     {
         await _db.Ado.ExecuteCommandAsync(
@@ -246,8 +406,8 @@ public class IpqcInspectionMdpSyncService : ITransient
             """,
             new SugarParameter("@EndTime", finishedAt),
             new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
-            new SugarParameter("@StageRows", result.HeadStageRows),
-            new SugarParameter("@StandardRows", result.HeadStandardRows),
+            new SugarParameter("@StageRows", result.HeadStageRows + result.DetailStageRows),
+            new SugarParameter("@StandardRows", result.HeadStandardRows + result.DetailStandardRows),
             new SugarParameter("@Id", runLogId));
     }
 
@@ -278,4 +438,6 @@ public sealed class IpqcInspectionMdpSyncResult
     public string BatchId { get; set; } = string.Empty;
     public int HeadStageRows { get; set; }
     public int HeadStandardRows { get; set; }
+    public int DetailStageRows { get; set; }
+    public int DetailStandardRows { get; set; }
 }

+ 81 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionService.cs

@@ -93,6 +93,87 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
         return new { total, page, pageSize, list };
     }
 
+    /// <summary>
+    /// 过程检验单详情(只读:头 + 明细)。查不到返回 null。
+    /// </summary>
+    [DisplayName("过程检验单详情")]
+    [HttpGet("detail")]
+    public async Task<IpqcInspectionDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
+    {
+        if (id <= 0) return null;
+
+        var headPars = new List<SugarParameter> { new("@Id", id) };
+        var headWhere = "m.id = @Id";
+        if (tenantId is > 0)
+        {
+            headWhere += " AND m.tenant_id = @TenantId";
+            headPars.Add(new SugarParameter("@TenantId", tenantId));
+        }
+
+        var head = await _db.Ado.SqlQuerySingleAsync<IpqcInspectionDetailDto>(
+            $"""
+            SELECT
+                m.id                       AS Id,
+                m.bill_no                  AS BillNo,
+                m.product_model            AS ProductModel,
+                m.production_batch_no      AS ProductionBatchNo,
+                m.production_work_order    AS ProductionWorkOrder,
+                m.result_judgement         AS ResultJudgement,
+                m.attachment               AS Attachment,
+                m.remark                   AS Remark,
+                m.inspector                AS Inspector,
+                m.process_code             AS ProcessCode,
+                m.process_name             AS ProcessName,
+                m.production_person        AS ProductionPerson,
+                m.sample_qty               AS SampleQty,
+                m.form_no                  AS FormNo,
+                m.version_no               AS VersionNo,
+                m.effective_date           AS EffectiveDate,
+                m.material_code            AS MaterialCode,
+                m.material_name            AS MaterialName,
+                m.inspec_standard_version  AS InspecStandardVersion,
+                m.inspec_standard_code     AS InspecStandardCode,
+                m.inspection_status        AS InspectionStatus
+            FROM mdp_std_ipqc_inspection m
+            WHERE {headWhere}
+            LIMIT 1
+            """,
+            headPars);
+
+        if (head == null || head.Id <= 0) return null;
+
+        head.Lines = await _db.Ado.SqlQueryAsync<IpqcInspectionDetailLineDto>(
+            """
+            SELECT
+                d.id                     AS Id,
+                d.inspection_item        AS InspectionItem,
+                d.inspection_basis       AS InspectionBasis,
+                d.technical_standard     AS TechnicalStandard,
+                d.inspection_method      AS InspectionMethod,
+                d.upper_limit            AS UpperLimit,
+                d.lower_limit            AS LowerLimit,
+                d.process_code           AS ProcessCode,
+                d.process_name           AS ProcessName,
+                d.measurement_instrument AS MeasurementInstrument,
+                d.inspector              AS Inspector,
+                d.inspection_time        AS InspectionTime,
+                d.sample_qty             AS SampleQty,
+                d.inspection_qty         AS InspectionQty,
+                d.unqualified_qty        AS UnqualifiedQty,
+                d.result_judgement       AS ResultJudgement,
+                d.non_numeric_ok         AS NonNumericOk,
+                d.non_numeric_ng         AS NonNumericNg,
+                d.remark                 AS Remark,
+                d.attachment             AS Attachment
+            FROM mdp_std_ipqc_inspection_detail d
+            WHERE d.std_head_id = @HeadId
+            ORDER BY d.id ASC
+            """,
+            new List<SugarParameter> { new("@HeadId", head.Id) });
+
+        return head;
+    }
+
     /// <summary>
     /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。
     /// </summary>