瀏覽代碼

fix(s8): demo bugfix pack — datasource mask, dashboard severity/REJECTED, route+button hardening

YY968XX 1 月之前
父節點
當前提交
09d3ade79b

+ 9 - 1
Web/src/views/aidop/s8/components/config/S8CrudConfigPage.vue

@@ -158,7 +158,15 @@ onMounted(() => {
 		</div>
 
 		<el-table :data="rows" v-loading="loading" border stripe>
-			<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :width="column.width" show-overflow-tooltip />
+			<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :width="column.width" show-overflow-tooltip>
+				<template #default="{ row }">
+					<!-- BUG-27:enabled 等布尔列显示中文 tag,避免裸 true/false -->
+					<el-tag v-if="column.key === 'enabled'" :type="row.enabled ? 'success' : 'info'" size="small">
+						{{ row.enabled ? '启用' : '禁用' }}
+					</el-tag>
+					<span v-else>{{ row[column.key] }}</span>
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="220" fixed="right">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>

+ 1 - 1
Web/src/views/aidop/s8/dashboard/S8DashboardPage.vue

@@ -670,7 +670,7 @@ onBeforeUnmount(() => { mainChart?.dispose(); trendChart?.dispose(); });
 					<el-table-column label="操作" width="68" fixed="right" align="center">
 						<template #default="{ row }">
 							<el-button link type="primary" size="small"
-								@click="router.push({ path: '/aidop/s8/exceptions', query: { id: String(row.id) } })">
+								@click="router.push({ path: `/aidop/s8/exceptions/${row.id}` })">
 								查看
 							</el-button>
 						</template>

+ 22 - 5
Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue

@@ -40,8 +40,10 @@ const hasActiveFlow = computed(() => !!detail.value?.activeFlowInstanceId);
 const activeBizType = computed(() => detail.value?.activeFlowBizType ?? '');
 const canClaim = computed(() => currentStatus.value === 'NEW');
 const canStartProgress = computed(() => currentStatus.value === 'ASSIGNED');
-const canTransfer = computed(() => !['', 'CLOSED'].includes(currentStatus.value));
-const canUpgrade = computed(() => !hasActiveFlow.value && ['ASSIGNED', 'IN_PROGRESS'].includes(currentStatus.value));
+// BUG-16:转派只在已分派后允许,NEW(未认领/未分派)和 CLOSED 禁用,避免后端 400 无反馈。
+const canTransfer = computed(() => ['ASSIGNED', 'IN_PROGRESS', 'PENDING_VERIFICATION', 'REJECTED'].includes(currentStatus.value));
+// BUG-15:本轮不实现 escalation。前端永久禁用升级按钮,避免点击调用不存在的 /escalate 接口(404)。
+const canUpgrade = computed(() => false);
 const canReject = computed(() => ['NEW', 'ASSIGNED', 'IN_PROGRESS'].includes(currentStatus.value));
 const canSubmitVerification = computed(
 	() => currentStatus.value === 'IN_PROGRESS' && !!detail.value?.assigneeId
@@ -187,6 +189,16 @@ function ruleTypeLabel(t: string | null | undefined) {
 	}
 }
 
+// BUG-3:部门字段兜底。优先 deptName;deptName 为空时若 deptId 为 0/"0"/null/undefined 显示"未分派",
+// 否则显示 deptId(保留有意义的 id 引用)。
+function formatDept(deptName: string | null | undefined, deptId: number | string | null | undefined) {
+	if (deptName) return deptName;
+	if (deptId === null || deptId === undefined) return '未分派';
+	const idStr = String(deptId).trim();
+	if (idStr === '' || idStr === '0') return '未分派';
+	return idStr;
+}
+
 onMounted(async () => {
 	await loadDetail();
 	await loadEmployees();
@@ -204,8 +216,11 @@ onMounted(async () => {
 						<el-descriptions-item label="状态">{{ detail.statusLabel }}</el-descriptions-item>
 						<el-descriptions-item label="严重度">{{ detail.severityLabel }}</el-descriptions-item>
 						<el-descriptions-item label="场景">{{ detail.sceneName || detail.sceneCode }}</el-descriptions-item>
-						<el-descriptions-item label="发生部门">{{ detail.occurrenceDeptName || detail.occurrenceDeptId || '—' }}</el-descriptions-item>
-						<el-descriptions-item label="责任部门">{{ detail.responsibleDeptName || detail.responsibleDeptId || '—' }}</el-descriptions-item>
+						<!-- BUG-9/24:补异常类型,优先 typeName,回退 typeCode,无则 '—' -->
+						<el-descriptions-item label="异常类型">{{ detail.exceptionTypeName || detail.exceptionTypeCode || '—' }}</el-descriptions-item>
+						<!-- BUG-3:部门 0/"0"/null/undefined 都视为未分派,不显示裸 0 -->
+						<el-descriptions-item label="发生部门">{{ formatDept(detail.occurrenceDeptName, detail.occurrenceDeptId) }}</el-descriptions-item>
+						<el-descriptions-item label="责任部门">{{ formatDept(detail.responsibleDeptName, detail.responsibleDeptId) }}</el-descriptions-item>
 						<el-descriptions-item label="处理人">{{ detail.assigneeName || detail.assigneeId || '—' }}</el-descriptions-item>
 						<el-descriptions-item label="检验人">{{ detail.verifierName || detail.verifierId || '—' }}</el-descriptions-item>
 						<el-descriptions-item label="检验结果">{{ detail.verificationResult || '—' }}</el-descriptions-item>
@@ -233,7 +248,9 @@ onMounted(async () => {
 								<el-button type="primary" :disabled="!canClaim" @click="openAction('claim')">认领</el-button>
 								<el-button type="primary" :disabled="!canStartProgress" @click="openAction('startProgress')">开始处理</el-button>
 								<el-button :disabled="!canTransfer" @click="openAction('transfer')">转派</el-button>
-								<el-button type="warning" :disabled="!canUpgrade" @click="openAction('upgrade')">升级</el-button>
+								<el-tooltip content="升级功能暂未启用" placement="top">
+									<el-button type="warning" disabled>升级</el-button>
+								</el-tooltip>
 								<el-button type="danger" :disabled="!canReject" @click="openAction('reject')">驳回</el-button>
 								<el-button type="primary" :disabled="!canSubmitVerification" @click="openAction('submitVerification')">提交复检</el-button>
 								<el-button type="success" :disabled="!canApproveVerification" @click="openAction('approveVerification')">检验通过</el-button>

+ 3 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8DashboardController.cs

@@ -27,8 +27,9 @@ public class AdoS8DashboardController : ControllerBase
         [FromQuery] long tenantId = 1,
         [FromQuery] long factoryId = 1,
         [FromQuery] DateTime? beginTime = null,
-        [FromQuery] DateTime? endTime = null) =>
-        Ok(await _svc.GetOverviewAsync(tenantId, factoryId, beginTime, endTime));
+        [FromQuery] DateTime? endTime = null,
+        [FromQuery] string? severity = null) =>
+        Ok(await _svc.GetOverviewAsync(tenantId, factoryId, beginTime, endTime, severity));
 
     [HttpGet("trends")]
     public async Task<IActionResult> TrendsAsync([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,

+ 6 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardService.cs

@@ -20,16 +20,18 @@ public class S8DashboardService : ITransient
         _processNodeRep = processNodeRep;
     }
 
-    public async Task<object> GetOverviewAsync(long tenantId, long factoryId, DateTime? beginTime = null, DateTime? endTime = null)
+    public async Task<object> GetOverviewAsync(long tenantId, long factoryId, DateTime? beginTime = null, DateTime? endTime = null, string? severity = null)
     {
         // 看板顶部的"开始/结束日期"通过 beginTime/endTime 透传,与列表的 beginTime/endTime 同语义。
-        // pending 桶必须与列表 statusBucket="pending" 严格对齐:NEW/ASSIGNED/IN_PROGRESS/PENDING_VERIFICATION。
+        // BUG-12:补 severity 过滤,与异常列表 API 的 severity 口径一致(精确匹配 LOW/MEDIUM/HIGH/CRITICAL)。
+        // BUG-22:REJECTED 状态归入 pending 桶,避免从总数视角"消失";与列表 statusBucket="pending" 同步对齐。
         var q = _rep.AsQueryable()
             .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
             .WhereIF(beginTime.HasValue, x => x.CreatedAt >= beginTime!.Value)
-            .WhereIF(endTime.HasValue, x => x.CreatedAt <= endTime!.Value);
+            .WhereIF(endTime.HasValue, x => x.CreatedAt <= endTime!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(severity), x => x.Severity == severity);
         var total      = await q.CountAsync();
-        var pending    = await q.CountAsync(x => x.Status == "NEW" || x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS" || x.Status == "PENDING_VERIFICATION");
+        var pending    = await q.CountAsync(x => x.Status == "NEW" || x.Status == "ASSIGNED" || x.Status == "IN_PROGRESS" || x.Status == "PENDING_VERIFICATION" || x.Status == "REJECTED");
         var inProgress = await q.CountAsync(x => x.Status == "IN_PROGRESS");
         var timeout    = await q.CountAsync(x => x.TimeoutFlag);
         var closed     = await q.CountAsync(x => x.Status == "CLOSED");

+ 42 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DataSourceService.cs

@@ -1,4 +1,5 @@
 using Admin.NET.Plugin.AiDOP.Entity.S8;
+using System.Text.RegularExpressions;
 
 namespace Admin.NET.Plugin.AiDOP.Service.S8;
 
@@ -8,10 +9,14 @@ public class S8DataSourceService : ITransient
 
     public S8DataSourceService(SqlSugarRepository<AdoS8DataSource> rep) => _rep = rep;
 
-    public async Task<List<AdoS8DataSource>> ListAsync(long tenantId, long factoryId) =>
-        await _rep.AsQueryable()
+    public async Task<List<AdoS8DataSource>> ListAsync(long tenantId, long factoryId)
+    {
+        var rows = await _rep.AsQueryable()
             .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId)
             .ToListAsync();
+        foreach (var r in rows) r.Endpoint = MaskSecret(r.Endpoint);
+        return rows;
+    }
 
     public async Task<AdoS8DataSource> CreateAsync(AdoS8DataSource body)
     {
@@ -23,6 +28,7 @@ public class S8DataSourceService : ITransient
         body.Id = 0;
         body.CreatedAt = DateTime.Now;
         await _rep.InsertAsync(body);
+        body.Endpoint = MaskSecret(body.Endpoint);
         return body;
     }
 
@@ -34,10 +40,14 @@ public class S8DataSourceService : ITransient
         var exists = await _rep.AsQueryable()
             .AnyAsync(x => x.Id != id && x.TenantId == body.TenantId && x.FactoryId == body.FactoryId && x.DataSourceCode == body.DataSourceCode);
         if (exists) throw new S8BizException("数据源编码已存在");
+        // 入参 endpoint 含掩码占位符(Pwd=****** / Password=******)时保留旧值的真实密码段,避免前端
+        // 回填脱敏值后误覆盖。Endpoint 全空时也不覆盖原密码。
+        body.Endpoint = MergeEndpointPreservingSecret(body.Endpoint, e.Endpoint);
         body.Id = id;
         body.CreatedAt = e.CreatedAt;
         body.UpdatedAt = DateTime.Now;
         await _rep.UpdateAsync(body);
+        body.Endpoint = MaskSecret(body.Endpoint);
         return body;
     }
 
@@ -60,4 +70,34 @@ public class S8DataSourceService : ITransient
             entity.LastCheckStatus
         };
     }
+
+    // BUG-13:endpoint 中的 Pwd=xxx / Password=xxx(大小写不敏感)替换为 ******,保留其它字段。
+    private static readonly Regex SecretPattern = new(
+        @"(?i)(Pwd|Password)\s*=\s*([^;]*)",
+        RegexOptions.Compiled);
+
+    private static string? MaskSecret(string? endpoint)
+    {
+        if (string.IsNullOrWhiteSpace(endpoint)) return endpoint;
+        return SecretPattern.Replace(endpoint, m => $"{m.Groups[1].Value}=******");
+    }
+
+    private static string? MergeEndpointPreservingSecret(string? incoming, string? existing)
+    {
+        if (string.IsNullOrWhiteSpace(incoming)) return existing;
+        if (string.IsNullOrWhiteSpace(existing)) return incoming;
+        // 提取旧 endpoint 中的真实密码值(首个匹配为准)
+        var oldMatch = SecretPattern.Match(existing);
+        if (!oldMatch.Success) return incoming;
+        var realSecret = oldMatch.Groups[2].Value;
+        // 把入参里 Pwd=****** 之类的占位还原为真实密码
+        return SecretPattern.Replace(incoming, m =>
+        {
+            var v = m.Groups[2].Value;
+            return IsMaskedPlaceholder(v) ? $"{m.Groups[1].Value}={realSecret}" : m.Value;
+        });
+    }
+
+    private static bool IsMaskedPlaceholder(string? v) =>
+        !string.IsNullOrEmpty(v) && v.All(c => c == '*');
 }