Prechádzať zdrojové kódy

fix(s5,s7): 收口前端 TenantId 越权范式 P0 全量 9 对象 (server 1.0.299 / Web 2.4.275)

9 个 S5/S7 只读+流程服务不再信任前端 tenantId、移除 [AllowAnonymous],
租户只来自认证后 JWT(严格:超管未选/无 Token 拒绝,无默认回退、无条件式全表)。

S5: OutsourceIssue/ProductionIssue/ProductionReceipt/ProductionReturn/PurchaseReceipt
S7: FqcInspBill/ProductionInbound/SalesDeliveryNotice/FqcInspBillFlow(写)

- 各服务:注入 UserManager + 私有 ResolveTenantOrThrow;条件式/前端优先 → 无条件可信 tenant 过滤
- Detail 头 id+tenant;OutsourceIssue/ProductionReturn 明细补 d.tenant_id 纵深过滤
- DTO 移除 Input.TenantId 过滤字段;前端 9 API 去 withAidopTenantParams(不动公共实现)
- FqcInspBillFlow 写库前严格解析,WHERE id+tenant 不变
- 不碰 QmsTenantScope/AidopTenantHelper/AdoS0TenantScope/S1-S4;RuleConfig 另移交 S3

运行时实测(真实数据):匿名→401、超管未选→400、攻击者传他租户 tenantId→0、属主见真实数据。
YY968XX 1 týždeň pred
rodič
commit
882c113450
27 zmenil súbory, kde vykonal 224 pridanie a 141 odobranie
  1. 1 1
      Web/package.json
  2. 2 3
      Web/src/views/aidop/s5/api/outsourceIssue.ts
  3. 1 2
      Web/src/views/aidop/s5/api/productionIssue.ts
  4. 2 3
      Web/src/views/aidop/s5/api/productionReceipt.ts
  5. 2 3
      Web/src/views/aidop/s5/api/productionReturn.ts
  6. 1 2
      Web/src/views/aidop/s5/api/purchaseReceipt.ts
  7. 3 4
      Web/src/views/aidop/s7/api/fqcInspBill.ts
  8. 1 2
      Web/src/views/aidop/s7/api/productionInbound.ts
  9. 1 2
      Web/src/views/aidop/s7/api/salesDeliveryNotice.ts
  10. 3 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  11. 0 6
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/FqcInspBillDto.cs
  12. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/ProductionInboundDto.cs
  13. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/SalesDeliveryNoticeDto.cs
  14. 21 5
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowService.cs
  15. 20 7
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillService.cs
  16. 21 7
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/ProductionInboundService.cs
  17. 21 7
      server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/SalesDeliveryNoticeService.cs
  18. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/OutsourceIssueDto.cs
  19. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionIssueDto.cs
  20. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionReceiptDto.cs
  21. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionReturnDto.cs
  22. 0 3
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/PurchaseReceiptDto.cs
  23. 28 17
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/OutsourceIssueService.cs
  24. 21 7
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionIssueService.cs
  25. 26 15
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionReceiptService.cs
  26. 28 17
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionReturnService.cs
  27. 21 7
      server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/PurchaseReceiptService.cs

+ 1 - 1
Web/package.json

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

+ 2 - 3
Web/src/views/aidop/s5/api/outsourceIssue.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface Paged<T> {
 	total: number;
@@ -64,12 +63,12 @@ export interface OutsourceIssueDetail {
 
 export function fetchOutsourceIssueList(params: any) {
 	return service
-		.get<Paged<OutsourceIssueRow>>('/api/OutsourceIssue/list', { params: withAidopTenantParams(params) })
+		.get<Paged<OutsourceIssueRow>>('/api/OutsourceIssue/list', { params })
 		.then((r) => r.data);
 }
 
 export function fetchOutsourceIssueDetail(id: number) {
 	return service
-		.get<OutsourceIssueDetail>('/api/OutsourceIssue/detail', { params: withAidopTenantParams({ id }) })
+		.get<OutsourceIssueDetail>('/api/OutsourceIssue/detail', { params: { id } })
 		.then((r) => r.data);
 }

+ 1 - 2
Web/src/views/aidop/s5/api/productionIssue.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface Paged<T> {
 	total: number;
@@ -37,6 +36,6 @@ export interface ProductionIssueRow {
 
 export function fetchProductionIssueList(params: any) {
 	return service
-		.get<Paged<ProductionIssueRow>>('/api/ProductionIssue/list', { params: withAidopTenantParams(params) })
+		.get<Paged<ProductionIssueRow>>('/api/ProductionIssue/list', { params })
 		.then((r) => r.data);
 }

+ 2 - 3
Web/src/views/aidop/s5/api/productionReceipt.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface Paged<T> {
 	total: number;
@@ -56,12 +55,12 @@ export interface ProductionReceiptRow {
 
 export function fetchProductionReceiptList(params: any) {
 	return service
-		.get<Paged<ProductionReceiptRow>>('/api/S5ProductionReceipt/list', { params: withAidopTenantParams(params) })
+		.get<Paged<ProductionReceiptRow>>('/api/S5ProductionReceipt/list', { params })
 		.then((r) => r.data);
 }
 
 export function fetchProductionReceiptDetail(nbr: string) {
 	return service
-		.get<ProductionReceiptRow[]>('/api/S5ProductionReceipt/detail', { params: withAidopTenantParams({ nbr }) })
+		.get<ProductionReceiptRow[]>('/api/S5ProductionReceipt/detail', { params: { nbr } })
 		.then((r) => r.data);
 }

+ 2 - 3
Web/src/views/aidop/s5/api/productionReturn.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface Paged<T> {
 	total: number;
@@ -66,12 +65,12 @@ export interface ProductionReturnDetail {
 
 export function fetchProductionReturnList(params: any) {
 	return service
-		.get<Paged<ProductionReturnRow>>('/api/ProductionReturn/list', { params: withAidopTenantParams(params) })
+		.get<Paged<ProductionReturnRow>>('/api/ProductionReturn/list', { params })
 		.then((r) => r.data);
 }
 
 export function fetchProductionReturnDetail(id: number) {
 	return service
-		.get<ProductionReturnDetail>('/api/ProductionReturn/detail', { params: withAidopTenantParams({ id }) })
+		.get<ProductionReturnDetail>('/api/ProductionReturn/detail', { params: { id } })
 		.then((r) => r.data);
 }

+ 1 - 2
Web/src/views/aidop/s5/api/purchaseReceipt.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface Paged<T> {
 	total: number;
@@ -53,6 +52,6 @@ export interface PurchaseReceiptRow {
 
 export function fetchPurchaseReceiptList(params: any) {
 	return service
-		.get<Paged<PurchaseReceiptRow>>('/api/PurchaseReceipt/list', { params: withAidopTenantParams(params) })
+		.get<Paged<PurchaseReceiptRow>>('/api/PurchaseReceipt/list', { params })
 		.then((r) => r.data);
 }

+ 3 - 4
Web/src/views/aidop/s7/api/fqcInspBill.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface FqcPaged<T> {
 	total: number;
@@ -87,14 +86,14 @@ export interface FqcResultRow {
 /** FQC 检验任务列表(只读) */
 export function fetchFqcTaskList(params: any) {
 	return service
-		.get<FqcPaged<FqcTaskRow>>('/api/S7FqcInspBill/task-list', { params: withAidopTenantParams(params) })
+		.get<FqcPaged<FqcTaskRow>>('/api/S7FqcInspBill/task-list', { params })
 		.then((r) => r.data);
 }
 
 /** FQC 检验单(结果)列表(只读) */
 export function fetchFqcResultList(params: any) {
 	return service
-		.get<FqcPaged<FqcResultRow>>('/api/S7FqcInspBill/result-list', { params: withAidopTenantParams(params) })
+		.get<FqcPaged<FqcResultRow>>('/api/S7FqcInspBill/result-list', { params })
 		.then((r) => r.data);
 }
 
@@ -158,6 +157,6 @@ export interface FqcDetail {
 /** FQC 检验单只读详情(id<=0/未命中返回 null) */
 export function fetchFqcDetail(id: number) {
 	return service
-		.get<FqcDetail>('/api/S7FqcInspBill/detail', { params: withAidopTenantParams({ id }) })
+		.get<FqcDetail>('/api/S7FqcInspBill/detail', { params: { id } })
 		.then((r) => r.data);
 }

+ 1 - 2
Web/src/views/aidop/s7/api/productionInbound.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface ProductionInboundPaged<T> {
 	total: number;
@@ -48,6 +47,6 @@ export interface ProductionInboundRow {
 
 export function fetchProductionInboundList(params: any) {
 	return service
-		.get<ProductionInboundPaged<ProductionInboundRow>>('/api/S7ProductionInbound/list', { params: withAidopTenantParams(params) })
+		.get<ProductionInboundPaged<ProductionInboundRow>>('/api/S7ProductionInbound/list', { params })
 		.then((r) => r.data);
 }

+ 1 - 2
Web/src/views/aidop/s7/api/salesDeliveryNotice.ts

@@ -1,5 +1,4 @@
 import service from '/@/utils/request';
-import { withAidopTenantParams } from '../../api/aidopTenant';
 
 export interface SalesDeliveryNoticePaged<T> {
 	total: number;
@@ -37,6 +36,6 @@ export interface SalesDeliveryNoticeRow {
 
 export function fetchSalesDeliveryNoticeList(params: any) {
 	return service
-		.get<SalesDeliveryNoticePaged<SalesDeliveryNoticeRow>>('/api/S7SalesDeliveryNotice/list', { params: withAidopTenantParams(params) })
+		.get<SalesDeliveryNoticePaged<SalesDeliveryNoticeRow>>('/api/S7SalesDeliveryNotice/list', { params })
 		.then((r) => r.data);
 }

+ 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.298</AssemblyVersion>
-    <FileVersion>1.0.298</FileVersion>
-    <Version>1.0.298</Version>
+    <AssemblyVersion>1.0.299</AssemblyVersion>
+    <FileVersion>1.0.299</FileVersion>
+    <Version>1.0.299</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -48,9 +48,6 @@ public class FqcTaskListInput
 
     /// <summary>检验负责人(qms_fqcbj.jyfzr,模糊)</summary>
     public string? Inspector { get; set; }
-
-    /// <summary>租户 ID(前端注入;WP0-D4 过滤用)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>FQC 检验任务列表 行(只读)。prop 对齐前端列;源列见注释(qms_fqcbj)。</summary>
@@ -155,9 +152,6 @@ public class FqcResultListInput
 
     /// <summary>判定(qms_qcpp_inspbill.pd,原样精确)</summary>
     public string? Judgment { get; set; }
-
-    /// <summary>租户 ID(前端注入;WP0-D4 起按 tenant_id 过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>FQC 检验单(结果)列表 行(只读)。prop 对齐前端列;源列见注释。</summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/ProductionInboundDto.cs

@@ -32,9 +32,6 @@ public class ProductionInboundListInput
 
     /// <summary>入库日期止(NbrMaster.Date,含当日,yyyy-MM-dd)</summary>
     public string? InboundDateEnd { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;按 NbrMaster.tenant_id 过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/Dto/SalesDeliveryNoticeDto.cs

@@ -37,9 +37,6 @@ public class SalesDeliveryNoticeListInput
 
     /// <summary>排序方向(asc/desc,非法回落 desc)</summary>
     public string? OrderDir { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;&gt;0 时按 a.tenant_id 过滤,&lt;=0 不过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 21 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillFlowService.cs

@@ -27,6 +27,9 @@ namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 [NonUnify]
 public class FqcInspBillFlowService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
     private readonly FlowEngineService _flowEngine;
     private readonly UserManager _userManager;
@@ -38,6 +41,17 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
         _userManager = userManager;
     }
 
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
+    }
+
     /// <summary>检验单流程状态(只读,供前端按钮控制)。</summary>
     [DisplayName("FQC检验单流程状态")]
     [HttpGet("state")]
@@ -95,7 +109,7 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
             if (myTask.NodeId != FqcInspBillFlowConst.NodeInspect)
                 throw Oops.Oh("当前不在检验录入节点,无法提交检验结果");
 
-            var tid = QmsTenantScope.Resolve(null, _userManager.TenantId);
+            var tid = ResolveTenantOrThrow();
 
             // ① 检验业务结果 → qms_qcpp_inspbill(仅 pd/hgsl/bhgsl/clfs,不覆盖继承字段)
             await _db.Ado.ExecuteCommandAsync(
@@ -217,11 +231,13 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
             .AnyAsync();
         if (hasEffective) throw Oops.Oh("该检验单已有有效 QE 处置,不可重复提交");
 
+        // 写库前严格解析可信租户(无 Token / 超管未选主租户 → 抛错,不落库)
+        var tid = ResolveTenantOrThrow();
         var now = DateTime.Now;
         var disposition = new AdoS7FqcQeDisposition
         {
             Id = YitIdHelper.NextId(),
-            TenantId = _userManager.TenantId,
+            TenantId = tid,
             OrgId = inst.OrgId,
             InspbillId = input.Id,
             InspbillNo = billNo,
@@ -268,7 +284,7 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
     private async Task<(string billNo, string? sourceBillNo)> EnsureBillAsync(long id)
     {
         if (id <= 0) throw Oops.Oh("检验单 id 非法");
-        var tid = QmsTenantScope.Resolve(null, _userManager.TenantId);
+        var tid = ResolveTenantOrThrow();
         var rows = await _db.Ado.SqlQueryAsync<FqcBillKey>(
             "SELECT FBILLNO AS BillNo, lydjbh AS SourceBillNo FROM qms_qcpp_inspbill WHERE id=@id AND tenant_id=@TenantFilter LIMIT 1",
             new List<SugarParameter> { new("@id", id), new("@TenantFilter", tid) });
@@ -347,7 +363,7 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
             output.DispositionType = disp.DispositionType;
             output.DispositionOpinion = disp.DispositionOpinion;
         }
-        var tid = QmsTenantScope.Resolve(null, _userManager.TenantId);
+        var tid = ResolveTenantOrThrow();
         var pdRows = await _db.Ado.SqlQueryAsync<string>(
             "SELECT pd FROM qms_qcpp_inspbill WHERE id=@id AND tenant_id=@TenantFilter LIMIT 1",
             new List<SugarParameter> { new("@id", bizId), new("@TenantFilter", tid) });
@@ -412,7 +428,7 @@ public class FqcInspBillFlowService : IDynamicApiController, ITransient
             if (row == null || row.Pd != 0) return; // 仅合格回写
 
             string billNo = string.IsNullOrWhiteSpace(row.BillNo) ? billId.ToString() : row.BillNo!;
-            long tenantId = _userManager.TenantId;
+            long tenantId = ResolveTenantOrThrow();
             var payload = JsonSerializer.Serialize(new
             {
                 path = "/fqc/result",

+ 20 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/FqcInspBillService.cs

@@ -19,10 +19,12 @@ namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 330, Description = "FQC检验单列表")]
 [Route("api/S7FqcInspBill")]
-[AllowAnonymous]
 [NonUnify]
 public class FqcInspBillService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
     private readonly UserManager _userManager;
     private readonly FqcMdpSyncService _fqcMdpSyncService;
@@ -34,6 +36,17 @@ public class FqcInspBillService : IDynamicApiController, ITransient
         _fqcMdpSyncService = fqcMdpSyncService;
     }
 
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
+    }
+
     /// <summary>检验任务列表 排序白名单(前端 prop → std 物理列,杜绝 orderBy 注入)。</summary>
     private static readonly Dictionary<string, string> TaskOrderWhitelist = new(StringComparer.OrdinalIgnoreCase)
     {
@@ -81,9 +94,9 @@ public class FqcInspBillService : IDynamicApiController, ITransient
         var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
         var offset = (page - 1) * pageSize;
 
-        var tenantId = QmsTenantScope.Resolve(input.TenantId, _userManager.TenantId);
+        var tid = ResolveTenantOrThrow();
         var where = new List<string> { "t.tenant_id = @TenantFilter" };
-        var pars = new List<SugarParameter> { new("@TenantFilter", tenantId) };
+        var pars = new List<SugarParameter> { new("@TenantFilter", tid) };
 
         void AddLike(string? v, string col, string p)
         {
@@ -161,9 +174,9 @@ public class FqcInspBillService : IDynamicApiController, ITransient
         var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
         var offset = (page - 1) * pageSize;
 
-        var tenantId = QmsTenantScope.Resolve(input.TenantId, _userManager.TenantId);
+        var tid = ResolveTenantOrThrow();
         var where = new List<string> { "a.tenant_id = @TenantFilter" };
-        var pars = new List<SugarParameter> { new("@TenantFilter", tenantId) };
+        var pars = new List<SugarParameter> { new("@TenantFilter", tid) };
 
         void AddLike(string? v, string col, string p)
         {
@@ -236,11 +249,11 @@ public class FqcInspBillService : IDynamicApiController, ITransient
     /// </summary>
     [DisplayName("FQC检验单详情")]
     [HttpGet("detail")]
-    public async Task<FqcInspBillDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
+    public async Task<FqcInspBillDetailDto?> GetDetail([FromQuery] long id)
     {
         if (id <= 0) return null;
 
-        var tid = QmsTenantScope.Resolve(tenantId, _userManager.TenantId);
+        var tid = ResolveTenantOrThrow();
         var head = (await _db.Ado.SqlQueryAsync<FqcInspBillDetailDto>(
             """
             SELECT

+ 21 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/ProductionInboundService.cs

@@ -18,15 +18,30 @@ namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 310, Description = "生产入库单列表")]
 [Route("api/S7ProductionInbound")]
-[AllowAnonymous]
 [NonUnify]
 public class ProductionInboundService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public ProductionInboundService(ISqlSugarClient db)
+    public ProductionInboundService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -40,14 +55,13 @@ public class ProductionInboundService : IDynamicApiController, ITransient
         var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
         var offset = (page - 1) * pageSize;
 
+        // 强制租户过滤:租户只来自认证后的可信上下文(严格 JWT),忽略前端传入的 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
         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));
-        }
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.InboundNo))
         {
             where.Add("m.nbr LIKE @InboundNo");

+ 21 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/FinishedWarehouse/SalesDeliveryNoticeService.cs

@@ -19,15 +19,30 @@ namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 320, Description = "销售发货通知")]
 [Route("api/S7SalesDeliveryNotice")]
-[AllowAnonymous]
 [NonUnify]
 public class SalesDeliveryNoticeService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public SalesDeliveryNoticeService(ISqlSugarClient db)
+    public SalesDeliveryNoticeService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>排序字段白名单(前端 prop → 物理列),杜绝 orderBy 注入。</summary>
@@ -52,14 +67,13 @@ public class SalesDeliveryNoticeService : IDynamicApiController, ITransient
         var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
         var offset = (page - 1) * pageSize;
 
+        // 强制租户过滤:租户只来自认证后的可信上下文(严格 JWT),忽略前端传入的 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
         var where = new List<string> { "a.ShType = 'SH'", "a.Typed <> 'S'", "a.IsActive = 1" };
         var pars = new List<SugarParameter>();
 
-        if (input.TenantId.HasValue && input.TenantId.Value > 0)
-        {
-            where.Add("a.tenant_id = @TenantId");
-            pars.Add(new SugarParameter("@TenantId", input.TenantId.Value));
-        }
+        where.Add("a.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.Id))
         {
             where.Add("a.Id LIKE @Id");

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/OutsourceIssueDto.cs

@@ -28,9 +28,6 @@ public class OutsourceIssueListInput
 
     /// <summary>排序方向(asc / desc)</summary>
     public string? SortOrder { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionIssueDto.cs

@@ -34,9 +34,6 @@ public class ProductionIssueListInput
 
     /// <summary>排序方向(asc / desc)</summary>
     public string? OrderDir { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionReceiptDto.cs

@@ -34,9 +34,6 @@ public class ProductionReceiptListInput
 
     /// <summary>排序方向(asc / desc)</summary>
     public string? OrderDir { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/ProductionReturnDto.cs

@@ -34,9 +34,6 @@ public class ProductionReturnListInput
 
     /// <summary>排序方向(asc / desc)</summary>
     public string? OrderDir { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 0 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/PurchaseReceiptDto.cs

@@ -43,9 +43,6 @@ public class PurchaseReceiptListInput
 
     /// <summary>排序方向(asc / desc)</summary>
     public string? OrderDir { get; set; }
-
-    /// <summary>租户 ID(前端 withAidopTenantParams 注入;为空则不按租户过滤)</summary>
-    public long? TenantId { get; set; }
 }
 
 /// <summary>

+ 28 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/OutsourceIssueService.cs

@@ -13,15 +13,30 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 304, Description = "委外发料单")]
 [Route("api/OutsourceIssue")]
-[AllowAnonymous]
 [NonUnify]
 public class OutsourceIssueService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public OutsourceIssueService(ISqlSugarClient db)
+    public OutsourceIssueService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -38,11 +53,10 @@ public class OutsourceIssueService : IDynamicApiController, ITransient
         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));
-        }
+        // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.BillNo))
         {
             where.Add("m.bill_no LIKE @BillNo");
@@ -98,17 +112,14 @@ public class OutsourceIssueService : IDynamicApiController, ITransient
     /// </summary>
     [DisplayName("委外发料单详情")]
     [HttpGet("detail")]
-    public async Task<OutsourceIssueDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
+    public async Task<OutsourceIssueDetailDto?> GetDetail([FromQuery] long id)
     {
         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));
-        }
+        // 强制租户过滤:头必须归属可信 JWT 租户,忽略前端 tenantId。
+        var tid = ResolveTenantOrThrow();
+        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<OutsourceIssueDetailDto>(
             $"""
@@ -146,10 +157,10 @@ public class OutsourceIssueService : IDynamicApiController, ITransient
                 d.status        AS Status,
                 d.remark        AS Remark
             FROM mdp_std_outsource_issue_detail d
-            WHERE d.std_head_id = @HeadId
+            WHERE d.std_head_id = @HeadId AND d.tenant_id = @TenantId
             ORDER BY d.line ASC, d.id ASC
             """,
-            new List<SugarParameter> { new("@HeadId", head.Id) });
+            new List<SugarParameter> { new("@HeadId", head.Id), new("@TenantId", tid) });
 
         return head;
     }

+ 21 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionIssueService.cs

@@ -13,15 +13,30 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 305, Description = "生产领料单")]
 [Route("api/ProductionIssue")]
-[AllowAnonymous]
 [NonUnify]
 public class ProductionIssueService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public ProductionIssueService(ISqlSugarClient db)
+    public ProductionIssueService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -38,11 +53,10 @@ public class ProductionIssueService : IDynamicApiController, ITransient
         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));
-        }
+        // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.Nbr))
         {
             where.Add("m.nbr LIKE @Nbr");

+ 26 - 15
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionReceiptService.cs

@@ -14,15 +14,30 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 307, Description = "生产入库单")]
 [Route("api/S5ProductionReceipt")]
-[AllowAnonymous]
 [NonUnify]
 public class ProductionReceiptService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public ProductionReceiptService(ISqlSugarClient db)
+    public ProductionReceiptService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -39,11 +54,10 @@ public class ProductionReceiptService : IDynamicApiController, ITransient
         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));
-        }
+        // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.Nbr))
         {
             where.Add("m.nbr LIKE @Nbr");
@@ -98,17 +112,14 @@ public class ProductionReceiptService : IDynamicApiController, ITransient
     /// </summary>
     [DisplayName("生产入库单详情")]
     [HttpGet("detail")]
-    public async Task<List<ProductionReceiptListRow>> GetDetail([FromQuery] string nbr, [FromQuery] long? tenantId)
+    public async Task<List<ProductionReceiptListRow>> GetDetail([FromQuery] string nbr)
     {
         if (string.IsNullOrWhiteSpace(nbr)) return new List<ProductionReceiptListRow>();
 
-        var pars = new List<SugarParameter> { new("@Nbr", nbr.Trim()) };
-        var where = "m.nbr = @Nbr";
-        if (tenantId is > 0)
-        {
-            where += " AND m.tenant_id = @TenantId";
-            pars.Add(new SugarParameter("@TenantId", tenantId));
-        }
+        // 强制租户过滤:明细行必须归属可信 JWT 租户,忽略前端 tenantId。
+        var tid = ResolveTenantOrThrow();
+        var pars = new List<SugarParameter> { new("@Nbr", nbr.Trim()), new("@TenantId", tid) };
+        var where = "m.nbr = @Nbr AND m.tenant_id = @TenantId";
 
         return await _db.Ado.SqlQueryAsync<ProductionReceiptListRow>(
             $"""

+ 28 - 17
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/ProductionReturnService.cs

@@ -13,15 +13,30 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 306, Description = "生产退料单")]
 [Route("api/ProductionReturn")]
-[AllowAnonymous]
 [NonUnify]
 public class ProductionReturnService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public ProductionReturnService(ISqlSugarClient db)
+    public ProductionReturnService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -38,11 +53,10 @@ public class ProductionReturnService : IDynamicApiController, ITransient
         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));
-        }
+        // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.Nbr))
         {
             where.Add("m.nbr LIKE @Nbr");
@@ -108,17 +122,14 @@ public class ProductionReturnService : IDynamicApiController, ITransient
     /// </summary>
     [DisplayName("生产退料单详情")]
     [HttpGet("detail")]
-    public async Task<ProductionReturnDetailDto?> GetDetail([FromQuery] long id, [FromQuery] long? tenantId)
+    public async Task<ProductionReturnDetailDto?> GetDetail([FromQuery] long id)
     {
         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));
-        }
+        // 强制租户过滤:头必须归属可信 JWT 租户,忽略前端 tenantId。
+        var tid = ResolveTenantOrThrow();
+        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<ProductionReturnDetailDto>(
             $"""
@@ -158,10 +169,10 @@ public class ProductionReturnService : IDynamicApiController, ITransient
                 d.status_desc      AS StatusDesc,
                 d.remark           AS Remark
             FROM mdp_std_production_return_detail d
-            WHERE d.std_head_id = @HeadId
+            WHERE d.std_head_id = @HeadId AND d.tenant_id = @TenantId
             ORDER BY d.line ASC, d.id ASC
             """,
-            new List<SugarParameter> { new("@HeadId", head.Id) });
+            new List<SugarParameter> { new("@HeadId", head.Id), new("@TenantId", tid) });
 
         return head;
     }

+ 21 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/PurchaseReceiptService.cs

@@ -12,15 +12,30 @@ namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
 /// </summary>
 [ApiDescriptionSettings(Order = 305, Description = "采购收货单")]
 [Route("api/PurchaseReceipt")]
-[AllowAnonymous]
 [NonUnify]
 public class PurchaseReceiptService : IDynamicApiController, ITransient
 {
+    /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
+    private const long MainTenantId = 1300000000001L;
+
     private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
 
-    public PurchaseReceiptService(ISqlSugarClient db)
+    public PurchaseReceiptService(ISqlSugarClient db, UserManager userManager)
     {
         _db = db;
+        _userManager = userManager;
+    }
+
+    /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
+    private long ResolveTenantOrThrow()
+    {
+        var tid = _userManager.TenantId;
+        if (tid <= 0)
+            throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
+        if (_userManager.SuperAdmin && tid == MainTenantId)
+            throw Oops.Oh("超级管理员操作前必须选择目标租户");
+        return tid;
     }
 
     /// <summary>
@@ -37,11 +52,10 @@ public class PurchaseReceiptService : IDynamicApiController, ITransient
         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));
-        }
+        // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
+        var tid = ResolveTenantOrThrow();
+        where.Add("m.tenant_id = @TenantId");
+        pars.Add(new SugarParameter("@TenantId", tid));
         if (!string.IsNullOrWhiteSpace(input.Supp))
         {
             where.Add("(m.supp LIKE @Supp OR m.sort_name LIKE @Supp)");