瀏覽代碼

feat(s0,s8): add tenant contracts and detection logs

YY968XX 6 月之前
父節點
當前提交
808bf058fd
共有 45 個文件被更改,包括 549 次插入 和 68 次删除
  1. 8 0
      Web/src/views/aidop/s8/api/s8ExceptionApi.ts
  2. 50 0
      Web/src/views/aidop/s8/exceptions/S8ExceptionListPage.vue
  3. 52 0
      Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue
  4. 71 0
      server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Tenant/S0TenantEntityContractTests.cs
  5. 24 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ConfigDetectionLogsController.cs
  6. 26 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Quality/AdoS0QualityDtos.cs
  7. 46 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8DetectionLogDtos.cs
  8. 14 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs
  9. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0DevMonitor.cs
  10. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0EmpSkills.cs
  11. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ImageType.cs
  12. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ItemOpCondition.cs
  13. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineMaster.cs
  14. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineSkillDetail.cs
  15. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineSkillMaster.cs
  16. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0MfgMastersAndSop.cs
  17. 1 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0MfgRoutingOpDetail.cs
  18. 8 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0PreprocessEntities.cs
  19. 4 5
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ProdLineDetail.cs
  20. 8 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ProductStructure.cs
  21. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0QualitySegmentImage.cs
  22. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0StdOpMaster.cs
  23. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0WorkCtrMaster.cs
  24. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0WorkOrdControl.cs
  25. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0CustMaster.cs
  26. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemMaster.cs
  27. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemSubstituteDetail.cs
  28. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0PriorityCode.cs
  29. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/IcSubstitute.cs
  30. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Supply/AdoS0SrmPurchase.cs
  31. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Supply/AdoS0SuppMaster.cs
  32. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0BarCodeType.cs
  33. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0CostCtrMaster.cs
  34. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0DepartmentMaster.cs
  35. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0EmpWorkDutyMaster.cs
  36. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0EmployeeMaster.cs
  37. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0ItemPackMaster.cs
  38. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0LocationMaster.cs
  39. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0LocationShelfMaster.cs
  40. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0NbrControl.cs
  41. 4 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0NbrTypeMaster.cs
  42. 65 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DetectionLogQueryService.cs
  43. 43 19
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs
  44. 8 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MasterDataAdapter.cs
  45. 5 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8TaskFlowService.cs

+ 8 - 0
Web/src/views/aidop/s8/api/s8ExceptionApi.ts

@@ -36,6 +36,14 @@ export interface S8ExceptionRow {
 	verifiedAt?: string | null;
 	verificationResult?: string | null;
 	verificationRemark?: string | null;
+	exceptionTypeCode?: string | null;
+	recoveredAt?: string | null;
+	sourceRuleCode?: string | null;
+	sourceObjectType?: string | null;
+	sourceObjectId?: string | null;
+	dedupKey?: string | null;
+	lastDetectedAt?: string | null;
+	ruleType?: string | null;
 }
 
 export interface S8DecisionRow {

+ 50 - 0
Web/src/views/aidop/s8/exceptions/S8ExceptionListPage.vue

@@ -24,6 +24,22 @@
 					<el-option v-for="s in filterOpts.departments" :key="s.value" :label="s.label" :value="s.value" />
 				</el-select>
 			</el-form-item>
+			<el-form-item label="规则类型">
+				<el-select v-model="query.ruleType" clearable placeholder="全部" style="width: 150px">
+					<el-option label="OUT_OF_RANGE" value="OUT_OF_RANGE" />
+					<el-option label="TIMEOUT" value="TIMEOUT" />
+					<el-option label="SHORTAGE" value="SHORTAGE" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="恢复状态">
+				<el-select v-model="query.recoveredStatus" clearable placeholder="全部" style="width: 130px">
+					<el-option label="已恢复" value="RECOVERED" />
+					<el-option label="未恢复" value="ACTIVE" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="包含未分类">
+				<el-switch v-model="query.includeUnclassified" />
+			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="loadList">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
@@ -42,6 +58,18 @@
 					<el-tag :type="row.timeoutFlag ? 'danger' : 'info'" size="small">{{ row.timeoutFlag ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
+			<el-table-column label="规则类型" width="130" align="center">
+				<template #default="{ row }">
+					<el-tag v-if="row.ruleType" :type="ruleTypeTagType(row.ruleType)" size="small">{{ row.ruleType }}</el-tag>
+					<el-tag v-else-if="!row.exceptionTypeCode" type="info" size="small">未分类</el-tag>
+					<span v-else>-</span>
+				</template>
+			</el-table-column>
+			<el-table-column label="恢复" width="90" align="center">
+				<template #default="{ row }">
+					<el-tag :type="row.recoveredAt ? 'success' : 'warning'" size="small">{{ row.recoveredAt ? '已恢复' : '未恢复' }}</el-tag>
+				</template>
+			</el-table-column>
 			<el-table-column prop="createdAt" label="创建时间" width="170" />
 		</el-table>
 
@@ -91,12 +119,28 @@ const query = reactive({
 	sceneCode: '' as string,
 	deptId: undefined as number | undefined,
 	timeoutFlag: undefined as boolean | undefined,
+	ruleType: '' as string,
+	recoveredStatus: '' as string,
+	includeUnclassified: false,
 	page: 1,
 	pageSize: 20,
 	tenantId: 1,
 	factoryId: 1,
 });
 
+function ruleTypeTagType(t: string | null | undefined) {
+	switch (t) {
+		case 'OUT_OF_RANGE':
+			return 'danger';
+		case 'TIMEOUT':
+			return 'warning';
+		case 'SHORTAGE':
+			return 'primary';
+		default:
+			return 'info';
+	}
+}
+
 async function loadFilters() {
 	const fo = (await s8ExceptionApi.filterOptions()) as {
 		statuses: { value: string; label: string }[];
@@ -122,6 +166,9 @@ async function loadList() {
 			deptId: query.deptId || undefined,
 			keyword: query.keyword || undefined,
 			timeoutFlag: query.timeoutFlag,
+			ruleType: query.ruleType || undefined,
+			recoveredStatus: query.recoveredStatus || undefined,
+			includeUnclassified: query.includeUnclassified || undefined,
 		});
 		rows.value = res.list;
 		total.value = res.total;
@@ -138,6 +185,9 @@ function resetQuery() {
 	query.sceneCode = '';
 	query.deptId = undefined;
 	query.timeoutFlag = undefined;
+	query.ruleType = '';
+	query.recoveredStatus = '';
+	query.includeUnclassified = false;
 	query.page = 1;
 	void loadList();
 }

+ 52 - 0
Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue

@@ -160,6 +160,19 @@ async function submitAction() {
 	}
 }
 
+function ruleTypeTagType(t: string | null | undefined) {
+	switch (t) {
+		case 'OUT_OF_RANGE':
+			return 'danger';
+		case 'TIMEOUT':
+			return 'warning';
+		case 'SHORTAGE':
+			return 'primary';
+		default:
+			return 'info';
+	}
+}
+
 onMounted(async () => {
 	await loadDetail();
 	await loadEmployees();
@@ -220,6 +233,34 @@ onMounted(async () => {
 				</el-col>
 			</el-row>
 
+			<el-row :gutter="16" class="mt16">
+				<el-col :span="24">
+					<el-card shadow="never">
+						<template #header>规则检测信息</template>
+						<el-descriptions :column="3" border>
+							<el-descriptions-item label="规则类型">
+								<el-tag v-if="detail.ruleType" :type="ruleTypeTagType(detail.ruleType)" size="small">{{ detail.ruleType }}</el-tag>
+								<span v-else>—</span>
+							</el-descriptions-item>
+							<el-descriptions-item label="规则编码">{{ detail.sourceRuleCode || '—' }}</el-descriptions-item>
+							<el-descriptions-item label="来源对象类型">{{ detail.sourceObjectType || '—' }}</el-descriptions-item>
+							<el-descriptions-item label="来源对象 ID">{{ detail.sourceObjectId || '—' }}</el-descriptions-item>
+							<el-descriptions-item label="最近检测时间">{{ detail.lastDetectedAt || '—' }}</el-descriptions-item>
+							<el-descriptions-item label="恢复时间">
+								<el-tag v-if="detail.recoveredAt" type="success" size="small">{{ detail.recoveredAt }}</el-tag>
+								<el-tag v-else type="warning" size="small">未恢复</el-tag>
+							</el-descriptions-item>
+							<el-descriptions-item label="去重键" :span="3">
+								<el-tooltip v-if="detail.dedupKey" :content="detail.dedupKey" placement="top">
+									<span class="dedup-key">{{ detail.dedupKey }}</span>
+								</el-tooltip>
+								<span v-else>—</span>
+							</el-descriptions-item>
+						</el-descriptions>
+					</el-card>
+				</el-col>
+			</el-row>
+
 			<el-row :gutter="16" class="mt16">
 				<el-col :span="12">
 					<el-card shadow="never">
@@ -386,4 +427,15 @@ onMounted(async () => {
 	justify-content: space-between;
 	gap: 12px;
 }
+
+.dedup-key {
+	display: inline-block;
+	max-width: 100%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	vertical-align: bottom;
+	font-family: ui-monospace, monospace;
+	font-size: 12px;
+}
 </style>

+ 71 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Tenant/S0TenantEntityContractTests.cs

@@ -0,0 +1,71 @@
+using Admin.NET.Core;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
+using SqlSugar;
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.S0.Tenant;
+
+/// <summary>
+/// S0 tenant isolation contract for entities whose DB tables already contain tenant_id.
+/// </summary>
+public class S0TenantEntityContractTests
+{
+    public static IEnumerable<object[]> TenantScopedEntityTypes()
+    {
+        yield return [typeof(AdoS0BarCodeType)];
+        yield return [typeof(AdoS0CostCtrMaster)];
+        yield return [typeof(AdoS0CustMaster)];
+        yield return [typeof(AdoS0DepartmentMaster)];
+        yield return [typeof(AdoS0DevMonitor)];
+        yield return [typeof(AdoS0EmpSkills)];
+        yield return [typeof(AdoS0EmpWorkDutyMaster)];
+        yield return [typeof(AdoS0EmployeeMaster)];
+        yield return [typeof(AdoS0ImageType)];
+        yield return [typeof(AdoS0ItemMaster)];
+        yield return [typeof(AdoS0ItemOpCondition)];
+        yield return [typeof(AdoS0ItemPackMaster)];
+        yield return [typeof(AdoS0ItemSubstituteDetail)];
+        yield return [typeof(AdoS0LineMaster)];
+        yield return [typeof(AdoS0LineSkillDetail)];
+        yield return [typeof(AdoS0LineSkillMaster)];
+        yield return [typeof(AdoS0LocationMaster)];
+        yield return [typeof(AdoS0LocationShelfMaster)];
+        yield return [typeof(AdoS0NbrControl)];
+        yield return [typeof(AdoS0NbrTypeMaster)];
+        yield return [typeof(AdoS0MfgPersonSkillAssignment)];
+        yield return [typeof(AdoS0MfgPreprocessElement)];
+        yield return [typeof(AdoS0MfgPreprocessElementParam)];
+        yield return [typeof(AdoS0PriorityCode)];
+        yield return [typeof(AdoS0ProdLineDetail)];
+        yield return [typeof(AdoS0ProductStructureMaster)];
+        yield return [typeof(AdoS0ProductStructureOp)];
+        yield return [typeof(AdoS0QualitySegmentImage)];
+        yield return [typeof(AdoS0MfgRoutingOpDetail)];
+        yield return [typeof(AdoS0StdOpMaster)];
+        yield return [typeof(AdoS0SuppMaster)];
+        yield return [typeof(AdoS0WorkCtrMaster)];
+        yield return [typeof(AdoS0WorkOrdControl)];
+        yield return [typeof(IcSubstitute)];
+        yield return [typeof(AdoS0SrmPurchase)];
+    }
+
+    [Theory]
+    [MemberData(nameof(TenantScopedEntityTypes))]
+    public void TenantScopedS0Entities_ImplementTenantFilterAndMapTenantId(Type entityType)
+    {
+        Assert.True(typeof(ITenantIdFilter).IsAssignableFrom(entityType), $"{entityType.Name} must implement ITenantIdFilter");
+
+        var property = entityType.GetProperty(nameof(ITenantIdFilter.TenantId));
+        Assert.NotNull(property);
+        Assert.Equal(typeof(long?), property!.PropertyType);
+
+        var sugarColumn = property.GetCustomAttributes(typeof(SugarColumn), inherit: true)
+            .OfType<SugarColumn>()
+            .SingleOrDefault();
+        Assert.NotNull(sugarColumn);
+        Assert.Equal("tenant_id", sugarColumn!.ColumnName);
+    }
+}

+ 24 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ConfigDetectionLogsController.cs

@@ -0,0 +1,24 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Service.S8;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
+
+[ApiController]
+[Route("api/aidop/s8/config/detection-logs")]
+[NonUnify]
+public class AdoS8ConfigDetectionLogsController : ControllerBase
+{
+    private readonly S8DetectionLogQueryService _svc;
+
+    public AdoS8ConfigDetectionLogsController(S8DetectionLogQueryService svc)
+    {
+        _svc = svc;
+    }
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS8DetectionLogQueryDto q)
+    {
+        var (total, list) = await _svc.GetPagedAsync(q);
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+}

+ 26 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Quality/AdoS0QualityDtos.cs

@@ -13,13 +13,38 @@ public class AdoS0QualitySimpleOptionDto
     public string Label { get; set; } = string.Empty;
 }
 
-public class AdoS0QmsRawWhitelistUpsertDto
+public class AdoS0QmsRawWhitelistUpsertDto : IValidatableObject
 {
     public string? SupplierCode { get; set; }
     public string? SupplierName { get; set; }
     public string? MaterialCode { get; set; }
     public string? MaterialName { get; set; }
     public string? DimensionType { get; set; }
+
+    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+    {
+        var dimensionType = string.IsNullOrWhiteSpace(DimensionType)
+            ? "supplier"
+            : DimensionType.Trim().ToLowerInvariant();
+
+        if (dimensionType is not ("supplier" or "material" or "material_supplier"))
+        {
+            yield return new ValidationResult("维度类型仅支持 supplier/material/material_supplier", [nameof(DimensionType)]);
+            yield break;
+        }
+
+        if ((dimensionType == "supplier" || dimensionType == "material_supplier")
+            && string.IsNullOrWhiteSpace(SupplierCode))
+            yield return new ValidationResult("供应商编码不能为空", [nameof(SupplierCode)]);
+
+        if ((dimensionType == "supplier" || dimensionType == "material_supplier")
+            && string.IsNullOrWhiteSpace(SupplierName))
+            yield return new ValidationResult("供应商名称不能为空", [nameof(SupplierName)]);
+
+        if ((dimensionType == "material" || dimensionType == "material_supplier")
+            && string.IsNullOrWhiteSpace(MaterialCode))
+            yield return new ValidationResult("物料编码不能为空", [nameof(MaterialCode)]);
+    }
 }
 
 public class AdoS0QmsSamplingSchemeUpsertDto

+ 46 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8DetectionLogDtos.cs

@@ -0,0 +1,46 @@
+namespace Admin.NET.Plugin.AiDOP.Dto.S8;
+
+public class AdoS8DetectionLogQueryDto
+{
+    public long TenantId { get; set; } = 1;
+    public long FactoryId { get; set; } = 1;
+    public string? RuleCode { get; set; }
+    public string? RuleType { get; set; }
+    public string? DetectResult { get; set; }
+    public long? ExceptionId { get; set; }
+    public string? DedupKey { get; set; }
+    public string? SourceObjectType { get; set; }
+    public string? SourceObjectId { get; set; }
+    public string? TriggerSource { get; set; }
+    [Microsoft.AspNetCore.Mvc.FromQuery(Name = "from")]
+    public DateTime? BeginTime { get; set; }
+    [Microsoft.AspNetCore.Mvc.FromQuery(Name = "to")]
+    public DateTime? EndTime { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS8DetectionLogListItemDto
+{
+    public long Id { get; set; }
+    public long TenantId { get; set; }
+    public long FactoryId { get; set; }
+    public string? RunId { get; set; }
+    public long? RuleId { get; set; }
+    public string? RuleCode { get; set; }
+    public string? RuleType { get; set; }
+    public string? SceneCode { get; set; }
+    public string DetectResult { get; set; } = string.Empty;
+    public long? ExceptionId { get; set; }
+    public string? DedupKey { get; set; }
+    public string? SourceObjectType { get; set; }
+    public string? SourceObjectId { get; set; }
+    public string? RelatedObjectCode { get; set; }
+    public string? TriggerSource { get; set; }
+    public DateTime DetectedAt { get; set; }
+    public DateTime CreatedAt { get; set; }
+    public string? PayloadSnapshot { get; set; }
+    public string? FailureReason { get; set; }
+    public string? FailureMessage { get; set; }
+    public string? Remark { get; set; }
+}

+ 14 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs

@@ -17,6 +17,12 @@ public class AdoS8ExceptionQueryDto
     public string? RelatedObjectCode { get; set; }
     public DateTime? BeginTime { get; set; }
     public DateTime? EndTime { get; set; }
+    /// <summary>是否包含 exception_type_code 为 NULL 的未分类历史异常;默认 false(隐藏)。</summary>
+    public bool? IncludeUnclassified { get; set; }
+    /// <summary>恢复状态筛选:RECOVERED / ACTIVE / 空=不限。</summary>
+    public string? RecoveredStatus { get; set; }
+    /// <summary>规则类型筛选:OUT_OF_RANGE / TIMEOUT / SHORTAGE / 空=不限;通过 source_rule_code join watch_rule.rule_type。</summary>
+    public string? RuleType { get; set; }
     public int Page { get; set; } = 1;
     public int PageSize { get; set; } = 20;
 }
@@ -43,6 +49,14 @@ public class AdoS8ExceptionListItemDto
     public bool TimeoutFlag { get; set; }
     public DateTime CreatedAt { get; set; }
     public DateTime? ClosedAt { get; set; }
+    public string? ExceptionTypeCode { get; set; }
+    public DateTime? RecoveredAt { get; set; }
+    public string? SourceRuleCode { get; set; }
+    public string? SourceObjectType { get; set; }
+    public string? SourceObjectId { get; set; }
+    public string? DedupKey { get; set; }
+    public DateTime? LastDetectedAt { get; set; }
+    public string? RuleType { get; set; }
 }
 
 public class AdoS8ExceptionDetailDto : AdoS8ExceptionListItemDto
@@ -66,12 +80,6 @@ public class AdoS8ExceptionDetailDto : AdoS8ExceptionListItemDto
     public string? VerificationRemark { get; set; }
     public long? SourceRuleId { get; set; }
     public string? RelatedObjectCode { get; set; }
-    public string? DedupKey { get; set; }
-    public DateTime? LastDetectedAt { get; set; }
-    public DateTime? RecoveredAt { get; set; }
-    public string? SourceRuleCode { get; set; }
-    public string? SourceObjectType { get; set; }
-    public string? SourceObjectId { get; set; }
 }
 
 public class AdoS8ManualReportCreateDto

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0DevMonitor.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 生产要素参数(源平台 DevMonitor)。
 /// </summary>
 [SugarTable("DevMonitor", "生产要素参数表")]
-public class AdoS0DevMonitor
+public class AdoS0DevMonitor : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "生产要素参数主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -56,4 +56,7 @@ public class AdoS0DevMonitor
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0EmpSkills.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 人员技能维护(源平台 EmpSkills:员工与技能关系表)。
 /// </summary>
 [SugarTable("EmpSkills", "人员技能关系表")]
-public class AdoS0EmpSkills
+public class AdoS0EmpSkills : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "人员技能主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -50,5 +50,8 @@ public class AdoS0EmpSkills
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ImageType.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("ImageType", "S0 SOP 文件类型(ImageType)")]
 [SugarIndex("uk_ImageType_scope", nameof(Domain), OrderByType.Asc, nameof(ImageTypeID), OrderByType.Asc, IsUnique = true)]
-public class AdoS0ImageType
+public class AdoS0ImageType : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "文件类型主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -18,5 +18,8 @@ public class AdoS0ImageType
 
     [SugarColumn(ColumnName = "domain", ColumnDescription = "工厂域编码", Length = 50)]
     public string Domain { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ItemOpCondition.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 物料工序生产要素(源平台 ItemOpCondition)。
 /// </summary>
 [SugarTable("ItemOpCondition", "物料工序生产要素表")]
-public class AdoS0ItemOpCondition
+public class AdoS0ItemOpCondition : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "物料工序生产要素主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -47,4 +47,7 @@ public class AdoS0ItemOpCondition
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("LineMaster", "生产线主数据表")]
 [SugarIndex("uk_LineMaster_scope_line", nameof(CompanyRefId), OrderByType.Asc, nameof(FactoryRefId), OrderByType.Asc, nameof(Line), OrderByType.Asc, IsUnique = true)]
-public class AdoS0LineMaster
+public class AdoS0LineMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "生产线主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -66,4 +66,7 @@ public class AdoS0LineMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineSkillDetail.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 产线岗位技能明细(子表)。源平台子表结构待确认;当前以“岗位→人员技能主数据引用”占位实现。
 /// </summary>
 [SugarTable("LineSkillDetail", "产线岗位技能明细")]
-public class AdoS0LineSkillDetail
+public class AdoS0LineSkillDetail : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "明细主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -29,5 +29,8 @@ public class AdoS0LineSkillDetail
 
     [SugarColumn(ColumnName = "CreateTime", ColumnDescription = "创建时间")]
     public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0LineSkillMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("LineSkillMaster", "产线岗位主表")]
 [SugarIndex("uk_LineSkillMaster_domain_job", nameof(Domain), OrderByType.Asc, nameof(JOBNo), OrderByType.Asc, IsUnique = true)]
-public class AdoS0LineSkillMaster
+public class AdoS0LineSkillMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "产线岗位主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -33,5 +33,8 @@ public class AdoS0LineSkillMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0MfgMastersAndSop.cs

@@ -1,7 +1,7 @@
 namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 
 [SugarTable("PersonSkillAssignment", "S0 人员技能分配")]
-public class AdoS0MfgPersonSkillAssignment
+public class AdoS0MfgPersonSkillAssignment : ITenantIdFilter
 {
     [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -38,6 +38,9 @@ public class AdoS0MfgPersonSkillAssignment
 
     [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdatedAt { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 
 [SugarTable("WorkOrderControl", "S0 工单控制参数")]

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0MfgRoutingOpDetail.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 标准工艺路线明细(源平台 RoutingOpDetail 行级复刻,方案 A)。
 /// </summary>
 [SugarTable("RoutingOpDetail", "S0 标准工艺路线明细")]
-public class AdoS0MfgRoutingOpDetail
+public class AdoS0MfgRoutingOpDetail : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }

+ 8 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0PreprocessEntities.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 前处理要素独立表。
 /// </summary>
 [SugarTable("PreprocessElement", "前处理要素表")]
-public class AdoS0MfgPreprocessElement
+public class AdoS0MfgPreprocessElement : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "前处理要素主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -44,13 +44,16 @@ public class AdoS0MfgPreprocessElement
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 
 /// <summary>
 /// 前处理要素参数独立表。
 /// </summary>
 [SugarTable("PreprocessElementParam", "前处理要素参数表")]
-public class AdoS0MfgPreprocessElementParam
+public class AdoS0MfgPreprocessElementParam : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "前处理要素参数主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -99,4 +102,7 @@ public class AdoS0MfgPreprocessElementParam
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ProdLineDetail.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// 生产线物料维护(源平台 ProdLineDetail)。
 /// </summary>
 [SugarTable("ProdLineDetail", "生产线物料维护表")]
-public class AdoS0ProdLineDetail
+public class AdoS0ProdLineDetail : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "生产线物料主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "int")]
     public int Id { get; set; }
@@ -27,10 +27,10 @@ public class AdoS0ProdLineDetail
     [SugarColumn(ColumnName = "SetupTime", ColumnDescription = "换模时间(小时)", DecimalDigits = 6, Length = 18, IsNullable = true)]
     public decimal? SetupTime { get; set; }
 
-    [SugarColumn(ColumnName = "OverlapTime", ColumnDescription = "重叠时间(小时)", DecimalDigits = 6, Length = 18, ColumnDataType = "decimal(18,6)")]
+    [SugarColumn(ColumnName = "OverlapTime", ColumnDescription = "重叠时间(小时)", DecimalDigits = 6, Length = 18)]
     public decimal OverlapTime { get; set; }
 
-    [SugarColumn(ColumnName = "Sequence", ColumnDescription = "顺序值", DecimalDigits = 6, Length = 18, ColumnDataType = "decimal(18,6)")]
+    [SugarColumn(ColumnName = "Sequence", ColumnDescription = "顺序值", DecimalDigits = 6, Length = 18)]
     public decimal Sequence { get; set; }
 
     [SugarColumn(ColumnName = "SkillNo", ColumnDescription = "员工技能编码", Length = 100, IsNullable = true)]
@@ -45,7 +45,7 @@ public class AdoS0ProdLineDetail
     [SugarColumn(ColumnName = "MoldTypeCode", ColumnDescription = "模具类型编码", Length = 100, IsNullable = true)]
     public string? MoldTypeCode { get; set; }
 
-    [SugarColumn(ColumnName = "StandardStaffCount", ColumnDescription = "标准人数", DecimalDigits = 6, Length = 18, ColumnDataType = "decimal(18,6)")]
+    [SugarColumn(ColumnName = "StandardStaffCount", ColumnDescription = "标准人数", DecimalDigits = 6, Length = 18)]
     public decimal StandardStaffCount { get; set; }
 
     [SugarColumn(ColumnName = "Start", ColumnDescription = "起始标记/顺序", IsNullable = true)]
@@ -84,4 +84,3 @@ public class AdoS0ProdLineDetail
     [SugarColumn(ColumnName = "Bom", ColumnDescription = "BOM编码", Length = 24, IsNullable = true)]
     public string? Bom { get; set; }
 }
-

+ 8 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ProductStructure.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("ProductStructureMaster", "标准BOM行维护(复刻 ProductStructureMaster)")]
 [SugarIndex("ix_ProductStructureMaster_factory_parent", nameof(CompanyRefId), OrderByType.Asc, nameof(FactoryRefId), OrderByType.Asc, nameof(ParentMaterialId), OrderByType.Asc)]
-public class AdoS0ProductStructureMaster
+public class AdoS0ProductStructureMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -61,6 +61,9 @@ public class AdoS0ProductStructureMaster
 
     [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdatedAt { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 
 /// <summary>
@@ -68,7 +71,7 @@ public class AdoS0ProductStructureMaster
 /// </summary>
 [SugarTable("ProductStructureOp", "标准BOM行工序(复刻 ProductStructureOp)")]
 [SugarIndex("ix_ProductStructureOp_factory_pc", nameof(CompanyRefId), OrderByType.Asc, nameof(FactoryRefId), OrderByType.Asc, nameof(ParentMaterialId), OrderByType.Asc, nameof(ComponentMaterialId), OrderByType.Asc)]
-public class AdoS0ProductStructureOp
+public class AdoS0ProductStructureOp : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -96,4 +99,7 @@ public class AdoS0ProductStructureOp
 
     [SugarColumn(ColumnDescription = "创建时间")]
     public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0QualitySegmentImage.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("QualitySegmentImage", "S0 SOP 维护(QualitySegmentImage)")]
 [SugarIndex("uk_QualitySegmentImage_scope", nameof(Domain), OrderByType.Asc, nameof(Line), OrderByType.Asc, nameof(ItemNum), OrderByType.Asc, nameof(Op), OrderByType.Asc, nameof(ImageTypeID), OrderByType.Asc, IsUnique = false)]
-public class AdoS0QualitySegmentImage
+public class AdoS0QualitySegmentImage : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "SOP 主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -36,5 +36,8 @@ public class AdoS0QualitySegmentImage
 
     [SugarColumn(ColumnName = "domain", ColumnDescription = "工厂域编码", Length = 50)]
     public string Domain { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0StdOpMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("StdOpMaster", "标准工序主数据表")]
 [SugarIndex("uk_StdOpMaster_scope_name", nameof(CompanyRefId), OrderByType.Asc, nameof(FactoryRefId), OrderByType.Asc, nameof(StdOp), OrderByType.Asc, IsUnique = true)]
-public class AdoS0StdOpMaster
+public class AdoS0StdOpMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "id", ColumnDescription = "标准工序主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -39,4 +39,7 @@ public class AdoS0StdOpMaster
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0WorkCtrMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("WorkCtrMaster", "工作中心主数据表")]
 [SugarIndex("uk_WorkCtrMaster_domain_ctr", nameof(Domain), OrderByType.Asc, nameof(WorkCtr), OrderByType.Asc, IsUnique = true)]
-public class AdoS0WorkCtrMaster
+public class AdoS0WorkCtrMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "工作中心主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -36,5 +36,8 @@ public class AdoS0WorkCtrMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0WorkOrdControl.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
 /// </summary>
 [SugarTable("WorkOrdControl", "S0 工单控制参数设置")]
 [SugarIndex("uk_WorkOrdControl_domain", nameof(Domain), OrderByType.Asc, IsUnique = true)]
-public class AdoS0WorkOrdControl
+public class AdoS0WorkOrdControl : ITenantIdFilter
 {
     [SugarColumn(ColumnDescription = "工单控制参数主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -54,5 +54,8 @@ public class AdoS0WorkOrdControl
 
     [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0CustMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 /// </summary>
 [SugarTable("CustMaster", "客户主数据(复刻 CustMaster)")]
 [SugarIndex("uk_CustMaster_factory_cust", nameof(FactoryRefId), OrderByType.Asc, nameof(Cust), OrderByType.Asc, IsUnique = true)]
-public class AdoS0CustMaster
+public class AdoS0CustMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -87,4 +87,7 @@ public class AdoS0CustMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 /// </summary>
 [SugarTable("ItemMaster", "物料主数据(复刻 ItemMaster)")]
 [SugarIndex("uk_ItemMaster_factory_item", nameof(FactoryRefId), OrderByType.Asc, nameof(ItemNum), OrderByType.Asc, IsUnique = true)]
-public class AdoS0ItemMaster
+public class AdoS0ItemMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -187,4 +187,7 @@ public class AdoS0ItemMaster
     /// <summary>列表展示:库位+库位说明拼接(不落库;无 LocationMaster 时仅库位)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? LocationDescr { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemSubstituteDetail.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 /// 物料替代关系(源平台 ItemSubstituteDetail 行级复刻)。
 /// </summary>
 [SugarTable("ItemSubstituteDetail", "物料替代关系表")]
-public class AdoS0ItemSubstituteDetail
+public class AdoS0ItemSubstituteDetail : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "物料替代关系主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -41,5 +41,8 @@ public class AdoS0ItemSubstituteDetail
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0PriorityCode.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 /// 订单优先级配置(复刻 PriorityCode)
 /// </summary>
 [SugarTable("PriorityCode", "订单优先级配置(复刻 PriorityCode)")]
-public class AdoS0PriorityCode
+public class AdoS0PriorityCode : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -76,4 +76,7 @@ public class AdoS0PriorityCode
     /// <summary>列表展示:正序/倒序(不落库)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? OrderByText { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/IcSubstitute.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 /// 注意:这是对既有业务表的映射,不代表新增业务实体表。
 /// </summary>
 [SugarTable("ic_substitute", "替代方案(源平台表映射)")]
-public class IcSubstitute
+public class IcSubstitute : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "Id", ColumnDescription = "主键", IsPrimaryKey = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -30,5 +30,8 @@ public class IcSubstitute
 
     [SugarColumn(ColumnName = "IsDeleted", ColumnDescription = "逻辑删除标记", IsNullable = true, ColumnDataType = "int")]
     public int? IsDeleted { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Supply/AdoS0SrmPurchase.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
 /// 货源清单 / 物料采购报价(复刻 srm_purchase,左联物料主数据仅用于展示)
 /// </summary>
 [SugarTable("srm_purchase", "货源清单(srm_purchase)")]
-public class AdoS0SrmPurchase
+public class AdoS0SrmPurchase : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "Id", ColumnDescription = "货源清单主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -119,4 +119,7 @@ public class AdoS0SrmPurchase
     /// <summary>列表:供应商名称+编码组合</summary>
     [SugarColumn(IsIgnore = true)]
     public string? Supplier { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Supply/AdoS0SuppMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
 /// </summary>
 [SugarTable("SuppMaster", "供应商主数据(复刻 SuppMaster)")]
 [SugarIndex("uk_SuppMaster_factory_supp", nameof(FactoryRefId), OrderByType.Asc, nameof(Supp), OrderByType.Asc, IsUnique = true)]
-public class AdoS0SuppMaster
+public class AdoS0SuppMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -108,5 +108,8 @@ public class AdoS0SuppMaster
     /// <summary>列表展示:对账周期2结束(展示字段,不落库)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? APEndDay2Text { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }
 

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0BarCodeType.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("BarCodeType", "标签格式头表(复刻 BarCodeType)")]
 [SugarIndex("uk_BarCodeType_domain_bartype", nameof(DomainCode), OrderByType.Asc, nameof(BarType), OrderByType.Asc, IsUnique = true)]
-public class AdoS0BarCodeType
+public class AdoS0BarCodeType : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "rec_id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -39,4 +39,7 @@ public class AdoS0BarCodeType
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0CostCtrMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("CostCtrMaster", "成本中心主数据(复刻 CostCtrMaster)")]
 [SugarIndex("uk_CostCtrMaster_domain_code", nameof(DomainCode), OrderByType.Asc, nameof(CostCtr), OrderByType.Asc, IsUnique = true)]
-public class AdoS0CostCtrMaster
+public class AdoS0CostCtrMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -42,4 +42,7 @@ public class AdoS0CostCtrMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0DepartmentMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("DepartmentMaster", "部门主数据(复刻 DepartmentMaster)")]
 [SugarIndex("uk_DepartmentMaster_domain_dept", nameof(DomainCode), OrderByType.Asc, nameof(Department), OrderByType.Asc, IsUnique = true)]
-public class AdoS0DepartmentMaster
+public class AdoS0DepartmentMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -39,4 +39,7 @@ public class AdoS0DepartmentMaster
 
     [SugarColumn(ColumnName = "UpdateTime", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0EmpWorkDutyMaster.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// 物料职责主数据(复刻 EmpWorkDutyMaster)
 /// </summary>
 [SugarTable("EmpWorkDutyMaster", "物料职责主数据(复刻 EmpWorkDutyMaster)")]
-public class AdoS0EmpWorkDutyMaster
+public class AdoS0EmpWorkDutyMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -76,4 +76,7 @@ public class AdoS0EmpWorkDutyMaster
     /// <summary>列表展示:物料范围说明(不落库)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? ItemNumDisplay { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0EmployeeMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("EmployeeMaster", "雇员主数据(复刻 EmployeeMaster)")]
 [SugarIndex("uk_EmployeeMaster_domain_employee", nameof(DomainCode), OrderByType.Asc, nameof(Employee), OrderByType.Asc, IsUnique = true)]
-public class AdoS0EmployeeMaster
+public class AdoS0EmployeeMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -89,4 +89,7 @@ public class AdoS0EmployeeMaster
     /// <summary>列表展示:岗位名称(不落库,来自 GeneralizedCodeMaster 翻译)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? JobTitleDisplay { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0ItemPackMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("ItemPackMaster", "零件包装规格主数据(复刻 ItemPackMaster)")]
 [SugarIndex("uk_ItemPackMaster_domain_itemnum", nameof(DomainCode), OrderByType.Asc, nameof(ItemNum), OrderByType.Asc, IsUnique = true)]
-public class AdoS0ItemPackMaster
+public class AdoS0ItemPackMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "RecID", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -77,4 +77,7 @@ public class AdoS0ItemPackMaster
     /// <summary>列表展示:物料型号(不落库,来自 ItemMaster.Descr1)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? ItemDescr1 { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0LocationMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("LocationMaster", "库位主数据(复刻 LocationMaster)")]
 [SugarIndex("uk_LocationMaster_domain_location", nameof(DomainCode), OrderByType.Asc, nameof(Location), OrderByType.Asc, IsUnique = true)]
-public class AdoS0LocationMaster
+public class AdoS0LocationMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "rec_id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -48,4 +48,7 @@ public class AdoS0LocationMaster
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0LocationShelfMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("LocationShelfMaster", "货架主数据(复刻 LocationShelfMaster)")]
 [SugarIndex("uk_LocationShelfMaster_domain_loc_shelf", nameof(DomainCode), OrderByType.Asc, nameof(Location), OrderByType.Asc, nameof(InvShelf), OrderByType.Asc, IsUnique = true)]
-public class AdoS0LocationShelfMaster
+public class AdoS0LocationShelfMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "rec_id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -50,4 +50,7 @@ public class AdoS0LocationShelfMaster
     /// <summary>列表展示:库位:货架 拼接(不落库)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? KwhjName { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0NbrControl.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("NbrControl", "单号规则主数据(复刻 NbrControl)")]
 [SugarIndex("uk_NbrControl_domain_nbrtype", nameof(DomainCode), OrderByType.Asc, nameof(NbrType), OrderByType.Asc, IsUnique = true)]
-public class AdoS0NbrControl
+public class AdoS0NbrControl : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "rec_id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -69,4 +69,7 @@ public class AdoS0NbrControl
 
     [SugarColumn(ColumnName = "update_time", ColumnDescription = "更新时间", IsNullable = true)]
     public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 4 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0NbrTypeMaster.cs

@@ -5,7 +5,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 /// </summary>
 [SugarTable("NbrTypeMaster", "单号类型主数据(复刻 NbrTypeMaster)")]
 [SugarIndex("uk_NbrTypeMaster_domain_nbrtype", nameof(DomainCode), OrderByType.Asc, nameof(NbrType), OrderByType.Asc, IsUnique = true)]
-public class AdoS0NbrTypeMaster
+public class AdoS0NbrTypeMaster : ITenantIdFilter
 {
     [SugarColumn(ColumnName = "rec_id", ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
     public long Id { get; set; }
@@ -49,4 +49,7 @@ public class AdoS0NbrTypeMaster
     /// <summary>列表展示:部门说明(不落库,来自 DepartmentMaster 关联)</summary>
     [SugarColumn(IsIgnore = true)]
     public string? DeptDescr { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
 }

+ 65 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DetectionLogQueryService.cs

@@ -0,0 +1,65 @@
+using Admin.NET.Plugin.AiDOP.Dto.S8;
+using Admin.NET.Plugin.AiDOP.Entity.S8;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Service.S8;
+
+public class S8DetectionLogQueryService : ITransient
+{
+    private readonly SqlSugarRepository<AdoS8DetectionLog> _rep;
+
+    public S8DetectionLogQueryService(SqlSugarRepository<AdoS8DetectionLog> rep)
+    {
+        _rep = rep;
+    }
+
+    public async Task<(int total, List<AdoS8DetectionLogListItemDto> list)> GetPagedAsync(AdoS8DetectionLogQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .Where(x => x.TenantId == q.TenantId && x.FactoryId == q.FactoryId && !x.IsDeleted)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.RuleCode), x => x.RuleCode == q.RuleCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.RuleType), x => x.RuleType == q.RuleType)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DetectResult), x => x.DetectResult == q.DetectResult)
+            .WhereIF(q.ExceptionId.HasValue, x => x.ExceptionId == q.ExceptionId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DedupKey), x => x.DedupKey == q.DedupKey)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.SourceObjectType), x => x.SourceObjectType == q.SourceObjectType)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.SourceObjectId), x => x.SourceObjectId == q.SourceObjectId)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.TriggerSource), x => x.TriggerSource == q.TriggerSource)
+            .WhereIF(q.BeginTime.HasValue, x => x.DetectedAt >= q.BeginTime!.Value)
+            .WhereIF(q.EndTime.HasValue, x => x.DetectedAt <= q.EndTime!.Value);
+
+        var total = await query.CountAsync();
+        var list = await query
+            .OrderBy(x => x.DetectedAt, OrderByType.Desc)
+            .OrderBy(x => x.Id, OrderByType.Desc)
+            .Select(x => new AdoS8DetectionLogListItemDto
+            {
+                Id = x.Id,
+                TenantId = x.TenantId,
+                FactoryId = x.FactoryId,
+                RunId = x.RunId,
+                RuleId = x.RuleId,
+                RuleCode = x.RuleCode,
+                RuleType = x.RuleType,
+                SceneCode = x.SceneCode,
+                DetectResult = x.DetectResult,
+                ExceptionId = x.ExceptionId,
+                DedupKey = x.DedupKey,
+                SourceObjectType = x.SourceObjectType,
+                SourceObjectId = x.SourceObjectId,
+                RelatedObjectCode = x.RelatedObjectCode,
+                TriggerSource = x.TriggerSource,
+                DetectedAt = x.DetectedAt,
+                CreatedAt = x.CreatedAt,
+                PayloadSnapshot = x.PayloadSnapshot,
+                FailureReason = x.FailureReason,
+                FailureMessage = x.FailureMessage,
+                Remark = x.Remark
+            })
+            .ToPageListAsync(q.Page, q.PageSize);
+
+        return (total, list);
+    }
+}

+ 43 - 19
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs

@@ -31,29 +31,37 @@ public class S8ExceptionService : ITransient
         (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
         var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" };
 
+        var includeUnclassified = q.IncludeUnclassified == true;
+
         var query = _rep.Context.Queryable<AdoS8Exception>()
             .LeftJoin<AdoS8SceneConfig>((e, sc) =>
                 e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
-            .Where((e, sc) => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.Status), (e, sc) => e.Status == q.Status)
+            .LeftJoin<AdoS8WatchRule>((e, sc, wr) =>
+                e.TenantId == wr.TenantId && e.FactoryId == wr.FactoryId && e.SourceRuleCode == wr.RuleCode)
+            .Where((e, sc, wr) => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
+            .WhereIF(!includeUnclassified, (e, sc, wr) => !SqlFunc.IsNullOrEmpty(e.ExceptionTypeCode))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Status), (e, sc, wr) => e.Status == q.Status)
             .WhereIF(string.IsNullOrWhiteSpace(q.Status) && q.StatusBucket == "pending",
-                (e, sc) => pendingStatuses.Contains(e.Status))
-            .WhereIF(!string.IsNullOrWhiteSpace(q.Severity), (e, sc) => e.Severity == q.Severity)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), (e, sc) => e.SceneCode == q.SceneCode)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), (e, sc) => e.ModuleCode == q.ModuleCode)
-            .WhereIF(q.DeptId.HasValue, (e, sc) => e.ResponsibleDeptId == q.DeptId!.Value || e.OccurrenceDeptId == q.DeptId!.Value)
-            .WhereIF(q.TimeoutFlag.HasValue, (e, sc) => e.TimeoutFlag == q.TimeoutFlag!.Value)
-            .WhereIF(q.BeginTime.HasValue, (e, sc) => e.CreatedAt >= q.BeginTime!.Value)
-            .WhereIF(q.EndTime.HasValue, (e, sc) => e.CreatedAt <= q.EndTime!.Value)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.ProcessNodeCode), (e, sc) => e.ProcessNodeCode == q.ProcessNodeCode)
-            .WhereIF(!string.IsNullOrWhiteSpace(q.RelatedObjectCode), (e, sc) => e.RelatedObjectCode == q.RelatedObjectCode)
+                (e, sc, wr) => pendingStatuses.Contains(e.Status))
+            .WhereIF(!string.IsNullOrWhiteSpace(q.Severity), (e, sc, wr) => e.Severity == q.Severity)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.SceneCode), (e, sc, wr) => e.SceneCode == q.SceneCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.ModuleCode), (e, sc, wr) => e.ModuleCode == q.ModuleCode)
+            .WhereIF(q.DeptId.HasValue, (e, sc, wr) => e.ResponsibleDeptId == q.DeptId!.Value || e.OccurrenceDeptId == q.DeptId!.Value)
+            .WhereIF(q.TimeoutFlag.HasValue, (e, sc, wr) => e.TimeoutFlag == q.TimeoutFlag!.Value)
+            .WhereIF(q.BeginTime.HasValue, (e, sc, wr) => e.CreatedAt >= q.BeginTime!.Value)
+            .WhereIF(q.EndTime.HasValue, (e, sc, wr) => e.CreatedAt <= q.EndTime!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.ProcessNodeCode), (e, sc, wr) => e.ProcessNodeCode == q.ProcessNodeCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.RelatedObjectCode), (e, sc, wr) => e.RelatedObjectCode == q.RelatedObjectCode)
+            .WhereIF(q.RecoveredStatus == "RECOVERED", (e, sc, wr) => e.RecoveredAt != null)
+            .WhereIF(q.RecoveredStatus == "ACTIVE", (e, sc, wr) => e.RecoveredAt == null)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.RuleType), (e, sc, wr) => wr.RuleType == q.RuleType)
             .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
-                (e, sc) => e.Title.Contains(q.Keyword!) || e.ExceptionCode.Contains(q.Keyword!));
+                (e, sc, wr) => e.Title.Contains(q.Keyword!) || e.ExceptionCode.Contains(q.Keyword!));
 
         var total = await query.CountAsync();
         var list = await query
-            .OrderBy((e, sc) => e.CreatedAt, OrderByType.Desc)
-            .Select((e, sc) => new AdoS8ExceptionListItemDto
+            .OrderBy((e, sc, wr) => e.CreatedAt, OrderByType.Desc)
+            .Select((e, sc, wr) => new AdoS8ExceptionListItemDto
             {
                 Id = e.Id,
                 FactoryId = e.FactoryId,
@@ -70,7 +78,15 @@ public class S8ExceptionService : ITransient
                 SlaDeadline = e.SlaDeadline,
                 TimeoutFlag = e.TimeoutFlag,
                 CreatedAt = e.CreatedAt,
-                ClosedAt = e.ClosedAt
+                ClosedAt = e.ClosedAt,
+                ExceptionTypeCode = e.ExceptionTypeCode,
+                RecoveredAt = e.RecoveredAt,
+                SourceRuleCode = e.SourceRuleCode,
+                SourceObjectType = e.SourceObjectType,
+                SourceObjectId = e.SourceObjectId,
+                DedupKey = e.DedupKey,
+                LastDetectedAt = e.LastDetectedAt,
+                RuleType = wr.RuleType
             })
             .ToPageListAsync(q.Page, q.PageSize);
 
@@ -112,8 +128,10 @@ public class S8ExceptionService : ITransient
         var rows = await _rep.Context.Queryable<AdoS8Exception>()
             .LeftJoin<AdoS8SceneConfig>((e, sc) =>
                 e.TenantId == sc.TenantId && e.FactoryId == sc.FactoryId && e.SceneCode == sc.SceneCode)
-            .Where((e, sc) => e.Id == id && e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
-            .Select((e, sc) => new AdoS8ExceptionDetailDto
+            .LeftJoin<AdoS8WatchRule>((e, sc, wr) =>
+                e.TenantId == wr.TenantId && e.FactoryId == wr.FactoryId && e.SourceRuleCode == wr.RuleCode)
+            .Where((e, sc, wr) => e.Id == id && e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
+            .Select((e, sc, wr) => new AdoS8ExceptionDetailDto
             {
                 Id = e.Id,
                 FactoryId = e.FactoryId,
@@ -153,6 +171,8 @@ public class S8ExceptionService : ITransient
                 SourceRuleCode = e.SourceRuleCode,
                 SourceObjectType = e.SourceObjectType,
                 SourceObjectId = e.SourceObjectId,
+                ExceptionTypeCode = e.ExceptionTypeCode,
+                RuleType = wr.RuleType,
             })
             .Take(1)
             .ToListAsync();
@@ -193,9 +213,13 @@ public class S8ExceptionService : ITransient
                 .ToListAsync())
                 .ToDictionary(x => x.Id, x => x.Name);
 
+        // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位(797403760988229 ≠ 系统租户),
+        // 走全局 multi-tenant filter 会全过滤为空 → 详情页处理人/检验人姓名显示数字 ID。
+        // 与 S8MasterDataAdapter.GetEmployeesAsync / S8TaskFlowService.GetEmployeeSysUserIdAsync 同口径,
+        // 局部 ClearFilter + employee.Id 主键集合作为安全边界,无跨租户泄漏。
         var empMap = empIds.Count == 0
             ? new Dictionary<long, string>()
-            : (await _empRep.AsQueryable()
+            : (await _empRep.AsQueryable().ClearFilter()
                 .Where(x => empIds.Contains(x.Id))
                 .Select(x => new { x.Id, Name = x.Name ?? x.Employee })
                 .ToListAsync())

+ 8 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8MasterDataAdapter.cs

@@ -31,8 +31,14 @@ public class S8MasterDataAdapter : ITransient
 
     public async Task<object> GetEmployeesAsync(long? factoryRefId)
     {
-        var emps = await _empRep.AsQueryable()
-            .WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value)
+        // S8 选员工下拉:EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位(业务租户 ≠ 系统租户),
+        // 走全局 multi-tenant filter 会全过滤为空。本接口仅作 S8 处理人/检验人下拉,必须强约束 factoryRefId
+        // 边界 + IsActive,避免越界;不依赖 token tenant 做隔离。
+        if (!factoryRefId.HasValue || factoryRefId.Value <= 0)
+            return new List<object>();
+
+        var emps = await _empRep.AsQueryable().ClearFilter()
+            .Where(x => x.FactoryRefId == factoryRefId!.Value && x.IsActive)
             .Take(500)
             .Select(x => new { x.Id, x.Name, x.Employee, x.SysUserId })
             .ToListAsync();

+ 5 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8TaskFlowService.cs

@@ -276,10 +276,14 @@ public class S8TaskFlowService : ITransient
     }
 
     // 把异常上的处理人/检验人(employeeId) 经 EmployeeMaster.SysUserId 解析到系统账号 ID。
+    // EmployeeMaster.tenant_id 与 SysUser.TenantId 历史错位,必须 ClearFilter 跳过多租户全局 filter;
+    // 通过 employee.Id 主键精确查询作为安全边界,无跨租户泄漏风险。
     private async Task<long?> GetEmployeeSysUserIdAsync(long? employeeId)
     {
         if (!employeeId.HasValue || employeeId.Value <= 0) return null;
-        var emp = await _employeeRep.GetFirstAsync(x => x.Id == employeeId.Value);
+        var emp = await _employeeRep.AsQueryable().ClearFilter()
+            .Where(x => x.Id == employeeId.Value)
+            .FirstAsync();
         return emp?.SysUserId;
     }