Refactor store dwell alert management API and dwell engine

- Updated argument parsing in manage_api.py to include new threshold parameters.
- Enhanced _config_payload to include thresholds and webhook configurations.
- Modified _build_summary to track queue metrics and adjust alert reporting.
- Refactored DwellEngine to utilize queue thresholds for alerting and reporting.
- Added queue metrics calculations and status change tracking in dwell_engine.py.
- Updated notifier.py to support posting JSON events to webhooks.
- Adjusted example configuration to reflect new threshold parameters.
- Enhanced Docker entrypoint script for better process management.
- Updated tests to cover new queue metrics and thresholds.
- Improved ManagedServiceDetail and ManagedServices Vue components to display queue metrics.
This commit is contained in:
2026-05-09 11:35:55 +08:00
parent be5014c582
commit ea618fd674
26 changed files with 1605 additions and 117 deletions

View File

@@ -0,0 +1,249 @@
# 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`