Преглед изворни кода

Add new routes for cameras, sessions, and stats; implement audit logging and soft delete functionality

yb пре 3 недеља
родитељ
комит
01b0543b1e
10 измењених фајлова са 4223 додато и 25 уклоњено
  1. 1025 0
      postman_collection.json
  2. 45 1
      src/index.ts
  3. 324 0
      src/routes/audit.ts
  4. 331 0
      src/routes/cameras.ts
  5. 382 0
      src/routes/sessions.ts
  6. 371 0
      src/routes/stats.ts
  7. 14 13
      src/routes/user.ts
  8. 11 11
      src/services/auth.ts
  9. 6 0
      src/types/index.ts
  10. 1714 0
      tg-live-game.postman_collection.json

+ 1025 - 0
postman_collection.json

@@ -0,0 +1,1025 @@
+{
+  "info": {
+    "name": "tg-live-game",
+    "description": "TG Live Game Backend API Collection",
+    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+  },
+  "variable": [
+    {
+      "key": "baseUrl",
+      "value": "http://localhost:8787",
+      "type": "string"
+    },
+    {
+      "key": "accessToken",
+      "value": "",
+      "type": "string"
+    },
+    {
+      "key": "refreshToken",
+      "value": "",
+      "type": "string"
+    }
+  ],
+  "auth": {
+    "type": "bearer",
+    "bearer": [
+      {
+        "key": "token",
+        "value": "{{accessToken}}",
+        "type": "string"
+      }
+    ]
+  },
+  "item": [
+    {
+      "name": "tg-live-game-hono",
+      "item": [
+        {
+          "name": "auth",
+          "item": [
+            {
+              "name": "register",
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"username\": \"testuser\",\n  \"password\": \"test123456\",\n  \"email\": \"test@example.com\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/register",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "register"]
+                }
+              }
+            },
+            {
+              "name": "login",
+              "event": [
+                {
+                  "listen": "test",
+                  "script": {
+                    "exec": [
+                      "var jsonData = pm.response.json();",
+                      "if (jsonData.code === 200 && jsonData.data) {",
+                      "    pm.collectionVariables.set('accessToken', jsonData.data.accessToken);",
+                      "    pm.collectionVariables.set('refreshToken', jsonData.data.refreshToken);",
+                      "}"
+                    ],
+                    "type": "text/javascript"
+                  }
+                }
+              ],
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"username\": \"admin\",\n  \"password\": \"admin123\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/login",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "login"]
+                }
+              }
+            },
+            {
+              "name": "refresh",
+              "event": [
+                {
+                  "listen": "test",
+                  "script": {
+                    "exec": [
+                      "var jsonData = pm.response.json();",
+                      "if (jsonData.code === 200 && jsonData.data) {",
+                      "    pm.collectionVariables.set('accessToken', jsonData.data.accessToken);",
+                      "    pm.collectionVariables.set('refreshToken', jsonData.data.refreshToken);",
+                      "}"
+                    ],
+                    "type": "text/javascript"
+                  }
+                }
+              ],
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"refreshToken\": \"{{refreshToken}}\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/refresh",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "refresh"]
+                }
+              }
+            },
+            {
+              "name": "me",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/me",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "me"]
+                }
+              }
+            },
+            {
+              "name": "change-password",
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"oldPassword\": \"admin123\",\n  \"newPassword\": \"newpassword123\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/change-password",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "change-password"]
+                }
+              }
+            },
+            {
+              "name": "logout",
+              "request": {
+                "method": "POST",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/auth/logout",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "auth", "logout"]
+                }
+              }
+            }
+          ]
+        },
+        {
+          "name": "users",
+          "item": [
+            {
+              "name": "list",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/users?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "users"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" },
+                    { "key": "role", "value": "", "disabled": true },
+                    { "key": "status", "value": "", "disabled": true },
+                    { "key": "search", "value": "", "disabled": true }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "get",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/users/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "users", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "user_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "create",
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"username\": \"newuser\",\n  \"password\": \"password123\",\n  \"email\": \"newuser@example.com\",\n  \"role\": \"viewer\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/users",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "users"]
+                }
+              }
+            },
+            {
+              "name": "update",
+              "request": {
+                "method": "PUT",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"email\": \"updated@example.com\",\n  \"role\": \"operator\",\n  \"status\": \"active\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/users/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "users", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "user_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "delete",
+              "request": {
+                "method": "DELETE",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/users/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "users", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "user_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "permissions",
+              "item": [
+                {
+                  "name": "list",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/users/:id/permissions",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "users", ":id", "permissions"],
+                      "variable": [
+                        { "key": "id", "value": "user_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "add",
+                  "request": {
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"camera_id\": \"camera_id_here\",\n  \"permission\": \"view\"\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/users/:id/permissions",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "users", ":id", "permissions"],
+                      "variable": [
+                        { "key": "id", "value": "user_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "delete",
+                  "request": {
+                    "method": "DELETE",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/users/:id/permissions/:permissionId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "users", ":id", "permissions", ":permissionId"],
+                      "variable": [
+                        { "key": "id", "value": "user_id_here" },
+                        { "key": "permissionId", "value": "permission_id_here" }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "name": "cameras",
+          "item": [
+            {
+              "name": "list",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" },
+                    { "key": "status", "value": "", "disabled": true },
+                    { "key": "type", "value": "", "disabled": true },
+                    { "key": "search", "value": "", "disabled": true }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "get",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "camera_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "create",
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"name\": \"Camera 1\",\n  \"type\": \"rtsp\",\n  \"protocol\": \"rtmps\",\n  \"rtsp_url\": \"rtsp://example.com/stream\",\n  \"location\": \"Room 101\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras"]
+                }
+              }
+            },
+            {
+              "name": "update",
+              "request": {
+                "method": "PUT",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"name\": \"Camera 1 Updated\",\n  \"location\": \"Room 102\",\n  \"status\": \"online\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "camera_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "delete",
+              "request": {
+                "method": "DELETE",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "camera_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "sessions",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/cameras/:id/sessions?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "cameras", ":id", "sessions"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" },
+                    { "key": "status", "value": "", "disabled": true }
+                  ],
+                  "variable": [
+                    { "key": "id", "value": "camera_id_here" }
+                  ]
+                }
+              }
+            }
+          ]
+        },
+        {
+          "name": "sessions",
+          "item": [
+            {
+              "name": "list",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" },
+                    { "key": "status", "value": "", "disabled": true },
+                    { "key": "camera_id", "value": "", "disabled": true }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "live",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/live",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", "live"]
+                }
+              }
+            },
+            {
+              "name": "get",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "session_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "start",
+              "request": {
+                "method": "POST",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"camera_id\": \"camera_id_here\",\n  \"live_input_id\": \"live_input_id_here\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions"]
+                }
+              }
+            },
+            {
+              "name": "end",
+              "request": {
+                "method": "PUT",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"recording_id\": \"recording_id_here\"\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/:id/end",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", ":id", "end"],
+                  "variable": [
+                    { "key": "id", "value": "session_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "update-viewers",
+              "request": {
+                "method": "PUT",
+                "header": [
+                  { "key": "Content-Type", "value": "application/json" }
+                ],
+                "body": {
+                  "mode": "raw",
+                  "raw": "{\n  \"viewer_count\": 100\n}"
+                },
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/:id/viewers",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", ":id", "viewers"],
+                  "variable": [
+                    { "key": "id", "value": "session_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "stats",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/:id/stats",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", ":id", "stats"],
+                  "variable": [
+                    { "key": "id", "value": "session_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "delete",
+              "request": {
+                "method": "DELETE",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/sessions/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "sessions", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "session_id_here" }
+                  ]
+                }
+              }
+            }
+          ]
+        },
+        {
+          "name": "stats",
+          "item": [
+            {
+              "name": "view",
+              "item": [
+                {
+                  "name": "start",
+                  "request": {
+                    "auth": { "type": "noauth" },
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"video_id\": \"video_id_here\"\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stats/view/start",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stats", "view", "start"]
+                    }
+                  }
+                },
+                {
+                  "name": "end",
+                  "request": {
+                    "auth": { "type": "noauth" },
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"view_id\": \"view_id_here\",\n  \"watch_duration\": 300\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stats/view/end",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stats", "view", "end"]
+                    }
+                  }
+                },
+                {
+                  "name": "heartbeat",
+                  "request": {
+                    "auth": { "type": "noauth" },
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"view_id\": \"view_id_here\",\n  \"watch_duration\": 60\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stats/view/heartbeat",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stats", "view", "heartbeat"]
+                    }
+                  }
+                }
+              ]
+            },
+            {
+              "name": "video",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/stats/video/:videoId",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "stats", "video", ":videoId"],
+                  "variable": [
+                    { "key": "videoId", "value": "video_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "session",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/stats/session/:sessionId",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "stats", "session", ":sessionId"],
+                  "variable": [
+                    { "key": "sessionId", "value": "session_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "overview",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/stats/overview?days=7",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "stats", "overview"],
+                  "query": [
+                    { "key": "days", "value": "7" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "views",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/stats/views?page=1&pageSize=50",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "stats", "views"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "50" },
+                    { "key": "video_id", "value": "", "disabled": true },
+                    { "key": "session_id", "value": "", "disabled": true },
+                    { "key": "user_id", "value": "", "disabled": true }
+                  ]
+                }
+              }
+            }
+          ]
+        },
+        {
+          "name": "audit-logs",
+          "item": [
+            {
+              "name": "list",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/audit-logs?page=1&pageSize=50",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "audit-logs"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "50" },
+                    { "key": "action", "value": "", "disabled": true },
+                    { "key": "resource", "value": "", "disabled": true },
+                    { "key": "user_id", "value": "", "disabled": true },
+                    { "key": "start_date", "value": "", "disabled": true },
+                    { "key": "end_date", "value": "", "disabled": true }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "get",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/audit-logs/:id",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "audit-logs", ":id"],
+                  "variable": [
+                    { "key": "id", "value": "log_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "stats",
+              "item": [
+                {
+                  "name": "summary",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/audit-logs/stats/summary?days=7",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "audit-logs", "stats", "summary"],
+                      "query": [
+                        { "key": "days", "value": "7" }
+                      ]
+                    }
+                  }
+                }
+              ]
+            },
+            {
+              "name": "user",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/audit-logs/user/:userId?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "audit-logs", "user", ":userId"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" }
+                  ],
+                  "variable": [
+                    { "key": "userId", "value": "user_id_here" }
+                  ]
+                }
+              }
+            },
+            {
+              "name": "resource",
+              "request": {
+                "method": "GET",
+                "header": [],
+                "url": {
+                  "raw": "{{baseUrl}}/api/audit-logs/resource/:resource/:resourceId?page=1&pageSize=20",
+                  "host": ["{{baseUrl}}"],
+                  "path": ["api", "audit-logs", "resource", ":resource", ":resourceId"],
+                  "query": [
+                    { "key": "page", "value": "1" },
+                    { "key": "pageSize", "value": "20" }
+                  ],
+                  "variable": [
+                    { "key": "resource", "value": "camera" },
+                    { "key": "resourceId", "value": "resource_id_here" }
+                  ]
+                }
+              }
+            }
+          ]
+        },
+        {
+          "name": "stream",
+          "item": [
+            {
+              "name": "video",
+              "item": [
+                {
+                  "name": "list",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/list",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", "list"]
+                    }
+                  }
+                },
+                {
+                  "name": "get",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/:videoId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", ":videoId"],
+                      "variable": [
+                        { "key": "videoId", "value": "video_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "delete",
+                  "request": {
+                    "method": "DELETE",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/:videoId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", ":videoId"],
+                      "variable": [
+                        { "key": "videoId", "value": "video_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "import",
+                  "request": {
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"url\": \"https://example.com/video.mp4\",\n  \"meta\": {\n    \"name\": \"My Video\"\n  }\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/import",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", "import"]
+                    }
+                  }
+                },
+                {
+                  "name": "upload-url",
+                  "request": {
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"maxDurationSeconds\": 3600,\n  \"meta\": {\n    \"name\": \"My Upload\"\n  }\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/upload-url",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", "upload-url"]
+                    }
+                  }
+                },
+                {
+                  "name": "playback",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/video/:videoId/playback",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "video", ":videoId", "playback"],
+                      "variable": [
+                        { "key": "videoId", "value": "video_id_here" }
+                      ]
+                    }
+                  }
+                }
+              ]
+            },
+            {
+              "name": "live",
+              "item": [
+                {
+                  "name": "list",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/list",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", "list"]
+                    }
+                  }
+                },
+                {
+                  "name": "create",
+                  "request": {
+                    "method": "POST",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"meta\": {\n    \"name\": \"My Live Stream\"\n  },\n  \"recording\": {\n    \"mode\": \"automatic\"\n  }\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live"]
+                    }
+                  }
+                },
+                {
+                  "name": "get",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", ":liveInputId"],
+                      "variable": [
+                        { "key": "liveInputId", "value": "live_input_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "update",
+                  "request": {
+                    "method": "PUT",
+                    "header": [
+                      { "key": "Content-Type", "value": "application/json" }
+                    ],
+                    "body": {
+                      "mode": "raw",
+                      "raw": "{\n  \"meta\": {\n    \"name\": \"Updated Live Stream\"\n  }\n}"
+                    },
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", ":liveInputId"],
+                      "variable": [
+                        { "key": "liveInputId", "value": "live_input_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "delete",
+                  "request": {
+                    "method": "DELETE",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", ":liveInputId"],
+                      "variable": [
+                        { "key": "liveInputId", "value": "live_input_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "playback",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/:liveInputId/playback",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", ":liveInputId", "playback"],
+                      "variable": [
+                        { "key": "liveInputId", "value": "live_input_id_here" }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "name": "recordings",
+                  "request": {
+                    "method": "GET",
+                    "header": [],
+                    "url": {
+                      "raw": "{{baseUrl}}/api/stream/live/:liveInputId/recordings",
+                      "host": ["{{baseUrl}}"],
+                      "path": ["api", "stream", "live", ":liveInputId", "recordings"],
+                      "variable": [
+                        { "key": "liveInputId", "value": "live_input_id_here" }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 45 - 1
src/index.ts

@@ -4,6 +4,10 @@ import { logger } from 'hono/logger'
 import stream from './routes/stream'
 import auth from './routes/auth'
 import user from './routes/user'
+import cameras from './routes/cameras'
+import sessions from './routes/sessions'
+import stats from './routes/stats'
+import audit from './routes/audit'
 import { authMiddleware } from './middleware/auth'
 import type { Env } from './types'
 
@@ -23,7 +27,7 @@ app.get('/', (c) => {
     code: 200,
     msg: 'TG Live Game API',
     data: {
-      version: '1.0.0',
+      version: '1.1.0',
       endpoints: {
         auth: [
           'POST /api/auth/login',
@@ -43,6 +47,40 @@ app.get('/', (c) => {
           'POST /api/users/:id/permissions',
           'DELETE /api/users/:id/permissions/:permissionId',
         ],
+        cameras: [
+          'GET  /api/cameras',
+          'GET  /api/cameras/:id',
+          'POST /api/cameras',
+          'PUT  /api/cameras/:id',
+          'DELETE /api/cameras/:id',
+          'GET  /api/cameras/:id/sessions',
+        ],
+        sessions: [
+          'GET  /api/sessions',
+          'GET  /api/sessions/live',
+          'GET  /api/sessions/:id',
+          'POST /api/sessions',
+          'PUT  /api/sessions/:id/end',
+          'PUT  /api/sessions/:id/viewers',
+          'GET  /api/sessions/:id/stats',
+          'DELETE /api/sessions/:id',
+        ],
+        stats: [
+          'POST /api/stats/view/start',
+          'POST /api/stats/view/end',
+          'POST /api/stats/view/heartbeat',
+          'GET  /api/stats/video/:videoId',
+          'GET  /api/stats/session/:sessionId',
+          'GET  /api/stats/overview',
+          'GET  /api/stats/views',
+        ],
+        audit: [
+          'GET  /api/audit-logs',
+          'GET  /api/audit-logs/:id',
+          'GET  /api/audit-logs/stats/summary',
+          'GET  /api/audit-logs/user/:userId',
+          'GET  /api/audit-logs/resource/:resource/:resourceId',
+        ],
         stream: [
           'GET  /api/stream/video/list',
           'GET  /api/stream/video/:videoId',
@@ -66,8 +104,14 @@ app.get('/', (c) => {
 // 挂载公开路由
 app.route('/api/auth', auth)
 
+// 挂载观看统计路由(部分公开)
+app.route('/api/stats', stats)
+
 // 挂载需要认证的路由
 app.route('/api/users', user)
+app.route('/api/cameras', cameras)
+app.route('/api/sessions', sessions)
+app.route('/api/audit-logs', audit)
 app.route('/api/stream', stream)
 
 // 404 处理

+ 324 - 0
src/routes/audit.ts

@@ -0,0 +1,324 @@
+import { Hono } from 'hono'
+import { authMiddleware, requireRole } from '../middleware/auth'
+import type { Env, ApiResponse, PageResponse, AuditLog } from '../types'
+
+const audit = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+// 所有审计日志路由都需要认证和 admin 角色
+audit.use('*', authMiddleware())
+audit.use('*', requireRole('admin'))
+
+/**
+ * 获取审计日志列表
+ * GET /api/audit-logs
+ */
+audit.get('/', async (c) => {
+  try {
+    const {
+      action,
+      resource,
+      user_id,
+      start_date,
+      end_date,
+      page = '1',
+      pageSize = '50'
+    } = c.req.query()
+
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    let sql = `
+      SELECT al.*, u.username
+      FROM audit_logs al
+      LEFT JOIN users u ON al.user_id = u.id
+      WHERE 1=1
+    `
+    const params: unknown[] = []
+
+    if (action) {
+      sql += ' AND al.action = ?'
+      params.push(action)
+    }
+
+    if (resource) {
+      sql += ' AND al.resource = ?'
+      params.push(resource)
+    }
+
+    if (user_id) {
+      sql += ' AND al.user_id = ?'
+      params.push(user_id)
+    }
+
+    if (start_date) {
+      const startTimestamp = Math.floor(new Date(start_date).getTime() / 1000)
+      sql += ' AND al.created_at >= ?'
+      params.push(startTimestamp)
+    }
+
+    if (end_date) {
+      const endTimestamp = Math.floor(new Date(end_date).getTime() / 1000) + 86400 // 加一天
+      sql += ' AND al.created_at < ?'
+      params.push(endTimestamp)
+    }
+
+    // 获取总数
+    const countSql = `
+      SELECT COUNT(*) as total
+      FROM audit_logs al
+      WHERE 1=1
+      ${action ? ' AND al.action = ?' : ''}
+      ${resource ? ' AND al.resource = ?' : ''}
+      ${user_id ? ' AND al.user_id = ?' : ''}
+      ${start_date ? ' AND al.created_at >= ?' : ''}
+      ${end_date ? ' AND al.created_at < ?' : ''}
+    `
+    const countParams = params.slice()
+    const countResult = await c.env.DB.prepare(countSql).bind(...countParams).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    sql += ' ORDER BY al.created_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const result = await c.env.DB.prepare(sql).bind(...params).all()
+
+    // 解析 details JSON
+    const rows = (result.results || []).map((row: any) => {
+      if (row.details && typeof row.details === 'string') {
+        try {
+          row.parsedDetails = JSON.parse(row.details)
+        } catch {
+          // 保持原样
+        }
+      }
+      return row
+    })
+
+    const data: PageResponse<any> = {
+      rows,
+      total,
+    }
+
+    return c.json(success(data))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取审计日志详情
+ * GET /api/audit-logs/:id
+ */
+audit.get('/:id', async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    const log = await c.env.DB.prepare(`
+      SELECT al.*, u.username
+      FROM audit_logs al
+      LEFT JOIN users u ON al.user_id = u.id
+      WHERE al.id = ?
+    `).bind(id).first()
+
+    if (!log) {
+      return c.json(error('Audit log not found', 404))
+    }
+
+    // 解析 details JSON
+    const result = log as any
+    if (result.details && typeof result.details === 'string') {
+      try {
+        result.parsedDetails = JSON.parse(result.details)
+      } catch {
+        // 保持原样
+      }
+    }
+
+    return c.json(success(result))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取审计日志统计
+ * GET /api/audit-logs/stats/summary
+ */
+audit.get('/stats/summary', async (c) => {
+  try {
+    const { days = '7' } = c.req.query()
+    const daysAgo = Math.floor(Date.now() / 1000) - parseInt(days) * 24 * 60 * 60
+
+    // 按操作类型统计
+    const byAction = await c.env.DB.prepare(`
+      SELECT action, COUNT(*) as count
+      FROM audit_logs
+      WHERE created_at >= ?
+      GROUP BY action
+      ORDER BY count DESC
+    `).bind(daysAgo).all()
+
+    // 按资源类型统计
+    const byResource = await c.env.DB.prepare(`
+      SELECT resource, COUNT(*) as count
+      FROM audit_logs
+      WHERE created_at >= ?
+      GROUP BY resource
+      ORDER BY count DESC
+    `).bind(daysAgo).all()
+
+    // 按用户统计
+    const byUser = await c.env.DB.prepare(`
+      SELECT al.user_id, u.username, COUNT(*) as count
+      FROM audit_logs al
+      LEFT JOIN users u ON al.user_id = u.id
+      WHERE al.created_at >= ?
+      GROUP BY al.user_id
+      ORDER BY count DESC
+      LIMIT 10
+    `).bind(daysAgo).all()
+
+    // 每日统计
+    const dailyStats = await c.env.DB.prepare(`
+      SELECT
+        date(created_at, 'unixepoch') as date,
+        COUNT(*) as count
+      FROM audit_logs
+      WHERE created_at >= ?
+      GROUP BY date(created_at, 'unixepoch')
+      ORDER BY date ASC
+    `).bind(daysAgo).all()
+
+    // 总计
+    const totalCount = await c.env.DB.prepare(`
+      SELECT COUNT(*) as total
+      FROM audit_logs
+      WHERE created_at >= ?
+    `).bind(daysAgo).first<{ total: number }>()
+
+    return c.json(success({
+      period_days: parseInt(days),
+      total: totalCount?.total || 0,
+      by_action: byAction.results || [],
+      by_resource: byResource.results || [],
+      by_user: byUser.results || [],
+      daily: dailyStats.results || [],
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取用户操作历史
+ * GET /api/audit-logs/user/:userId
+ */
+audit.get('/user/:userId', async (c) => {
+  try {
+    const userId = c.req.param('userId')
+    const { page = '1', pageSize = '20' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    // 获取总数
+    const countResult = await c.env.DB.prepare(`
+      SELECT COUNT(*) as total FROM audit_logs WHERE user_id = ?
+    `).bind(userId).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    const result = await c.env.DB.prepare(`
+      SELECT * FROM audit_logs
+      WHERE user_id = ?
+      ORDER BY created_at DESC
+      LIMIT ? OFFSET ?
+    `).bind(userId, limit, offset).all()
+
+    // 解析 details JSON
+    const rows = (result.results || []).map((row: any) => {
+      if (row.details && typeof row.details === 'string') {
+        try {
+          row.parsedDetails = JSON.parse(row.details)
+        } catch {
+          // 保持原样
+        }
+      }
+      return row
+    })
+
+    return c.json(success({
+      rows,
+      total,
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取资源操作历史
+ * GET /api/audit-logs/resource/:resource/:resourceId
+ */
+audit.get('/resource/:resource/:resourceId', async (c) => {
+  try {
+    const resource = c.req.param('resource')
+    const resourceId = c.req.param('resourceId')
+    const { page = '1', pageSize = '20' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    // 获取总数
+    const countResult = await c.env.DB.prepare(`
+      SELECT COUNT(*) as total
+      FROM audit_logs
+      WHERE resource = ? AND resource_id = ?
+    `).bind(resource, resourceId).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    const result = await c.env.DB.prepare(`
+      SELECT al.*, u.username
+      FROM audit_logs al
+      LEFT JOIN users u ON al.user_id = u.id
+      WHERE al.resource = ? AND al.resource_id = ?
+      ORDER BY al.created_at DESC
+      LIMIT ? OFFSET ?
+    `).bind(resource, resourceId, limit, offset).all()
+
+    // 解析 details JSON
+    const rows = (result.results || []).map((row: any) => {
+      if (row.details && typeof row.details === 'string') {
+        try {
+          row.parsedDetails = JSON.parse(row.details)
+        } catch {
+          // 保持原样
+        }
+      }
+      return row
+    })
+
+    return c.json(success({
+      rows,
+      total,
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+export default audit

+ 331 - 0
src/routes/cameras.ts

@@ -0,0 +1,331 @@
+import { Hono } from 'hono'
+import { authMiddleware, requireRole } from '../middleware/auth'
+import { generateId } from '../utils/jwt'
+import type { Env, ApiResponse, PageResponse, Camera } from '../types'
+
+const cameras = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+// 所有摄像头路由都需要认证
+cameras.use('*', authMiddleware())
+
+/**
+ * 获取摄像头列表
+ * GET /api/cameras
+ */
+cameras.get('/', async (c) => {
+  try {
+    const { status, type, search, page = '1', pageSize = '20' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    let sql = 'SELECT * FROM cameras WHERE is_deleted = 0'
+    const params: unknown[] = []
+
+    if (status) {
+      sql += ' AND status = ?'
+      params.push(status)
+    }
+
+    if (type) {
+      sql += ' AND type = ?'
+      params.push(type)
+    }
+
+    if (search) {
+      sql += ' AND (name LIKE ? OR location LIKE ?)'
+      params.push(`%${search}%`, `%${search}%`)
+    }
+
+    // 获取总数
+    const countSql = sql.replace('SELECT *', 'SELECT COUNT(*) as total')
+    const countResult = await c.env.DB.prepare(countSql).bind(...params).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const result = await c.env.DB.prepare(sql).bind(...params).all<Camera>()
+
+    const data: PageResponse<Camera> = {
+      rows: result.results || [],
+      total,
+    }
+
+    return c.json(success(data))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取摄像头详情
+ * GET /api/cameras/:id
+ */
+cameras.get('/:id', async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    const camera = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<Camera>()
+
+    if (!camera) {
+      return c.json(error('Camera not found', 404))
+    }
+
+    return c.json(success(camera))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 创建摄像头
+ * POST /api/cameras
+ * 需要 admin 或 operator 角色
+ */
+cameras.post('/', requireRole('admin', 'operator'), async (c) => {
+  try {
+    const body = await c.req.json<Partial<Camera>>()
+
+    if (!body.name) {
+      return c.json(error('Name is required', 400))
+    }
+
+    const id = generateId()
+    const now = Math.floor(Date.now() / 1000)
+
+    await c.env.DB.prepare(`
+      INSERT INTO cameras (id, name, type, protocol, rtsp_url, location, status, live_input_id, meta, created_at, updated_at)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `).bind(
+      id,
+      body.name,
+      body.type || 'rtsp',
+      body.protocol || 'rtmps',
+      body.rtsp_url || null,
+      body.location || null,
+      body.status || 'offline',
+      body.live_input_id || null,
+      body.meta || null,
+      now,
+      now
+    ).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'create', 'camera', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify({ name: body.name }),
+      now
+    ).run()
+
+    const camera = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ?'
+    ).bind(id).first<Camera>()
+
+    return c.json(success(camera, 'Camera created'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 更新摄像头
+ * PUT /api/cameras/:id
+ * 需要 admin 或 operator 角色
+ */
+cameras.put('/:id', requireRole('admin', 'operator'), async (c) => {
+  try {
+    const id = c.req.param('id')
+    const body = await c.req.json<Partial<Camera>>()
+
+    // 检查摄像头是否存在
+    const existing = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<Camera>()
+
+    if (!existing) {
+      return c.json(error('Camera not found', 404))
+    }
+
+    const now = Math.floor(Date.now() / 1000)
+    const updates: string[] = []
+    const params: unknown[] = []
+
+    if (body.name !== undefined) {
+      updates.push('name = ?')
+      params.push(body.name)
+    }
+    if (body.type !== undefined) {
+      updates.push('type = ?')
+      params.push(body.type)
+    }
+    if (body.protocol !== undefined) {
+      updates.push('protocol = ?')
+      params.push(body.protocol)
+    }
+    if (body.rtsp_url !== undefined) {
+      updates.push('rtsp_url = ?')
+      params.push(body.rtsp_url)
+    }
+    if (body.location !== undefined) {
+      updates.push('location = ?')
+      params.push(body.location)
+    }
+    if (body.status !== undefined) {
+      updates.push('status = ?')
+      params.push(body.status)
+    }
+    if (body.live_input_id !== undefined) {
+      updates.push('live_input_id = ?')
+      params.push(body.live_input_id)
+    }
+    if (body.meta !== undefined) {
+      updates.push('meta = ?')
+      params.push(body.meta)
+    }
+
+    if (updates.length === 0) {
+      return c.json(error('No fields to update', 400))
+    }
+
+    updates.push('updated_at = ?')
+    params.push(now, id)
+
+    await c.env.DB.prepare(`
+      UPDATE cameras SET ${updates.join(', ')} WHERE id = ?
+    `).bind(...params).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'update', 'camera', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify(body),
+      now
+    ).run()
+
+    const camera = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ?'
+    ).bind(id).first<Camera>()
+
+    return c.json(success(camera, 'Camera updated'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 删除摄像头
+ * DELETE /api/cameras/:id
+ * 需要 admin 角色
+ */
+cameras.delete('/:id', requireRole('admin'), async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    // 检查摄像头是否存在
+    const existing = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<Camera>()
+
+    if (!existing) {
+      return c.json(error('Camera not found', 404))
+    }
+
+    const now = Math.floor(Date.now() / 1000)
+    await c.env.DB.prepare('UPDATE cameras SET is_deleted = 1, updated_at = ? WHERE id = ?').bind(now, id).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'delete', 'camera', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify({ name: existing.name }),
+      now
+    ).run()
+
+    return c.json(success(null, 'Camera deleted'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取摄像头的直播会话历史
+ * GET /api/cameras/:id/sessions
+ */
+cameras.get('/:id/sessions', async (c) => {
+  try {
+    const id = c.req.param('id')
+    const { status, page = '1', pageSize = '20' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    // 检查摄像头是否存在
+    const camera = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<Camera>()
+
+    if (!camera) {
+      return c.json(error('Camera not found', 404))
+    }
+
+    let sql = 'SELECT * FROM live_sessions WHERE camera_id = ? AND is_deleted = 0'
+    const params: unknown[] = [id]
+
+    if (status) {
+      sql += ' AND status = ?'
+      params.push(status)
+    }
+
+    // 获取总数
+    const countSql = sql.replace('SELECT *', 'SELECT COUNT(*) as total')
+    const countResult = await c.env.DB.prepare(countSql).bind(...params).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    sql += ' ORDER BY started_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const result = await c.env.DB.prepare(sql).bind(...params).all()
+
+    return c.json(success({
+      rows: result.results || [],
+      total,
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+export default cameras

+ 382 - 0
src/routes/sessions.ts

@@ -0,0 +1,382 @@
+import { Hono } from 'hono'
+import { authMiddleware, requireRole } from '../middleware/auth'
+import { generateId } from '../utils/jwt'
+import type { Env, ApiResponse, PageResponse, LiveSession, Camera } from '../types'
+
+const sessions = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+// 所有会话路由都需要认证
+sessions.use('*', authMiddleware())
+
+/**
+ * 获取直播会话列表
+ * GET /api/sessions
+ */
+sessions.get('/', async (c) => {
+  try {
+    const { status, camera_id, page = '1', pageSize = '20' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    let sql = `
+      SELECT s.*, c.name as camera_name
+      FROM live_sessions s
+      LEFT JOIN cameras c ON s.camera_id = c.id
+      WHERE s.is_deleted = 0
+    `
+    const params: unknown[] = []
+
+    if (status) {
+      sql += ' AND s.status = ?'
+      params.push(status)
+    }
+
+    if (camera_id) {
+      sql += ' AND s.camera_id = ?'
+      params.push(camera_id)
+    }
+
+    // 获取总数
+    const countSql = `
+      SELECT COUNT(*) as total
+      FROM live_sessions s
+      WHERE s.is_deleted = 0
+      ${status ? ' AND s.status = ?' : ''}
+      ${camera_id ? ' AND s.camera_id = ?' : ''}
+    `
+    const countParams = params.slice(0, (status ? 1 : 0) + (camera_id ? 1 : 0))
+    const countResult = await c.env.DB.prepare(countSql).bind(...countParams).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    sql += ' ORDER BY s.started_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const result = await c.env.DB.prepare(sql).bind(...params).all()
+
+    const data: PageResponse<LiveSession & { camera_name?: string }> = {
+      rows: (result.results || []) as unknown as (LiveSession & { camera_name?: string })[],
+      total,
+    }
+
+    return c.json(success(data))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取当前正在直播的会话
+ * GET /api/sessions/live
+ */
+sessions.get('/live', async (c) => {
+  try {
+    const result = await c.env.DB.prepare(`
+      SELECT s.*, c.name as camera_name
+      FROM live_sessions s
+      LEFT JOIN cameras c ON s.camera_id = c.id
+      WHERE s.status = 'live' AND s.is_deleted = 0
+      ORDER BY s.started_at DESC
+    `).all()
+
+    return c.json(success(result.results || []))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取直播会话详情
+ * GET /api/sessions/:id
+ */
+sessions.get('/:id', async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    const session = await c.env.DB.prepare(`
+      SELECT s.*, c.name as camera_name
+      FROM live_sessions s
+      LEFT JOIN cameras c ON s.camera_id = c.id
+      WHERE s.id = ? AND s.is_deleted = 0
+    `).bind(id).first()
+
+    if (!session) {
+      return c.json(error('Session not found', 404))
+    }
+
+    return c.json(success(session))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 开始直播会话
+ * POST /api/sessions
+ * 需要 admin 或 operator 角色
+ */
+sessions.post('/', requireRole('admin', 'operator'), async (c) => {
+  try {
+    const body = await c.req.json<{
+      camera_id: string
+      live_input_id: string
+      meta?: string
+    }>()
+
+    if (!body.camera_id || !body.live_input_id) {
+      return c.json(error('camera_id and live_input_id are required', 400))
+    }
+
+    // 检查摄像头是否存在
+    const camera = await c.env.DB.prepare(
+      'SELECT * FROM cameras WHERE id = ? AND is_deleted = 0'
+    ).bind(body.camera_id).first<Camera>()
+
+    if (!camera) {
+      return c.json(error('Camera not found', 404))
+    }
+
+    // 检查是否已有正在进行的会话
+    const existingSession = await c.env.DB.prepare(
+      "SELECT * FROM live_sessions WHERE camera_id = ? AND status = 'live' AND is_deleted = 0"
+    ).bind(body.camera_id).first()
+
+    if (existingSession) {
+      return c.json(error('Camera already has an active session', 400))
+    }
+
+    const id = generateId()
+    const now = Math.floor(Date.now() / 1000)
+
+    await c.env.DB.prepare(`
+      INSERT INTO live_sessions (id, camera_id, live_input_id, started_at, status, viewer_count, peak_viewers, meta, created_at)
+      VALUES (?, ?, ?, ?, 'live', 0, 0, ?, ?)
+    `).bind(
+      id,
+      body.camera_id,
+      body.live_input_id,
+      now,
+      body.meta || null,
+      now
+    ).run()
+
+    // 更新摄像头状态
+    await c.env.DB.prepare(
+      "UPDATE cameras SET status = 'online', live_input_id = ?, updated_at = ? WHERE id = ?"
+    ).bind(body.live_input_id, now, body.camera_id).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'create', 'session', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify({ camera_id: body.camera_id, camera_name: camera.name }),
+      now
+    ).run()
+
+    const session = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ?'
+    ).bind(id).first()
+
+    return c.json(success(session, 'Session started'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 结束直播会话
+ * PUT /api/sessions/:id/end
+ * 需要 admin 或 operator 角色
+ */
+sessions.put('/:id/end', requireRole('admin', 'operator'), async (c) => {
+  try {
+    const id = c.req.param('id')
+    const body = await c.req.json<{
+      recording_id?: string
+    }>()
+
+    // 检查会话是否存在
+    const session = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<LiveSession>()
+
+    if (!session) {
+      return c.json(error('Session not found', 404))
+    }
+
+    if (session.status !== 'live') {
+      return c.json(error('Session is not live', 400))
+    }
+
+    const now = Math.floor(Date.now() / 1000)
+    const duration = now - session.started_at
+
+    await c.env.DB.prepare(`
+      UPDATE live_sessions
+      SET status = 'ended', ended_at = ?, duration = ?, recording_id = ?
+      WHERE id = ?
+    `).bind(now, duration, body.recording_id || null, id).run()
+
+    // 更新摄像头状态
+    await c.env.DB.prepare(
+      "UPDATE cameras SET status = 'offline', updated_at = ? WHERE id = ?"
+    ).bind(now, session.camera_id).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'update', 'session', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify({ action: 'end', duration }),
+      now
+    ).run()
+
+    const updated = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ?'
+    ).bind(id).first()
+
+    return c.json(success(updated, 'Session ended'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 更新会话观看人数
+ * PUT /api/sessions/:id/viewers
+ */
+sessions.put('/:id/viewers', async (c) => {
+  try {
+    const id = c.req.param('id')
+    const body = await c.req.json<{
+      viewer_count: number
+    }>()
+
+    if (typeof body.viewer_count !== 'number') {
+      return c.json(error('viewer_count is required', 400))
+    }
+
+    const session = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<LiveSession>()
+
+    if (!session) {
+      return c.json(error('Session not found', 404))
+    }
+
+    const peak_viewers = Math.max(session.peak_viewers, body.viewer_count)
+
+    await c.env.DB.prepare(`
+      UPDATE live_sessions SET viewer_count = ?, peak_viewers = ? WHERE id = ?
+    `).bind(body.viewer_count, peak_viewers, id).run()
+
+    return c.json(success({ viewer_count: body.viewer_count, peak_viewers }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取会话统计
+ * GET /api/sessions/:id/stats
+ */
+sessions.get('/:id/stats', async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    const session = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<LiveSession>()
+
+    if (!session) {
+      return c.json(error('Session not found', 404))
+    }
+
+    // 获取观看统计
+    const viewStats = await c.env.DB.prepare(`
+      SELECT
+        COUNT(*) as total_views,
+        COUNT(DISTINCT user_id) as unique_users,
+        COUNT(DISTINCT ip_address) as unique_ips,
+        SUM(watch_duration) as total_watch_time,
+        AVG(watch_duration) as avg_watch_time
+      FROM view_stats
+      WHERE session_id = ?
+    `).bind(id).first()
+
+    return c.json(success({
+      session,
+      stats: viewStats,
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 删除会话
+ * DELETE /api/sessions/:id
+ * 需要 admin 角色
+ */
+sessions.delete('/:id', requireRole('admin'), async (c) => {
+  try {
+    const id = c.req.param('id')
+
+    const session = await c.env.DB.prepare(
+      'SELECT * FROM live_sessions WHERE id = ? AND is_deleted = 0'
+    ).bind(id).first<LiveSession>()
+
+    if (!session) {
+      return c.json(error('Session not found', 404))
+    }
+
+    if (session.status === 'live') {
+      return c.json(error('Cannot delete active session', 400))
+    }
+
+    await c.env.DB.prepare('UPDATE live_sessions SET is_deleted = 1 WHERE id = ?').bind(id).run()
+
+    // 记录审计日志
+    const user = c.get('user')
+    const now = Math.floor(Date.now() / 1000)
+    await c.env.DB.prepare(`
+      INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+      VALUES (?, ?, 'delete', 'session', ?, ?, ?)
+    `).bind(
+      generateId(),
+      user.sub,
+      id,
+      JSON.stringify({ camera_id: session.camera_id }),
+      now
+    ).run()
+
+    return c.json(success(null, 'Session deleted'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+export default sessions

+ 371 - 0
src/routes/stats.ts

@@ -0,0 +1,371 @@
+import { Hono } from 'hono'
+import { authMiddleware, requireRole, optionalAuth } from '../middleware/auth'
+import { generateId } from '../utils/jwt'
+import type { Env, ApiResponse, PageResponse, ViewStat } from '../types'
+
+const stats = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+/**
+ * 记录观看开始(公开接口,可选认证)
+ * POST /api/stats/view/start
+ */
+stats.post('/view/start', optionalAuth(), async (c) => {
+  try {
+    const body = await c.req.json<{
+      video_id?: string
+      session_id?: string
+    }>()
+
+    if (!body.video_id && !body.session_id) {
+      return c.json(error('video_id or session_id is required', 400))
+    }
+
+    const id = generateId()
+    const now = Math.floor(Date.now() / 1000)
+    const user = c.get('user')
+
+    // 获取客户端信息
+    const ip_address = c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || null
+    const user_agent = c.req.header('user-agent') || null
+    const country = c.req.header('cf-ipcountry') || null
+    const city = c.req.header('cf-ipcity') || null
+
+    await c.env.DB.prepare(`
+      INSERT INTO view_stats (id, video_id, session_id, user_id, ip_address, user_agent, started_at, country, city, created_at)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `).bind(
+      id,
+      body.video_id || null,
+      body.session_id || null,
+      user?.sub || null,
+      ip_address,
+      user_agent,
+      now,
+      country,
+      city,
+      now
+    ).run()
+
+    return c.json(success({ view_id: id }, 'View started'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 记录观看结束(公开接口)
+ * POST /api/stats/view/end
+ */
+stats.post('/view/end', async (c) => {
+  try {
+    const body = await c.req.json<{
+      view_id: string
+      watch_duration?: number
+    }>()
+
+    if (!body.view_id) {
+      return c.json(error('view_id is required', 400))
+    }
+
+    const now = Math.floor(Date.now() / 1000)
+
+    // 检查观看记录是否存在
+    const viewStat = await c.env.DB.prepare(
+      'SELECT * FROM view_stats WHERE id = ?'
+    ).bind(body.view_id).first<ViewStat>()
+
+    if (!viewStat) {
+      return c.json(error('View record not found', 404))
+    }
+
+    // 计算观看时长
+    const watch_duration = body.watch_duration || (now - viewStat.started_at)
+
+    await c.env.DB.prepare(`
+      UPDATE view_stats SET ended_at = ?, watch_duration = ? WHERE id = ?
+    `).bind(now, watch_duration, body.view_id).run()
+
+    // 如果是视频,更新视频观看计数
+    if (viewStat.video_id) {
+      await c.env.DB.prepare(`
+        UPDATE videos SET view_count = view_count + 1 WHERE id = ?
+      `).bind(viewStat.video_id).run()
+    }
+
+    return c.json(success({ watch_duration }, 'View ended'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 心跳更新(用于长时间观看)
+ * POST /api/stats/view/heartbeat
+ */
+stats.post('/view/heartbeat', async (c) => {
+  try {
+    const body = await c.req.json<{
+      view_id: string
+      watch_duration: number
+    }>()
+
+    if (!body.view_id) {
+      return c.json(error('view_id is required', 400))
+    }
+
+    await c.env.DB.prepare(`
+      UPDATE view_stats SET watch_duration = ? WHERE id = ?
+    `).bind(body.watch_duration, body.view_id).run()
+
+    return c.json(success(null, 'Heartbeat received'))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+// 以下路由需要认证
+stats.use('/video/*', authMiddleware())
+stats.use('/session/*', authMiddleware())
+stats.use('/overview', authMiddleware())
+
+/**
+ * 获取视频统计
+ * GET /api/stats/video/:videoId
+ */
+stats.get('/video/:videoId', async (c) => {
+  try {
+    const videoId = c.req.param('videoId')
+
+    // 基本统计
+    const basicStats = await c.env.DB.prepare(`
+      SELECT
+        COUNT(*) as total_views,
+        COUNT(DISTINCT user_id) as unique_users,
+        COUNT(DISTINCT ip_address) as unique_ips,
+        COALESCE(SUM(watch_duration), 0) as total_watch_time,
+        COALESCE(AVG(watch_duration), 0) as avg_watch_time,
+        COALESCE(MAX(watch_duration), 0) as max_watch_time
+      FROM view_stats
+      WHERE video_id = ?
+    `).bind(videoId).first()
+
+    // 地区分布
+    const geoStats = await c.env.DB.prepare(`
+      SELECT country, COUNT(*) as count
+      FROM view_stats
+      WHERE video_id = ? AND country IS NOT NULL
+      GROUP BY country
+      ORDER BY count DESC
+      LIMIT 10
+    `).bind(videoId).all()
+
+    // 每日统计(最近7天)
+    const sevenDaysAgo = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60
+    const dailyStats = await c.env.DB.prepare(`
+      SELECT
+        date(started_at, 'unixepoch') as date,
+        COUNT(*) as views,
+        COUNT(DISTINCT user_id) as unique_users
+      FROM view_stats
+      WHERE video_id = ? AND started_at >= ?
+      GROUP BY date(started_at, 'unixepoch')
+      ORDER BY date DESC
+    `).bind(videoId, sevenDaysAgo).all()
+
+    return c.json(success({
+      basic: basicStats,
+      geo: geoStats.results || [],
+      daily: dailyStats.results || [],
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取会话统计
+ * GET /api/stats/session/:sessionId
+ */
+stats.get('/session/:sessionId', async (c) => {
+  try {
+    const sessionId = c.req.param('sessionId')
+
+    // 基本统计
+    const basicStats = await c.env.DB.prepare(`
+      SELECT
+        COUNT(*) as total_views,
+        COUNT(DISTINCT user_id) as unique_users,
+        COUNT(DISTINCT ip_address) as unique_ips,
+        COALESCE(SUM(watch_duration), 0) as total_watch_time,
+        COALESCE(AVG(watch_duration), 0) as avg_watch_time
+      FROM view_stats
+      WHERE session_id = ?
+    `).bind(sessionId).first()
+
+    // 地区分布
+    const geoStats = await c.env.DB.prepare(`
+      SELECT country, COUNT(*) as count
+      FROM view_stats
+      WHERE session_id = ? AND country IS NOT NULL
+      GROUP BY country
+      ORDER BY count DESC
+      LIMIT 10
+    `).bind(sessionId).all()
+
+    return c.json(success({
+      basic: basicStats,
+      geo: geoStats.results || [],
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取整体统计概览
+ * GET /api/stats/overview
+ * 需要 admin 或 operator 角色
+ */
+stats.get('/overview', requireRole('admin', 'operator'), async (c) => {
+  try {
+    const { days = '7' } = c.req.query()
+    const daysAgo = Math.floor(Date.now() / 1000) - parseInt(days) * 24 * 60 * 60
+
+    // 总体统计
+    const totalStats = await c.env.DB.prepare(`
+      SELECT
+        COUNT(*) as total_views,
+        COUNT(DISTINCT user_id) as unique_users,
+        COUNT(DISTINCT ip_address) as unique_ips,
+        COALESCE(SUM(watch_duration), 0) as total_watch_time
+      FROM view_stats
+      WHERE started_at >= ?
+    `).bind(daysAgo).first()
+
+    // 每日趋势
+    const dailyTrend = await c.env.DB.prepare(`
+      SELECT
+        date(started_at, 'unixepoch') as date,
+        COUNT(*) as views,
+        COUNT(DISTINCT user_id) as unique_users,
+        COALESCE(SUM(watch_duration), 0) as watch_time
+      FROM view_stats
+      WHERE started_at >= ?
+      GROUP BY date(started_at, 'unixepoch')
+      ORDER BY date ASC
+    `).bind(daysAgo).all()
+
+    // 热门视频
+    const topVideos = await c.env.DB.prepare(`
+      SELECT
+        v.id, v.title, v.view_count,
+        COUNT(vs.id) as recent_views
+      FROM videos v
+      LEFT JOIN view_stats vs ON v.id = vs.video_id AND vs.started_at >= ? AND vs.is_deleted = 0
+      WHERE v.is_deleted = 0
+      GROUP BY v.id
+      ORDER BY recent_views DESC
+      LIMIT 10
+    `).bind(daysAgo).all()
+
+    // 热门会话
+    const topSessions = await c.env.DB.prepare(`
+      SELECT
+        s.id, c.name as camera_name, s.peak_viewers,
+        COUNT(vs.id) as total_views
+      FROM live_sessions s
+      LEFT JOIN cameras c ON s.camera_id = c.id AND c.is_deleted = 0
+      LEFT JOIN view_stats vs ON s.id = vs.session_id AND vs.is_deleted = 0
+      WHERE s.started_at >= ? AND s.is_deleted = 0
+      GROUP BY s.id
+      ORDER BY total_views DESC
+      LIMIT 10
+    `).bind(daysAgo).all()
+
+    // 地区分布
+    const geoStats = await c.env.DB.prepare(`
+      SELECT country, COUNT(*) as count
+      FROM view_stats
+      WHERE started_at >= ? AND country IS NOT NULL
+      GROUP BY country
+      ORDER BY count DESC
+      LIMIT 20
+    `).bind(daysAgo).all()
+
+    return c.json(success({
+      period_days: parseInt(days),
+      total: totalStats,
+      daily_trend: dailyTrend.results || [],
+      top_videos: topVideos.results || [],
+      top_sessions: topSessions.results || [],
+      geo_distribution: geoStats.results || [],
+    }))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+/**
+ * 获取观看记录列表
+ * GET /api/stats/views
+ * 需要 admin 角色
+ */
+stats.get('/views', authMiddleware(), requireRole('admin'), async (c) => {
+  try {
+    const { video_id, session_id, user_id, page = '1', pageSize = '50' } = c.req.query()
+    const offset = (parseInt(page) - 1) * parseInt(pageSize)
+    const limit = parseInt(pageSize)
+
+    let sql = 'SELECT * FROM view_stats WHERE 1=1'
+    const params: unknown[] = []
+
+    if (video_id) {
+      sql += ' AND video_id = ?'
+      params.push(video_id)
+    }
+    if (session_id) {
+      sql += ' AND session_id = ?'
+      params.push(session_id)
+    }
+    if (user_id) {
+      sql += ' AND user_id = ?'
+      params.push(user_id)
+    }
+
+    // 获取总数
+    const countSql = sql.replace('SELECT *', 'SELECT COUNT(*) as total')
+    const countResult = await c.env.DB.prepare(countSql).bind(...params).first<{ total: number }>()
+    const total = countResult?.total || 0
+
+    // 获取分页数据
+    sql += ' ORDER BY started_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const result = await c.env.DB.prepare(sql).bind(...params).all<ViewStat>()
+
+    const data: PageResponse<ViewStat> = {
+      rows: result.results || [],
+      total,
+    }
+
+    return c.json(success(data))
+  } catch (err) {
+    return c.json(error(err instanceof Error ? err.message : 'Unknown error'))
+  }
+})
+
+export default stats

+ 14 - 13
src/routes/user.ts

@@ -36,7 +36,7 @@ user.get('/', async (c) => {
     const limit = Math.min(parseInt(pageSize), 100)
     const offset = (parseInt(page) - 1) * limit
 
-    let sql = 'SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE 1=1'
+    let sql = 'SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE is_deleted = 0'
     const params: (string | number)[] = []
 
     if (role) {
@@ -90,7 +90,7 @@ user.get('/:id', async (c) => {
     const userId = c.req.param('id')
 
     const userData = await c.env.DB
-      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ?')
+      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first<User>()
 
@@ -190,7 +190,7 @@ user.put('/:id', async (c) => {
     }>()
 
     const existing = await c.env.DB
-      .prepare('SELECT id FROM users WHERE id = ?')
+      .prepare('SELECT id FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first()
 
@@ -273,7 +273,7 @@ user.delete('/:id', async (c) => {
     }
 
     const existing = await c.env.DB
-      .prepare('SELECT id, username FROM users WHERE id = ?')
+      .prepare('SELECT id, username FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first<User>()
 
@@ -281,13 +281,14 @@ user.delete('/:id', async (c) => {
       return c.json(error('用户不存在', 404), 404)
     }
 
+    const now = Math.floor(Date.now() / 1000)
+    // 软删除时清空 email,避免 UNIQUE 约束冲突
     await c.env.DB
-      .prepare('DELETE FROM users WHERE id = ?')
-      .bind(userId)
+      .prepare('UPDATE users SET is_deleted = 1, email = NULL, updated_at = ? WHERE id = ?')
+      .bind(now, userId)
       .run()
 
     // 记录操作日志
-    const now = Math.floor(Date.now() / 1000)
     await c.env.DB
       .prepare(`
         INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
@@ -318,7 +319,7 @@ user.get('/:id/permissions', async (c) => {
         SELECT up.*, c.name as camera_name
         FROM user_permissions up
         LEFT JOIN cameras c ON up.camera_id = c.id
-        WHERE up.user_id = ?
+        WHERE up.user_id = ? AND up.is_deleted = 0
         ORDER BY up.granted_at DESC
       `)
       .bind(userId)
@@ -350,7 +351,7 @@ user.post('/:id/permissions', async (c) => {
 
     // 检查用户是否存在
     const userExists = await c.env.DB
-      .prepare('SELECT id FROM users WHERE id = ?')
+      .prepare('SELECT id FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first()
 
@@ -360,7 +361,7 @@ user.post('/:id/permissions', async (c) => {
 
     // 检查摄像头是否存在
     const cameraExists = await c.env.DB
-      .prepare('SELECT id FROM cameras WHERE id = ?')
+      .prepare('SELECT id FROM cameras WHERE id = ? AND is_deleted = 0')
       .bind(body.camera_id)
       .first()
 
@@ -370,7 +371,7 @@ user.post('/:id/permissions', async (c) => {
 
     // 检查是否已存在权限
     const existing = await c.env.DB
-      .prepare('SELECT id FROM user_permissions WHERE user_id = ? AND camera_id = ?')
+      .prepare('SELECT id FROM user_permissions WHERE user_id = ? AND camera_id = ? AND is_deleted = 0')
       .bind(userId, body.camera_id)
       .first()
 
@@ -410,7 +411,7 @@ user.delete('/:id/permissions/:permissionId', async (c) => {
     const permissionId = c.req.param('permissionId')
 
     const existing = await c.env.DB
-      .prepare('SELECT id FROM user_permissions WHERE id = ? AND user_id = ?')
+      .prepare('SELECT id FROM user_permissions WHERE id = ? AND user_id = ? AND is_deleted = 0')
       .bind(permissionId, userId)
       .first()
 
@@ -419,7 +420,7 @@ user.delete('/:id/permissions/:permissionId', async (c) => {
     }
 
     await c.env.DB
-      .prepare('DELETE FROM user_permissions WHERE id = ?')
+      .prepare('UPDATE user_permissions SET is_deleted = 1 WHERE id = ?')
       .bind(permissionId)
       .run()
 

+ 11 - 11
src/services/auth.ts

@@ -26,7 +26,7 @@ export class AuthService {
 
     // 查询用户
     const user = await this.db
-      .prepare('SELECT * FROM users WHERE username = ? AND status = ?')
+      .prepare('SELECT * FROM users WHERE username = ? AND status = ? AND is_deleted = 0')
       .bind(username, 'active')
       .first<User>()
 
@@ -93,7 +93,7 @@ export class AuthService {
   async register(data: RegisterRequest): Promise<{ success: boolean; data?: AuthResponse; error?: string }> {
     const { username, password, email } = data
 
-    // 检查用户名是否已存在
+    // 检查用户名是否已存在(包括已删除的用户)
     const existing = await this.db
       .prepare('SELECT id FROM users WHERE username = ?')
       .bind(username)
@@ -103,10 +103,10 @@ export class AuthService {
       return { success: false, error: '用户名已存在' }
     }
 
-    // 检查邮箱是否已存在
+    // 检查邮箱是否已存在(只检查未删除的用户)
     if (email) {
       const emailExists = await this.db
-        .prepare('SELECT id FROM users WHERE email = ?')
+        .prepare('SELECT id FROM users WHERE email = ? AND is_deleted = 0')
         .bind(email)
         .first()
 
@@ -125,14 +125,14 @@ export class AuthService {
         INSERT INTO users (id, username, email, password_hash, role, status, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)
       `)
-      .bind(userId, username, email || null, passwordHash, 'viewer', 'active', now, now)
+      .bind(userId, username, email || null, passwordHash, 'admin', 'active', now, now)
       .run()
 
     // 生成 Token
     const accessToken = await createAccessToken(
       userId,
       username,
-      'viewer',
+      'admin',
       this.jwtSecret,
       this.accessTokenExpiry
     )
@@ -140,7 +140,7 @@ export class AuthService {
     const refreshToken = await createRefreshToken(
       userId,
       username,
-      'viewer',
+      'admin',
       this.jwtSecret,
       this.refreshTokenExpiry
     )
@@ -157,7 +157,7 @@ export class AuthService {
         user: {
           id: userId,
           username,
-          role: 'viewer',
+          role: 'admin',
         },
       },
     }
@@ -175,7 +175,7 @@ export class AuthService {
 
     // 查询用户确保仍然有效
     const user = await this.db
-      .prepare('SELECT * FROM users WHERE id = ? AND status = ?')
+      .prepare('SELECT * FROM users WHERE id = ? AND status = ? AND is_deleted = 0')
       .bind(result.payload.sub, 'active')
       .first<User>()
 
@@ -220,7 +220,7 @@ export class AuthService {
    */
   async getCurrentUser(userId: string): Promise<User | null> {
     return this.db
-      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ?')
+      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first<User>()
   }
@@ -234,7 +234,7 @@ export class AuthService {
     newPassword: string
   ): Promise<{ success: boolean; error?: string }> {
     const user = await this.db
-      .prepare('SELECT * FROM users WHERE id = ?')
+      .prepare('SELECT * FROM users WHERE id = ? AND is_deleted = 0')
       .bind(userId)
       .first<User>()
 

+ 6 - 0
src/types/index.ts

@@ -184,6 +184,7 @@ export interface Camera {
   status: 'online' | 'offline' | 'error'
   live_input_id?: string
   meta?: string  // JSON
+  is_deleted?: number  // 0 = active, 1 = deleted
   created_at: number
   updated_at: number
 }
@@ -201,6 +202,7 @@ export interface LiveSession {
   peak_viewers: number
   recording_id?: string
   meta?: string  // JSON
+  is_deleted?: number  // 0 = active, 1 = deleted
   created_at: number
 }
 
@@ -219,6 +221,7 @@ export interface Video {
   status: 'ready' | 'processing' | 'error'
   is_public: number
   view_count: number
+  is_deleted?: number  // 0 = active, 1 = deleted
   created_at: number
   updated_at: number
 }
@@ -232,6 +235,7 @@ export interface User {
   role: 'admin' | 'operator' | 'viewer'
   status: 'active' | 'disabled'
   last_login?: number
+  is_deleted?: number  // 0 = active, 1 = deleted
   created_at: number
   updated_at: number
 }
@@ -244,6 +248,7 @@ export interface UserPermission {
   permission: 'view' | 'control' | 'manage'
   granted_at: number
   granted_by?: string
+  is_deleted?: number  // 0 = active, 1 = deleted
 }
 
 // 观看统计
@@ -259,6 +264,7 @@ export interface ViewStat {
   ended_at?: number
   country?: string
   city?: string
+  is_deleted?: number  // 0 = active, 1 = deleted
   created_at: number
 }
 

+ 1714 - 0
tg-live-game.postman_collection.json

@@ -0,0 +1,1714 @@
+{
+	"info": {
+		"_postman_id": "a670efda-9591-4304-af67-a8eb87c8b938",
+		"name": "tg-live-game",
+		"description": "TG Live Game Backend API Collection",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+		"_exporter_id": "42537936"
+	},
+	"item": [
+		{
+			"name": "tg-live-game-hono",
+			"item": [
+				{
+					"name": "auth",
+					"item": [
+						{
+							"name": "register",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.environment.set(\"accessToken\", pm.response.json().data.accessToken);",
+											"pm.environment.set(\"refreshToken\", pm.response.json().data.refreshToken);"
+										],
+										"type": "text/javascript",
+										"packages": {},
+										"requests": {}
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"username\": \"pwtk004\",\n  \"password\": \"test123456\",\n  \"email\": \"pwtk004@pwtk.cc\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/register",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"register"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "login",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"var jsonData = pm.response.json();",
+											"if (jsonData.code === 200 && jsonData.data) {",
+											"    pm.collectionVariables.set('accessToken', jsonData.data.accessToken);",
+											"    pm.collectionVariables.set('refreshToken', jsonData.data.refreshToken);",
+											"}"
+										],
+										"type": "text/javascript",
+										"packages": {},
+										"requests": {}
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"username\": \"pwtk004\",\n  \"password\": \"test123456\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/login",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"login"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "refresh",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"var jsonData = pm.response.json();",
+											"if (jsonData.code === 200 && jsonData.data) {",
+											"    pm.collectionVariables.set('accessToken', jsonData.data.accessToken);",
+											"    pm.collectionVariables.set('refreshToken', jsonData.data.refreshToken);",
+											"}"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"refreshToken\": \"{{refreshToken}}\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/refresh",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"refresh"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "me",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/me",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"me"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "change-password",
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"oldPassword\": \"admin123\",\n  \"newPassword\": \"newpassword123\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/change-password",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"change-password"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "logout",
+							"request": {
+								"method": "POST",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/auth/logout",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"auth",
+										"logout"
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "users",
+					"item": [
+						{
+							"name": "permissions",
+							"item": [
+								{
+									"name": "list",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/users/:id/permissions",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"users",
+												":id",
+												"permissions"
+											],
+											"variable": [
+												{
+													"key": "id",
+													"value": "user_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "add",
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"camera_id\": \"camera_id_here\",\n  \"permission\": \"view\"\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/users/:id/permissions",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"users",
+												":id",
+												"permissions"
+											],
+											"variable": [
+												{
+													"key": "id",
+													"value": "user_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "delete",
+									"request": {
+										"method": "DELETE",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/users/:id/permissions/:permissionId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"users",
+												":id",
+												"permissions",
+												":permissionId"
+											],
+											"variable": [
+												{
+													"key": "id",
+													"value": "user_id_here"
+												},
+												{
+													"key": "permissionId",
+													"value": "permission_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								}
+							]
+						},
+						{
+							"name": "list",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/users?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"users"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										},
+										{
+											"key": "role",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "status",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "search",
+											"value": "",
+											"disabled": true
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/users/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"users",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "user_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "create",
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"username\": \"newuser\",\n  \"password\": \"password123\",\n  \"email\": \"newuser@example.com\",\n  \"role\": \"viewer\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/users",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"users"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "update",
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"email\": \"updated@example.com\",\n  \"role\": \"operator\",\n  \"status\": \"active\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/users/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"users",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "user_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "delete",
+							"request": {
+								"method": "DELETE",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/users/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"users",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "user_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "cameras",
+					"item": [
+						{
+							"name": "list",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										},
+										{
+											"key": "status",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "type",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "search",
+											"value": "",
+											"disabled": true
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "camera_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "create",
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"name\": \"Camera 1\",\n  \"type\": \"rtsp\",\n  \"protocol\": \"rtmps\",\n  \"rtsp_url\": \"rtsp://example.com/stream\",\n  \"location\": \"Room 101\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "update",
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"name\": \"Camera 1 Updated\",\n  \"location\": \"Room 102\",\n  \"status\": \"online\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "camera_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "delete",
+							"request": {
+								"method": "DELETE",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "camera_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "sessions",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/cameras/:id/sessions?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"cameras",
+										":id",
+										"sessions"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										},
+										{
+											"key": "status",
+											"value": "",
+											"disabled": true
+										}
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "camera_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "sessions",
+					"item": [
+						{
+							"name": "list",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										},
+										{
+											"key": "status",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "camera_id",
+											"value": "",
+											"disabled": true
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "live",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/live",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										"live"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "start",
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"camera_id\": \"camera_id_here\",\n  \"live_input_id\": \"live_input_id_here\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "end",
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"recording_id\": \"recording_id_here\"\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/:id/end",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										":id",
+										"end"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "update-viewers",
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"viewer_count\": 100\n}"
+								},
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/:id/viewers",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										":id",
+										"viewers"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "stats",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/:id/stats",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										":id",
+										"stats"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "delete",
+							"request": {
+								"method": "DELETE",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/sessions/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"sessions",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "stats",
+					"item": [
+						{
+							"name": "view",
+							"item": [
+								{
+									"name": "start",
+									"request": {
+										"auth": {
+											"type": "noauth"
+										},
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"video_id\": \"video_id_here\"\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stats/view/start",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stats",
+												"view",
+												"start"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "end",
+									"request": {
+										"auth": {
+											"type": "noauth"
+										},
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"view_id\": \"view_id_here\",\n  \"watch_duration\": 300\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stats/view/end",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stats",
+												"view",
+												"end"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "heartbeat",
+									"request": {
+										"auth": {
+											"type": "noauth"
+										},
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"view_id\": \"view_id_here\",\n  \"watch_duration\": 60\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stats/view/heartbeat",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stats",
+												"view",
+												"heartbeat"
+											]
+										}
+									},
+									"response": []
+								}
+							]
+						},
+						{
+							"name": "video",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/stats/video/:videoId",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"stats",
+										"video",
+										":videoId"
+									],
+									"variable": [
+										{
+											"key": "videoId",
+											"value": "video_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "session",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/stats/session/:sessionId",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"stats",
+										"session",
+										":sessionId"
+									],
+									"variable": [
+										{
+											"key": "sessionId",
+											"value": "session_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "overview",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/stats/overview?days=7",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"stats",
+										"overview"
+									],
+									"query": [
+										{
+											"key": "days",
+											"value": "7"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "views",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/stats/views?page=1&pageSize=50",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"stats",
+										"views"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "50"
+										},
+										{
+											"key": "video_id",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "session_id",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "user_id",
+											"value": "",
+											"disabled": true
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "audit-logs",
+					"item": [
+						{
+							"name": "stats",
+							"item": [
+								{
+									"name": "summary",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/audit-logs/stats/summary?days=7",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"audit-logs",
+												"stats",
+												"summary"
+											],
+											"query": [
+												{
+													"key": "days",
+													"value": "7"
+												}
+											]
+										}
+									},
+									"response": []
+								}
+							]
+						},
+						{
+							"name": "list",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/audit-logs?page=1&pageSize=50",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"audit-logs"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "50"
+										},
+										{
+											"key": "action",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "resource",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "user_id",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "start_date",
+											"value": "",
+											"disabled": true
+										},
+										{
+											"key": "end_date",
+											"value": "",
+											"disabled": true
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/audit-logs/:id",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"audit-logs",
+										":id"
+									],
+									"variable": [
+										{
+											"key": "id",
+											"value": "log_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "user",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/audit-logs/user/:userId?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"audit-logs",
+										"user",
+										":userId"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										}
+									],
+									"variable": [
+										{
+											"key": "userId",
+											"value": "user_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "resource",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/api/audit-logs/resource/:resource/:resourceId?page=1&pageSize=20",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"api",
+										"audit-logs",
+										"resource",
+										":resource",
+										":resourceId"
+									],
+									"query": [
+										{
+											"key": "page",
+											"value": "1"
+										},
+										{
+											"key": "pageSize",
+											"value": "20"
+										}
+									],
+									"variable": [
+										{
+											"key": "resource",
+											"value": "camera"
+										},
+										{
+											"key": "resourceId",
+											"value": "resource_id_here"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				},
+				{
+					"name": "stream",
+					"item": [
+						{
+							"name": "video",
+							"item": [
+								{
+									"name": "list",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/list",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												"list"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "get",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/:videoId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												":videoId"
+											],
+											"variable": [
+												{
+													"key": "videoId",
+													"value": "video_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "delete",
+									"request": {
+										"method": "DELETE",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/:videoId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												":videoId"
+											],
+											"variable": [
+												{
+													"key": "videoId",
+													"value": "video_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "import",
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"url\": \"https://example.com/video.mp4\",\n  \"meta\": {\n    \"name\": \"My Video\"\n  }\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/import",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												"import"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "upload-url",
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"maxDurationSeconds\": 3600,\n  \"meta\": {\n    \"name\": \"My Upload\"\n  }\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/upload-url",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												"upload-url"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "playback",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/video/:videoId/playback",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"video",
+												":videoId",
+												"playback"
+											],
+											"variable": [
+												{
+													"key": "videoId",
+													"value": "video_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								}
+							]
+						},
+						{
+							"name": "live",
+							"item": [
+								{
+									"name": "list",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/list",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												"list"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "create",
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"meta\": {\n    \"name\": \"My Live Stream\"\n  },\n  \"recording\": {\n    \"mode\": \"automatic\"\n  }\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "get",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												":liveInputId"
+											],
+											"variable": [
+												{
+													"key": "liveInputId",
+													"value": "live_input_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "update",
+									"request": {
+										"method": "PUT",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"meta\": {\n    \"name\": \"Updated Live Stream\"\n  }\n}"
+										},
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												":liveInputId"
+											],
+											"variable": [
+												{
+													"key": "liveInputId",
+													"value": "live_input_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "delete",
+									"request": {
+										"method": "DELETE",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/:liveInputId",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												":liveInputId"
+											],
+											"variable": [
+												{
+													"key": "liveInputId",
+													"value": "live_input_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "playback",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/:liveInputId/playback",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												":liveInputId",
+												"playback"
+											],
+											"variable": [
+												{
+													"key": "liveInputId",
+													"value": "live_input_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "recordings",
+									"request": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "{{baseUrl}}/api/stream/live/:liveInputId/recordings",
+											"host": [
+												"{{baseUrl}}"
+											],
+											"path": [
+												"api",
+												"stream",
+												"live",
+												":liveInputId",
+												"recordings"
+											],
+											"variable": [
+												{
+													"key": "liveInputId",
+													"value": "live_input_id_here"
+												}
+											]
+										}
+									},
+									"response": []
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	],
+	"auth": {
+		"type": "bearer",
+		"bearer": [
+			{
+				"key": "token",
+				"value": "{{accessToken}}",
+				"type": "string"
+			}
+		]
+	},
+	"variable": [
+		{
+			"key": "baseUrl",
+			"value": "http://localhost:8787",
+			"type": "string"
+		},
+		{
+			"key": "accessToken",
+			"value": "",
+			"type": "string"
+		},
+		{
+			"key": "refreshToken",
+			"value": "",
+			"type": "string"
+		}
+	]
+}