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

feat(s6): add ipqc inspection readonly list

归口 qms_gcjyd(过程检验单头)→ mdp_stg/mdp_std 只读链路 + /api/IpqcInspection/list + 前端 s6 过程检验单列表接真实接口(空态)。
bump version Web 2.4.240 / server 1.0.239
YY968XX 1 день назад
Родитель
Сommit
d58ac0ac42

+ 1 - 1
Web/package.json

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

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

@@ -0,0 +1,58 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+// ── S6 过程检验单(IPQC,只读,数据中台标准层 mdp_std_ipqc_inspection)──
+export interface IpqcInspectionRow {
+	id?: number;
+	/** 单据编号 bill_no */
+	billNo?: string;
+	/** 产品型号 product_model */
+	productModel?: string;
+	/** 生产批号 production_batch_no */
+	productionBatchNo?: string;
+	/** 生产工单 production_work_order */
+	productionWorkOrder?: string;
+	/** 结果判定 result_judgement(字典码原样透出) */
+	resultJudgement?: string;
+	/** 附件 attachment */
+	attachment?: string;
+	/** 备注 remark */
+	remark?: string;
+	/** 检验人 inspector */
+	inspector?: string;
+	/** 工序编码 process_code */
+	processCode?: string;
+	/** 工序名称 process_name */
+	processName?: string;
+	/** 生产人员 production_person */
+	productionPerson?: string;
+	/** 样本量 sample_qty */
+	sampleQty?: number;
+	/** 表单编号 form_no */
+	formNo?: string;
+	/** 版本号 version_no */
+	versionNo?: string;
+	/** 生效日期 effective_date */
+	effectiveDate?: string;
+	/** 物料编码 material_code */
+	materialCode?: string;
+	/** 物料名称 material_name */
+	materialName?: string;
+	/** 检规版本 inspec_standard_version */
+	inspecStandardVersion?: string;
+	/** 检规编号 inspec_standard_code */
+	inspecStandardCode?: string;
+}
+
+export function fetchIpqcInspectionList(params: any) {
+	return service
+		.get<Paged<IpqcInspectionRow>>('/api/IpqcInspection/list', { params: withAidopTenantParams(params) })
+		.then((r) => r.data);
+}

+ 46 - 4
Web/src/views/aidop/s6/process-quality/processInspectionList.vue

@@ -28,7 +28,9 @@
 			<el-table-column prop="sampleQty" label="样本量" min-width="100" resizable />
 			<el-table-column prop="formNo" label="表单编号" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="versionNo" label="版本号" min-width="100" resizable />
-			<el-table-column prop="effectiveDate" label="生效日期" min-width="150" resizable />
+			<el-table-column prop="effectiveDate" label="生效日期" min-width="150" resizable>
+					<template #default="{ row }">{{ fmtDate(row.effectiveDate) }}</template>
+				</el-table-column>
 			<el-table-column prop="materialCode" label="物料编码" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip resizable />
 			<el-table-column prop="inspecStandardVersion" label="检规版本" min-width="110" resizable />
@@ -45,15 +47,19 @@
 				:total="total"
 				:page-sizes="[10, 20, 50]"
 				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="onSizeChange"
 			/>
 		</div>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS6ProcessQualityInspectionList">
-import { computed, reactive, ref } from 'vue';
+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';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '过程检验单列表');
@@ -65,20 +71,56 @@ const query = reactive({
 	pageSize: 10,
 });
 
-// 本批次为只读 UI 骨架,尚无真实过程检验单接口:渲染空表,不发请求、不造数据。
+// 只读列表:数据源为数据中台标准层 mdp_std_ipqc_inspection(归口自旧系统 qms_gcjyd)。
+// 上游尚未抽数时标准层为空,接口返回 total=0/list=[],页面走空态,无写按钮。
 const loading = ref(false);
-const rows = ref<Record<string, unknown>[]>([]);
+const rows = ref<IpqcInspectionRow[]>([]);
 const total = ref(0);
 
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchIpqcInspectionList({
+			billNo: query.billNo,
+			productionBatchNo: query.productionBatchNo,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data?.list || [];
+		total.value = data?.total || 0;
+	} catch (e: any) {
+		ElMessage.error(e?.message || '加载过程检验单列表失败');
+		rows.value = [];
+		total.value = 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
 function doSearch() {
 	query.page = 1;
+	loadList();
 }
 
 function resetQuery() {
 	query.billNo = '';
 	query.productionBatchNo = '';
 	query.page = 1;
+	loadList();
+}
+
+function onSizeChange() {
+	query.page = 1;
+	loadList();
+}
+
+function fmtDate(v?: string) {
+	if (!v) return '';
+	return v.length >= 10 ? v.slice(0, 10) : v;
 }
+
+onMounted(() => loadList());
+onActivated(() => loadList());
 </script>
 
 <style scoped lang="scss">

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

+ 21 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/AidopKanbanController.cs

@@ -32,6 +32,7 @@ public partial class AidopKanbanController : ControllerBase
     private readonly PurchaseReceiptMdpSyncService _purchaseReceiptMdpSyncService;
     private readonly ProductionReturnMdpSyncService _productionReturnMdpSyncService;
     private readonly ProductionReceiptMdpSyncService _productionReceiptMdpSyncService;
+    private readonly IpqcInspectionMdpSyncService _ipqcInspectionMdpSyncService;
 
     public AidopKanbanController(
         ISqlSugarClient db,
@@ -46,7 +47,8 @@ public partial class AidopKanbanController : ControllerBase
         OutsourceIssueMdpSyncService outsourceIssueMdpSyncService,
         PurchaseReceiptMdpSyncService purchaseReceiptMdpSyncService,
         ProductionReturnMdpSyncService productionReturnMdpSyncService,
-        ProductionReceiptMdpSyncService productionReceiptMdpSyncService)
+        ProductionReceiptMdpSyncService productionReceiptMdpSyncService,
+        IpqcInspectionMdpSyncService ipqcInspectionMdpSyncService)
     {
         _db = db;
         _s1MdpSyncTransformService = s1MdpSyncTransformService;
@@ -61,6 +63,7 @@ public partial class AidopKanbanController : ControllerBase
         _purchaseReceiptMdpSyncService = purchaseReceiptMdpSyncService;
         _productionReturnMdpSyncService = productionReturnMdpSyncService;
         _productionReceiptMdpSyncService = productionReceiptMdpSyncService;
+        _ipqcInspectionMdpSyncService = ipqcInspectionMdpSyncService;
     }
 
     [HttpGet("home-l1")]
@@ -461,6 +464,23 @@ LIMIT 60
         });
     }
 
+    /// <summary>
+    /// S6 过程检验单(IPQC)数据中台只读链路手动刷新(DOP 内部:归口源 qms_gcjyd → mdp_stg_ipqc_inspection → mdp_std_ipqc_inspection)。
+    /// 独立于 s6-mdp/refresh 的 KPI 管线;只读源、只写 mdp 表;不碰 T8;源为 0 行时成功、处理数 0。
+    /// </summary>
+    [HttpPost("s6-ipqc-inspection-mdp/refresh")]
+    public async Task<IActionResult> RefreshS6IpqcInspectionMdp(CancellationToken cancellationToken)
+    {
+        var result = await _ipqcInspectionMdpSyncService.RunFullAsync(cancellationToken, "MANUAL");
+        return Ok(new
+        {
+            ok = true,
+            result.BatchId,
+            result.HeadStageRows,
+            result.HeadStandardRows
+        });
+    }
+
     /// <summary>
     /// S5 采购收货单 数据中台只读链路手动刷新(DOP 内部方案 1:PurOrdRctDetail/Master RctType='rc' → mdp_std_purchase_receipt)。
     /// 独立于 s5-mdp/refresh 的 KPI 管线;只读源、只写 mdp_std_purchase_receipt;不读/不改 S3 mdp_stg_receipt、S4 ado_s4_receipt;不碰 T8;rc 为 0 行时成功、处理数 0。

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

@@ -0,0 +1,96 @@
+namespace Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+
+/// <summary>
+/// S6 过程检验单(IPQC)列表 查询入参(只读)。读 mdp_std_ipqc_inspection。
+/// 查询条件对齐旧系统「过程检验单列表」页:单据编号 + 生产批号。
+/// </summary>
+public class IpqcInspectionListInput
+{
+    /// <summary>页码(从 1 开始)</summary>
+    public int Page { get; set; } = 1;
+
+    /// <summary>每页条数</summary>
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>单据编号(bill_no,模糊匹配)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>生产批号(production_batch_no,模糊匹配)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>排序字段(前端列 prop:billNo / effectiveDate)</summary>
+    public string? SortField { get; set; }
+
+    /// <summary>排序方向(asc / desc)</summary>
+    public string? SortOrder { get; set; }
+
+    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
+    public long? TenantId { get; set; }
+}
+
+/// <summary>
+/// S6 过程检验单(IPQC)列表 行(只读)。字段对齐前端骨架列 prop(camelCase)。
+/// 全部来自过程检验单头表 qms_gcjyd(归口后 mdp_std_ipqc_inspection)。
+/// </summary>
+public class IpqcInspectionListRow
+{
+    /// <summary>主键 id(mdp_std_ipqc_inspection.id)</summary>
+    public long Id { get; set; }
+
+    /// <summary>单据编号(bill_no ← qms_gcjyd.djbh)</summary>
+    public string? BillNo { get; set; }
+
+    /// <summary>产品型号(product_model ← cplx)</summary>
+    public string? ProductModel { get; set; }
+
+    /// <summary>生产批号(production_batch_no ← scph)</summary>
+    public string? ProductionBatchNo { get; set; }
+
+    /// <summary>生产工单(production_work_order ← lydjbh 来源单据编号)</summary>
+    public string? ProductionWorkOrder { get; set; }
+
+    /// <summary>结果判定(result_judgement ← jgpd,字典码原样透出,语义解码后置)</summary>
+    public string? ResultJudgement { get; set; }
+
+    /// <summary>附件(attachment ← fj)</summary>
+    public string? Attachment { get; set; }
+
+    /// <summary>备注(remark ← bz)</summary>
+    public string? Remark { get; set; }
+
+    /// <summary>检验人(inspector ← jyr)</summary>
+    public string? Inspector { get; set; }
+
+    /// <summary>工序编码(process_code ← gxbm)</summary>
+    public string? ProcessCode { get; set; }
+
+    /// <summary>工序名称(process_name ← gxmc)</summary>
+    public string? ProcessName { get; set; }
+
+    /// <summary>生产人员(production_person ← sczyry)</summary>
+    public string? ProductionPerson { get; set; }
+
+    /// <summary>样本量(sample_qty ← ybl)</summary>
+    public decimal? SampleQty { get; set; }
+
+    /// <summary>表单编号(form_no ← bdbh)</summary>
+    public string? FormNo { get; set; }
+
+    /// <summary>版本号(version_no ← bbh)</summary>
+    public string? VersionNo { get; set; }
+
+    /// <summary>生效日期(effective_date ← sxrq)</summary>
+    public DateTime? EffectiveDate { get; set; }
+
+    /// <summary>物料编码(material_code ← wlbm)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>物料名称(material_name ← wlmc)</summary>
+    public string? MaterialName { get; set; }
+
+    /// <summary>检规版本(inspec_standard_version ← jgbb)</summary>
+    public string? InspecStandardVersion { get; set; }
+
+    /// <summary>检规编号(inspec_standard_code ← jgbh)</summary>
+    public string? InspecStandardCode { get; set; }
+}

+ 281 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionMdpSyncService.cs

@@ -0,0 +1,281 @@
+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)。
+///
+/// 归口说明(CTO 拍板方案 A):
+///   qms_gcjyd 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
+///   本服务 EnsureTablesAsync 在 aidopdev 建同构空表 qms_gcjyd(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)为字典码,原样透出,语义解码后置。
+///   - 源为 0 行时转换成功完成、处理数为 0,不报错。
+/// </summary>
+public class IpqcInspectionMdpSyncService : ITransient
+{
+    private const string JobCode = "S6_IPQC_INSPECTION_MDP_SYNC";
+
+    private readonly ISqlSugarClient _db;
+
+    public IpqcInspectionMdpSyncService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>
+    /// 全量同步转换:贴源(头) → 标准化(头)。
+    /// </summary>
+    public async Task<IpqcInspectionMdpSyncResult> RunFullAsync(CancellationToken cancellationToken = default, string triggerType = "AUTO")
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        await EnsureTablesAsync();
+
+        var now = DateTime.Now;
+        var batchId = $"S6_IPQC_INSP_FULL_{now:yyyyMMddHHmmss}";
+        var runLogId = await InsertRunLogAsync(batchId, now, triggerType);
+        var result = new IpqcInspectionMdpSyncResult { BatchId = batchId, RunLogId = runLogId };
+
+        try
+        {
+            result.HeadStageRows = await SyncHeadStagingAsync(batchId, now);
+            result.HeadStandardRows = await TransformHeadStandardAsync(batchId, now);
+            await MarkRunSuccessAsync(runLogId, now, result);
+            return result;
+        }
+        catch (Exception ex)
+        {
+            await MarkRunFailedAsync(runLogId, now, ex.Message);
+            throw;
+        }
+    }
+
+    /// <summary>防御式建表(幂等):归口源空表 qms_gcjyd + 贴源层 + 标准层。</summary>
+    private async Task EnsureTablesAsync()
+    {
+        // 归口源表(方案 A):同构自旧系统 dopdemo.qms_gcjyd 头表,空表,待上游抽数填充。
+        // id 不设 AUTO_INCREMENT:抽数将携带旧系统主键。
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            CREATE TABLE IF NOT EXISTS qms_gcjyd (
+                id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
+                djbh VARCHAR(255) NULL COMMENT '单据编号',
+                cplx VARCHAR(255) NULL COMMENT '产品型号',
+                scph VARCHAR(255) NULL COMMENT '生产批号',
+                jyrq VARCHAR(255) NULL COMMENT '检验日期',
+                lydjbh VARCHAR(255) NULL COMMENT '来源单据编号',
+                jgpd BIGINT NULL COMMENT '结果判定',
+                jyhgsl DECIMAL(10,0) NULL COMMENT '检验合格数量',
+                jybhgsl DECIMAL(10,0) NULL COMMENT '检验不合格数量',
+                fj TEXT NULL COMMENT '附件',
+                bz TEXT NULL COMMENT '备注',
+                jyr VARCHAR(255) NULL COMMENT '检验人',
+                gxbm VARCHAR(255) NULL COMMENT '工序编码',
+                gxmc VARCHAR(255) NULL COMMENT '工序名称',
+                sczyry VARCHAR(255) NULL COMMENT '生产作业人员',
+                ybl DECIMAL(10,0) NULL COMMENT '样本量',
+                bdbh VARCHAR(255) NULL COMMENT '表单编号',
+                bbh VARCHAR(255) NULL COMMENT '版本号',
+                sxrq DATETIME NULL COMMENT '生效日期',
+                wlbm VARCHAR(255) NULL COMMENT '物料编码',
+                wlmc VARCHAR(255) NULL COMMENT '物料名称',
+                lysl DECIMAL(10,0) NULL COMMENT '留样数量',
+                phsl DECIMAL(10,0) NULL COMMENT '破坏数量',
+                jygfid VARCHAR(255) NULL COMMENT '检验规范',
+                jygfbbm VARCHAR(255) NULL COMMENT '没有用',
+                jgbb VARCHAR(255) NULL COMMENT '检规版本',
+                jgbh VARCHAR(255) NULL COMMENT '检规编号',
+                title VARCHAR(255) NULL,
+                jyr1 VARCHAR(255) NULL,
+                ChangeStatus INT NULL,
+                status VARCHAR(255) NULL COMMENT '检验状态'
+            ) 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 (
+                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 (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 (
+                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',
+                bill_no VARCHAR(64) NOT NULL,
+                product_model VARCHAR(255) NULL,
+                production_batch_no VARCHAR(255) NULL,
+                production_work_order VARCHAR(255) NULL,
+                result_judgement VARCHAR(32) NULL,
+                attachment TEXT NULL,
+                remark TEXT NULL,
+                inspector VARCHAR(255) NULL,
+                process_code VARCHAR(255) NULL,
+                process_name VARCHAR(255) NULL,
+                production_person VARCHAR(255) NULL,
+                sample_qty DECIMAL(18,6) NULL,
+                form_no VARCHAR(255) NULL,
+                version_no VARCHAR(255) NULL,
+                effective_date DATETIME NULL,
+                material_code VARCHAR(255) NULL,
+                material_name VARCHAR(255) NULL,
+                inspec_standard_version VARCHAR(255) NULL,
+                inspec_standard_code VARCHAR(255) NULL,
+                inspection_status VARCHAR(255) 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 (tenant_id, source_row_id),
+                KEY idx_mdp_std_ipqc_insp_bill (tenant_id, bill_no),
+                KEY idx_mdp_std_ipqc_insp_effdate (tenant_id, effective_date)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S6过程检验单头标准层'
+            """);
+    }
+
+    /// <summary>贴源头:qms_gcjyd -> mdp_stg_ipqc_inspection(raw_data JSON 快照)。返回源头行数。</summary>
+    private async Task<int> SyncHeadStagingAsync(string batchId, DateTime now)
+    {
+        var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjyd");
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            INSERT INTO mdp_stg_ipqc_inspection
+            (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_gcjyd',
+                CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now, 'PENDING',
+                JSON_OBJECT(
+                    'id', m.id, 'djbh', m.djbh, 'cplx', m.cplx, 'scph', m.scph, 'lydjbh', m.lydjbh,
+                    'jgpd', m.jgpd, 'fj', CAST(m.fj AS CHAR), 'bz', CAST(m.bz AS CHAR), 'jyr', m.jyr,
+                    'gxbm', m.gxbm, 'gxmc', m.gxmc, 'sczyry', m.sczyry, 'ybl', m.ybl,
+                    'bdbh', m.bdbh, 'bbh', m.bbh, 'sxrq', m.sxrq, 'wlbm', m.wlbm, 'wlmc', m.wlmc,
+                    'jgbb', m.jgbb, 'jgbh', m.jgbh, 'status', m.status
+                )
+            FROM qms_gcjyd m
+            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_gcjyd -> mdp_std_ipqc_inspection(typed)。返回处理头行数。</summary>
+    private async Task<int> TransformHeadStandardAsync(string batchId, DateTime now)
+    {
+        var rows = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_gcjyd");
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            INSERT INTO mdp_std_ipqc_inspection
+            (tenant_id, factory_id, source_system, bill_no, product_model, production_batch_no, production_work_order,
+             result_judgement, attachment, remark, inspector, process_code, process_name, production_person,
+             sample_qty, form_no, version_no, effective_date, material_code, material_name,
+             inspec_standard_version, inspec_standard_code, inspection_status,
+             source_row_id, source_biz_key, sync_batch_id, sync_time)
+            SELECT
+                0, 1, 'AIDOP', IFNULL(m.djbh, CAST(m.id AS CHAR)), m.cplx, m.scph, m.lydjbh,
+                CAST(m.jgpd AS CHAR), CAST(m.fj AS CHAR), CAST(m.bz AS CHAR), m.jyr, m.gxbm, m.gxmc, m.sczyry,
+                m.ybl, m.bdbh, m.bbh, m.sxrq, m.wlbm, m.wlmc,
+                m.jgbb, m.jgbh, m.status,
+                CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now
+            FROM qms_gcjyd m
+            ON DUPLICATE KEY UPDATE
+                bill_no=VALUES(bill_no), product_model=VALUES(product_model), production_batch_no=VALUES(production_batch_no),
+                production_work_order=VALUES(production_work_order), result_judgement=VALUES(result_judgement),
+                attachment=VALUES(attachment), remark=VALUES(remark), inspector=VALUES(inspector),
+                process_code=VALUES(process_code), process_name=VALUES(process_name), production_person=VALUES(production_person),
+                sample_qty=VALUES(sample_qty), form_no=VALUES(form_no), version_no=VALUES(version_no),
+                effective_date=VALUES(effective_date), material_code=VALUES(material_code), material_name=VALUES(material_name),
+                inspec_standard_version=VALUES(inspec_standard_version), inspec_standard_code=VALUES(inspec_standard_code),
+                inspection_status=VALUES(inspection_status),
+                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(
+            """
+            INSERT INTO mdp_transform_run_log
+            (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time)
+            VALUES (0, @JobCode, 'S6过程检验单MDP同步与标准化转换', @TriggerType, @BatchId, 'RUNNING', @StartTime)
+            """,
+            new SugarParameter("@JobCode", JobCode),
+            new SugarParameter("@TriggerType", NormalizeTriggerType(triggerType)),
+            new SugarParameter("@BatchId", batchId),
+            new SugarParameter("@StartTime", startedAt));
+        return await _db.Ado.GetLongAsync(
+            "SELECT id FROM mdp_transform_run_log WHERE batch_id=@BatchId ORDER BY id DESC LIMIT 1",
+            new List<SugarParameter> { new("@BatchId", batchId) });
+    }
+
+    private async Task MarkRunSuccessAsync(long runLogId, DateTime startedAt, IpqcInspectionMdpSyncResult result)
+    {
+        var finishedAt = DateTime.Now;
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            UPDATE mdp_transform_run_log
+            SET status='SUCCESS', end_time=@EndTime, duration_ms=@DurationMs,
+                stage_rows=@StageRows, standard_rows=@StandardRows, dwd_rows=0, update_time=CURRENT_TIMESTAMP
+            WHERE id=@Id
+            """,
+            new SugarParameter("@EndTime", finishedAt),
+            new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
+            new SugarParameter("@StageRows", result.HeadStageRows),
+            new SugarParameter("@StandardRows", result.HeadStandardRows),
+            new SugarParameter("@Id", runLogId));
+    }
+
+    private async Task MarkRunFailedAsync(long runLogId, DateTime startedAt, string message)
+    {
+        var finishedAt = DateTime.Now;
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            UPDATE mdp_transform_run_log
+            SET status='FAILED', end_time=@EndTime, duration_ms=@DurationMs,
+                error_message=@ErrorMessage, update_time=CURRENT_TIMESTAMP
+            WHERE id=@Id
+            """,
+            new SugarParameter("@EndTime", finishedAt),
+            new SugarParameter("@DurationMs", (int)(finishedAt - startedAt).TotalMilliseconds),
+            new SugarParameter("@ErrorMessage", message.Length > 2000 ? message[..2000] : message),
+            new SugarParameter("@Id", runLogId));
+    }
+
+    private static string NormalizeTriggerType(string? triggerType)
+        => string.IsNullOrWhiteSpace(triggerType) ? "AUTO" : triggerType.Trim().ToUpperInvariant();
+}
+
+/// <summary>过程检验单 MDP 同步转换结果。</summary>
+public sealed class IpqcInspectionMdpSyncResult
+{
+    public long RunLogId { get; set; }
+    public string BatchId { get; set; } = string.Empty;
+    public int HeadStageRows { get; set; }
+    public int HeadStandardRows { get; set; }
+}

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

@@ -0,0 +1,110 @@
+using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
+
+namespace Admin.NET.Plugin.AiDOP.Manufacturing;
+
+/// <summary>
+/// S6 过程检验单(IPQC)只读 list 服务。
+///
+/// 数据源:DOP 数据中台标准层 mdp_std_ipqc_inspection(头)。
+/// 由 IpqcInspectionMdpSyncService 从归口后的 aidopdev.qms_gcjyd(过程检验单头)同步标准化而来。
+///
+/// 本服务仅 SELECT:无新增/编辑/删除/写入。
+/// 当前上游尚未抽数,归口源表 qms_gcjyd 为空 → 标准层为空 → list 返回空数组(前端空态)。
+/// 明细(qms_gcjydzb)与详情不在本批范围,留待 IPQC-INSPECTION-DETAIL-READONLY-1。
+/// </summary>
+[ApiDescriptionSettings(Order = 306, Description = "过程检验单")]
+[Route("api/IpqcInspection")]
+[AllowAnonymous]
+[NonUnify]
+public class IpqcInspectionService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    public IpqcInspectionService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>
+    /// 过程检验单列表(只读分页查询)。
+    /// </summary>
+    [DisplayName("过程检验单列表")]
+    [HttpGet("list")]
+    public async Task<object> GetList([FromQuery] IpqcInspectionListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var where = new List<string> { "1=1" };
+        var pars = new List<SugarParameter>();
+
+        if (input.TenantId is > 0)
+        {
+            where.Add("m.tenant_id = @TenantId");
+            pars.Add(new SugarParameter("@TenantId", input.TenantId));
+        }
+        if (!string.IsNullOrWhiteSpace(input.BillNo))
+        {
+            where.Add("m.bill_no LIKE @BillNo");
+            pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ProductionBatchNo))
+        {
+            where.Add("m.production_batch_no LIKE @ProductionBatchNo");
+            pars.Add(new SugarParameter("@ProductionBatchNo", $"%{input.ProductionBatchNo.Trim()}%"));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+
+        var total = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(1) FROM mdp_std_ipqc_inspection m WHERE {whereSql}", pars);
+
+        var list = await _db.Ado.SqlQueryAsync<IpqcInspectionListRow>(
+            $"""
+            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
+            FROM mdp_std_ipqc_inspection m
+            WHERE {whereSql}
+            ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    /// <summary>
+    /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。
+    /// </summary>
+    private static string BuildOrderBy(string? sortField, string? sortOrder)
+    {
+        var column = sortField switch
+        {
+            "billNo" => "m.bill_no",
+            "effectiveDate" => "m.effective_date",
+            _ => "m.effective_date",
+        };
+        var direction = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return $"{column} {direction}, m.id DESC";
+    }
+}