Commit 43fa2672 by luoqi

feat(admin): GET /admin/mapping-miss — 映射覆盖漏审计端点

扫最近 N 次 sync run 的 SyncLog.metadata.mappingMisses,按出现量倒序聚合,给运维看
"摄入时哪些原值没映射上、落了 _default"(诊断名/获客渠道长尾)→ 扩 yaml → reparse 闭环。
权限 PLATFORM_MANAGE,host self scope(同其它 admin 端点)。

本地端到端验证(:3101):
- POST /push/rows + HMAC → {code:0, transactionsWritten:4, personaEnqueued:1}(验签+落库+画像入队)
- GET /admin/mapping-miss + JWT(admin) → 精确返回推送行里的未映射诊断 "恒牙列"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 7aa3e1a8
...@@ -2,12 +2,19 @@ import { Module } from '@nestjs/common'; ...@@ -2,12 +2,19 @@ import { Module } from '@nestjs/common';
import { HostsAdminController } from './hosts.controller'; import { HostsAdminController } from './hosts.controller';
import { HostSelfAdminController } from './host-self.controller'; import { HostSelfAdminController } from './host-self.controller';
import { AiInvocationsAdminController } from './ai-invocations.controller'; import { AiInvocationsAdminController } from './ai-invocations.controller';
import { MappingMissAdminController } from './mapping-miss.controller';
import { HostsService } from './hosts.service'; import { HostsService } from './hosts.service';
import { AiInvocationsService } from './ai-invocations.service'; import { AiInvocationsService } from './ai-invocations.service';
import { MappingMissService } from './mapping-miss.service';
@Module({ @Module({
controllers: [HostsAdminController, HostSelfAdminController, AiInvocationsAdminController], controllers: [
providers: [HostsService, AiInvocationsService], HostsAdminController,
HostSelfAdminController,
AiInvocationsAdminController,
MappingMissAdminController,
],
providers: [HostsService, AiInvocationsService, MappingMissService],
exports: [HostsService, AiInvocationsService], exports: [HostsService, AiInvocationsService],
}) })
export class AdminModule {} export class AdminModule {}
import { Controller, Get, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permission } from '@pac/types';
import { RequirePermission } from '../../common/decorators/permissions.decorator';
import {
TenantScope,
TenantScopeContext,
} from '../../common/decorators/tenant-scope.decorator';
import { MappingMissService } from './mapping-miss.service';
/**
* 映射覆盖漏审计 — GET /admin/mapping-miss
*
* 看"摄入时哪些原值没映射上、落了 _default"(如诊断名长尾 / 获客渠道长尾),按出现量倒序。
* 闭环:看到 → 扩 assembler enum/keyword yaml → reparse → 该值不再 miss。
*
* Scope:host self(只看自家 host_id)。权限 PLATFORM_MANAGE。
*/
@ApiTags('admin')
@ApiBearerAuth('accessToken')
@Controller('admin/mapping-miss')
@RequirePermission(Permission.PLATFORM_MANAGE)
export class MappingMissAdminController {
constructor(private readonly svc: MappingMissService) {}
@Get()
@ApiOperation({
summary: '映射覆盖漏(落 _default 的原值)按量倒序;扫最近 N 次 sync run 的 metadata(默认 100,上限 500)',
})
recent(@TenantScope() scope: TenantScopeContext, @Query('limit') limit?: string) {
return this.svc.recent(scope, { limit: limit ? Number(limit) : undefined });
}
}
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import {
mergeMappingMisses,
type MappingMiss,
} from '../sync/assembler/assembler-engine';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';
/**
* MappingMissService — 映射覆盖漏审计。
*
* 数据来源:摄入(cold-import / push / reparse)时 assembler 把"enum_mapping 精确 + keyword
* 都没命中、落到 _default"的原值聚合写进 SyncLog.metadata.mappingMisses。本服务扫最近 N 次
* sync run 的 metadata 合并计数 → 给运维"看还有哪些原值没映射上"(如诊断名长尾)→ 扩字典 → reparse。
*
* 注:落 _default 既包含"故意不映射"(如 乳牙列 正常态)也包含"真漏"(如 松动牙);本接口只负责
* 可见化,人工区分。
*/
@Injectable()
export class MappingMissService {
constructor(private readonly prisma: PrismaService) {}
async recent(
scope: TenantScopeContext,
opts: { limit?: number },
): Promise<{ scannedRuns: number; runsWithMisses: number; distinct: number; misses: MappingMiss[] }> {
const take = Math.min(Math.max(opts.limit ?? 100, 1), 500);
const logs = await this.prisma.syncLog.findMany({
where: { hostId: scope.hostId },
orderBy: { startedAt: 'desc' },
take,
select: { metadata: true },
});
const lists: MappingMiss[][] = [];
for (const l of logs) {
const md = l.metadata as { mappingMisses?: MappingMiss[] } | null;
if (md?.mappingMisses?.length) lists.push(md.mappingMisses);
}
const misses = mergeMappingMisses(lists);
return {
scannedRuns: logs.length,
runsWithMisses: lists.length,
distinct: misses.length,
misses,
};
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment