feat(webhook/content-review): 新增功能与查询过滤能力

1. 为webhook入站请求合并URL query参数到headers,支持微信公众号等渠道的验签
2. 为内容审核列表接口新增resource_type和trace_id查询过滤支持
This commit is contained in:
Kris 2026-07-09 04:21:51 +08:00
parent b8ac375e8e
commit b7ceeb11f3
2 changed files with 43 additions and 3 deletions

View File

@ -158,6 +158,23 @@ def _parse_verdict(raw: str) -> ContentReviewVerdict:
) from exc ) from exc
def _parse_resource_type(raw: str) -> ContentReviewResourceType:
"""string → ContentReviewResourceType失败抛 ValidationError。
CR-02 ``resource_type`` 查询参数string翻译为
``ContentReviewResourceType`` 枚举非法值由全局异常处理器映射为
400 ``VALIDATION_ERROR``
"""
try:
return ContentReviewResourceType(raw)
except ValueError as exc:
raise ValidationError(
"resource_type",
f"unsupported resource_type: {raw} "
"(expected: message_text | message_attachment | user_profile)",
) from exc
# ---------------- 静态路径端点(须先于动态路径声明) ---------------- # ---------------- 静态路径端点(须先于动态路径声明) ----------------
@ -211,6 +228,14 @@ async def list_review_history(
default=None, default=None,
description="按审核结论过滤pass / review / block", description="按审核结论过滤pass / review / block",
), ),
resource_type: str | None = Query(
default=None,
description="按资源类型过滤message_text / message_attachment / user_profile",
),
trace_id: str | None = Query(
default=None,
description="按链路追踪 ID 过滤",
),
start_time: str | None = Query( start_time: str | None = Query(
default=None, default=None,
description="起始时间过滤ISO 8601", description="起始时间过滤ISO 8601",
@ -228,11 +253,13 @@ async def list_review_history(
非控制面管道路径模板 B ``ContentReviewQueryService`` 直接读取 非控制面管道路径模板 B ``ContentReviewQueryService`` 直接读取
``ContentReviewRepositoryPort``不经控制面管道不写审计日志查询 ``ContentReviewRepositoryPort``不经控制面管道不写审计日志查询
操作无副作用``verdict`` / ``start_time`` / ``end_time`` 查询参数 操作无副作用``verdict`` / ``resource_type`` / ``start_time`` /
经协议翻译辅助函数转换为枚举 / datetime失败抛 ``ValidationError`` ``end_time`` 查询参数经协议翻译辅助函数转换为枚举 / datetime失败抛
``ValidationError``
编排链路HTTP 入参 协议翻译``_parse_verdict`` / 编排链路HTTP 入参 协议翻译``_parse_verdict`` /
``parse_datetime`` 构造 ``ContentReviewHistoryQueryCmd`` ``_parse_resource_type`` / ``parse_datetime`` 构造
``ContentReviewHistoryQueryCmd``
DTO ``__post_init__`` 校验 limit/offset/time_range DTO ``__post_init__`` 校验 limit/offset/time_range
``use_cases.content_review_query.listHistory`` ``use_cases.content_review_query.listHistory``
``dataclass_to_dict`` 序列化 ``ContentReviewHistoryList`` 返回 ``dataclass_to_dict`` 序列化 ``ContentReviewHistoryList`` 返回
@ -242,6 +269,8 @@ async def list_review_history(
channel_type=channel_type, channel_type=channel_type,
account_id=account_id, account_id=account_id,
verdict=_parse_verdict(verdict) if verdict is not None else None, verdict=_parse_verdict(verdict) if verdict is not None else None,
resource_type=_parse_resource_type(resource_type) if resource_type is not None else None,
trace_id=trace_id,
start_time=parse_datetime("start_time", start_time), start_time=parse_datetime("start_time", start_time),
end_time=parse_datetime("end_time", end_time), end_time=parse_datetime("end_time", end_time),
limit=limit, limit=limit,

View File

@ -23,6 +23,7 @@ from yuxi.channels.contract.dtos.channel import ChannelType, WebhookTestCmd
from yuxi.channels.contract.dtos.inbound import ReceiveInboundCmd from yuxi.channels.contract.dtos.inbound import ReceiveInboundCmd
from yuxi.channels.contract.errors import InternalError, ValidationError from yuxi.channels.contract.errors import InternalError, ValidationError
from yuxi.storage.postgres.models_business import User from yuxi.storage.postgres.models_business import User
from yuxi.utils.crypto import SENSITIVE_HTTP_HEADERS
from yuxi.utils.trace_context import get_trace_id from yuxi.utils.trace_context import get_trace_id
from server.routers.channels import ( from server.routers.channels import (
@ -140,6 +141,16 @@ async def receive_webhook(
"webhook body is not valid UTF-8", "webhook body is not valid UTF-8",
) from exc ) from exc
sanitized_headers = sanitize_headers(request.headers) sanitized_headers = sanitize_headers(request.headers)
# 合并 URL query 参数到 headersHTTP headers 优先query 参数补充)。
# 部分渠道(如微信公众号)将签名 / 时间戳 / nonce 等放在 URL query 中
# 而非 HTTP headers合并后供入站管道 ``verifySignature`` 校验。
# 合并时跳过敏感字段(如 token / authorization避免 query 参数绕过
# ``sanitize_headers`` 过滤导致敏感信息泄露到日志。
for key, value in request.query_params.items():
if key.lower() in SENSITIVE_HTTP_HEADERS:
continue
if key not in sanitized_headers:
sanitized_headers[key] = value
cmd = ReceiveInboundCmd( cmd = ReceiveInboundCmd(
channel_type=channel_type, channel_type=channel_type,
raw_event=raw_event, raw_event=raw_event,