跳转至

06. 通联记录与日志

1. 通信记录 Comm Records(JWT)

通信记录用于“本人设备发信记录”和管理员审计,不等同于频道聊天历史。普通用户的列表和详情只允许访问本人实体设备或本人幽灵设备产生的记录;管理员只有显式使用后台模式时读取全局记录。音频签名 URL 仅在记录级授权通过后生成。

GET /api/comm-records

参数:

  • page
  • page_size
  • device_id
  • group_id
  • user_id(仅管理员全局模式)
  • admin_mode=true(管理员后台查看全局)

返回示例:

{
  "code": 200,
  "message": "成功",
  "data": {
    "list": [
      {
        "id": 10001,
        "device_id": 123,
        "device_name": "BG7XXX-11",
        "dev_model": 107,
        "group_id": 1001,
        "group_name": "深圳应急群",
        "start_time": "2026-04-05 14:22:01",
        "end_time": "2026-04-05 14:22:06",
        "duration_ms": 5000,
        "msg_type": 0,
        "is_auto_broadcast": false,
        "audio_url": "https://.../xxx.opus"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  }
}

GET /api/comm-records/:id

普通用户跨账号按 ID 访问返回 403。需要查看频道中其他发送者的消息时,必须使用下述频道消息 API,不能依靠枚举通信记录 ID。

系统自动播报返回 is_auto_broadcast=true,发送者快照固定为 system-broadcast / AUTO / 自动播报。该记录只保存实际播放时间和投递群组快照,audio_path 为空、audio_size=0,不会生成第二份通信录音;原始文件与协议播放文件只存在于自动播报音频库。

DELETE /api/comm-records/:id(Admin)

2. 频道消息历史(JWT + Approved)

频道消息与通信记录来源于同一张 comm_records 表,但查询边界、授权和响应模型独立。消息接口按群组 ACL 返回频道内所有可见发送者的已完成消息,不按当前用户设备过滤。

GET /api/groups/:id/messages

参数:

  • limit:范围由 MessageAPI.MaxPageSize 配置,默认 1-100;缺省值由 MessageAPI.DefaultPageSize 配置,默认 50。
  • cursor:上一页的 next_cursor,不透明字符串。
  • message_type:all、voice 或 text,默认 all。
{
  "code": 200,
  "data": {
    "messages": [
      {
        "id": 10001,
        "message_type": "voice",
        "source_group_id": 1002,
        "source_group_name": "互联来源群组",
        "requested_group_id": 1001,
        "is_auto_broadcast": false,
        "sender": {
          "user_id": 12,
          "username": "radio-user",
          "callsign": "BG7XXX",
          "nickname": "Operator",
          "ssid": 101,
          "dev_model": 101,
          "is_ghost": true
        },
        "sent_at": "2026-08-05T12:00:00Z",
        "end_time": "2026-08-05T12:00:03Z",
        "duration_ms": 3000,
        "audio_url": "https://storage.example/authorized.opus?...",
        "audio_size": 20480,
        "audio_format": "draarl-opus-raw",
        "status": 2
      }
    ],
    "next_cursor": "eyJzdGFydF90aW1lIjoiLi4uIiwiaWQiOjEwMDAxfQ",
    "has_more": true,
    "server_time": "2026-08-05T12:01:00Z"
  }
}

文本消息使用 text_content,不返回音频字段。普通语音和 is_auto_broadcast=true 的自动播报语音均返回可回放的 audio_url、audio_size 与 audio_format=draarl-opus-raw。列表只返回 status=2 的完整记录。发送者优先使用消息写入时的快照,因此用户改名、设备删除或转让不会改写历史身份。

游标按 (start_time, id) 降序。服务端查询请求频道的投递快照,再按记录时间合并和去重;并发插入的新消息不会混入使用旧游标请求的下一页。

GET /api/groups/:id/messages/:message_id

返回一条频道消息。请求群组先通过 ACL,再验证消息发送时是否实际投递到该群组;无权限时返回 403,未投递到该群组或不存在时返回 404。语音签名 URL 只在以上检查全部通过后生成。

列表和详情共享部署级查询保护:账号/IP 固定一分钟窗口超限分别返回 429 message_api_user_rate_limited 或 429 message_api_ip_rate_limited,并携带 Retry-After;并发查询槽满返回 503 message_api_busy 和 Retry-After: 1。客户端应按响应等待,不应立即循环重试。

权限与互联语义

  • 公开群组允许已审核用户查看。
  • 私有群组只允许管理员、群主或已验证成员查看;成员退出或被移除后立即失去列表和详情权限。
  • 禁用、虚拟或不存在的请求群组不可查询。
  • 历史可见性按发送时写入的投递快照判断,不按当前互联拓扑回算。A/B 解除互联后,互联期间实际投递到 A 的 B 消息仍可在 A 中查询。
  • 同一消息即使因多个投递关系命中也只返回一次。
  • requested_group_id 是客户端请求的频道,source_group_id 是消息实际发送频道,两者不能互换。
  • /api/comm-records?group_id=... 只筛选当前用户自己的发信记录,不提供频道内其他发送者的历史。

数据库迁移与查询索引

显式执行 -auto-migrate 会增加 message_type、text_content、发送者快照字段,并创建 comm_record_delivery_groups。其复合主键为 (record_id, group_id);快照保存 start_time 和 message_type,频道游标索引为 idx_delivery_group_cursor(group_id, start_time, record_id) 与 idx_delivery_group_type_cursor(group_id, message_type, start_time, record_id)。迁移会分批处理旧 audio_path='text:...' 数据,为旧记录只补录仍存在的来源组快照,并从通信记录补齐快照时间和类型,不猜测历史互联范围。来源组已删除的孤儿通信记录会保留并输出跳过数量,不会创建违反群组外键的快照。回滚服务端时不得删除新增列、快照表或索引。

百万行验收使用 DRAARL_MESSAGE_PERFORMANCE_E2E=true,并检查普通和类型查询分别使用对应的快照游标索引。发信记录分页另使用 DRAARL_COMM_RECORDS_PERFORMANCE_E2E=true 做 12 万行验收。完整方法见幽灵多收与消息查询性能验证;发布环境仍需在真实数据副本执行自己的 EXPLAIN ANALYZE。

3. 通信统计

用户侧(JWT)

  • GET /api/comm-records/user-stats
  • GET /api/comm-records/user-trend

返回(示例):

{ "code": 200, "data": { "total_count": 120, "total_size": 93842012, "total_duration": 1280000 } }

管理侧(Admin)

  • GET /api/comm-records/stats(录制器内部统计)
  • GET /api/comm-records/system-stats
  • GET /api/comm-records/system-trend

4. 通信设置(Admin)

GET /api/config/comm-settings

PUT /api/config/comm-settings

请求:

{
  "enabled": true,
  "retention_days": 30,
  "min_duration_ms": 300,
  "max_duration_sec": 180,
  "batch_upload_sec": 10
}

5. 通联日志 Logbook(JWT)

用户接口

  • GET /api/logbooks
  • GET /api/logbooks/:id
  • POST /api/logbooks
  • PUT /api/logbooks/:id
  • DELETE /api/logbooks/:id
  • DELETE /api/logbooks/batch

创建请求示例:

{
  "my_callsign": "BG7XXX",
  "time_utc": "2026-04-05 06:35:20",
  "tx_frequency": 439.5000,
  "rx_frequency": 439.5000,
  "mode": "FM",
  "callsign": "BG7YYY",
  "their_rst": "59",
  "my_rst": "59",
  "notes": "首次通联"
}

批量删除请求:

{ "ids": [101, 102, 103] }

管理员接口(Admin)

  • GET /api/admin/logbooks
  • GET /api/admin/logbooks/:id
  • PUT /api/admin/logbooks/:id
  • DELETE /api/admin/logbooks/:id
  • DELETE /api/admin/logbooks/batch

6. 电台预设(JWT)

  • GET /api/user/radio-presets
  • POST /api/user/radio-presets
  • PUT /api/user/radio-presets/:id
  • DELETE /api/user/radio-presets/:id
  • PUT /api/user/radio-presets/reorder

创建请求示例:

{
  "name": "车载台",
  "radio": "YAESU FTM-300",
  "antenna": "车载吸盘",
  "power": 25,
  "qth": "Shenzhen",
  "sort_order": 10
}

排序请求示例:

{ "orders": [{ "id": 1, "order": 1 }, { "id": 2, "order": 2 }] }