Просмотр исходного кода

视频点播签名功能已成功实现并集成到 server/routes_vod.ts 中

yourname 8 месяцев назад
Родитель
Сommit
f88303cf0c
9 измененных файлов с 279 добавлено и 197 удалено
  1. 17 12
      client/admin/api/users.ts
  2. 12 0
      client/admin/api/vod.ts
  3. 0 1
      client/admin/menu.tsx
  4. 85 93
      client/admin/pages_vod_upload.tsx
  5. 1 1
      deno.json
  6. 19 45
      deno.lock
  7. 29 0
      docs/vod_sign_gen_demo.md
  8. 50 45
      server/router.ts
  9. 66 0
      server/routes_vod.ts

+ 17 - 12
client/admin/api/users.ts

@@ -1,5 +1,5 @@
-import axios from 'axios';
-import type { User } from '../../share/types.ts';
+import axios from "axios";
+import type { User } from "../../share/types.ts";
 
 interface UsersResponse {
   data: User[];
@@ -32,15 +32,17 @@ interface UserDeleteResponse {
 }
 
 export const UserAPI = {
-  getUsers: async (params?: { page?: number, limit?: number, search?: string }): Promise<UsersResponse> => {
+  getUsers: async (
+    params?: { page?: number; limit?: number; search?: string },
+  ): Promise<UsersResponse> => {
     try {
-      const response = await axios.get('/users', { params });
+      const response = await axios.get("/users", { params });
       return response.data;
     } catch (error) {
       throw error;
     }
   },
-  
+
   getUser: async (userId: number): Promise<UserResponse> => {
     try {
       const response = await axios.get(`/users/${userId}`);
@@ -49,17 +51,20 @@ export const UserAPI = {
       throw error;
     }
   },
-  
+
   createUser: async (userData: Partial<User>): Promise<UserCreateResponse> => {
     try {
-      const response = await axios.post('/users', userData);
+      const response = await axios.post("/users", userData);
       return response.data;
     } catch (error) {
       throw error;
     }
   },
-  
-  updateUser: async (userId: number, userData: Partial<User>): Promise<UserUpdateResponse> => {
+
+  updateUser: async (
+    userId: number,
+    userData: Partial<User>,
+  ): Promise<UserUpdateResponse> => {
     try {
       const response = await axios.put(`/users/${userId}`, userData);
       return response.data;
@@ -67,7 +72,7 @@ export const UserAPI = {
       throw error;
     }
   },
-  
+
   deleteUser: async (userId: number): Promise<UserDeleteResponse> => {
     try {
       const response = await axios.delete(`/users/${userId}`);
@@ -75,5 +80,5 @@ export const UserAPI = {
     } catch (error) {
       throw error;
     }
-  }
-};
+  },
+};

+ 12 - 0
client/admin/api/vod.ts

@@ -0,0 +1,12 @@
+import axios from "axios";
+
+export const VodAPI = {
+  getSignature: async (): Promise<string> => {
+    try {
+      const response = await axios.get("/vod/signature");
+      return response.data.signature;
+    } catch (error) {
+      throw error;
+    }
+  },
+};

+ 0 - 1
client/admin/menu.tsx

@@ -160,7 +160,6 @@ export const useMenu = () => {
           label: 'VOD上传',
           path: '/admin/vod-upload',
           permission: 'content:manage',
-          icon: <FileTextOutlined />
         },
         {
           key: 'file-library',

+ 85 - 93
client/admin/pages_vod_upload.tsx

@@ -1,12 +1,12 @@
-import React, { useState, useRef } from 'react';
-import { Button, Card, Progress, Space, Tag, message } from 'antd';
-import TcVod from 'vod-js-sdk-v6';
-import axios from "axios";
+import React, { useRef, useState } from "react";
+import { Button, Card, message, Progress, Space, Tag } from "antd";
+import TcVod from "vod-js-sdk-v6";
+import { VodAPI } from "./api/vod.ts";
 
 interface UploadTask {
   file: File;
   progress: number;
-  status: 'pending' | 'uploading' | 'success' | 'error' | 'canceled';
+  status: "pending" | "uploading" | "success" | "error" | "canceled";
   fileId?: string;
   videoUrl?: string;
   coverUrl?: string;
@@ -23,32 +23,16 @@ export const VodUploadPage = () => {
   // 获取上传签名
   const getSignature = async () => {
     try {
-      // TODO: 替换为实际获取签名的API调用
-      const response = await fetch('/api/vod/signature');
-      const data = await response.json();
-      return data.signature;
+      return await VodAPI.getSignature();
     } catch (error) {
-      message.error('获取上传签名失败');
+      message.error("获取上传签名失败");
       throw error;
     }
   };
 
-  
-      /**
-       * 计算签名。
-      **/
-      function getDemoSignature() {
-        return axios.post('https://demo.vod2.myqcloud.com/ugc-upload/', JSON.stringify({
-          "Action": "GetUgcUploadSign"
-        })).then(function (response) {
-          return response.data.data.sign
-        })
-      };
-
   // 初始化VOD SDK
-  const tcVod = new TcVod.default({
-    // getSignature: getSignature
-    getSignature: getDemoSignature
+  const tcVod = new TcVod({
+    getSignature: getSignature,
   });
 
   // 处理视频文件选择
@@ -68,18 +52,18 @@ export const VodUploadPage = () => {
   // 开始上传
   const startUpload = async () => {
     if (!selectedVideo) {
-      message.warning('请先选择视频文件');
+      message.warning("请先选择视频文件");
       return;
     }
 
     const newTask: UploadTask = {
       file: selectedVideo,
       progress: 0,
-      status: 'pending',
+      status: "pending",
     };
 
-    setUploadTasks(prev => [...prev, newTask]);
-    
+    setUploadTasks((prev) => [...prev, newTask]);
+
     try {
       const uploader = tcVod.upload({
         mediaFile: selectedVideo,
@@ -88,94 +72,88 @@ export const VodUploadPage = () => {
 
       const updatedTask: UploadTask = {
         ...newTask,
-        status: 'uploading',
+        status: "uploading",
         cancel: () => {
           uploader.cancel();
-          setUploadTasks(prev => 
-            prev.map(task => 
-              task.file === newTask.file 
-                ? { ...task, status: 'canceled' } 
+          setUploadTasks((prev) =>
+            prev.map((task) =>
+              task.file === newTask.file
+                ? { ...task, status: "canceled" }
                 : task
             )
           );
-        }
+        },
       };
 
-      setUploadTasks(prev => 
-        prev.map(task => 
-          task.file === newTask.file ? updatedTask : task
-        )
+      setUploadTasks((prev) =>
+        prev.map((task) => task.file === newTask.file ? updatedTask : task)
       );
 
       // 监听上传进度
-      uploader.on('media_progress', (info) => {
-        setUploadTasks(prev => 
-          prev.map(task => 
-            task.file === newTask.file 
-              ? { ...task, progress: info.percent * 100 } 
+      uploader.on("media_progress", (info) => {
+        setUploadTasks((prev) =>
+          prev.map((task) =>
+            task.file === newTask.file
+              ? { ...task, progress: info.percent * 100 }
               : task
           )
         );
       });
 
       // 监听上传完成
-      uploader.on('media_upload', (info) => {
-        setUploadTasks(prev => 
-          prev.map(task => 
-            task.file === newTask.file 
-              ? { ...task, status: 'success' } 
-              : task
+      uploader.on("media_upload", (info) => {
+        setUploadTasks((prev) =>
+          prev.map((task) =>
+            task.file === newTask.file ? { ...task, status: "success" } : task
           )
         );
       });
 
       // 执行上传
       const result = await uploader.done();
-      
-      setUploadTasks(prev => 
-        prev.map(task => 
-          task.file === newTask.file 
-            ? { 
-                ...task, 
-                fileId: result.fileId,
-                videoUrl: result.video.url,
-                coverUrl: result.cover?.url,
-                status: 'success'
-              } 
+
+      setUploadTasks((prev) =>
+        prev.map((task) =>
+          task.file === newTask.file
+            ? {
+              ...task,
+              fileId: result.fileId,
+              videoUrl: result.video.url,
+              coverUrl: result.cover?.url,
+              status: "success",
+            }
             : task
         )
       );
 
-      message.success('视频上传成功');
+      message.success("视频上传成功");
     } catch (error) {
-      setUploadTasks(prev => 
-        prev.map(task => 
-          task.file === newTask.file 
-            ? { ...task, status: 'error' } 
-            : task
+      setUploadTasks((prev) =>
+        prev.map((task) =>
+          task.file === newTask.file ? { ...task, status: "error" } : task
         )
       );
-      message.error('视频上传失败');
+      message.error("视频上传失败");
     } finally {
       setSelectedVideo(null);
       setSelectedCover(null);
-      if (videoInputRef.current) videoInputRef.current.value = '';
-      if (coverInputRef.current) coverInputRef.current.value = '';
+      if (videoInputRef.current) videoInputRef.current.value = "";
+      if (coverInputRef.current) coverInputRef.current.value = "";
     }
   };
 
   // 渲染上传状态标签
-  const renderStatusTag = (status: UploadTask['status']) => {
+  const renderStatusTag = (status: UploadTask["status"]) => {
     switch (status) {
-      case 'pending':
+      case "pending":
         return <Tag color="default">等待上传</Tag>;
-      case 'uploading':
+      case "uploading":
         return <Tag color="processing">上传中</Tag>;
-      case 'success':
+      case "success":
         return <Tag color="success">上传成功</Tag>;
-      case 'error':
+      case "error":
         return <Tag color="error">上传失败</Tag>;
-      case 'canceled':
+      case "canceled":
         return <Tag color="warning">已取消</Tag>;
       default:
         return <Tag color="default">未知状态</Tag>;
@@ -185,20 +163,20 @@ export const VodUploadPage = () => {
   return (
     <div>
       <Card title="视频上传" className="mb-4">
-        <Space direction="vertical" style={{ width: '100%' }}>
+        <Space direction="vertical" style={{ width: "100%" }}>
           <div>
             <input
               type="file"
               ref={videoInputRef}
               onChange={handleVideoSelect}
               accept="video/*"
-              style={{ display: 'none' }}
+              style={{ display: "none" }}
             />
-            <Button 
+            <Button
               onClick={() => videoInputRef.current?.click()}
               type="primary"
             >
-              {selectedVideo ? selectedVideo.name : '选择视频文件'}
+              {selectedVideo ? selectedVideo.name : "选择视频文件"}
             </Button>
 
             <input
@@ -206,17 +184,17 @@ export const VodUploadPage = () => {
               ref={coverInputRef}
               onChange={handleCoverSelect}
               accept="image/*"
-              style={{ display: 'none', marginLeft: 16 }}
+              style={{ display: "none", marginLeft: 16 }}
             />
-            <Button 
+            <Button
               onClick={() => coverInputRef.current?.click()}
               style={{ marginLeft: 16 }}
             >
-              {selectedCover ? selectedCover.name : '选择封面(可选)'}
+              {selectedCover ? selectedCover.name : "选择封面(可选)"}
             </Button>
 
-            <Button 
-              type="primary" 
+            <Button
+              type="primary"
               onClick={startUpload}
               disabled={!selectedVideo}
               style={{ marginLeft: 16 }}
@@ -231,10 +209,10 @@ export const VodUploadPage = () => {
                 <div style={{ marginBottom: 8 }}>
                   <span style={{ marginRight: 8 }}>{task.file.name}</span>
                   {renderStatusTag(task.status)}
-                  {task.status === 'uploading' && task.cancel && (
-                    <Button 
-                      type="link" 
-                      danger 
+                  {task.status === "uploading" && task.cancel && (
+                    <Button
+                      type="link"
+                      danger
                       onClick={task.cancel}
                       style={{ marginLeft: 8 }}
                     >
@@ -242,7 +220,7 @@ export const VodUploadPage = () => {
                     </Button>
                   )}
                 </div>
-                {task.status === 'uploading' && (
+                {task.status === "uploading" && (
                   <Progress percent={task.progress} status="active" />
                 )}
                 {task.fileId && (
@@ -250,12 +228,26 @@ export const VodUploadPage = () => {
                     <div>File ID: {task.fileId}</div>
                     {task.videoUrl && (
                       <div>
-                        视频地址: <a href={task.videoUrl} target="_blank" rel="noreferrer">{task.videoUrl}</a>
+                        视频地址:{" "}
+                        <a
+                          href={task.videoUrl}
+                          target="_blank"
+                          rel="noreferrer"
+                        >
+                          {task.videoUrl}
+                        </a>
                       </div>
                     )}
                     {task.coverUrl && (
                       <div>
-                        封面地址: <a href={task.coverUrl} target="_blank" rel="noreferrer">{task.coverUrl}</a>
+                        封面地址:{" "}
+                        <a
+                          href={task.coverUrl}
+                          target="_blank"
+                          rel="noreferrer"
+                        >
+                          {task.coverUrl}
+                        </a>
                       </div>
                     )}
                   </div>
@@ -267,4 +259,4 @@ export const VodUploadPage = () => {
       </Card>
     </div>
   );
-};
+};

+ 1 - 1
deno.json

@@ -40,7 +40,7 @@
     "decimal.js":"https://esm.d8d.fun/decimal.js@10.4.3",
     "echarts":"https://esm.d8d.fun/echarts@5.6.0",
     "echarts/types/src/util/types":"https://esm.d8d.fun/echarts@5.6.0/types/src/util/types",
-    "vod-js-sdk-v6": "npm:vod-js-sdk-v6"
+    "vod-js-sdk-v6": "https://esm.d8d.fun/vod-js-sdk-v6@1.7.1-beta.1"
   },
   "compilerOptions": {
     "lib": ["dom", "dom.iterable", "esnext", "deno.ns"]

+ 19 - 45
deno.lock

@@ -9,8 +9,7 @@
     "npm:@alicloud/live20161101@^1.1.1": "1.1.1",
     "npm:@alicloud/openapi-client@~0.4.14": "0.4.14",
     "npm:@alicloud/pop-core@1.8.0": "1.8.0",
-    "npm:@types/node@*": "22.12.0",
-    "npm:vod-js-sdk-v6@*": "1.7.1-beta.1"
+    "npm:@types/node@*": "22.12.0"
   },
   "jsr": {
     "@std/async@1.0.13": {
@@ -186,13 +185,6 @@
       ],
       "tarball": "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz"
     },
-    "axios@0.21.4": {
-      "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
-      "dependencies": [
-        "follow-redirects"
-      ],
-      "tarball": "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz"
-    },
     "bignumber.js@9.3.0": {
       "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
       "tarball": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.0.tgz"
@@ -211,14 +203,6 @@
       ],
       "tarball": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz"
     },
-    "eventemitter3@4.0.7": {
-      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
-      "tarball": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz"
-    },
-    "follow-redirects@1.15.9": {
-      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
-      "tarball": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz"
-    },
     "fsevents@2.3.2": {
       "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
       "os": ["darwin"],
@@ -237,10 +221,6 @@
       "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
       "tarball": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz"
     },
-    "js-sha1@0.6.0": {
-      "integrity": "sha512-01gwBFreYydzmU9BmZxpVk6svJJHrVxEN3IOiGl6VO93bVKYETJ0sIth6DASI6mIFdt7NmfX9UiByRzsYHGU9w==",
-      "tarball": "https://registry.npmmirror.com/js-sha1/-/js-sha1-0.6.0.tgz"
-    },
     "json-bigint@1.0.0": {
       "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
       "dependencies": [
@@ -286,11 +266,6 @@
       "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==",
       "tarball": "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz"
     },
-    "typescript@5.8.3": {
-      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
-      "bin": true,
-      "tarball": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz"
-    },
     "undici-types@6.19.8": {
       "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
       "tarball": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz"
@@ -303,23 +278,6 @@
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
       "tarball": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz"
     },
-    "uuid@3.4.0": {
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-      "deprecated": true,
-      "bin": true,
-      "tarball": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz"
-    },
-    "vod-js-sdk-v6@1.7.1-beta.1": {
-      "integrity": "sha512-YZljTVwogoCcx7VtlAhem9w3AP7RCnDlqKsxaU3bv7sylwTsC5/jE9QlvpCKEorkOHRBsbu37m3mYXUkelwBaw==",
-      "dependencies": [
-        "axios",
-        "eventemitter3",
-        "js-sha1",
-        "typescript",
-        "uuid"
-      ],
-      "tarball": "https://registry.npmmirror.com/vod-js-sdk-v6/-/vod-js-sdk-v6-1.7.1-beta.1.tgz"
-    },
     "xml2js@0.5.0": {
       "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
       "dependencies": [
@@ -358,6 +316,7 @@
     "https://esm.d8d.fun/@types/combined-stream@~1.0.6/index.d.ts": "https://esm.d8d.fun/@types/combined-stream@1.0.6/index.d.ts",
     "https://esm.d8d.fun/@types/debug@~4.1.12/index.d.ts": "https://esm.d8d.fun/@types/debug@4.1.12/index.d.ts",
     "https://esm.d8d.fun/@types/follow-redirects@~1.14.4/index.d.ts": "https://esm.d8d.fun/@types/follow-redirects@1.14.4/index.d.ts",
+    "https://esm.d8d.fun/@types/js-sha1@~0.6.3/index.d.ts": "https://esm.d8d.fun/@types/js-sha1@0.6.3/index.d.ts",
     "https://esm.d8d.fun/@types/jsonwebtoken@~9.0.9/index.d.ts": "https://esm.d8d.fun/@types/jsonwebtoken@9.0.9/index.d.ts",
     "https://esm.d8d.fun/@types/jws@~3.2.10/index.d.ts": "https://esm.d8d.fun/@types/jws@3.2.10/index.d.ts",
     "https://esm.d8d.fun/@types/lodash.includes@~4.3.9/index.d.ts": "https://esm.d8d.fun/@types/lodash.includes@4.3.9/index.d.ts",
@@ -381,8 +340,10 @@
     "https://esm.d8d.fun/@types/sdp-transform@~2.4.9/index.d.ts": "https://esm.d8d.fun/@types/sdp-transform@2.4.9/index.d.ts",
     "https://esm.d8d.fun/@types/semver@~7.7.0/index.d.ts": "https://esm.d8d.fun/@types/semver@7.7.0/index.d.ts",
     "https://esm.d8d.fun/@types/set-cookie-parser@~2.4.10/index.d.ts": "https://esm.d8d.fun/@types/set-cookie-parser@2.4.10/index.d.ts",
+    "https://esm.d8d.fun/@types/uuid@~3.4.13/v4.d.ts": "https://esm.d8d.fun/@types/uuid@3.4.13/v4.d.ts",
     "https://esm.d8d.fun/@types/ws@~8.18.0/index.d.mts": "https://esm.d8d.fun/@types/ws@8.18.1/index.d.mts",
     "https://esm.d8d.fun/asynckit@^0.4.0?target=denonext": "https://esm.d8d.fun/asynckit@0.4.0?target=denonext",
+    "https://esm.d8d.fun/axios@^0.21.1?target=denonext": "https://esm.d8d.fun/axios@0.21.4?target=denonext",
     "https://esm.d8d.fun/axios@^1.7.2?target=denonext": "https://esm.d8d.fun/axios@1.9.0?target=denonext",
     "https://esm.d8d.fun/bufferutil@^4.0.1?target=denonext": "https://esm.d8d.fun/bufferutil@4.0.9?target=denonext",
     "https://esm.d8d.fun/clsx@^2.1.1?target=denonext&dev": "https://esm.d8d.fun/clsx@2.1.1?target=denonext&dev",
@@ -392,11 +353,14 @@
     "https://esm.d8d.fun/delayed-stream@~1.0.0?target=denonext": "https://esm.d8d.fun/delayed-stream@1.0.0?target=denonext",
     "https://esm.d8d.fun/engine.io-client@~6.6.1?target=denonext": "https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext",
     "https://esm.d8d.fun/engine.io-parser@~5.2.1?target=denonext": "https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext",
+    "https://esm.d8d.fun/eventemitter3@^4.0.7?target=denonext": "https://esm.d8d.fun/eventemitter3@4.0.7?target=denonext",
     "https://esm.d8d.fun/eventemitter3@^5.0.1?target=denonext": "https://esm.d8d.fun/eventemitter3@5.0.1?target=denonext",
+    "https://esm.d8d.fun/follow-redirects@^1.14.0?target=denonext": "https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext",
     "https://esm.d8d.fun/follow-redirects@^1.15.0?target=denonext": "https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext",
     "https://esm.d8d.fun/follow-redirects@^1.15.6?target=denonext": "https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext",
     "https://esm.d8d.fun/form-data@^4.0.0?target=denonext": "https://esm.d8d.fun/form-data@4.0.2?target=denonext",
     "https://esm.d8d.fun/isexe@^3.1.1?target=denonext": "https://esm.d8d.fun/isexe@3.1.1?target=denonext",
+    "https://esm.d8d.fun/js-sha1@^0.6.0?target=denonext": "https://esm.d8d.fun/js-sha1@0.6.0?target=denonext",
     "https://esm.d8d.fun/jsonwebtoken@^9.0.2?target=denonext": "https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext",
     "https://esm.d8d.fun/jwa@^1.4.1?target=denonext": "https://esm.d8d.fun/jwa@1.4.1?target=denonext",
     "https://esm.d8d.fun/jws@^3.2.2?target=denonext": "https://esm.d8d.fun/jws@3.2.2?target=denonext",
@@ -421,6 +385,7 @@
     "https://esm.d8d.fun/socket.io-parser@~4.2.4?target=denonext": "https://esm.d8d.fun/socket.io-parser@4.2.4?target=denonext",
     "https://esm.d8d.fun/supports-color?target=denonext": "https://esm.d8d.fun/supports-color@10.0.0?target=denonext",
     "https://esm.d8d.fun/utf-8-validate@%3E=5.0.2?target=denonext": "https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext",
+    "https://esm.d8d.fun/uuid@^3.4.0/v4?target=denonext": "https://esm.d8d.fun/uuid@3.4.0/v4?target=denonext",
     "https://esm.d8d.fun/which@^4.0.0?target=denonext": "https://esm.d8d.fun/which@4.0.0?target=denonext",
     "https://esm.d8d.fun/ws@~8.17.1?target=denonext": "https://esm.d8d.fun/ws@8.17.1?target=denonext",
     "https://esm.d8d.fun/xmlhttprequest-ssl@~2.1.1?target=denonext": "https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext"
@@ -662,6 +627,8 @@
     "https://esm.d8d.fun/antd@5.24.6?dev&standalone&deps=react@18.3.1,react-dom@18.3.1": "5be16b606719a9ef43337aaa8c0f26439056149e67b2da4c38127f58e902af1c",
     "https://esm.d8d.fun/asynckit@0.4.0/denonext/asynckit.mjs": "4ef3be6eb52c104699b90ca5524db55ec15bc76b361432f05c16b6106279ba72",
     "https://esm.d8d.fun/asynckit@0.4.0?target=denonext": "c6bd8832d6d16b648e22d124a16d33c3a7f7076e92be9444f2e4f6b27545708d",
+    "https://esm.d8d.fun/axios@0.21.4/denonext/axios.mjs": "f07d16667944382bcb4656fa6c52fed0906056767430c1b788a591e0f6fc3890",
+    "https://esm.d8d.fun/axios@0.21.4?target=denonext": "ab976c52efd2f998f5d48f79e1a9c955c584aab27d6aa63cc87e24e3eeadee20",
     "https://esm.d8d.fun/axios@1.6.2": "6ed5cb6f7f773d035e3a7d6097a25361d77c2d6f63b9df6d9ba9e2af5a4f4a3e",
     "https://esm.d8d.fun/axios@1.6.2/denonext/axios.mjs": "cecb6586bc9779aeb64fb22e87824508e49e178fd2ddf2adaa138a5dc6b6945e",
     "https://esm.d8d.fun/axios@1.6.2/denonext/lib/adapters/http.mjs": "69f9d3b5924fe24d68867eee0de817aecc19ff21e6543c74552fc6cf59a39024",
@@ -1191,6 +1158,8 @@
     "https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext": "d97129d74541438ec8167b8232ff764b408b8bf4c065924c60823795fa3e038d",
     "https://esm.d8d.fun/engine.io-parser@5.2.3/denonext/engine.io-parser.mjs": "dfb40060c00806566e236f3112b950f43fa6b5e3a142f14ba2e83ad651f4a451",
     "https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext": "1dd633858ff4fd2affd2343c0f16f4d0727524919f53f0a5cf240baefd3c91fd",
+    "https://esm.d8d.fun/eventemitter3@4.0.7/denonext/eventemitter3.mjs": "d26d8683d890f259d39cc334512a888d3079917477d7eb10c3cd3775831cdcc3",
+    "https://esm.d8d.fun/eventemitter3@4.0.7?target=denonext": "84748b6b17ebdb58719651ec0b5e6f84e5923d97f43231c5835d74be6ce050f1",
     "https://esm.d8d.fun/eventemitter3@5.0.1/denonext/eventemitter3.mjs": "8f5abddb39876fcb8a4ea0c464c7de3755b244f99e4149b1524e77053a7f71a7",
     "https://esm.d8d.fun/eventemitter3@5.0.1?target=denonext": "70d8b94c6397b6df7daeb699ebcf8d78a00c961d5ea9abd48674805bcfba040a",
     "https://esm.d8d.fun/follow-redirects@1.15.9/denonext/follow-redirects.mjs": "90866d22d80eface74d3161906835600fbb1d5c9ded05dc72fd55b40960cfce7",
@@ -1248,6 +1217,8 @@
     "https://esm.d8d.fun/isexe@3.1.1/denonext/posix.mjs": "f99f8d2aacd0b5424625cee480f36b47639cfbad44c64b7b21cbba18ad77a1b2",
     "https://esm.d8d.fun/isexe@3.1.1/denonext/win32.mjs": "f52981ee6555549c246db8e9e6c0ee1e2947a35367c3bcec0ba31834387991c0",
     "https://esm.d8d.fun/isexe@3.1.1?target=denonext": "b3c61e7e70b9d56865de461fbcdae702ebf93743143457079a15a60e30dfcf83",
+    "https://esm.d8d.fun/js-sha1@0.6.0/denonext/js-sha1.mjs": "11ff6220208daf0e7f5a876ec05e21cdcebba1d6c95df4fca91da4b36dbffe96",
+    "https://esm.d8d.fun/js-sha1@0.6.0?target=denonext": "665489153856a41b8af6b89d328b467d71b3f36aef9a2925c3c06119ca192f5b",
     "https://esm.d8d.fun/jsonwebtoken@9.0.2/denonext/jsonwebtoken.mjs": "4365e66f5fa04a6903d106e17166fd93ddef582caf66f19d281dc066711c4f6e",
     "https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext": "76a17587b04be9dfee91197bb0119b0be3a6a53ec05cc63d44292077bc03c0f7",
     "https://esm.d8d.fun/jwa@1.4.1/denonext/jwa.mjs": "fb2de82e653ed6ed5432adb25567772ea9044f241d209c569b23eee07ab50176",
@@ -1315,6 +1286,10 @@
     "https://esm.d8d.fun/turbo-stream@2.4.0/denonext/turbo-stream.development.mjs": "fd8a6367c8b4e07cbf32d313f512fd054dbebed8c6858357d74b3a8582bda7a4",
     "https://esm.d8d.fun/utf-8-validate@6.0.5/denonext/utf-8-validate.mjs": "90c0c88a13bc4749b497361480d618bf4809153f5d5ba694fac79ae9dbf634a9",
     "https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext": "071bc33ba1a58297e23a34d69dd589fd06df04b0f373b382ff5da544a623f271",
+    "https://esm.d8d.fun/uuid@3.4.0/denonext/v4.mjs": "8b957a5849b03b73ee2658fab1b20a2003875c00900a4ad197d15e92fb9d8aed",
+    "https://esm.d8d.fun/uuid@3.4.0/v4?target=denonext": "07d4965f7cc4c0d0b90555fbdeaf99e18f927c807e003e1c3e77d54a4c2afb21",
+    "https://esm.d8d.fun/vod-js-sdk-v6@1.7.1-beta.1": "3615145f9094d85a1c7fd3dfd37778f6b7869ab330c0c8cd42f53a49758d2624",
+    "https://esm.d8d.fun/vod-js-sdk-v6@1.7.1-beta.1/denonext/vod-js-sdk-v6.mjs": "2165a022cd4f8787760786ef464ea79d3d7687e84a9a26dc9f886163209e9546",
     "https://esm.d8d.fun/which@4.0.0/denonext/which.mjs": "9f47207c6dc9684fe3d852f2290c474577babaeabf60616652630c0b90421a53",
     "https://esm.d8d.fun/which@4.0.0?target=denonext": "50b06c1a68e3ef88dc8e2c68c17b732a6d1917000d5d59637496da3b61549c8e",
     "https://esm.d8d.fun/ws@8.17.1/denonext/ws.mjs": "7b349f9bcf5af35a422b01ece5189ac693f84f07cc2e9be12023ec818a18ba71",
@@ -1517,8 +1492,7 @@
       "npm:@alicloud/live-interaction20201214@2.1.6",
       "npm:@alicloud/live20161101@^1.1.1",
       "npm:@alicloud/openapi-client@~0.4.14",
-      "npm:@alicloud/pop-core@1.8.0",
-      "npm:vod-js-sdk-v6@*"
+      "npm:@alicloud/pop-core@1.8.0"
     ]
   }
 }

+ 29 - 0
docs/vod_sign_gen_demo.md

@@ -0,0 +1,29 @@
+var querystring = require("querystring");
+var crypto = require('crypto');
+
+// 确定 app 的云 API 密钥
+var secret_id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
+var secret_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+
+// 确定签名的当前时间和失效时间
+var current = parseInt((new Date()).getTime() / 1000)
+var expired = current + 86400;  // 签名有效期:1天
+
+// 向参数列表填入参数
+var arg_list = {
+    secretId : secret_id,
+    currentTimeStamp : current,
+    expireTime : expired,
+    random : Math.round(Math.random() * Math.pow(2, 32))
+}
+
+// 计算签名
+var orignal = querystring.stringify(arg_list);
+var orignal_buffer = new Buffer(orignal, "utf8");
+
+var hmac = crypto.createHmac("sha1", secret_key);
+var hmac_buffer = hmac.update(orignal_buffer).digest();
+
+var signature = Buffer.concat([hmac_buffer, orignal_buffer]).toString("base64");
+
+console.log(signature);

+ 50 - 45
server/router.ts

@@ -1,66 +1,71 @@
 /** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
-import { Hono } from 'hono'
-import { corsMiddleware, withAuth, setEnvVariables } from './middlewares.ts'
-import type { APIClient } from '@d8d-appcontainer/api'
-import { Auth } from '@d8d-appcontainer/auth';
+import { Hono } from "hono";
+import { corsMiddleware, setEnvVariables, withAuth } from "./middlewares.ts";
+import type { APIClient } from "@d8d-appcontainer/api";
+import { Auth } from "@d8d-appcontainer/auth";
 
 // 导入路由模块
-import { createAuthRoutes } from "./routes_auth.ts"
-import { createUserRoutes } from "./routes_users.ts"
-import { createKnowInfoRoutes } from "./routes_know_info.ts"
-import { createFileUploadRoutes } from "./routes_file_upload.ts"
-import { createFileCategoryRoutes } from "./routes_file_category.ts"
-import { createThemeRoutes } from "./routes_theme.ts"
-import { createChartRoutes } from "./routes_charts.ts"
-import { createMapRoutes } from "./routes_maps.ts"
-import { createSystemSettingsRoutes } from "./routes_system_settings.ts"
-import { createMessagesRoutes } from "./routes_messages.ts"
-import { createMigrationsRoutes } from "./routes_migrations.ts"
-import { createHomeRoutes } from "./routes_home.ts"
+import { createAuthRoutes } from "./routes_auth.ts";
+import { createUserRoutes } from "./routes_users.ts";
+import { createKnowInfoRoutes } from "./routes_know_info.ts";
+import { createFileUploadRoutes } from "./routes_file_upload.ts";
+import { createFileCategoryRoutes } from "./routes_file_category.ts";
+import { createThemeRoutes } from "./routes_theme.ts";
+import { createChartRoutes } from "./routes_charts.ts";
+import { createMapRoutes } from "./routes_maps.ts";
+import { createSystemSettingsRoutes } from "./routes_system_settings.ts";
+import { createMessagesRoutes } from "./routes_messages.ts";
+import { createMigrationsRoutes } from "./routes_migrations.ts";
+import { createHomeRoutes } from "./routes_home.ts";
 
-import { createClassRoomRoutes } from "./routes_classroom.ts"
+import { createClassRoomRoutes } from "./routes_classroom.ts";
 import { createStockRoutes } from "./routes_stock.ts";
 import { createClassroomDataRoutes } from "./routes_classroom_data.ts";
 import { createSubmissionRoutes } from "./routes_submission_records.ts";
 import { createDateNotesRoutes } from "./routes_date_notes.ts";
 import { createXunlianCodesRoutes } from "./routes_xunlian_codes.ts";
+import { createVodRoutes } from "./routes_vod.ts";
 
-export function createRouter(apiClient: APIClient, moduleDir: string , auth: Auth) {
-  const router = new Hono()
+export function createRouter(
+  apiClient: APIClient,
+  moduleDir: string,
+  auth: Auth,
+) {
+  const router = new Hono();
 
   // 添加CORS中间件
-  router.use('/*', corsMiddleware)
+  router.use("/*", corsMiddleware);
 
   // 创建API路由
-  const api = new Hono()
-  
+  const api = new Hono();
+
   // 设置环境变量
-  api.use('*', setEnvVariables(apiClient, moduleDir, auth))
+  api.use("*", setEnvVariables(apiClient, moduleDir, auth));
 
   // 注册所有路由
-  api.route('/auth', createAuthRoutes(withAuth))
-  api.route('/users', createUserRoutes(withAuth))
-  api.route('/know-infos', createKnowInfoRoutes(withAuth))
-  api.route('/upload', createFileUploadRoutes(withAuth))
-  api.route('/file-categories', createFileCategoryRoutes(withAuth))
-  api.route('/theme', createThemeRoutes(withAuth))
-  api.route('/charts', createChartRoutes(withAuth))
-  api.route('/map', createMapRoutes(withAuth))
-  api.route('/settings', createSystemSettingsRoutes(withAuth))
-  api.route('/messages', createMessagesRoutes(withAuth))
-  api.route('/migrations', createMigrationsRoutes(withAuth))
-  api.route('/home', createHomeRoutes(withAuth))
+  api.route("/auth", createAuthRoutes(withAuth));
+  api.route("/users", createUserRoutes(withAuth));
+  api.route("/know-infos", createKnowInfoRoutes(withAuth));
+  api.route("/upload", createFileUploadRoutes(withAuth));
+  api.route("/file-categories", createFileCategoryRoutes(withAuth));
+  api.route("/theme", createThemeRoutes(withAuth));
+  api.route("/charts", createChartRoutes(withAuth));
+  api.route("/map", createMapRoutes(withAuth));
+  api.route("/settings", createSystemSettingsRoutes(withAuth));
+  api.route("/messages", createMessagesRoutes(withAuth));
+  api.route("/migrations", createMigrationsRoutes(withAuth));
+  api.route("/home", createHomeRoutes(withAuth));
 
-  api.route('/classroom', createClassRoomRoutes(withAuth))
-  api.route('/stock', createStockRoutes(withAuth));
-  api.route('/classroom-datas', createClassroomDataRoutes(withAuth));
-  api.route('/submission-records', createSubmissionRoutes(withAuth));
-  api.route('/date-notes', createDateNotesRoutes(withAuth));
-  api.route('/xunlian-codes', createXunlianCodesRoutes(withAuth)); 
-  
+  api.route("/classroom", createClassRoomRoutes(withAuth));
+  api.route("/stock", createStockRoutes(withAuth));
+  api.route("/classroom-datas", createClassroomDataRoutes(withAuth));
+  api.route("/submission-records", createSubmissionRoutes(withAuth));
+  api.route("/date-notes", createDateNotesRoutes(withAuth));
+  api.route("/xunlian-codes", createXunlianCodesRoutes(withAuth));
+  api.route("/vod", createVodRoutes(withAuth));
 
   // 注册API路由到主路由器
-  router.route('/api', api)
+  router.route("/api", api);
 
-  return router
-}
+  return router;
+}

+ 66 - 0
server/routes_vod.ts

@@ -0,0 +1,66 @@
+import { Hono } from "hono";
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+import querystring from "node:querystring";
+import crypto from "node:crypto";
+import { Buffer } from "node:buffer";
+
+function generateVodSignature(secret_id: string, secret_key: string) {
+  // 确定 app 的云 API 密钥
+  // var secret_id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
+  // var secret_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+
+  // 确定签名的当前时间和失效时间
+  const current = parseInt(((new Date()).getTime() / 1000).toString());
+  const expired = current + 86400; // 签名有效期:1天
+
+  // 向参数列表填入参数
+  const arg_list = {
+    secretId: secret_id,
+    currentTimeStamp: current,
+    expireTime: expired,
+    random: Math.round(Math.random() * Math.pow(2, 32)),
+  };
+
+  // 计算签名
+  const orignal = querystring.stringify(arg_list);
+  const orignal_buffer = Buffer.from(orignal, "utf8");
+
+  const hmac = crypto.createHmac("sha1", secret_key);
+  const hmac_buffer = hmac.update(orignal_buffer).digest();
+
+  const signature = Buffer.concat([hmac_buffer, orignal_buffer]).toString(
+    "base64",
+  );
+
+  return signature;
+}
+
+export function createVodRoutes(withAuth: WithAuth) {
+  const vodRoutes = new Hono<{ Variables: Variables }>();
+
+  // VOD签名生成
+  vodRoutes.get("/signature", withAuth, async (c) => {
+    try {
+      const secretId = Deno.env.get("VOD_SECRET_ID");
+      const secretKey = Deno.env.get("VOD_SECRET_KEY");
+
+      if (!secretId || !secretKey) {
+        throw new Error("缺少VOD密钥配置");
+      }
+
+      const signature = generateVodSignature(secretId, secretKey);
+
+      return c.json({
+        signature,
+      });
+    } catch (error) {
+      console.error("VOD签名生成错误:", error);
+      return c.json({
+        error: "签名生成失败",
+      }, 500);
+    }
+  });
+
+  return vodRoutes;
+}