feat: 发现列表改为服务端分页 + 全表聚合(去掉 500 上限)

- db.ListFindingsPage:WHERE(严重度/状态/类型)+ ORDER BY(严重度 CASE 或时间)+ LIMIT/OFFSET,返回当前页与匹配总数
- db.FindingStats:全表 COUNT FILTER 聚合(总数/待处理/高/中/低)+ DISTINCT 漏洞类型,供统计卡与类型下拉,不受分页影响
- GET /api/exploration/findings 带 page/limit → 分页对象{items,total,page,page_size};不带 → 保留裸数组(dashboard 汇总,与 intents 端点兼容策略一致)
- 新增 GET /api/exploration/findings/stats
- 筛选/排序全部下推到 SQL,前端不再一次拉全量再本地切片
- 资产按当前页所需批量解析;per-task 视图经 node_id 关联独立行,复用同一状态并补齐绑定资产
- 前端:发现页改为服务端驱动分页/筛选/排序,统计卡与类型下拉走 stats 接口
- db 层加 findings 分页/筛选/聚合测试(隔离标记 vulnclass,自清理)
This commit is contained in:
Autumn-27 committed 2026-08-18 10:05:18 -04:00
1 parent 05f0b69469
commit 30dc09ae08
9 files changed
+512 -140

No files matched your search

+22 -17
View File
@@ -250,27 +250,38 @@ func findingDTO(n *db.Node) FindingDTO {
// findingDTOsForTask converts a task's finding nodes to DTOs, stamping each with
// the owning task's id/description so the global 发现 page can group across tasks.
// triage maps node id → the standalone findings row (id + status), so the per-task
// view shows the same triage state as the global page; nodes with no row keep the
// 'pending' default and no finding_id (status is then not editable).
func findingDTOsForTask(t *Task, in []*db.Node, triage map[int64]struct {
ID int64
Status string
}) []FindingDTO {
// meta maps node id → the standalone findings row (id + status + asset ids), so the
// per-task view shows the same triage state and anchored assets as the global page;
// nodes with no row keep the 'pending' default and no finding_id (not editable).
// assets pre-resolves the anchored asset rows for label rendering.
func findingDTOsForTask(t *Task, in []*db.Node, meta map[int64]db.FindingMeta, assets map[int64]*db.Asset) []FindingDTO {
out := make([]FindingDTO, 0, len(in))
for _, n := range in {
d := findingDTO(n)
d.TaskID = t.ID
d.TaskDescription = t.Description
if r, ok := triage[n.ID]; ok {
d.FindingID = i64s(r.ID)
d.Status = r.Status
if m, ok := meta[n.ID]; ok {
d.FindingID = i64s(m.ID)
d.Status = m.Status
d.Assets = findingAssetDTOs(m.AssetIDs, assets)
}
out = append(out, d)
}
return out
}
// findingAssetDTOs maps anchored asset ids to display DTOs, skipping ids whose
// asset row is missing (e.g. deleted).
func findingAssetDTOs(ids []int64, assets map[int64]*db.Asset) []FindingAssetDTO {
var out []FindingAssetDTO
for _, aid := range ids {
if a := assets[aid]; a != nil {
out = append(out, FindingAssetDTO{ID: i64s(a.ID), Type: a.Type, Label: assetLabel(a)})
}
}
return out
}
// findingFromDB converts a standalone DBFinding row to a FindingDTO. task_id and
// task_description are empty when the originating task has been deleted (NULL).
func findingFromDB(f *db.DBFinding, assets map[int64]*db.Asset) FindingDTO {
@@ -288,13 +299,7 @@ func findingFromDB(f *db.DBFinding, assets map[int64]*db.Asset) FindingDTO {
Evidence: f.Evidence,
TS: rfc3339(f.CreatedAt),
}
for _, aid := range f.AssetIDs {
a := assets[aid]
if a == nil {
continue
}
d.Assets = append(d.Assets, FindingAssetDTO{ID: i64s(a.ID), Type: a.Type, Label: assetLabel(a)})
}
d.Assets = findingAssetDTOs(f.AssetIDs, assets)
if f.TaskID != nil {
d.TaskID = i64s(*f.TaskID)
d.TaskDescription = f.TaskDescription