Commit ec026cb2 by luoqi

feat(push): 推送记录支持 HMAC 免登录查询 + API 文档补全 dryRun/limit 参数

对接反馈:① 文档站 API 参考没更新 ② 查推送记录还要登录,联调不便。

1) 新增 GET /pac/v1/push/logs(HMAC 验签,免登录)
   复用宿主推送时已在用的 X-PAC-Host-Id / Timestamp / Signature 三件套,联调不必另开账号;
   GET 无 body,签名对空串算 hex(HMAC-SHA256(`<ts>.`, secret))。
   **不是公开接口** —— 签名错误返回 10106,拿不到任何数据,不泄露宿主推送情况。
   与登录态 /admin/host/self/push-logs 同一实现(sync.module 引入 AdminModule 复用
   HostsService.getPushLogs,两模块无环),返回同一份数据。

2) API 文档补全:@Query 缺 @ApiQuery 导致 Swagger 采不到 —— push/rows 的 dryRun、
   push/logs 的 limit 现已进 spec;重新 dump 静态 spec(apps/pac-docs/openapi/pac.json,
   59 paths),文档站 API 参考随之更新。

3) 契约文档 §7.2 改写为「两种查法」:免登录(HMAC,联调推荐)+ 登录态,并说明签名对空串计算。

验证:正确签名返回推送记录、错误签名被拒(10106 签名不匹配);typecheck 干净,299 测试绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent aaf8805f
Pipeline #3431 failed in 0 seconds
...@@ -270,12 +270,24 @@ POST /pac/v1/push/rows?dryRun=1 ...@@ -270,12 +270,24 @@ POST /pac/v1/push/rows?dryRun=1
**自测通过标准**:`failed=0` 且 `mappingMisses=0` 且 `suspectFields=0`,再看 `samples` 里的字段值对不对 —— 都 OK 才去掉 `?dryRun=1` 正式推。 **自测通过标准**:`failed=0` 且 `mappingMisses=0` 且 `suspectFields=0`,再看 `samples` 里的字段值对不对 —— 都 OK 才去掉 `?dryRun=1` 正式推。
### 7.2 推送记录:事后可查 ### 7.2 推送记录:事后可查(两种查法)
**① 免登录 —— 用已有 push 凭据验签(联调推荐)**
``` ```
GET /pac/v1/admin/host/self/push-logs?limit=50 # 需登录态(宿主自助权限) GET /pac/v1/push/logs?limit=50
Headers: X-PAC-Host-Id / X-PAC-Timestamp / X-PAC-Signature
``` ```
按时间倒序列最近 N 批(默认 50、上限 200),每批含 `source / status / dryRun / fetched / transactionsWritten / duplicates / failed / errorMessage`。 鉴权方式与推送完全一致,**不必为联调另开账号**。GET 无 body,签名对**空串**计算:
`hex(HMAC-SHA256("<timestamp>.", secret))`(注意 timestamp 后那个点)。
这**不是公开接口** —— 没有正确签名拿不到任何数据。
**② 登录态 —— 宿主自助页同源接口**
```
GET /pac/v1/admin/host/self/push-logs?limit=50
```
两者返回**同一份数据**:按时间倒序最近 N 批(默认 50、上限 200),每批含
`source / status / dryRun / fetched / transactionsWritten / duplicates / failed / errorMessage`。
当时的同步响应没留存也能事后查:**哪批错了、错在哪**。预检批次由 `dryRun:true` 标识,不与正式推送混淆。 当时的同步响应没留存也能事后查:**哪批错了、错在哪**。预检批次由 `dryRun:true` 标识,不与正式推送混淆。
### 7.3 其他可用面 ### 7.3 其他可用面
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import { import {
Body, Body,
Controller, Controller,
Get,
Headers, Headers,
HttpCode, HttpCode,
Post, Post,
...@@ -8,12 +9,13 @@ import { ...@@ -8,12 +9,13 @@ import {
Req, Req,
} from '@nestjs/common'; } from '@nestjs/common';
import type { Request } from 'express'; import type { Request } from 'express';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ApiCode } from '@pac/types'; import { ApiCode } from '@pac/types';
import { BizError } from '../../../common/errors/biz-error'; import { BizError } from '../../../common/errors/biz-error';
import { Public } from '../../../common/decorators/public.decorator'; import { Public } from '../../../common/decorators/public.decorator';
import { HmacVerifier } from './hmac-verifier.service'; import { HmacVerifier } from './hmac-verifier.service';
import { PushReceiverService } from './push-receiver.service'; import { PushReceiverService } from './push-receiver.service';
import { HostsService } from '../../admin/hosts.service';
import { import {
PushEventsRequestSchema, PushEventsRequestSchema,
type PushEventsRequest, type PushEventsRequest,
...@@ -45,6 +47,7 @@ export class PushController { ...@@ -45,6 +47,7 @@ export class PushController {
constructor( constructor(
private readonly verifier: HmacVerifier, private readonly verifier: HmacVerifier,
private readonly receiver: PushReceiverService, private readonly receiver: PushReceiverService,
private readonly hosts: HostsService,
) {} ) {}
/** /**
...@@ -144,6 +147,11 @@ export class PushController { ...@@ -144,6 +147,11 @@ export class PushController {
' 加 ?dryRun=1 走**预检**:跑完整管线(transforms+装配+归一)但不落库、不触发重算,' + ' 加 ?dryRun=1 走**预检**:跑完整管线(transforms+装配+归一)但不落库、不触发重算,' +
'响应带各资源样本 canonical —— 对接自测先预检到 failed=0 且 mappingMisses/suspectFields 为 0,再正式推。', '响应带各资源样本 canonical —— 对接自测先预检到 failed=0 且 mappingMisses/suspectFields 为 0,再正式推。',
}) })
@ApiQuery({
name: 'dryRun',
required: false,
description: "预检开关:'1' / 'true' / 'yes' = 跑完整管线但不落库、不触发重算,响应带样本 canonical",
})
async receiveRows( async receiveRows(
@Req() req: Request & { rawBody?: Buffer }, @Req() req: Request & { rawBody?: Buffer },
@Headers('x-pac-host-id') hostIdHeader: string | undefined, @Headers('x-pac-host-id') hostIdHeader: string | undefined,
...@@ -176,4 +184,40 @@ export class PushController { ...@@ -176,4 +184,40 @@ export class PushController {
}), }),
); );
} }
/**
* 推送记录查询 —— **用已有 push 凭据验签,无需另外登录**(对接自测友好)。
*
* 与登录态的 `GET /admin/host/self/push-logs` 返回同一份数据(同一实现),区别只在鉴权方式:
* 这里复用宿主推送时已经在用的 HMAC 三件套,故不必为联调再开账号;
* 也**不是公开接口** —— 没有正确签名拿不到任何数据,不会泄露宿主推送情况。
*
* GET 无 body,签名对**空串**计算:`hex(HMAC-SHA256(`${timestamp}.`, secret))`。
*/
@Get('logs')
@Public()
@HttpCode(200)
@ApiOperation({
summary: '查询本宿主推送记录(HMAC 验签,免登录)',
description:
'Headers 同 push:X-PAC-Host-Id / X-PAC-Timestamp / X-PAC-Signature。' +
'GET 无 body,签名对空串算:hex(HMAC-SHA256(`<timestamp>.`, secret))。' +
'返回最近 N 批(默认 50、上限 200):source / status / dryRun / 收行数 / 落库数 / 去重 / 失败 / 报错。',
})
@ApiQuery({ name: 'limit', required: false, description: '返回条数,默认 50、上限 200' })
async logs(
@Headers('x-pac-host-id') hostIdHeader: string | undefined,
@Headers('x-pac-timestamp') timestampHeader: string | undefined,
@Headers('x-pac-signature') signatureHeader: string | undefined,
@Query('limit') limit?: string,
) {
const { hostId } = await this.verifier.verify({
hostIdHeader,
timestampHeader,
signatureHeader,
rawBody: '', // GET 无 body
});
const n = Number.parseInt(limit ?? '', 10);
return this.hosts.getPushLogs(hostId, Number.isFinite(n) && n > 0 ? n : 50);
}
} }
...@@ -32,8 +32,11 @@ import { HmacVerifier } from './push/hmac-verifier.service'; ...@@ -32,8 +32,11 @@ import { HmacVerifier } from './push/hmac-verifier.service';
* - AssemblerModule(raw → canonical;真实 HTTP adapter 才需要;mock/push 跳过) * - AssemblerModule(raw → canonical;真实 HTTP adapter 才需要;mock/push 跳过)
* - PipelineModule(synthesizer + parser pipeline) * - PipelineModule(synthesizer + parser pipeline)
*/ */
import { AdminModule } from '../admin/admin.module';
@Module({ @Module({
imports: [ imports: [
AdminModule, // push/logs 复用 HostsService.getPushLogs(与登录态自助页同一实现)
FactsModule, FactsModule,
AssemblerModule, AssemblerModule,
TransformsModule, TransformsModule,
......
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