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

feat(mdp): S6 IPQC 明细 bespoke→通用管线 + dopdemorq 双源只读入站(server 1.0.273)

- IpqcInspectionMdpSyncService: Detail(qms_gcjydzb) 由独立 bespoke stg 迁入执行器 pull stg(source_table='qms_gcjydzb',与 Head 共 mdp_stg_ipqc_pull)→ transform 读 pull stg → mdp_std_ipqc_inspection_detail
- P0 修复: ybl/jysl/bhgsl/NonNumericalType* 裸 CAST + InspectionTime 裸 STR_TO_DATE 全改 MdpJsonSql(Dec/Int/DateTimeSec 加 NULL 守卫 + ISO-T 兼容),消除 SQL Server 序列化 null/bit/ISO-T 触发的 STRICT 500
- 新增 RunDetailSourceSwitchFullAsync(明细 FULL Replace mdp_std_ipqc_inspection_detail);RunFull/RunInbound 加 Detail pull;Head 完全不碰(HARD GATE 隔离)
- 1.0.273.sql: 登记 2 明细实体(本库 DETAIL status=1,DOPDEMORQ_SQLSERVER DETAIL status=0),FULL
- 前端/核心/DTO 零改;本库明细冒烟 std=1 类型全安全转换(null→NULL 非500)、源表不写、pull 贴源 qms_gcjydzb=1;std_head_id 本库为既有 head/detail tenant-basis 不对称(非本次回归,dopdemorq 切源两侧 ctx tenant 可回填)
YY968XX 1 неделя назад
Родитель
Сommit
cbdf84b1ec

+ 6 - 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.272</AssemblyVersion>
-    <FileVersion>1.0.272</FileVersion>
-    <Version>1.0.272</Version>
+    <AssemblyVersion>1.0.273</AssemblyVersion>
+    <FileVersion>1.0.273</FileVersion>
+    <Version>1.0.273</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -238,6 +238,9 @@
     <None Update="UpdateScripts\1.0.272.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.273.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 35 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.273.sql

@@ -0,0 +1,35 @@
+-- 1.0.273:S6 IPQC 明细 bespoke→通用管线 + dopdemorq 双源入站实体登记(本库 status=1 / SQLSERVER status=0)
+--
+-- 背景:IPQC Head 已迁 pull stg(source_table='qms_gcjyd')双源就绪;本批把 Detail(qms_gcjydzb) 从 bespoke
+--       (读独立 mdp_stg_ipqc_inspection_detail + 裸 CAST)迁入通用管线:执行器灌 mdp_stg_ipqc_pull(source_table='qms_gcjydzb',与 Head 共表)
+--       → transform 读 pull stg(MdpJsonSql 修 ybl/jysl/bhgsl/NonNumerical*/InspectionTime 类型守卫)→ mdp_std_ipqc_inspection_detail。
+--       登记 2 个明细入站实体(本库 + dopdemorq SQL Server),均落 mdp_stg_ipqc_pull、FULL。Head 实体不动。
+-- 状态:正式脚本(1.0.273,已注册 csproj Copy,随 AutoVersionUpdate 执行)。
+--
+-- 硬约束:
+--   1) 本库实体 S6_IPQC_INSPECTION_DETAIL status=1:RunFullAsync 明细迁 pull 管线后依赖它(否则 s6 IPQC refresh 明细断)。
+--   2) SQLSERVER 实体 status=0:就位不启用,合入后不改变现有运行行为;启用需带外注密码+受控切源。
+--   3) 两实体均 sync_mode='FULL'(qms_gcjydzb 无 UpdateTime,FULL-only)。
+--   4) source_table_name='qms_gcjydzb'(两库同名);transform 按 source_table='qms_gcjydzb' 过滤,与 Head(qms_gcjyd) 共 pull stg。
+--      tenant_id 由入站上下文赋值,本库源含该列(raw 保留),SQL Server 源无此列(回落上下文默认 1300000000001)。
+-- 幂等:WHERE NOT EXISTS 守卫,可重复执行。
+
+-- 本库明细入站实体(AIDOPDEV_MYSQL, status=1)
+INSERT INTO mdp_entity
+  (tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name,
+   sync_mode, batch_size, biz_key_expr, status, remark, create_time, update_time)
+SELECT 0, (SELECT id FROM mdp_source WHERE source_code='AIDOPDEV_MYSQL'),
+       'S6_IPQC_INSPECTION_DETAIL', 'S6过程检验明细入站(本库)', 'TABLE',
+       'qms_gcjydzb', 'mdp_stg_ipqc_pull',
+       'FULL', 5000, 'id', 1, 'S6 IPQC 明细 B-① 本库入站', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM mdp_entity WHERE entity_code='S6_IPQC_INSPECTION_DETAIL');
+
+-- dopdemorq SQL Server 明细入站实体(DOPDEMORQ_SQLSERVER, status=0 就位不启用)
+INSERT INTO mdp_entity
+  (tenant_id, source_id, entity_code, entity_name, entity_type, source_table_name, target_table_name,
+   sync_mode, batch_size, biz_key_expr, status, remark, create_time, update_time)
+SELECT 0, (SELECT id FROM mdp_source WHERE source_code='DOPDEMORQ_SQLSERVER'),
+       'S6_IPQC_INSPECTION_DETAIL_SQLSERVER', 'S6过程检验明细入站(SQLServer)', 'TABLE',
+       'qms_gcjydzb', 'mdp_stg_ipqc_pull',
+       'FULL', 5000, 'id', 0, 'S6 IPQC 明细 B-① 双源 disabled', NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM mdp_entity WHERE entity_code='S6_IPQC_INSPECTION_DETAIL_SQLSERVER');

+ 127 - 49
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionMdpSyncService.cs

@@ -7,9 +7,9 @@ namespace Admin.NET.Plugin.AiDOP.Manufacturing;
 /// S6 过程检验单(IPQC)数据中台只读同步转换服务(DOP 内部,独立于 S6MdpSyncTransformService 的 KPI 管线)。
 ///
 /// 源:aidopdev.qms_gcjyd(过程检验单头,31 列)+ aidopdev.qms_gcjydzb(明细,100 列),归口自旧系统 dopdemo。
-/// 链路(Phase 1):执行器/贴源 → mdp_stg_ipqc_pull(头)+ mdp_stg_ipqc_inspection_detail(明细)→ std
-/// 双模式入站:mdp_entity=S6_IPQC_INSPECTION_HEAD → Writer → mdp_stg_ipqc_pull;头标准层读 pull stg;
-///     明细仍经 SyncDetailStagingAsync 入 inspection_detail 后读 stg 转 std
+/// 链路:执行器 → mdp_stg_ipqc_pull(头 source_table='qms_gcjyd' + 明细 source_table='qms_gcjydzb',共表)→ std(头/明细各读 pull stg)
+/// 双模式入站:mdp_entity=S6_IPQC_INSPECTION_HEAD / S6_IPQC_INSPECTION_DETAIL → Writer → mdp_stg_ipqc_pull;头/明细标准层均读 pull stg(MdpJsonSql 跨源类型兼容)。
+///     SyncHeadStagingAsync / SyncDetailStagingAsync 保留为 vestigial 可观测贴源写入(mdp_stg_ipqc_inspection(_detail)),不再被 transform 消费
 ///
 /// 归口说明(CTO 拍板方案 A):
 ///   qms_gcjyd/qms_gcjydzb 原仅存于旧系统 dopdemo(0 行)、aidopdev 无此表,且 dopdemo 非运行时连接。
@@ -29,10 +29,13 @@ public class IpqcInspectionMdpSyncService : ITransient
 {
     private const string JobCode = "S6_IPQC_INSPECTION_MDP_SYNC";
     private const string InboundEntityCode = "S6_IPQC_INSPECTION_HEAD";
+    // Phase 2:Detail(qms_gcjydzb) 迁通用管线,经执行器灌 mdp_stg_ipqc_pull(source_table='qms_gcjydzb',与 Head 共表),transform 读 pull stg。
+    private const string InboundDetailEntityCode = "S6_IPQC_INSPECTION_DETAIL";
 
-    // 双源(dopdemorq SQL Server)Phase 1:仅 Head(qms_gcjyd 无 UpdateTime → FULL)。实体默认 status=0(就位不启用)。Detail(qms_gcjydzb) 属 bespoke,留 Phase 2 不碰。
+    // 双源(dopdemorq SQL Server):Head(qms_gcjyd)+ Detail(qms_gcjydzb),均无 UpdateTime → FULL。实体默认 status=0(就位不启用)。
     private const string SqlServerSourceCode = "DOPDEMORQ_SQLSERVER";
     private const string SqlServerHeadEntityCode = "S6_IPQC_INSPECTION_HEAD_SQLSERVER";
+    private const string SqlServerDetailEntityCode = "S6_IPQC_INSPECTION_DETAIL_SQLSERVER";
 
     private readonly ISqlSugarClient _db;
     private readonly MdpSourcePullDispatcher _pullDispatcher;
@@ -66,11 +69,13 @@ public class IpqcInspectionMdpSyncService : ITransient
                 TaskCode = "S6_IPQC_INSPECTION_INBOUND",
                 BatchId = $"{batchId}_PULL"
             };
-            var pull = await _pullDispatcher.PullByEntityCodeAsync(InboundEntityCode, pullCtx, cancellationToken);
-            result.HeadStageRows = pull.RowsWritten;
-            // 保留富贴源头表写入(可观测);头 std 以 pull stg 为准
+            var pullHead = await _pullDispatcher.PullByEntityCodeAsync(InboundEntityCode, pullCtx, cancellationToken);
+            var pullDetail = await _pullDispatcher.PullByEntityCodeAsync(InboundDetailEntityCode, pullCtx, cancellationToken);
+            result.HeadStageRows = pullHead.RowsWritten;
+            result.DetailStageRows = pullDetail.RowsWritten;
+            // 保留富贴源头/明细表写入(vestigial 可观测);头/明细 std 均以 pull stg 为准
             await SyncHeadStagingAsync(batchId, now);
-            result.DetailStageRows = await SyncDetailStagingAsync(batchId, now);
+            await SyncDetailStagingAsync(batchId, now);
             result.HeadStandardRows = await TransformHeadStandardAsync(batchId, now);
             result.DetailStandardRows = await TransformDetailStandardAsync(batchId, now);
             await MarkRunSuccessAsync(runLogId, now, result);
@@ -103,16 +108,18 @@ public class IpqcInspectionMdpSyncService : ITransient
             TaskCode = "S6_IPQC_INSPECTION_INBOUND",
             BatchId = $"S6_IPQC_IN_{now:yyyyMMddHHmmss}"
         };
-        var pull = await _pullDispatcher.PullByEntityCodeAsync(InboundEntityCode, pullCtx, cancellationToken);
+        var pullHead = await _pullDispatcher.PullByEntityCodeAsync(InboundEntityCode, pullCtx, cancellationToken);
+        var pullDetail = await _pullDispatcher.PullByEntityCodeAsync(InboundDetailEntityCode, pullCtx, cancellationToken);
 
         var batchId = $"S6_IPQC_STD_{now:yyyyMMddHHmmss}";
         var runLogId = await InsertRunLogAsync(batchId, now, "INBOUND");
         var result = new IpqcInspectionMdpSyncResult { BatchId = batchId, RunLogId = runLogId };
         try
         {
-            result.HeadStageRows = pull.RowsWritten;
+            result.HeadStageRows = pullHead.RowsWritten;
+            result.DetailStageRows = pullDetail.RowsWritten;
             await SyncHeadStagingAsync(batchId, now);
-            result.DetailStageRows = await SyncDetailStagingAsync(batchId, now);
+            await SyncDetailStagingAsync(batchId, now);
             result.HeadStandardRows = await TransformHeadStandardAsync(batchId, now);
             result.DetailStandardRows = await TransformDetailStandardAsync(batchId, now);
             await MarkRunSuccessAsync(runLogId, now, result);
@@ -126,9 +133,9 @@ public class IpqcInspectionMdpSyncService : ITransient
         return new IpqcInspectionInboundResult
         {
             PullBatchId = pullCtx.BatchId,
-            RowsPulled = pull.RowsPulled,
-            RowsWrittenStg = pull.RowsWritten,
-            NewCursor = pull.NewCursor,
+            RowsPulled = pullHead.RowsPulled + pullDetail.RowsPulled,
+            RowsWrittenStg = pullHead.RowsWritten + pullDetail.RowsWritten,
+            NewCursor = pullDetail.NewCursor ?? pullHead.NewCursor,
             TransformBatchId = result.BatchId,
             HeadStageRows = result.HeadStageRows,
             HeadStandardRows = result.HeadStandardRows,
@@ -198,6 +205,66 @@ public class IpqcInspectionMdpSyncService : ITransient
         };
     }
 
+    /// <summary>
+    /// 双源切换 FULL Replace(Detail):从指定 DB 源(默认 DOPDEMORQ_SQLSERVER)PullAll 全量灌 pull-stg(source_table='qms_gcjydzb'),
+    /// 成功后在单事务内以「仅当前源」结果 FULL 重建 mdp_std_ipqc_inspection_detail(按 tenant 精确隔离),消除旧源独有业务键残留。
+    /// qms_gcjydzb 无 UpdateTime → FULL;用 PullAllByEntityCodeAsync + FullRefresh=true。std_head_id 回填依赖 Head std 已存在(LEFT JOIN,缺则 NULL)。
+    /// <b>只处理 Detail</b>:不碰 Head std(切 Head 用 RunHeadSourceSwitchFullAsync)。
+    /// SQLSERVER 实体默认 status=0(就位不启用);dopdemorq 该表当前空,Phase 2 不实际执行 destructive 切换。
+    /// </summary>
+    public async Task<IpqcInspectionInboundResult> RunDetailSourceSwitchFullAsync(
+        string sourceCode = SqlServerSourceCode,
+        string detailEntityCode = SqlServerDetailEntityCode,
+        long tenantId = 0,
+        CancellationToken cancellationToken = default)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        await EnsureTablesAsync();
+        await EnsurePullStgTableAsync();
+
+        var now = DateTime.Now;
+        // 1) PullAll 抽尽 + fullRefresh=true。Pull 失败会抛异常,std 未动。
+        var pullCtx = new MdpPullContext
+        {
+            TenantId = tenantId,
+            FullRefresh = true,
+            TaskCode = "S6_IPQC_INSPECTION_INBOUND",
+            BatchId = $"S6_IPQC_DTL_SW_{now:yyyyMMddHHmmss}"
+        };
+        var pull = await _pullDispatcher.PullAllByEntityCodeAsync(detailEntityCode, pullCtx, cancellationToken);
+
+        // 2) FULL Replace(仅明细 std):事务内 DELETE 当前 tenant,再 INSERT 仅当前 source_system 的明细结果。
+        var batchId = $"S6_IPQC_DTL_SWSTD_{now:yyyyMMddHHmmss}";
+        var runLogId = await InsertRunLogAsync(batchId, now, "SOURCE_SWITCH");
+        var result = new IpqcInspectionMdpSyncResult { BatchId = batchId, RunLogId = runLogId, DetailStageRows = pull.RowsWritten };
+        try
+        {
+            result.DetailStandardRows = await MdpStdFullReplace.ReplaceAsync(
+                _db, "mdp_std_ipqc_inspection_detail", tenantId, extraWhere: null,
+                insertScopedAsync: () => TransformDetailStandardAsync(batchId, now, sourceCode),
+                cancellationToken);
+            await MarkRunSuccessAsync(runLogId, now, result);
+        }
+        catch (Exception ex)
+        {
+            await MarkRunFailedAsync(runLogId, now, ex.Message);
+            throw;
+        }
+
+        return new IpqcInspectionInboundResult
+        {
+            PullBatchId = pullCtx.BatchId,
+            RowsPulled = pull.RowsPulled,
+            RowsWrittenStg = pull.RowsWritten,
+            NewCursor = pull.NewCursor,
+            TransformBatchId = batchId,
+            HeadStageRows = 0,
+            HeadStandardRows = 0,
+            DetailStageRows = result.DetailStageRows,
+            DetailStandardRows = result.DetailStandardRows
+        };
+    }
+
     private async Task EnsurePullStgTableAsync()
     {
         await _db.Ado.ExecuteCommandAsync(
@@ -544,48 +611,58 @@ public class IpqcInspectionMdpSyncService : ITransient
         return rows;
     }
 
-    /// <summary>标准化明细:mdp_stg_ipqc_inspection_detail → mdp_std_ipqc_inspection_detail。</summary>
-    private async Task<int> TransformDetailStandardAsync(string batchId, DateTime now)
+    /// <summary>
+    /// 标准化明细:mdp_stg_ipqc_pull(qms_gcjydzb) → mdp_std_ipqc_inspection_detail(回填 std_head_id)。
+    /// 全 typed 列经 MdpJsonSql 跨源类型兼容(ybl/jysl/bhgsl/NonNumerical* Dec/Int 加 NULL 守卫、InspectionTime 兼容 ISO-T)。
+    /// sourceSystem 非空时仅统计/转换当前 source(切源 FULL Replace 用);为空时保持既有全源行为。
+    /// </summary>
+    private async Task<int> TransformDetailStandardAsync(string batchId, DateTime now, string? sourceSystem = null)
     {
+        var srcClause = sourceSystem == null ? "" : " AND d.source_system=@Src";
+        var dTenant = $"IFNULL({MdpJsonSql.Int("d", "tenant_id")}, IFNULL(d.tenant_id, 1300000000001))";
+
+        var countPars = new List<SugarParameter>();
+        if (sourceSystem != null) countPars.Add(new SugarParameter("@Src", sourceSystem));
         var rows = await _db.Ado.GetIntAsync(
-            "SELECT COUNT(1) FROM mdp_stg_ipqc_inspection_detail WHERE source_table='qms_gcjydzb'");
-        await _db.Ado.ExecuteCommandAsync(
-            """
+            $"SELECT COUNT(1) FROM mdp_stg_ipqc_pull d WHERE d.source_table='qms_gcjydzb'{srcClause}", countPars);
+
+        var insertSql =
+            $"""
             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
-                IFNULL(d.tenant_id, 1300000000001), h.id,
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.glid')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.jyxm')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.jyyj')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.jsbz')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.jyff')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.sx')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.xx')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ProcessCode')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ProcessName')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.MeasurementInstrument')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Inspector')),
-                STR_TO_DATE(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.InspectionTime')),'null'),''), '%Y-%m-%d %H:%i:%s'),
-                CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.ybl')) AS DECIMAL(18,6)),
-                CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.jysl')) AS DECIMAL(18,6)),
-                CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bhgsl')) AS DECIMAL(18,6)),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.pd')),
-                CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.NonNumericalTypeOK')) AS SIGNED),
-                CAST(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.NonNumericalTypeNG')) AS SIGNED),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.bz')),
-                JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.fj')),
-                IFNULL(d.source_row_id, JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.id'))),
-                IFNULL(NULLIF(d.source_biz_key,''), JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.glid'))),
+                {dTenant}, h.id,
+                {MdpJsonSql.Str("d", "glid")},
+                {MdpJsonSql.Str("d", "jyxm")},
+                {MdpJsonSql.Str("d", "jyyj")},
+                {MdpJsonSql.Str("d", "jsbz")},
+                {MdpJsonSql.Str("d", "jyff")},
+                {MdpJsonSql.Str("d", "sx")},
+                {MdpJsonSql.Str("d", "xx")},
+                {MdpJsonSql.Str("d", "ProcessCode")},
+                {MdpJsonSql.Str("d", "ProcessName")},
+                {MdpJsonSql.Str("d", "MeasurementInstrument")},
+                {MdpJsonSql.Str("d", "Inspector")},
+                {MdpJsonSql.DateTimeSec("d", "InspectionTime")},
+                {MdpJsonSql.Int("d", "ybl")},
+                {MdpJsonSql.Dec("d", "jysl", 18, 6)},
+                {MdpJsonSql.Dec("d", "bhgsl", 18, 6)},
+                {MdpJsonSql.Str("d", "pd")},
+                {MdpJsonSql.Int("d", "NonNumericalTypeOK")},
+                {MdpJsonSql.Int("d", "NonNumericalTypeNG")},
+                {MdpJsonSql.Str("d", "bz")},
+                {MdpJsonSql.Str("d", "fj")},
+                IFNULL(d.source_row_id, {MdpJsonSql.Str("d", "id")}),
+                IFNULL(NULLIF(d.source_biz_key,''), {MdpJsonSql.Str("d", "glid")}),
                 @BatchId, @Now
-            FROM mdp_stg_ipqc_inspection_detail d
+            FROM mdp_stg_ipqc_pull d
             LEFT JOIN mdp_std_ipqc_inspection h
-              ON h.tenant_id = IFNULL(d.tenant_id, 1300000000001)
-             AND h.source_row_id = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.glid'))
-            WHERE d.source_table='qms_gcjydzb'
+              ON h.tenant_id = {dTenant}
+             AND h.source_row_id = {MdpJsonSql.Str("d", "glid")}
+            WHERE d.source_table='qms_gcjydzb'{srcClause}
             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),
@@ -595,9 +672,10 @@ public class IpqcInspectionMdpSyncService : ITransient
                 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));
+            """;
+        var insPars = new List<SugarParameter> { new("@BatchId", batchId), new("@Now", now) };
+        if (sourceSystem != null) insPars.Add(new SugarParameter("@Src", sourceSystem));
+        await _db.Ado.ExecuteCommandAsync(insertSql, insPars);
         return rows;
     }