# Managed Queue Webhook 对接说明 本文档说明 `managed/store_dwell_alert` 和 `managed/people_flow_project` 两个工程新增的排队统计与 webhook 推送结构,便于接收方按统一协议完成对接。 ## 业务规则 - 统计窗口固定为每 30 分钟一个窗口。 - 每个窗口统计两类人数: - `over_threshold_count`:该窗口内累计排队时间大于等于 5 分钟的人数。 - `under_threshold_count`:该窗口内累计排队时间小于 5 分钟但大于 0 的人数。 - 一阶段排队等级规则: - `crowded`:`over_threshold_count > 5` - `normal`:`2 <= over_threshold_count <= 5` - `few`:`over_threshold_count < 2` - 状态变化规则: - `queue_increased`:`normal -> crowded` 或 `few -> crowded` - `queue_decreased`:`normal -> few` 或 `crowded -> few` - `queue_normalized`:`crowded -> normal` 或 `few -> normal` - `unchanged`:窗口等级未变化 - `initial`:首个统计窗口,没有上一个窗口可比较 ## 配置方式 ### store_dwell_alert 配置文件示例:`managed/store_dwell_alert/config/config.example.yaml` ```yaml thresholds: queue_time_threshold_seconds: 300 crowded_count_threshold: 5 normal_count_threshold: 2 pause_timeout_seconds: 300 alert_cooldown_seconds: 600 webhook: url: "https://receiver.example.com/queue-report" timeout_seconds: 5.0 ``` ### people_flow_project 配置文件示例:`managed/people_flow_project/config/config.example.yaml` ```yaml queue: enabled: true area: [0.0, 0.0, 1.0, 1.0] area_mode: "normalized" queue_time_threshold_seconds: 300 crowded_count_threshold: 5 normal_count_threshold: 2 pause_timeout_seconds: 5 source_id: "queue_cam_01" webhook: url: "https://receiver.example.com/queue-report" timeout_seconds: 5.0 event_log_path: "outputs/rtsp_stream/webhook_events.jsonl" ``` 说明: - `queue.area` 用于限定排队区域,默认 `[0, 0, 1, 1]` 表示全画面。 - `queue.area_mode=normalized` 表示区域坐标按画面宽高归一化。 - 两个工程都会先把 webhook 数据写入本地结果文件,再尝试 HTTP POST。 ## 推送时机 - 两个工程都会在每个 30 分钟窗口结束时推送一次 `half_hour_report`。 - `store_dwell_alert` 仍会继续生成本地事件日志;用于对接的窗口报文以本文档的 `half_hour_report` 结构为准。 ## 公共字段 两个工程的 webhook 都包含以下公共字段: | 字段 | 类型 | 说明 | | --------------- | ------ | ---------------------------------------------------------------------------------------------- | | `event` | string | 固定为 `half_hour_report` | | `project_type` | string | 工程类型,值为 `store_dwell_alert` 或 `people_flow_project` | | `source_id` | string | 数据源标识。`store_dwell_alert` 使用 `camera_id`,`people_flow_project` 使用 `queue.source_id` | | `window_start` | string | 窗口开始时间,ISO 8601 | | `window_end` | string | 窗口结束时间,ISO 8601 | | `queue_metrics` | object | 排队统计主体 | `queue_metrics` 结构: | 字段 | 类型 | 说明 | | ------------------------------ | ----------- | ------------------------------------------------------------------------------------ | | `queue_time_threshold_seconds` | integer | 长等待阈值,当前默认 300 秒 | | `over_threshold_count` | integer | 窗口内累计排队时间大于等于阈值的人数 | | `under_threshold_count` | integer | 窗口内累计排队时间小于阈值但大于 0 的人数 | | `queue_level` | string | `few` / `normal` / `crowded` | | `previous_queue_level` | string/null | 上一个窗口的等级 | | `status_change` | string | `initial` / `unchanged` / `queue_increased` / `queue_decreased` / `queue_normalized` | | `people` | array | 当前窗口内参与统计的人员列表 | `queue_metrics.people[]` 结构: | 字段 | 类型 | 说明 | | --------------- | ------- | ------------------------------------- | | `person_id` | string | 人员标识 | | `queue_seconds` | integer | 该窗口内累计排队秒数 | | `bucket` | string | `over_threshold` 或 `under_threshold` | ## store_dwell_alert 完整报文 `store_dwell_alert` 会额外带上门店停留会话明细: ```json { "event": "half_hour_report", "project_type": "store_dwell_alert", "camera_id": "store_cam_01", "source_id": "store_cam_01", "window_start": "2026-05-08T09:00:00+08:00", "window_end": "2026-05-08T09:30:00+08:00", "active_customer_count": 3, "active_customers": [ { "person_id": "cust_101", "session_id": "cust_101-s1", "role": "customer", "status": "active", "dwell_seconds": 820, "window_queue_seconds": 820 }, { "person_id": "cust_102", "session_id": "cust_102-s1", "role": "customer", "status": "active", "dwell_seconds": 460, "window_queue_seconds": 460 } ], "closed_customers": [ { "person_id": "cust_103", "session_id": "cust_103-s1", "final_dwell_seconds": 260, "window_queue_seconds": 260 } ], "staff_seen_count": 1, "queue_metrics": { "queue_time_threshold_seconds": 300, "over_threshold_count": 6, "under_threshold_count": 2, "queue_level": "crowded", "previous_queue_level": "normal", "status_change": "queue_increased", "people": [ { "person_id": "cust_101", "queue_seconds": 820, "bucket": "over_threshold" }, { "person_id": "cust_102", "queue_seconds": 460, "bucket": "over_threshold" }, { "person_id": "cust_103", "queue_seconds": 260, "bucket": "under_threshold" } ] } } ``` ## people_flow_project 完整报文 `people_flow_project` 会额外带上过线统计和属性统计结果: ```json { "event": "half_hour_report", "project_type": "people_flow_project", "source_type": "rtsp", "source": "rtsp://user:password@camera-ip:554/h264/ch1/main/av_stream", "source_id": "queue_cam_01", "window_index": 12, "window_start": "2026-05-08T09:00:00+08:00", "window_end": "2026-05-08T09:30:00+08:00", "window_duration_seconds": 1800, "config_path": "/opt/people_flow_project/config/local.yaml", "line": { "coordinates": [0.1, 0.55, 0.9, 0.55], "mode": "normalized" }, "total_people": 7, "age_counts": { "minor": 1, "adult": 5, "senior": 1 }, "gender_counts": { "male": 4, "female": 3 }, "unknown_attributes": 2, "tracks": [ { "track_id": 1, "direction": "in", "age": 26, "age_bucket": "adult", "gender": "male", "samples_used": 3 } ], "queue_metrics": { "queue_time_threshold_seconds": 300, "over_threshold_count": 6, "under_threshold_count": 2, "queue_level": "crowded", "previous_queue_level": "normal", "status_change": "queue_increased", "people": [ { "person_id": "track_12", "queue_seconds": 810, "bucket": "over_threshold" }, { "person_id": "track_21", "queue_seconds": 180, "bucket": "under_threshold" } ] } } ``` ## 接收方建议 - 按 `event + project_type + source_id + window_end` 做幂等去重。 - 业务判断优先读取 `queue_metrics.queue_level` 和 `queue_metrics.status_change`。 - 如果只关心图片里的需求,最少只需要解析: - `source_id` - `window_start` - `window_end` - `queue_metrics.over_threshold_count` - `queue_metrics.under_threshold_count` - `queue_metrics.queue_level` - `queue_metrics.status_change`