正在显示
8 个修改的文件
包含
110 行增加
和
21 行删除
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | 4 | ||
| 5 | 查询指定课堂的录制文件是否已经生成,并在文件全部可用时返回访问地址。 | 5 | 查询指定课堂的录制文件是否已经生成,并在文件全部可用时返回访问地址。 |
| 6 | 6 | ||
| 7 | -接口自动识别整课录制和仅高光录制,调用方不需要指定录制类型。查询操作不会创建、重新执行或修改录制任务。 | 7 | +接口通过 `onlyHighlight` 区分整课录制和仅高光录制。查询操作不会创建、重新执行或修改录制任务。 |
| 8 | 8 | ||
| 9 | | 项目 | 内容 | | 9 | | 项目 | 内容 | |
| 10 | |---|---| | 10 | |---|---| |
| @@ -23,6 +23,7 @@ | @@ -23,6 +23,7 @@ | ||
| 23 | | `siteId` | String | 是 | 站点 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 | | 23 | | `siteId` | String | 是 | 站点 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 | |
| 24 | | `classId` | String | 是 | 课堂 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 | | 24 | | `classId` | String | 是 | 课堂 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 | |
| 25 | | `classStartTime` | String | 否 | 兼容历史整课文件查询时使用,格式为 `yyyyMMdd`;V2 录制任务通常不需要传入 | | 25 | | `classStartTime` | String | 否 | 兼容历史整课文件查询时使用,格式为 `yyyyMMdd`;V2 录制任务通常不需要传入 | |
| 26 | +| `onlyHighlight` | Integer | 否 | `0` 或不传表示整课录制,`1` 表示仅高光录制 | | ||
| 26 | 27 | ||
| 27 | ### 请求示例 | 28 | ### 请求示例 |
| 28 | 29 | ||
| @@ -47,6 +48,18 @@ Content-Type: application/json; charset=utf-8 | @@ -47,6 +48,18 @@ Content-Type: application/json; charset=utf-8 | ||
| 47 | } | 48 | } |
| 48 | ``` | 49 | ``` |
| 49 | 50 | ||
| 51 | +高光文件查询示例: | ||
| 52 | + | ||
| 53 | +```json | ||
| 54 | +{ | ||
| 55 | + "siteId": "doctest", | ||
| 56 | + "classId": "487012832", | ||
| 57 | + "onlyHighlight": 1 | ||
| 58 | +} | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +高光查询会调用 SaaS `getByClassPrivate.do` 获取该课堂的高光列表,再检查对应 MP4 是否已生成,不依赖 WebScreen 本地任务快照。 | ||
| 62 | + | ||
| 50 | ## 3. 响应参数 | 63 | ## 3. 响应参数 |
| 51 | 64 | ||
| 52 | | 参数 | 类型 | 必定返回 | 说明 | | 65 | | 参数 | 类型 | 必定返回 | 说明 | |
| @@ -185,3 +198,4 @@ HTTP 200 && code == 0 && fileExists == true | @@ -185,3 +198,4 @@ HTTP 200 && code == 0 && fileExists == true | ||
| 185 | | 版本 | 日期 | 说明 | | 198 | | 版本 | 日期 | 说明 | |
| 186 | |---|---|---| | 199 | |---|---|---| |
| 187 | | V2 | 2026-08-10 | 支持统一查询整课录制和仅高光录制结果 | | 200 | | V2 | 2026-08-10 | 支持统一查询整课录制和仅高光录制结果 | |
| 201 | +| V2 | 2026-08-12 | 高光查询改为以 `onlyHighlight=1` 和 `getByClassPrivate.do` 为准,不再依赖本地任务快照 | |
| @@ -32,7 +32,7 @@ | @@ -32,7 +32,7 @@ | ||
| 32 | 32 | ||
| 33 | ## 3. 查询 | 33 | ## 3. 查询 |
| 34 | 34 | ||
| 35 | -- `/fileExistsV2` 不触发录制或 SaaS 查询。 | 35 | +- `/fileExistsV2` 不触发录制;整课查询不访问 SaaS,高光查询通过 `getByClassPrivate.do` 获取高光列表。 |
| 36 | - 整课任务兼容返回 `classUrl`。 | 36 | - 整课任务兼容返回 `classUrl`。 |
| 37 | - 高光任务返回全部高光 URL。 | 37 | - 高光任务返回全部高光 URL。 |
| 38 | - 任一目标文件缺失时 `fileExists=false`。 | 38 | - 任一目标文件缺失时 `fileExists=false`。 |
| @@ -198,7 +198,8 @@ Content-Type: application/json | @@ -198,7 +198,8 @@ Content-Type: application/json | ||
| 198 | } | 198 | } |
| 199 | ``` | 199 | ``` |
| 200 | 200 | ||
| 201 | -V2 根据 `/recordingTaskV2` 保存的任务快照判断文件类型,客户不需要再次传 `onlyHighlight`。 | 201 | +V2 根据 `onlyHighlight` 判断文件类型:`0` 或不传查询整课文件,`1` 通过 SaaS |
| 202 | +`getByClassPrivate.do` 获取高光列表并检查对应文件。本地任务快照不作为高光文件查询的前置条件。 | ||
| 202 | 203 | ||
| 203 | 整课成功响应: | 204 | 整课成功响应: |
| 204 | 205 |
不能预览此文件类型
| @@ -379,10 +379,12 @@ router.post('/fileExistsV2', async (req, res) => { | @@ -379,10 +379,12 @@ router.post('/fileExistsV2', async (req, res) => { | ||
| 379 | const result = await recordingTaskService.getFileResult(req.body || {}); | 379 | const result = await recordingTaskService.getFileResult(req.body || {}); |
| 380 | return res.send(result); | 380 | return res.send(result); |
| 381 | } catch (err) { | 381 | } catch (err) { |
| 382 | - if (err instanceof RecordingTaskValidationError) { | 382 | + if (err instanceof RecordingTaskValidationError || err instanceof HighlightValidationError) { |
| 383 | return res.status(400).send({ | 383 | return res.status(400).send({ |
| 384 | - code: err.apiCode, | ||
| 385 | - message: err.message, | 384 | + code: err instanceof RecordingTaskValidationError ? err.apiCode : 2, |
| 385 | + message: err instanceof RecordingTaskValidationError | ||
| 386 | + ? err.message | ||
| 387 | + : 'siteId或classId不正确', | ||
| 386 | fileExists: false, | 388 | fileExists: false, |
| 387 | files: [] | 389 | files: [] |
| 388 | }); | 390 | }); |
| @@ -623,20 +623,36 @@ class RecordingTaskService { | @@ -623,20 +623,36 @@ class RecordingTaskService { | ||
| 623 | const key = buildManifestKey({ siteId, classId }); | 623 | const key = buildManifestKey({ siteId, classId }); |
| 624 | const manifest = this.loadManifest(key); | 624 | const manifest = this.loadManifest(key); |
| 625 | const requestedPeriod = normalizeTaskPeriod(input, null, false); | 625 | const requestedPeriod = normalizeTaskPeriod(input, null, false); |
| 626 | + const onlyHighlight = normalizeOnlyHighlight(input.onlyHighlight); | ||
| 627 | + | ||
| 628 | + if (onlyHighlight === 1) { | ||
| 629 | + const statuses = await this.highlightService.getClassFileStatuses({ siteId, classId }); | ||
| 630 | + const fileExists = statuses.length > 0 && | ||
| 631 | + statuses.every(item => item.generated && item.url); | ||
| 632 | + return { | ||
| 633 | + code: fileExists ? 0 : 1, | ||
| 634 | + message: fileExists ? '文件已生成' : '文件未生成', | ||
| 635 | + fileExists, | ||
| 636 | + onlyHighlight: 1, | ||
| 637 | + files: fileExists ? statuses.map(item => ({ | ||
| 638 | + type: 'highlight', | ||
| 639 | + highlightId: item.highlightId, | ||
| 640 | + url: item.url | ||
| 641 | + })) : [] | ||
| 642 | + }; | ||
| 643 | + } | ||
| 644 | + | ||
| 626 | if (manifest && requestedPeriod && requestedPeriod.beginTime != null && manifest.beginTime != null && | 645 | if (manifest && requestedPeriod && requestedPeriod.beginTime != null && manifest.beginTime != null && |
| 627 | (requestedPeriod.beginTime !== manifest.beginTime || requestedPeriod.endTime !== manifest.endTime)) { | 646 | (requestedPeriod.beginTime !== manifest.beginTime || requestedPeriod.endTime !== manifest.endTime)) { |
| 628 | - return { code: 1, message: '文件未生成', fileExists: false, files: [] }; | 647 | + return { code: 1, message: '文件未生成', fileExists: false, onlyHighlight: 0, files: [] }; |
| 629 | } | 648 | } |
| 630 | 649 | ||
| 631 | let expected = []; | 650 | let expected = []; |
| 632 | - let onlyHighlight; | ||
| 633 | if (manifest) { | 651 | if (manifest) { |
| 634 | - onlyHighlight = manifest.onlyHighlight; | ||
| 635 | - expected = onlyHighlight === 1 ? (manifest.highlights || []) : [this.buildFullFile(manifest)]; | 652 | + expected = [this.buildFullFile(manifest)]; |
| 636 | } else { | 653 | } else { |
| 637 | - onlyHighlight = normalizeOnlyHighlight(input.onlyHighlight); | ||
| 638 | - if (onlyHighlight === 1 || !requestedPeriod) { | ||
| 639 | - return { code: 1, message: '文件未生成', fileExists: false, files: [] }; | 654 | + if (!requestedPeriod) { |
| 655 | + return { code: 1, message: '文件未生成', fileExists: false, onlyHighlight: 0, files: [] }; | ||
| 640 | } | 656 | } |
| 641 | expected = [this.buildFullFile({ siteId, classId, classDate: requestedPeriod.classDate })]; | 657 | expected = [this.buildFullFile({ siteId, classId, classDate: requestedPeriod.classDate })]; |
| 642 | } | 658 | } |
| 1 | const assert = require('assert'); | 1 | const assert = require('assert'); |
| 2 | const http = require('http'); | 2 | const http = require('http'); |
| 3 | -const { recordingTaskService } = require('../services/recordingTaskService'); | 3 | +const { |
| 4 | + HighlightValidationError, | ||
| 5 | + recordingTaskService | ||
| 6 | +} = require('../services/recordingTaskService'); | ||
| 4 | const app = require('../app'); | 7 | const app = require('../app'); |
| 5 | 8 | ||
| 6 | function request(server, route, body) { | 9 | function request(server, route, body) { |
| @@ -119,13 +122,27 @@ async function run() { | @@ -119,13 +122,27 @@ async function run() { | ||
| 119 | assert.strictEqual(fileCalls, 0, '旧查询接口不得进入 V2 服务'); | 122 | assert.strictEqual(fileCalls, 0, '旧查询接口不得进入 V2 服务'); |
| 120 | 123 | ||
| 121 | const newFile = await request(server, '/fileExistsV2', { | 124 | const newFile = await request(server, '/fileExistsV2', { |
| 122 | - siteId: 'doctest', classId: '1001' | 125 | + siteId: 'doctest', classId: '1001', onlyHighlight: 1 |
| 123 | }); | 126 | }); |
| 124 | assert.strictEqual(newFile.status, 200); | 127 | assert.strictEqual(newFile.status, 200); |
| 125 | assert.strictEqual(newFile.body.fileExists, true); | 128 | assert.strictEqual(newFile.body.fileExists, true); |
| 126 | assert.strictEqual(newFile.body.onlyHighlight, 1); | 129 | assert.strictEqual(newFile.body.onlyHighlight, 1); |
| 127 | assert.strictEqual(newFile.body.files.length, 1); | 130 | assert.strictEqual(newFile.body.files.length, 1); |
| 128 | assert.strictEqual(fileCalls, 1); | 131 | assert.strictEqual(fileCalls, 1); |
| 132 | + | ||
| 133 | + recordingTaskService.getFileResult = async () => { | ||
| 134 | + throw new HighlightValidationError('高光录制功能未启用'); | ||
| 135 | + }; | ||
| 136 | + const disabledHighlight = await request(server, '/fileExistsV2', { | ||
| 137 | + siteId: 'kiwikids', classId: '258178512', onlyHighlight: 1 | ||
| 138 | + }); | ||
| 139 | + assert.strictEqual(disabledHighlight.status, 400); | ||
| 140 | + assert.deepStrictEqual(disabledHighlight.body, { | ||
| 141 | + code: 2, | ||
| 142 | + message: 'siteId或classId不正确', | ||
| 143 | + fileExists: false, | ||
| 144 | + files: [] | ||
| 145 | + }); | ||
| 129 | } finally { | 146 | } finally { |
| 130 | recordingTaskService.acceptTasks = originalAcceptTasks; | 147 | recordingTaskService.acceptTasks = originalAcceptTasks; |
| 131 | recordingTaskService.getFileResult = originalGetFileResult; | 148 | recordingTaskService.getFileResult = originalGetFileResult; |
| @@ -99,8 +99,15 @@ async function run() { | @@ -99,8 +99,15 @@ async function run() { | ||
| 99 | siteId: 'doctest', | 99 | siteId: 'doctest', |
| 100 | beginTime: 1785899498000, | 100 | beginTime: 1785899498000, |
| 101 | endTime: 1785899543000 | 101 | endTime: 1785899543000 |
| 102 | + }, { | ||
| 103 | + id: 8, | ||
| 104 | + meetingNumber: '1007', | ||
| 105 | + siteId: 'doctest', | ||
| 106 | + beginTime: 1785895298000, | ||
| 107 | + endTime: 1785895343000 | ||
| 102 | }]; | 108 | }]; |
| 103 | - const calls = { fetch: 0, enqueue: 0, wait: 0 }; | 109 | + const calls = { fetch: 0, enqueue: 0, wait: 0, fileStatus: 0 }; |
| 110 | + const objects = new Set(); | ||
| 104 | const highlightService = { | 111 | const highlightService = { |
| 105 | fetchByClass: async classId => { | 112 | fetchByClass: async classId => { |
| 106 | calls.fetch += 1; | 113 | calls.fetch += 1; |
| @@ -113,9 +120,24 @@ async function run() { | @@ -113,9 +120,24 @@ async function run() { | ||
| 113 | waitForTaskKeys: async keys => { | 120 | waitForTaskKeys: async keys => { |
| 114 | calls.wait += 1; | 121 | calls.wait += 1; |
| 115 | return keys.map(key => ({ key, status: 'uploading' })); | 122 | return keys.map(key => ({ key, status: 'uploading' })); |
| 123 | + }, | ||
| 124 | + getClassFileStatuses: async ({ siteId, classId }) => { | ||
| 125 | + calls.fileStatus += 1; | ||
| 126 | + return records.filter(item => item.siteId === siteId && item.meetingNumber === classId) | ||
| 127 | + .map(item => { | ||
| 128 | + const ossKey = `oss/${siteId}/20260805/${classId}_highlight_${item.id}.mp4`; | ||
| 129 | + const generated = objects.has(ossKey); | ||
| 130 | + return { | ||
| 131 | + highlightId: item.id, | ||
| 132 | + classId, | ||
| 133 | + siteId, | ||
| 134 | + generated, | ||
| 135 | + status: generated ? 'generated' : 'not_generated', | ||
| 136 | + url: generated ? `https://xdymp4.xuedianyun.com/${ossKey}` : null | ||
| 137 | + }; | ||
| 138 | + }); | ||
| 116 | } | 139 | } |
| 117 | }; | 140 | }; |
| 118 | - const objects = new Set(); | ||
| 119 | const statusUpdates = []; | 141 | const statusUpdates = []; |
| 120 | const service = new RecordingTaskService({ | 142 | const service = new RecordingTaskService({ |
| 121 | configPath, | 143 | configPath, |
| @@ -182,14 +204,29 @@ async function run() { | @@ -182,14 +204,29 @@ async function run() { | ||
| 182 | assert.deepStrictEqual(duplicate, { accepted: 0, duplicates: 1, noMedia: 0 }); | 204 | assert.deepStrictEqual(duplicate, { accepted: 0, duplicates: 1, noMedia: 0 }); |
| 183 | assert.strictEqual(calls.fetch, 1); | 205 | assert.strictEqual(calls.fetch, 1); |
| 184 | 206 | ||
| 185 | - let highFiles = await service.getFileResult({ siteId: 'doctest', classId: '1002' }); | 207 | + let highFiles = await service.getFileResult({ |
| 208 | + siteId: 'doctest', classId: '1002', onlyHighlight: 1 | ||
| 209 | + }); | ||
| 186 | assert.strictEqual(highFiles.fileExists, false); | 210 | assert.strictEqual(highFiles.fileExists, false); |
| 187 | objects.add('oss/doctest/20260805/1002_highlight_5.mp4'); | 211 | objects.add('oss/doctest/20260805/1002_highlight_5.mp4'); |
| 188 | objects.add('oss/doctest/20260805/1002_highlight_6.mp4'); | 212 | objects.add('oss/doctest/20260805/1002_highlight_6.mp4'); |
| 189 | - highFiles = await service.getFileResult({ siteId: 'doctest', classId: '1002' }); | 213 | + objects.add('oss/doctest/20260805/1002_highlight_7.mp4'); |
| 214 | + highFiles = await service.getFileResult({ | ||
| 215 | + siteId: 'doctest', classId: '1002', onlyHighlight: 1 | ||
| 216 | + }); | ||
| 190 | assert.strictEqual(highFiles.fileExists, true); | 217 | assert.strictEqual(highFiles.fileExists, true); |
| 191 | assert.strictEqual(highFiles.onlyHighlight, 1); | 218 | assert.strictEqual(highFiles.onlyHighlight, 1); |
| 192 | - assert.deepStrictEqual(highFiles.files.map(item => item.highlightId), [5, 6]); | 219 | + assert.deepStrictEqual(highFiles.files.map(item => item.highlightId), [5, 6, 7]); |
| 220 | + | ||
| 221 | + objects.add('oss/doctest/20260805/1007_highlight_8.mp4'); | ||
| 222 | + const manifestlessHighlight = await service.getFileResult({ | ||
| 223 | + siteId: 'doctest', classId: '1007', classStartTime: '20260805', onlyHighlight: 1 | ||
| 224 | + }); | ||
| 225 | + assert.strictEqual(manifestlessHighlight.fileExists, true, | ||
| 226 | + '高光查询不得依赖本地 manifest'); | ||
| 227 | + assert.strictEqual(manifestlessHighlight.onlyHighlight, 1); | ||
| 228 | + assert.deepStrictEqual(manifestlessHighlight.files.map(item => item.highlightId), [8]); | ||
| 229 | + assert.strictEqual(calls.fileStatus, 3, '每次高光文件查询都必须读取 getByClass 文件状态'); | ||
| 193 | 230 | ||
| 194 | const noMedia = await service.acceptTasks({ list: [{ | 231 | const noMedia = await service.acceptTasks({ list: [{ |
| 195 | id: 'task-high-empty', | 232 | id: 'task-high-empty', |
| @@ -200,7 +237,9 @@ async function run() { | @@ -200,7 +237,9 @@ async function run() { | ||
| 200 | }] }); | 237 | }] }); |
| 201 | assert.deepStrictEqual(noMedia, { accepted: 1, duplicates: 0, noMedia: 1 }); | 238 | assert.deepStrictEqual(noMedia, { accepted: 1, duplicates: 0, noMedia: 1 }); |
| 202 | assert.deepStrictEqual(statusUpdates, ['1001:2', '1002:2', '1003:3']); | 239 | assert.deepStrictEqual(statusUpdates, ['1001:2', '1002:2', '1003:3']); |
| 203 | - const emptyResult = await service.getFileResult({ siteId: 'doctest', classId: '1003' }); | 240 | + const emptyResult = await service.getFileResult({ |
| 241 | + siteId: 'doctest', classId: '1003', onlyHighlight: 1 | ||
| 242 | + }); | ||
| 204 | assert.strictEqual(emptyResult.fileExists, false); | 243 | assert.strictEqual(emptyResult.fileExists, false); |
| 205 | 244 | ||
| 206 | const mappedNoMedia = await service.acceptTasks({ list: [{ | 245 | const mappedNoMedia = await service.acceptTasks({ list: [{ |
-
请 注册 或 登录 后发表评论