""" 简易 Webhook 接收服务:监听 POST /webhook,打印请求内容与部分头信息。 生产请将手机/脚本转发指向萌芽小店后端:`POST {YOUR_API_BASE}/api/webhooks/mengya-pay`, 并在请求头附带与后端一致的 `X-Webhook-Secret`(对应环境变量 `WEBHOOK_MENGYA_SECRET`)。 本地测试环境变量: HOST 监听地址,默认 0.0.0.0 PORT 监听端口,默认 8000 WEBHOOK_SECRET 若设置,则要求请求头 X-Webhook-Secret 与其一致,否则返回 403 """ from __future__ import annotations import json import logging import os import secrets from datetime import datetime, timezone from typing import Annotated from fastapi import FastAPI, Header, HTTPException, Request, Response logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) log = logging.getLogger("webhook") app = FastAPI(title="Webhook 测试接收器", docs_url="/docs") def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() @app.get("/health") def health() -> dict: return {"status": "ok", "time": _now_iso()} @app.post("/webhook") async def webhook( request: Request, response: Response, x_webhook_secret: Annotated[str | None, Header(alias="X-Webhook-Secret")] = None, ) -> dict: expected = os.environ.get("WEBHOOK_SECRET") if expected is not None: if x_webhook_secret is None: raise HTTPException(status_code=403, detail="missing X-Webhook-Secret") if not secrets.compare_digest(x_webhook_secret, expected): raise HTTPException(status_code=403, detail="invalid X-Webhook-Secret") raw = await request.body() ct = request.headers.get("content-type", "") try: if "application/json" in ct and raw.strip(): body_preview = json.loads(raw.decode()) elif raw.strip(): body_preview = raw.decode(errors="replace") if len(body_preview) > 2000: body_preview = body_preview[:2000] + "…(truncated)" else: body_preview = None except json.JSONDecodeError: body_preview = raw.decode(errors="replace") interesting_headers = { k: v for k, v in request.headers.items() if k.lower() in ( "content-type", "user-agent", "x-github-event", "x-gitlab-token", "x-request-id", ) } event_id = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f") info = { "event_id": event_id, "time": _now_iso(), "client": getattr(request.client, "host", None), "method": request.method, "path": str(request.url.path), "query": dict(request.query_params), "content_type": ct or None, "interesting_headers": interesting_headers, "body": body_preview, "body_bytes": len(raw), } log.info("webhook %s | %s", event_id, json.dumps(info, ensure_ascii=False)) response.headers["X-Webhook-Received"] = event_id return {"ok": True, "event_id": event_id, "received_at": info["time"]} def main() -> None: import uvicorn host = os.environ.get("HOST", "0.0.0.0") port = int(os.environ.get("PORT", "8000")) uvicorn.run("server:app", host=host, port=port, reload=False) if __name__ == "__main__": main()