Kaynağa Gözat

feat(s5-s6): IQC/IPQC 同库分租户隔离(补 tenant_id + 强制租户过滤)

- 迁移 1.0.256.sql:qms_qcp_inspbill/inspecapplyn/insappnentry(IQC)、qms_gcjyd/gcjydzb(IPQC) 补 tenant_id 列;历史行(NULL 或默认租户 1300000000001)归 797403760988229,AIDOPB 等具体租户预置行不动
- IQC 6 服务:list/count/detail/流程 裸 SQL 补强制租户过滤(AidopTenantHelper.Resolve/UserManager.TenantId);堵 detail 按 id 越权读
- IPQC 5 服务:MDP 同步 tenant_id 由常量0改取源列(IFNULL);list/detail 强制过滤;流程/BizHandler 按当前租户校验
- 冒烟:默认租户 IQC list=0(改前=6)、?797=5、?824=1(互不可见)、detail 越权阻断
- bump server 1.0.255 -> 1.0.256
YY968XX 2 hafta önce
ebeveyn
işleme
39fcdded46

+ 6 - 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.255</AssemblyVersion>
-    <FileVersion>1.0.255</FileVersion>
-    <Version>1.0.255</Version>
+    <AssemblyVersion>1.0.256</AssemblyVersion>
+    <FileVersion>1.0.256</FileVersion>
+    <Version>1.0.256</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -217,6 +217,9 @@
     <None Update="UpdateScripts\1.0.219.sql">
     <None Update="UpdateScripts\1.0.219.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>
+    <None Update="UpdateScripts\1.0.256.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 53 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.256.sql

@@ -0,0 +1,53 @@
+-- =============================================================================
+-- 1.0.256 — S5 IQC / S6 IPQC 同库分租户:贴源源表补 tenant_id 列 + 历史归属回填
+--
+-- 背景:qms_qcp_inspbill / qms_qcp_inspecapplyn / qms_qcp_insappnentry(IQC)与
+--       qms_gcjyd / qms_gcjydzb(IPQC)均为外部贴源表,原无 tenant_id,全库共享 →
+--       同库多租户下跨租户可见。本迁移补 tenant_id 列并回填历史归属,配合服务层查询/
+--       详情/流程的强制租户过滤(本版本随附代码),实现"当前租户只见自己数据"。
+--
+-- 回填口径:无租户(NULL) 或落在框架默认租户 1300000000001 的历史行 → 参考租户
+--           797403760988229;已归具体租户(如 AIDOPB 824585161322565)的预置行不动。
+--           qms_qcp_* / qms_gcjyd* 为贴源表,新环境为空、1300000000001 为框架默认租户
+--           (非真实客户数据),故此口径安全。
+-- 幂等:ADD COLUMN 前用 information_schema 判存在;回填带条件;重复执行安全。
+-- 兼容:MySQL 5.x / 8.x(PREPARE/EXECUTE 模式,无需 DELIMITER)。
+-- =============================================================================
+
+SET NAMES utf8mb4;
+SET @db = DATABASE();
+SET @col = 'tenant_id';
+
+-- ── IQC 三表加列 ────────────────────────────────────────────────────────────
+SET @tbl = 'qms_qcp_inspbill';
+SET @exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME=@tbl AND COLUMN_NAME=@col);
+SET @sql = IF(@exists=0, CONCAT('ALTER TABLE ',@tbl,' ADD COLUMN ',@col,' BIGINT NULL COMMENT ''租户id(DOP 多租户隔离列)'''), 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @tbl = 'qms_qcp_inspecapplyn';
+SET @exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME=@tbl AND COLUMN_NAME=@col);
+SET @sql = IF(@exists=0, CONCAT('ALTER TABLE ',@tbl,' ADD COLUMN ',@col,' BIGINT NULL COMMENT ''租户id(DOP 多租户隔离列)'''), 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @tbl = 'qms_qcp_insappnentry';
+SET @exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME=@tbl AND COLUMN_NAME=@col);
+SET @sql = IF(@exists=0, CONCAT('ALTER TABLE ',@tbl,' ADD COLUMN ',@col,' BIGINT NULL COMMENT ''租户id(DOP 多租户隔离列)'''), 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ── IPQC 两表加列 ───────────────────────────────────────────────────────────
+SET @tbl = 'qms_gcjyd';
+SET @exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME=@tbl AND COLUMN_NAME=@col);
+SET @sql = IF(@exists=0, CONCAT('ALTER TABLE ',@tbl,' ADD COLUMN ',@col,' BIGINT NULL COMMENT ''租户id(DOP 多租户隔离列)'''), 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @tbl = 'qms_gcjydzb';
+SET @exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME=@tbl AND COLUMN_NAME=@col);
+SET @sql = IF(@exists=0, CONCAT('ALTER TABLE ',@tbl,' ADD COLUMN ',@col,' BIGINT NULL COMMENT ''租户id(DOP 多租户隔离列)'''), 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ── IQC 历史归属回填(NULL 或 1300000000001 → 797;具体租户预置行不动)────────
+UPDATE qms_qcp_inspbill      SET tenant_id=797403760988229 WHERE tenant_id IS NULL OR tenant_id=1300000000001;
+UPDATE qms_qcp_inspecapplyn  SET tenant_id=797403760988229 WHERE tenant_id IS NULL OR tenant_id=1300000000001;
+UPDATE qms_qcp_insappnentry  SET tenant_id=797403760988229 WHERE tenant_id IS NULL OR tenant_id=1300000000001;
+UPDATE ado_s5_iqc_sqe_disposition SET tenant_id=797403760988229 WHERE tenant_id IS NULL OR tenant_id=1300000000001;
+-- IPQC 两表历史为空,无需回填(新数据由抽数/预置携带 tenant_id)。

+ 5 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionDispositionService.cs

@@ -139,12 +139,13 @@ public class IpqcInspectionDispositionService : IDynamicApiController, ITransien
             await _db.Insertable(disposition).ExecuteCommandAsync();
             await _db.Insertable(disposition).ExecuteCommandAsync();
             // 更新扩展表业务状态 DISPOSED(不回写 qms_gcjyd.status)
             // 更新扩展表业务状态 DISPOSED(不回写 qms_gcjyd.status)
             await _db.Ado.ExecuteCommandAsync(
             await _db.Ado.ExecuteCommandAsync(
-                "UPDATE ado_s6_ipqc_inspection_flow_state SET business_status=@st, current_node=NULL, UpdateTime=@now WHERE inspection_id=@id AND IsDelete=0",
+                "UPDATE ado_s6_ipqc_inspection_flow_state SET business_status=@st, current_node=NULL, UpdateTime=@now WHERE inspection_id=@id AND tenant_id=@tid AND IsDelete=0",
                 new List<SugarParameter>
                 new List<SugarParameter>
                 {
                 {
                     new("@st", IpqcInspectionFlowConst.StatusDisposed),
                     new("@st", IpqcInspectionFlowConst.StatusDisposed),
                     new("@now", now),
                     new("@now", now),
                     new("@id", input.Id),
                     new("@id", input.Id),
+                    new("@tid", _userManager.TenantId),
                 });
                 });
             // 推进 N3 → end
             // 推进 N3 → end
             await _flowEngine.Approve(myTask.Id, input.Comment);
             await _flowEngine.Approve(myTask.Id, input.Comment);
@@ -159,9 +160,10 @@ public class IpqcInspectionDispositionService : IDynamicApiController, ITransien
     private async Task<string> EnsureBillExistsAsync(long id)
     private async Task<string> EnsureBillExistsAsync(long id)
     {
     {
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
+        // 强制租户隔离:qms_gcjyd 全库共享无框架租户过滤器,裸 SQL 显式按当前用户租户校验。
         var rows = await _db.Ado.SqlQueryAsync<string>(
         var rows = await _db.Ado.SqlQueryAsync<string>(
-            "SELECT djbh FROM qms_gcjyd WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", id) });
+            "SELECT djbh FROM qms_gcjyd WHERE id=@id AND tenant_id=@tid LIMIT 1",
+            new List<SugarParameter> { new("@id", id), new("@tid", _userManager.TenantId) });
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         return rows[0] ?? id.ToString();
         return rows[0] ?? id.ToString();
     }
     }

+ 9 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowBizHandler.cs

@@ -17,10 +17,12 @@ public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
     public string BizType => IpqcInspectionFlowConst.BizType;
     public string BizType => IpqcInspectionFlowConst.BizType;
 
 
     private readonly ISqlSugarClient _db;
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
 
-    public IpqcInspectionFlowBizHandler(ISqlSugarClient db)
+    public IpqcInspectionFlowBizHandler(ISqlSugarClient db, UserManager userManager)
     {
     {
         _db = db;
         _db = db;
+        _userManager = userManager;
     }
     }
 
 
     /// <summary>流程发起后回调:no-op(不回写 qms_gcjyd)。</summary>
     /// <summary>流程发起后回调:no-op(不回写 qms_gcjyd)。</summary>
@@ -35,14 +37,16 @@ public class IpqcInspectionFlowBizHandler : IFlowBizHandler, ITransient
     /// <summary>返回检验单基础信息 + 网关判据 disposition_required(GW_RESULT 按此分流:==1→N3,否则→end)。</summary>
     /// <summary>返回检验单基础信息 + 网关判据 disposition_required(GW_RESULT 按此分流:==1→N3,否则→end)。</summary>
     public async Task<Dictionary<string, object>> GetBizData(long bizId)
     public async Task<Dictionary<string, object>> GetBizData(long bizId)
     {
     {
+        // 强制租户隔离:qms_gcjyd/flow_state 裸 SQL 显式按当前用户租户过滤(GetBizData 在流程审批 HTTP 请求上下文内被调用)。
+        var tid = _userManager.TenantId;
         var rows = await _db.Ado.SqlQueryAsync<IpqcFlowBizData>(
         var rows = await _db.Ado.SqlQueryAsync<IpqcFlowBizData>(
-            "SELECT djbh AS BillNo, cplx AS ProductModel, wlbm AS MaterialCode, wlmc AS MaterialName, jgpd AS Jgpd FROM qms_gcjyd WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", bizId) });
+            "SELECT djbh AS BillNo, cplx AS ProductModel, wlbm AS MaterialCode, wlmc AS MaterialName, jgpd AS Jgpd FROM qms_gcjyd WHERE id=@id AND tenant_id=@tid LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId), new("@tid", tid) });
         var r = rows.FirstOrDefault();
         var r = rows.FirstOrDefault();
         // 网关判据:主管 N2 整批判定写入扩展表 disposition_required(1=不合格转处置 / 0=合格结束)。
         // 网关判据:主管 N2 整批判定写入扩展表 disposition_required(1=不合格转处置 / 0=合格结束)。
         var dispRows = await _db.Ado.SqlQueryAsync<int?>(
         var dispRows = await _db.Ado.SqlQueryAsync<int?>(
-            "SELECT disposition_required FROM ado_s6_ipqc_inspection_flow_state WHERE inspection_id=@id AND IsDelete=0 ORDER BY id DESC LIMIT 1",
-            new List<SugarParameter> { new("@id", bizId) });
+            "SELECT disposition_required FROM ado_s6_ipqc_inspection_flow_state WHERE inspection_id=@id AND tenant_id=@tid AND IsDelete=0 ORDER BY id DESC LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId), new("@tid", tid) });
         var dispositionRequired = dispRows.FirstOrDefault() ?? 0;
         var dispositionRequired = dispRows.FirstOrDefault() ?? 0;
         if (r == null)
         if (r == null)
             return new Dictionary<string, object> { ["disposition_required"] = dispositionRequired };
             return new Dictionary<string, object> { ["disposition_required"] = dispositionRequired };

+ 8 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionFlowService.cs

@@ -95,7 +95,7 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
 
 
             // 写检验结果(仅 qms_gcjyd 白名单列 jgpd/jyhgsl/jybhgsl/ybl/jyr/bz;jgpd 可空不强写)
             // 写检验结果(仅 qms_gcjyd 白名单列 jgpd/jyhgsl/jybhgsl/ybl/jyr/bz;jgpd 可空不强写)
             await _db.Ado.ExecuteCommandAsync(
             await _db.Ado.ExecuteCommandAsync(
-                "UPDATE qms_gcjyd SET jgpd=@jgpd, jyhgsl=@jyhgsl, jybhgsl=@jybhgsl, ybl=@ybl, jyr=@jyr, bz=@bz WHERE id=@id",
+                "UPDATE qms_gcjyd SET jgpd=@jgpd, jyhgsl=@jyhgsl, jybhgsl=@jybhgsl, ybl=@ybl, jyr=@jyr, bz=@bz WHERE id=@id AND tenant_id=@tid",
                 new List<SugarParameter>
                 new List<SugarParameter>
                 {
                 {
                     new("@jgpd", input.Jgpd.HasValue ? input.Jgpd.Value : (object?)null),
                     new("@jgpd", input.Jgpd.HasValue ? input.Jgpd.Value : (object?)null),
@@ -105,6 +105,7 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
                     new("@jyr", inspectorName),
                     new("@jyr", inspectorName),
                     new("@bz", input.Bz),
                     new("@bz", input.Bz),
                     new("@id", input.Id),
                     new("@id", input.Id),
+                    new("@tid", _userManager.TenantId),
                 });
                 });
 
 
             // 落轻量扩展表(提交人/提交时间/业务状态,不回写 qms_gcjyd.status)
             // 落轻量扩展表(提交人/提交时间/业务状态,不回写 qms_gcjyd.status)
@@ -140,8 +141,8 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
             // 整批判定 jgpd 写 qms_gcjyd 白名单列(可空,不硬编码枚举;由前端传测试/已确认合法值)
             // 整批判定 jgpd 写 qms_gcjyd 白名单列(可空,不硬编码枚举;由前端传测试/已确认合法值)
             if (input.Jgpd.HasValue)
             if (input.Jgpd.HasValue)
                 await _db.Ado.ExecuteCommandAsync(
                 await _db.Ado.ExecuteCommandAsync(
-                    "UPDATE qms_gcjyd SET jgpd=@jgpd WHERE id=@id",
-                    new List<SugarParameter> { new("@jgpd", input.Jgpd.Value), new("@id", input.Id) });
+                    "UPDATE qms_gcjyd SET jgpd=@jgpd WHERE id=@id AND tenant_id=@tid",
+                    new List<SugarParameter> { new("@jgpd", input.Jgpd.Value), new("@id", input.Id), new("@tid", _userManager.TenantId) });
 
 
             // 先写 disposition_required(GW_RESULT 网关判据,Approve 时 GetBizData 读取)再推进
             // 先写 disposition_required(GW_RESULT 网关判据,Approve 时 GetBizData 读取)再推进
             await UpsertFlowStateAsync(input.Id, billNo, inst.Id, businessStatus, nextNode, now,
             await UpsertFlowStateAsync(input.Id, billNo, inst.Id, businessStatus, nextNode, now,
@@ -187,9 +188,11 @@ public class IpqcInspectionFlowService : IDynamicApiController, ITransient
     private async Task<string> EnsureBillExistsAsync(long id)
     private async Task<string> EnsureBillExistsAsync(long id)
     {
     {
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
+        // 强制租户隔离:qms_gcjyd 全库共享无框架租户过滤器,裸 SQL 必须显式按当前用户租户校验,
+        // 防止跨租户读到/操作他租户检验单。
         var rows = await _db.Ado.SqlQueryAsync<string>(
         var rows = await _db.Ado.SqlQueryAsync<string>(
-            "SELECT djbh FROM qms_gcjyd WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", id) });
+            "SELECT djbh FROM qms_gcjyd WHERE id=@id AND tenant_id=@tid LIMIT 1",
+            new List<SugarParameter> { new("@id", id), new("@tid", _userManager.TenantId) });
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         return rows[0] ?? id.ToString();
         return rows[0] ?? id.ToString();
     }
     }

+ 8 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionMdpSyncService.cs

@@ -69,6 +69,7 @@ public class IpqcInspectionMdpSyncService : ITransient
             """
             """
             CREATE TABLE IF NOT EXISTS qms_gcjyd (
             CREATE TABLE IF NOT EXISTS qms_gcjyd (
                 id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
                 id BIGINT NOT NULL PRIMARY KEY COMMENT '主表id',
+                tenant_id BIGINT NULL COMMENT '租户id(DOP 多租户隔离列,上游抽数携带;为空回退0)',
                 djbh VARCHAR(255) NULL COMMENT '单据编号',
                 djbh VARCHAR(255) NULL COMMENT '单据编号',
                 cplx VARCHAR(255) NULL COMMENT '产品型号',
                 cplx VARCHAR(255) NULL COMMENT '产品型号',
                 scph VARCHAR(255) NULL COMMENT '生产批号',
                 scph VARCHAR(255) NULL COMMENT '生产批号',
@@ -162,6 +163,7 @@ public class IpqcInspectionMdpSyncService : ITransient
             """
             """
             CREATE TABLE IF NOT EXISTS qms_gcjydzb (
             CREATE TABLE IF NOT EXISTS qms_gcjydzb (
                 id BIGINT NOT NULL PRIMARY KEY COMMENT '子表id',
                 id BIGINT NOT NULL PRIMARY KEY COMMENT '子表id',
+                tenant_id BIGINT NULL COMMENT '租户id(DOP 多租户隔离列,上游抽数携带;为空回退0)',
                 glid BIGINT NULL COMMENT '关联id',
                 glid BIGINT NULL COMMENT '关联id',
                 dwsj TEXT NULL, jyxm TEXT NULL, jybz TEXT NULL, jyyj TEXT NULL,
                 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,
                 j1 TEXT NULL, j2 TEXT NULL, j3 TEXT NULL, j4 TEXT NULL, j5 TEXT NULL,
@@ -258,7 +260,7 @@ public class IpqcInspectionMdpSyncService : ITransient
             INSERT INTO mdp_stg_ipqc_inspection
             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)
             (tenant_id, factory_id, source_system, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time, process_status, raw_data)
             SELECT
             SELECT
-                0, 1, 'AIDOP', 'qms_gcjyd',
+                IFNULL(m.tenant_id, 0), 1, 'AIDOP', 'qms_gcjyd',
                 CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now, 'PENDING',
                 CAST(m.id AS CHAR), CAST(m.djbh AS CHAR), @BatchId, @Now, 'PENDING',
                 JSON_OBJECT(
                 JSON_OBJECT(
                     'id', m.id, 'djbh', m.djbh, 'cplx', m.cplx, 'scph', m.scph, 'lydjbh', m.lydjbh,
                     'id', m.id, 'djbh', m.djbh, 'cplx', m.cplx, 'scph', m.scph, 'lydjbh', m.lydjbh,
@@ -290,7 +292,7 @@ public class IpqcInspectionMdpSyncService : ITransient
              inspec_standard_version, inspec_standard_code, inspection_status,
              inspec_standard_version, inspec_standard_code, inspection_status,
              source_row_id, source_biz_key, sync_batch_id, sync_time)
              source_row_id, source_biz_key, sync_batch_id, sync_time)
             SELECT
             SELECT
-                0, 1, 'AIDOP', IFNULL(m.djbh, CAST(m.id AS CHAR)), m.cplx, m.scph, m.lydjbh,
+                IFNULL(m.tenant_id, 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,
                 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.ybl, m.bdbh, m.bbh, m.sxrq, m.wlbm, m.wlmc,
                 m.jgbb, m.jgbh, m.status,
                 m.jgbb, m.jgbh, m.status,
@@ -321,7 +323,7 @@ public class IpqcInspectionMdpSyncService : ITransient
             INSERT INTO mdp_stg_ipqc_inspection_detail
             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)
             (tenant_id, factory_id, source_system, source_table, source_row_id, source_biz_key, sync_batch_id, sync_time, process_status, raw_data)
             SELECT
             SELECT
-                0, 1, 'AIDOP', 'qms_gcjydzb',
+                IFNULL(d.tenant_id, 0), 1, 'AIDOP', 'qms_gcjydzb',
                 CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now, 'PENDING',
                 CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now, 'PENDING',
                 JSON_OBJECT(
                 JSON_OBJECT(
                     'id', d.id, 'glid', d.glid, 'jyxm', CAST(d.jyxm AS CHAR), 'jyyj', CAST(d.jyyj AS CHAR),
                     'id', d.id, 'glid', d.glid, 'jyxm', CAST(d.jyxm AS CHAR), 'jyyj', CAST(d.jyyj AS CHAR),
@@ -354,14 +356,14 @@ public class IpqcInspectionMdpSyncService : ITransient
              sample_qty, inspection_qty, unqualified_qty, result_judgement, non_numeric_ok, non_numeric_ng, remark, attachment,
              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)
              source_row_id, source_biz_key, sync_batch_id, sync_time)
             SELECT
             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),
+                IFNULL(h.tenant_id, IFNULL(d.tenant_id, 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.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,
                 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,
                 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.bz AS CHAR), CAST(d.fj AS CHAR),
                 CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now
                 CAST(d.id AS CHAR), CAST(d.glid AS CHAR), @BatchId, @Now
             FROM qms_gcjydzb d
             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)
+            LEFT JOIN mdp_std_ipqc_inspection h ON h.source_row_id = CAST(d.glid AS CHAR)
             ON DUPLICATE KEY UPDATE
             ON DUPLICATE KEY UPDATE
                 std_head_id=VALUES(std_head_id), source_head_id=VALUES(source_head_id), inspection_item=VALUES(inspection_item),
                 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),
                 inspection_basis=VALUES(inspection_basis), technical_standard=VALUES(technical_standard), inspection_method=VALUES(inspection_method),
@@ -383,6 +385,7 @@ public class IpqcInspectionMdpSyncService : ITransient
             """
             """
             INSERT INTO mdp_transform_run_log
             INSERT INTO mdp_transform_run_log
             (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time)
             (tenant_id, job_code, job_name, trigger_type, batch_id, status, start_time)
+            -- tenant_id 固定 0:run_log 为跨租户运维元数据(一次全量刷新覆盖所有租户),非业务数据、不参与业务层租户过滤。
             VALUES (0, @JobCode, 'S6过程检验单MDP同步与标准化转换', @TriggerType, @BatchId, 'RUNNING', @StartTime)
             VALUES (0, @JobCode, 'S6过程检验单MDP同步与标准化转换', @TriggerType, @BatchId, 'RUNNING', @StartTime)
             """,
             """,
             new SugarParameter("@JobCode", JobCode),
             new SugarParameter("@JobCode", JobCode),

+ 11 - 14
server/Plugins/Admin.NET.Plugin.AiDOP/Manufacturing/IpqcInspectionService.cs

@@ -39,11 +39,11 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
         var where = new List<string> { "1=1" };
         var where = new List<string> { "1=1" };
         var pars = new List<SugarParameter>();
         var pars = new List<SugarParameter>();
 
 
-        if (input.TenantId is > 0)
-        {
-            where.Add("m.tenant_id = @TenantId");
-            pars.Add(new SugarParameter("@TenantId", input.TenantId));
-        }
+        // 强制租户过滤:qms_gcjyd/qms_gcjydzb 全库共享无 tenant,标准层按源 tenant_id 落库,
+        // 这里必须强制按当前租户过滤(显式参数 > JWT > query > 默认租户),杜绝跨租户可见。
+        var tid = AidopTenantHelper.Resolve(App.HttpContext, input.TenantId);
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         {
         {
             where.Add("m.bill_no LIKE @BillNo");
             where.Add("m.bill_no LIKE @BillNo");
@@ -103,13 +103,10 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
     {
     {
         if (id <= 0) return null;
         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));
-        }
+        // 强制租户过滤(同 GetList):显式参数 > JWT > query > 默认租户,杜绝跨租户读取详情。
+        var tid = AidopTenantHelper.Resolve(App.HttpContext, tenantId);
+        var headPars = new List<SugarParameter> { new("@Id", id), new("@TenantId", tid) };
+        var headWhere = "m.id = @Id AND m.tenant_id = @TenantId";
 
 
         var head = await _db.Ado.SqlQuerySingleAsync<IpqcInspectionDetailDto>(
         var head = await _db.Ado.SqlQuerySingleAsync<IpqcInspectionDetailDto>(
             $"""
             $"""
@@ -168,10 +165,10 @@ public class IpqcInspectionService : IDynamicApiController, ITransient
                 d.remark                 AS Remark,
                 d.remark                 AS Remark,
                 d.attachment             AS Attachment
                 d.attachment             AS Attachment
             FROM mdp_std_ipqc_inspection_detail d
             FROM mdp_std_ipqc_inspection_detail d
-            WHERE d.std_head_id = @HeadId
+            WHERE d.std_head_id = @HeadId AND d.tenant_id = @TenantId
             ORDER BY d.id ASC
             ORDER BY d.id ASC
             """,
             """,
-            new List<SugarParameter> { new("@HeadId", head.Id) });
+            new List<SugarParameter> { new("@HeadId", head.Id), new("@TenantId", tid) });
 
 
         return head;
         return head;
     }
     }

+ 6 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IncomingInspectionApplicationService.cs

@@ -11,8 +11,8 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// 不查询 FORGID / FBIZTYPE / FAPPLYUSER —— 名称解析源(bos_org / qcbd_biztype / bos_user)
 /// 不查询 FORGID / FBIZTYPE / FAPPLYUSER —— 名称解析源(bos_org / qcbd_biztype / bos_user)
 /// 当前不在 aidopdev,不做脑补 join、不显示裸 ID。
 /// 当前不在 aidopdev,不做脑补 join、不显示裸 ID。
 ///
 ///
-/// 租户/工厂:qms_qcp_inspecapplyn 无 tenant_id / domain / factory 列(贴源表本身不带租户维度),
-/// 故本接口在表结构层面无可用租户过滤列,不新增前端 factory 选择,亦不脑补租户列
+/// 租户:qms_qcp_inspecapplyn 已补 tenant_id 列,按当前租户过滤(AllowAnonymous → AidopTenantHelper.Resolve);
+/// 无 domain / factory 列,不新增前端 factory 选择,亦不脑补工厂维度
 ///
 ///
 /// 本接口只读:仅 SELECT,无新增/编辑/删除/任务拆分/状态回写。
 /// 本接口只读:仅 SELECT,无新增/编辑/删除/任务拆分/状态回写。
 /// </summary>
 /// </summary>
@@ -40,8 +40,10 @@ public class IncomingInspectionApplicationService : IDynamicApiController, ITran
         var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
         var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
         var offset = (page - 1) * pageSize;
         var offset = (page - 1) * pageSize;
 
 
-        var where = new List<string> { "1=1" };
-        var pars = new List<SugarParameter>();
+        // 同库分租户:qms_qcp_inspecapplyn 已补 tenant_id 列,按当前租户过滤(AllowAnonymous → AidopTenantHelper.Resolve)。
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+        var where = new List<string> { "m.tenant_id = @TenantId" };
+        var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
 
 
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         {
         {

+ 6 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillFlowBizHandler.cs

@@ -62,10 +62,12 @@ public class IqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
     public string BizType => IqcInspBillFlowConst.BizType;
     public string BizType => IqcInspBillFlowConst.BizType;
 
 
     private readonly ISqlSugarClient _db;
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
 
-    public IqcInspBillFlowBizHandler(ISqlSugarClient db)
+    public IqcInspBillFlowBizHandler(ISqlSugarClient db, UserManager userManager)
     {
     {
         _db = db;
         _db = db;
+        _userManager = userManager;
     }
     }
 
 
     /// <summary>流程发起后回调:本批 no-op(不回写 qms)。</summary>
     /// <summary>流程发起后回调:本批 no-op(不回写 qms)。</summary>
@@ -80,9 +82,10 @@ public class IqcInspBillFlowBizHandler : IFlowBizHandler, ITransient
     /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
     /// <summary>返回检验单基础信息(本批线性流程无网关条件,仅供日志/展示)。</summary>
     public async Task<Dictionary<string, object>> GetBizData(long bizId)
     public async Task<Dictionary<string, object>> GetBizData(long bizId)
     {
     {
+        // 同库分租户:GetBizData 由流程引擎在请求上下文内同步调用(Approve/网关求值),用当前用户租户过滤。
         var rows = await _db.Ado.SqlQueryAsync<IqcInspBillFlowBizData>(
         var rows = await _db.Ado.SqlQueryAsync<IqcInspBillFlowBizData>(
-            "SELECT FBILLNO AS BillNo, FMATERIALCFG AS MaterialCode, wlmc AS MaterialName, pd AS Pd, clfs AS Clfs FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", bizId) });
+            "SELECT FBILLNO AS BillNo, FMATERIALCFG AS MaterialCode, wlmc AS MaterialName, pd AS Pd, clfs AS Clfs FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId LIMIT 1",
+            new List<SugarParameter> { new("@id", bizId), new("@TenantId", _userManager.TenantId) });
         var r = rows.FirstOrDefault();
         var r = rows.FirstOrDefault();
         if (r == null) return new Dictionary<string, object>();
         if (r == null) return new Dictionary<string, object>();
         return new Dictionary<string, object>
         return new Dictionary<string, object>

+ 8 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillFlowService.cs

@@ -97,7 +97,7 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
 
 
             // 写检验结果(仅 pd/dhsl/bhgsl/clfs)
             // 写检验结果(仅 pd/dhsl/bhgsl/clfs)
             await _db.Ado.ExecuteCommandAsync(
             await _db.Ado.ExecuteCommandAsync(
-                "UPDATE qms_qcp_inspbill SET pd=@pd, dhsl=@dhsl, bhgsl=@bhgsl, clfs=@clfs WHERE id=@id",
+                "UPDATE qms_qcp_inspbill SET pd=@pd, dhsl=@dhsl, bhgsl=@bhgsl, clfs=@clfs WHERE id=@id AND tenant_id=@TenantId",
                 new List<SugarParameter>
                 new List<SugarParameter>
                 {
                 {
                     new("@pd", input.Pd),
                     new("@pd", input.Pd),
@@ -105,6 +105,7 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
                     new("@bhgsl", input.Bhgsl),
                     new("@bhgsl", input.Bhgsl),
                     new("@clfs", input.Pd == 1 ? input.Clfs : null),
                     new("@clfs", input.Pd == 1 ? input.Clfs : null),
                     new("@id", input.Id),
                     new("@id", input.Id),
+                    new("@TenantId", _userManager.TenantId),
                 });
                 });
 
 
             // 推进 N1 → N2
             // 推进 N1 → N2
@@ -173,8 +174,8 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
 
 
         // 检验单必须不合格(合格单无 SQE 节点)
         // 检验单必须不合格(合格单无 SQE 节点)
         var pdRows = await _db.Ado.SqlQueryAsync<int?>(
         var pdRows = await _db.Ado.SqlQueryAsync<int?>(
-            "SELECT pd FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", input.Id) });
+            "SELECT pd FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId LIMIT 1",
+            new List<SugarParameter> { new("@id", input.Id), new("@TenantId", _userManager.TenantId) });
         if (pdRows.FirstOrDefault() != 1) throw Oops.Oh("仅不合格检验单(pd=1)可做 SQE 处置");
         if (pdRows.FirstOrDefault() != 1) throw Oops.Oh("仅不合格检验单(pd=1)可做 SQE 处置");
 
 
         var inst = await GetLatestInstanceAsync(input.Id);
         var inst = await GetLatestInstanceAsync(input.Id);
@@ -192,8 +193,8 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
 
 
         // 来源单号
         // 来源单号
         var srcRows = await _db.Ado.SqlQueryAsync<string>(
         var srcRows = await _db.Ado.SqlQueryAsync<string>(
-            "SELECT lydjbh FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", input.Id) });
+            "SELECT lydjbh FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId LIMIT 1",
+            new List<SugarParameter> { new("@id", input.Id), new("@TenantId", _userManager.TenantId) });
 
 
         var now = DateTime.Now;
         var now = DateTime.Now;
         var disposition = new AdoS5IqcSqeDisposition
         var disposition = new AdoS5IqcSqeDisposition
@@ -240,8 +241,8 @@ public class IqcInspBillFlowService : IDynamicApiController, ITransient
     {
     {
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
         var rows = await _db.Ado.SqlQueryAsync<string>(
         var rows = await _db.Ado.SqlQueryAsync<string>(
-            "SELECT FBILLNO FROM qms_qcp_inspbill WHERE id=@id LIMIT 1",
-            new List<SugarParameter> { new("@id", id) });
+            "SELECT FBILLNO FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId LIMIT 1",
+            new List<SugarParameter> { new("@id", id), new("@TenantId", _userManager.TenantId) });
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         if (rows.Count == 0) throw Oops.Oh("检验单不存在");
         return rows[0] ?? id.ToString();
         return rows[0] ?? id.ToString();
     }
     }

+ 11 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/IqcInspBillService.cs

@@ -10,8 +10,8 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// 批次=pch;数量=FRINSQTY/jysl/dhsl/bhgsl;判定=pd(raw);处理方式=clfs(raw);状态=FBILLSTATUS(raw);
 /// 批次=pch;数量=FRINSQTY/jysl/dhsl/bhgsl;判定=pd(raw);处理方式=clfs(raw);状态=FBILLSTATUS(raw);
 /// 检验员=FINSPECTORID;检验起止=FINSPESTARTDATE/FINSPEENDDATE;创建=FCREATETIME。默认排序 FCREATETIME DESC, id DESC。
 /// 检验员=FINSPECTORID;检验起止=FINSPESTARTDATE/FINSPEENDDATE;创建=FCREATETIME。默认排序 FCREATETIME DESC, id DESC。
 ///
 ///
-/// tenant/组织:qms_qcp_inspbill **无 tenant_id**,不做租户过滤;FORGID 当前全 NULL,不做 FORGID 过滤。
-/// TenantId/ForgId 接收即忽略。本服务仅 SELECT:无新增/编辑/删除/详情/判定录入/主管审核/SQE 处置/仓储通知/
+/// tenant/组织:qms_qcp_inspbill 已补 tenant_id 列,按当前租户过滤(AllowAnonymous → AidopTenantHelper.Resolve);FORGID 当前全 NULL,不做 FORGID 过滤。
+/// 本服务仅 SELECT:无新增/编辑/删除/详情/判定录入/主管审核/SQE 处置/仓储通知/
 /// 库存事务/流程实例;不写 qms_qcp_inspbill、不改 FBIZTYPE、不碰 ApprovalFlow。pd/clfs/FBILLSTATUS 字典未闭合,raw 展示。
 /// 库存事务/流程实例;不写 qms_qcp_inspbill、不改 FBIZTYPE、不碰 ApprovalFlow。pd/clfs/FBILLSTATUS 字典未闭合,raw 展示。
 /// </summary>
 /// </summary>
 [ApiDescriptionSettings(Order = 309, Description = "来料检验单列表")]
 [ApiDescriptionSettings(Order = 309, Description = "来料检验单列表")]
@@ -53,9 +53,10 @@ public class IqcInspBillService : IDynamicApiController, ITransient
         var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
         var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
         var offset = (page - 1) * pageSize;
         var offset = (page - 1) * pageSize;
 
 
-        // qms_qcp_inspbill 无 tenant_id、FORGID 全 NULL:不做租户/组织过滤(TenantId/ForgId 接收即忽略)。
-        var where = new List<string> { "1=1" };
-        var pars = new List<SugarParameter>();
+        // 同库分租户:qms_qcp_inspbill 已补 tenant_id 列,按当前租户过滤(AllowAnonymous 口径经 AidopTenantHelper 解析)。FORGID 全 NULL 不做组织过滤。
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+        var where = new List<string> { "ib.tenant_id = @TenantId" };
+        var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
 
 
         void AddLike(string? v, string col, string p)
         void AddLike(string? v, string col, string p)
         {
         {
@@ -164,7 +165,7 @@ public class IqcInspBillService : IDynamicApiController, ITransient
     /// 仅 SELECT 单行:不 join 申请表/明细/SuppMaster/ApprovalFlow/SQE,不查字典,不写任何表。
     /// 仅 SELECT 单行:不 join 申请表/明细/SuppMaster/ApprovalFlow/SQE,不查字典,不写任何表。
     /// id&lt;=0 或不存在返回 null(HTTP 200)。字典字段 pd/clfs/FBILLSTATUS/pcsq/blwtflid/FBIZTYPE 全 raw。
     /// id&lt;=0 或不存在返回 null(HTTP 200)。字典字段 pd/clfs/FBILLSTATUS/pcsq/blwtflid/FBIZTYPE 全 raw。
     /// 流程占位 BizType/BizId/BizCode 由本方法拼装,**不查 ApprovalFlowInstance、不生成流程实例**。
     /// 流程占位 BizType/BizId/BizCode 由本方法拼装,**不查 ApprovalFlowInstance、不生成流程实例**。
-    /// qms_qcp_inspbill 无 tenant_id、FORGID 全 NULL:不做租户/组织过滤,与 list 口径一致
+    /// qms_qcp_inspbill 已补 tenant_id:详情按 id + 当前租户过滤(与 list 口径一致),FORGID 全 NULL 不做组织过滤
     /// </summary>
     /// </summary>
     [DisplayName("来料检验单详情")]
     [DisplayName("来料检验单详情")]
     [HttpGet("detail")]
     [HttpGet("detail")]
@@ -172,7 +173,9 @@ public class IqcInspBillService : IDynamicApiController, ITransient
     {
     {
         if (id <= 0) return null;
         if (id <= 0) return null;
 
 
-        var pars = new List<SugarParameter> { new("@id", id) };
+        // 同库分租户:详情按 id + 当前租户过滤,杜绝跨租户越权取单(最高优先越权点)。
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+        var pars = new List<SugarParameter> { new("@id", id), new("@TenantId", tenantId) };
         var list = await _db.Ado.SqlQueryAsync<IqcInspBillDetailRow>(
         var list = await _db.Ado.SqlQueryAsync<IqcInspBillDetailRow>(
             """
             """
             SELECT
             SELECT
@@ -218,7 +221,7 @@ public class IqcInspBillService : IDynamicApiController, ITransient
                 ib.FAUDITORID      AS AuditorId,
                 ib.FAUDITORID      AS AuditorId,
                 ib.FAUDITDATE      AS AuditDate
                 ib.FAUDITDATE      AS AuditDate
             FROM qms_qcp_inspbill ib
             FROM qms_qcp_inspbill ib
-            WHERE ib.id = @id
+            WHERE ib.id = @id AND ib.tenant_id = @TenantId
             LIMIT 1
             LIMIT 1
             """,
             """,
             pars);
             pars);

+ 12 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/PendingInspectionService.cs

@@ -55,7 +55,10 @@ public class PendingInspectionService : IDynamicApiController, ITransient
             "m.Qty > 0",
             "m.Qty > 0",
             "IFNULL(m.Location,'') = ''",
             "IFNULL(m.Location,'') = ''",
         };
         };
-        var pars = new List<SugarParameter> { new("@Domain", domain) };
+        // 同库分租户:主表 MissedPrint 无 tenant_id(用 Domain 隔离);LEFT JOIN 的 qms 申请/明细表已补 tenant_id,
+        // 租户过滤置于 JOIN ON(避免退化为 INNER JOIN 丢失 MissedPrint 锚行)。
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+        var pars = new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) };
 
 
         if (!string.IsNullOrWhiteSpace(input.DemandNo))
         if (!string.IsNullOrWhiteSpace(input.DemandNo))
         {
         {
@@ -97,8 +100,8 @@ public class PendingInspectionService : IDynamicApiController, ITransient
                    MAX(m.Shelf) AS Shelf, SUM(m.Qty) AS Qty, MAX(m.CheckNo) AS CheckNo,
                    MAX(m.Shelf) AS Shelf, SUM(m.Qty) AS Qty, MAX(m.CheckNo) AS CheckNo,
                    MAX(IFNULL(d.FINSPECTSTATUS,'')) AS FINSPECTSTATUS
                    MAX(IFNULL(d.FINSPECTSTATUS,'')) AS FINSPECTSTATUS
             FROM MissedPrint m
             FROM MissedPrint m
-            LEFT JOIN qms_qcp_inspecapplyn q ON m.CheckNo = q.FBILLNO
-            LEFT JOIN qms_qcp_insappnentry d ON q.id = d.glid
+            LEFT JOIN qms_qcp_inspecapplyn q ON m.CheckNo = q.FBILLNO AND q.tenant_id = @TenantId
+            LEFT JOIN qms_qcp_insappnentry d ON q.id = d.glid AND d.tenant_id = @TenantId
             WHERE {whereSql}
             WHERE {whereSql}
             GROUP BY m.Supply, m.PurOrd, m.PurLine, m.ShipperNbr, m.ItemNum, m.LotSerial
             GROUP BY m.Supply, m.PurOrd, m.PurLine, m.ShipperNbr, m.ItemNum, m.LotSerial
             """;
             """;
@@ -122,7 +125,7 @@ public class PendingInspectionService : IDynamicApiController, ITransient
                 t.ShipperNbr AS DeliveryNo,
                 t.ShipperNbr AS DeliveryNo,
                 t.CheckNo    AS InspectNo,
                 t.CheckNo    AS InspectNo,
                 CONCAT(t.FINSPECTSTATUS,
                 CONCAT(t.FINSPECTSTATUS,
-                    CASE WHEN (SELECT MAX(ib.clfs) FROM qms_qcp_inspbill ib WHERE ib.lydjbh = t.CheckNo) = '3'
+                    CASE WHEN (SELECT MAX(ib.clfs) FROM qms_qcp_inspbill ib WHERE ib.lydjbh = t.CheckNo AND ib.tenant_id = @TenantId) = '3'
                          THEN '-挑选使用' ELSE '' END) AS InspectStatus,
                          THEN '-挑选使用' ELSE '' END) AS InspectStatus,
                 t.Supply     AS Supplier,
                 t.Supply     AS Supplier,
                 s.SortName   AS SupplierShortName
                 s.SortName   AS SupplierShortName
@@ -144,13 +147,16 @@ public class PendingInspectionService : IDynamicApiController, ITransient
     [HttpGet("status-options")]
     [HttpGet("status-options")]
     public async Task<object> GetStatusOptions()
     public async Task<object> GetStatusOptions()
     {
     {
+        // 同库分租户:qms_qcp_insappnentry 已补 tenant_id,选项按当前租户收敛。
+        var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
         var list = await _db.Ado.SqlQueryAsync<PendingInspectionStatusOption>(
         var list = await _db.Ado.SqlQueryAsync<PendingInspectionStatusOption>(
             """
             """
             SELECT DISTINCT FINSPECTSTATUS AS Val
             SELECT DISTINCT FINSPECTSTATUS AS Val
             FROM qms_qcp_insappnentry
             FROM qms_qcp_insappnentry
-            WHERE IFNULL(FINSPECTSTATUS,'') <> ''
+            WHERE IFNULL(FINSPECTSTATUS,'') <> '' AND tenant_id = @TenantId
             ORDER BY FINSPECTSTATUS
             ORDER BY FINSPECTSTATUS
-            """);
+            """,
+            new List<SugarParameter> { new("@TenantId", tenantId) });
         return list;
         return list;
     }
     }
 
 

+ 4 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs

@@ -537,6 +537,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
     // ── Tab7 IQC信息(请购/采购/检验链路)──
     // ── Tab7 IQC信息(请购/采购/检验链路)──
     private async Task<List<FlowIqcRow>> QueryIqc(long entryId)
     private async Task<List<FlowIqcRow>> QueryIqc(long entryId)
     {
     {
+        // 同库分租户:IQC 派生表锚表 qms_qcp_inspbill 已补 tenant_id,按当前租户过滤(避免跨租户检验单串入链路)。
         const string sql = """
         const string sql = """
             SELECT
             SELECT
                 ROW_NUMBER() OVER (ORDER BY iqcjy.FAPPLYTIME DESC) AS Line,
                 ROW_NUMBER() OVER (ORDER BY iqcjy.FAPPLYTIME DESC) AS Line,
@@ -564,14 +565,15 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                     a.FRINSQTY,
                     a.FRINSQTY,
                     c.FINSPECTSTATUS
                     c.FINSPECTSTATUS
                 FROM qms_qcp_inspbill a
                 FROM qms_qcp_inspbill a
-                LEFT JOIN qms_qcp_inspecapplyn b ON a.lydjbh = b.FBILLNO                LEFT JOIN qms_qcp_insappnentry c ON b.id = c.glid
+                LEFT JOIN qms_qcp_inspecapplyn b ON a.lydjbh = b.FBILLNO AND b.tenant_id = @TenantId                LEFT JOIN qms_qcp_insappnentry c ON b.id = c.glid AND c.tenant_id = @TenantId
                 LEFT JOIN scm_shd cd ON c.shdh = cd.shddh                LEFT JOIN scm_shdzb f ON f.shpc = c.FLOTNUMBER AND f.sh_material_code = c.FMATERIALCFG AND f.glid = cd.id
                 LEFT JOIN scm_shd cd ON c.shdh = cd.shddh                LEFT JOIN scm_shdzb f ON f.shpc = c.FLOTNUMBER AND f.sh_material_code = c.FMATERIALCFG AND f.glid = cd.id
+                WHERE a.tenant_id = @TenantId
             ) iqcjy ON pod.PurOrd = iqcjy.po_bill AND CAST(pod.Line AS CHAR(32)) = CAST(iqcjy.po_billline AS CHAR(32))            WHERE jq.flag = '1'
             ) iqcjy ON pod.PurOrd = iqcjy.po_bill AND CAST(pod.Line AS CHAR(32)) = CAST(iqcjy.po_billline AS CHAR(32))            WHERE jq.flag = '1'
               AND pr.sentry_id IS NOT NULL
               AND pr.sentry_id IS NOT NULL
               AND pr.sentry_id = @EntryId
               AND pr.sentry_id = @EntryId
             ORDER BY iqcjy.FAPPLYTIME DESC
             ORDER BY iqcjy.FAPPLYTIME DESC
             """;
             """;
-        return await _db.Ado.SqlQueryAsync<FlowIqcRow>(sql, new { EntryId = entryId });
+        return await _db.Ado.SqlQueryAsync<FlowIqcRow>(sql, new { EntryId = entryId, TenantId = _userManager.TenantId });
     }
     }
 
 
     // ── Tab8 FQC信息 ──
     // ── Tab8 FQC信息 ──