Toggle navigation
Toggle navigation
此项目
正在载入...
Sign in
张艾宸
/
webScreen
转到一个项目
Toggle navigation
项目
群组
代码片段
帮助
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
huz1xuan
2026-08-12 16:12:52 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
01b5ae26c76e790ec7834bf756954819f7527550
01b5ae26
1 parent
01c1cc83
完善高光时刻V2接口
显示空白字符变更
内嵌
并排对比
正在显示
10 个修改的文件
包含
217 行增加
和
39 行删除
docs/FILE_EXISTS_V2.md
docs/HIGHLIGHT_DEPLOYMENT_USAGE_TEST.md
docs/HIGHLIGHT_MP4_SPEC.md
docs/RECORDING_API.md
docs/录制任务启动文档.md
release-packages/webScreen-recording-v2-20260812.tar.gz
routes/index.js
services/recordingTaskService.js
test/recordingRoutes.test.js
test/recordingTaskService.test.js
docs/FILE_EXISTS_V2.md
查看文件 @
01b5ae2
...
...
@@ -4,7 +4,7 @@
查询指定课堂的录制文件是否已经生成,并在文件全部可用时返回访问地址。
接口
自动识别整课录制和仅高光录制,调用方不需要指定录制类型
。查询操作不会创建、重新执行或修改录制任务。
接口
通过
`onlyHighlight`
区分整课录制和仅高光录制
。查询操作不会创建、重新执行或修改录制任务。
| 项目 | 内容 |
|---|---|
...
...
@@ -23,6 +23,7 @@
|
`siteId`
| String | 是 | 站点 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 |
|
`classId`
| String | 是 | 课堂 ID;仅支持字母、数字、下划线和短横线,最长 128 个字符 |
|
`classStartTime` | String | 否 | 兼容历史整课文件查询时使用,格式为 `yyyyMMdd`
;V2 录制任务通常不需要传入 |
|
`onlyHighlight` | Integer | 否 | `0` 或不传表示整课录制,`1`
表示仅高光录制 |
### 请求示例
...
...
@@ -47,6 +48,18 @@ Content-Type: application/json; charset=utf-8
}
```
高光文件查询示例:
```
json
{
"siteId"
:
"doctest"
,
"classId"
:
"487012832"
,
"onlyHighlight"
:
1
}
```
高光查询会调用 SaaS
`getByClassPrivate.do`
获取该课堂的高光列表,再检查对应 MP4 是否已生成,不依赖 WebScreen 本地任务快照。
## 3. 响应参数
| 参数 | 类型 | 必定返回 | 说明 |
...
...
@@ -185,3 +198,4 @@ HTTP 200 && code == 0 && fileExists == true
| 版本 | 日期 | 说明 |
|---|---|---|
| V2 | 2026-08-10 | 支持统一查询整课录制和仅高光录制结果 |
| V2 | 2026-08-12 | 高光查询改为以
`onlyHighlight=1` 和 `getByClassPrivate.do`
为准,不再依赖本地任务快照 |
...
...
docs/HIGHLIGHT_DEPLOYMENT_USAGE_TEST.md
查看文件 @
01b5ae2
...
...
@@ -32,7 +32,7 @@
## 3. 查询
-
`/fileExistsV2`
不触发录制
或 SaaS 查询
。
-
`/fileExistsV2` 不触发录制
;整课查询不访问 SaaS,高光查询通过 `getByClassPrivate.do`
获取高光列表
。
-
整课任务兼容返回
`classUrl`
。
-
高光任务返回全部高光 URL。
-
任一目标文件缺失时
`fileExists=false`
。
...
...
docs/HIGHLIGHT_MP4_SPEC.md
已删除
100644 → 0
查看文件 @
01c1cc8
# 高光 MP4 内部约定
-
一个 SaaS 高光记录生成一个独立 MP4。
-
文件名为
`{classId}_highlight_{highlightId}.mp4`
。
-
回放地址携带绝对毫秒时间戳
`recBeginTime/recEndTime`
。
-
录制进程使用参数数组和
`spawn(..., {shell:false})`
,不拼接未校验的 shell 输入。
-
临时文件完成并校验非空后,原子移动到正式
`media`
目录。
-
相同
`siteId + highlightId`
不重复进入队列。
-
本地文件或 OSS 对象已存在时不重复录制。
-
SaaS 高光接口失败不能当作“没有高光”。
-
V2 客户查询只读取任务创建时保存的高光清单。
文件位置:
```
text
本地:media/{siteId}/{yyyyMMdd}/{classId}_highlight_{highlightId}.mp4
OSS:oss/{siteId}/{yyyyMMdd}/{classId}_highlight_{highlightId}.mp4
```
docs/RECORDING_API.md
查看文件 @
01b5ae2
...
...
@@ -198,7 +198,8 @@ Content-Type: application/json
}
```
V2 根据
`/recordingTaskV2` 保存的任务快照判断文件类型,客户不需要再次传 `onlyHighlight`
。
V2 根据
`onlyHighlight` 判断文件类型:`0` 或不传查询整课文件,`1`
通过 SaaS
`getByClassPrivate.do`
获取高光列表并检查对应文件。本地任务快照不作为高光文件查询的前置条件。
整课成功响应:
...
...
docs/录制任务启动文档.md
0 → 100644
查看文件 @
01b5ae2
# 高光录制调用说明
服务地址:
`http://47.93.239.64:3001`
## 1. 录制单个课堂的全部高光时刻
调用:
```
http
POST /recordingTaskV2
Content-Type: application/json
```
请求示例(
`xdyui2` 站点、课堂号 `1010945096`
、2026-08-11):
```
bash
curl -X POST
'http://47.93.239.64:3001/recordingTaskV2'
\
-H
'Content-Type: application/json'
\
--data
'{
"list": [
{
"siteId": "xdyui2",
"classId": "1010945096",
"beginTime": "2026-08-11 00:00:00",
"endTime": "2026-08-11 23:59:59",
"onlyHighlight": 1
}
]
}'
```
要点:
-
`onlyHighlight` 必须为 `1`
;否则会按整课堂录制。
-
`classId` 是课堂号,也可以改传兼容字段 `meetingNumber`
。
-
`beginTime`、`endTime`
建议传入课堂所在日期的北京时间范围;也支持 13 位毫秒时间戳。
-
返回
`code="0"` 且 `accepted=1`
表示任务已接收,录制和上传仍在后台异步执行。
查询最终文件:
```
bash
curl -X POST
'http://47.93.239.64:3001/fileExistsV2'
\
-H
'Content-Type: application/json'
\
--data
'{
"siteId": "xdyui2",
"classId": "1010945096",
"onlyHighlight": 1
}'
```
当返回
`fileExists=true` 时,`files` 数组中的 `url` 就是该课堂各段高光 MP4 的下载地址。返回 `fileExists=false`
表示仍在录制或上传,可稍后重试查询。
## 2. 录制前一天某个站点的全部高光时刻课堂
### 指定一个站点立即执行
调用:
```
http
POST /highlight/recording/by-site
Content-Type: application/json
```
请求中的
`beginTime`、`endTime`
必须是“北京时间前一天 00:00:00.000 至 23:59:59.999”对应的 13 位毫秒时间戳。
例如在 2026-08-13 执行,录制
`xdyui2`
站点 2026-08-12 全天的高光:
```
bash
curl -X POST
'http://47.93.239.64:3001/highlight/recording/by-site'
\
-H
'Content-Type: application/json'
\
--data
'{
"siteId": "xdyui2",
"beginTime": 1786464000000,
"endTime": 1786550399999
}'
```
成功响应中的字段含义:
-
`received`
:前一天该站点命中的高光总数。
-
`queued`、`recording`
:等待或正在录制的数量。
-
`uploading`
:本地 MP4 已生成,等待上传。
-
`generated`
:最终文件已经生成。
-
`invalid`、`failed`
:无效或失败的数量。
### 每天自动执行
先把目标站点放入服务器
`HIGHLIGHTCONFIG.siteIds`,例如只处理 `xdyui2`
:
```
json
{
"HIGHLIGHTCONFIG"
:
{
"siteIds"
:
[
"xdyui2"
]
}
}
```
然后每天调用:
```
bash
curl -X POST
'http://47.93.239.64:3001/highlight/recording/scheduled'
\
-H
'Content-Type: application/json'
\
--data
'{}'
```
该接口会自动按北京时间计算前一天的完整时间范围,并录制
`siteIds`
中所有站点的全部高光;请求中不用再传日期。
...
...
release-packages/webScreen-recording-v2-20260812.tar.gz
0 → 100644
查看文件 @
01b5ae2
不能预览此文件类型
routes/index.js
查看文件 @
01b5ae2
...
...
@@ -379,10 +379,12 @@ router.post('/fileExistsV2', async (req, res) => {
const
result
=
await
recordingTaskService
.
getFileResult
(
req
.
body
||
{});
return
res
.
send
(
result
);
}
catch
(
err
)
{
if
(
err
instanceof
RecordingTaskValidationError
)
{
if
(
err
instanceof
RecordingTaskValidationError
||
err
instanceof
HighlightValidationError
)
{
return
res
.
status
(
400
).
send
({
code
:
err
.
apiCode
,
message
:
err
.
message
,
code
:
err
instanceof
RecordingTaskValidationError
?
err
.
apiCode
:
2
,
message
:
err
instanceof
RecordingTaskValidationError
?
err
.
message
:
'siteId或classId不正确'
,
fileExists
:
false
,
files
:
[]
});
...
...
services/recordingTaskService.js
查看文件 @
01b5ae2
...
...
@@ -623,20 +623,36 @@ class RecordingTaskService {
const
key
=
buildManifestKey
({
siteId
,
classId
});
const
manifest
=
this
.
loadManifest
(
key
);
const
requestedPeriod
=
normalizeTaskPeriod
(
input
,
null
,
false
);
const
onlyHighlight
=
normalizeOnlyHighlight
(
input
.
onlyHighlight
);
if
(
onlyHighlight
===
1
)
{
const
statuses
=
await
this
.
highlightService
.
getClassFileStatuses
({
siteId
,
classId
});
const
fileExists
=
statuses
.
length
>
0
&&
statuses
.
every
(
item
=>
item
.
generated
&&
item
.
url
);
return
{
code
:
fileExists
?
0
:
1
,
message
:
fileExists
?
'文件已生成'
:
'文件未生成'
,
fileExists
,
onlyHighlight
:
1
,
files
:
fileExists
?
statuses
.
map
(
item
=>
({
type
:
'highlight'
,
highlightId
:
item
.
highlightId
,
url
:
item
.
url
}))
:
[]
};
}
if
(
manifest
&&
requestedPeriod
&&
requestedPeriod
.
beginTime
!=
null
&&
manifest
.
beginTime
!=
null
&&
(
requestedPeriod
.
beginTime
!==
manifest
.
beginTime
||
requestedPeriod
.
endTime
!==
manifest
.
endTime
))
{
return
{
code
:
1
,
message
:
'文件未生成'
,
fileExists
:
false
,
files
:
[]
};
return
{
code
:
1
,
message
:
'文件未生成'
,
fileExists
:
false
,
onlyHighlight
:
0
,
files
:
[]
};
}
let
expected
=
[];
let
onlyHighlight
;
if
(
manifest
)
{
onlyHighlight
=
manifest
.
onlyHighlight
;
expected
=
onlyHighlight
===
1
?
(
manifest
.
highlights
||
[])
:
[
this
.
buildFullFile
(
manifest
)];
expected
=
[
this
.
buildFullFile
(
manifest
)];
}
else
{
onlyHighlight
=
normalizeOnlyHighlight
(
input
.
onlyHighlight
);
if
(
onlyHighlight
===
1
||
!
requestedPeriod
)
{
return
{
code
:
1
,
message
:
'文件未生成'
,
fileExists
:
false
,
files
:
[]
};
if
(
!
requestedPeriod
)
{
return
{
code
:
1
,
message
:
'文件未生成'
,
fileExists
:
false
,
onlyHighlight
:
0
,
files
:
[]
};
}
expected
=
[
this
.
buildFullFile
({
siteId
,
classId
,
classDate
:
requestedPeriod
.
classDate
})];
}
...
...
test/recordingRoutes.test.js
查看文件 @
01b5ae2
const
assert
=
require
(
'assert'
);
const
http
=
require
(
'http'
);
const
{
recordingTaskService
}
=
require
(
'../services/recordingTaskService'
);
const
{
HighlightValidationError
,
recordingTaskService
}
=
require
(
'../services/recordingTaskService'
);
const
app
=
require
(
'../app'
);
function
request
(
server
,
route
,
body
)
{
...
...
@@ -119,13 +122,27 @@ async function run() {
assert
.
strictEqual
(
fileCalls
,
0
,
'旧查询接口不得进入 V2 服务'
);
const
newFile
=
await
request
(
server
,
'/fileExistsV2'
,
{
siteId
:
'doctest'
,
classId
:
'1001'
siteId
:
'doctest'
,
classId
:
'1001'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
newFile
.
status
,
200
);
assert
.
strictEqual
(
newFile
.
body
.
fileExists
,
true
);
assert
.
strictEqual
(
newFile
.
body
.
onlyHighlight
,
1
);
assert
.
strictEqual
(
newFile
.
body
.
files
.
length
,
1
);
assert
.
strictEqual
(
fileCalls
,
1
);
recordingTaskService
.
getFileResult
=
async
()
=>
{
throw
new
HighlightValidationError
(
'高光录制功能未启用'
);
};
const
disabledHighlight
=
await
request
(
server
,
'/fileExistsV2'
,
{
siteId
:
'kiwikids'
,
classId
:
'258178512'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
disabledHighlight
.
status
,
400
);
assert
.
deepStrictEqual
(
disabledHighlight
.
body
,
{
code
:
2
,
message
:
'siteId或classId不正确'
,
fileExists
:
false
,
files
:
[]
});
}
finally
{
recordingTaskService
.
acceptTasks
=
originalAcceptTasks
;
recordingTaskService
.
getFileResult
=
originalGetFileResult
;
...
...
test/recordingTaskService.test.js
查看文件 @
01b5ae2
...
...
@@ -99,8 +99,15 @@ async function run() {
siteId
:
'doctest'
,
beginTime
:
1785899498000
,
endTime
:
1785899543000
},
{
id
:
8
,
meetingNumber
:
'1007'
,
siteId
:
'doctest'
,
beginTime
:
1785895298000
,
endTime
:
1785895343000
}];
const
calls
=
{
fetch
:
0
,
enqueue
:
0
,
wait
:
0
};
const
calls
=
{
fetch
:
0
,
enqueue
:
0
,
wait
:
0
,
fileStatus
:
0
};
const
objects
=
new
Set
();
const
highlightService
=
{
fetchByClass
:
async
classId
=>
{
calls
.
fetch
+=
1
;
...
...
@@ -113,9 +120,24 @@ async function run() {
waitForTaskKeys
:
async
keys
=>
{
calls
.
wait
+=
1
;
return
keys
.
map
(
key
=>
({
key
,
status
:
'uploading'
}));
},
getClassFileStatuses
:
async
({
siteId
,
classId
})
=>
{
calls
.
fileStatus
+=
1
;
return
records
.
filter
(
item
=>
item
.
siteId
===
siteId
&&
item
.
meetingNumber
===
classId
)
.
map
(
item
=>
{
const
ossKey
=
`
oss
/
$
{
siteId
}
/20260805/
$
{
classId
}
_highlight_$
{
item
.
id
}.
mp4
`
;
const
generated
=
objects
.
has
(
ossKey
);
return
{
highlightId
:
item
.
id
,
classId
,
siteId
,
generated
,
status
:
generated
?
'generated'
:
'not_generated'
,
url
:
generated
?
`
https
:
//xdymp4.xuedianyun.com/${ossKey}` : null
};
});
}
};
const
objects
=
new
Set
();
const
statusUpdates
=
[];
const
service
=
new
RecordingTaskService
({
configPath
,
...
...
@@ -182,14 +204,29 @@ async function run() {
assert
.
deepStrictEqual
(
duplicate
,
{
accepted
:
0
,
duplicates
:
1
,
noMedia
:
0
});
assert
.
strictEqual
(
calls
.
fetch
,
1
);
let
highFiles
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1002'
});
let
highFiles
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1002'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
highFiles
.
fileExists
,
false
);
objects
.
add
(
'oss/doctest/20260805/1002_highlight_5.mp4'
);
objects
.
add
(
'oss/doctest/20260805/1002_highlight_6.mp4'
);
highFiles
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1002'
});
objects
.
add
(
'oss/doctest/20260805/1002_highlight_7.mp4'
);
highFiles
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1002'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
highFiles
.
fileExists
,
true
);
assert
.
strictEqual
(
highFiles
.
onlyHighlight
,
1
);
assert
.
deepStrictEqual
(
highFiles
.
files
.
map
(
item
=>
item
.
highlightId
),
[
5
,
6
]);
assert
.
deepStrictEqual
(
highFiles
.
files
.
map
(
item
=>
item
.
highlightId
),
[
5
,
6
,
7
]);
objects
.
add
(
'oss/doctest/20260805/1007_highlight_8.mp4'
);
const
manifestlessHighlight
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1007'
,
classStartTime
:
'20260805'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
manifestlessHighlight
.
fileExists
,
true
,
'高光查询不得依赖本地 manifest'
);
assert
.
strictEqual
(
manifestlessHighlight
.
onlyHighlight
,
1
);
assert
.
deepStrictEqual
(
manifestlessHighlight
.
files
.
map
(
item
=>
item
.
highlightId
),
[
8
]);
assert
.
strictEqual
(
calls
.
fileStatus
,
3
,
'每次高光文件查询都必须读取 getByClass 文件状态'
);
const
noMedia
=
await
service
.
acceptTasks
({
list
:
[{
id
:
'task-high-empty'
,
...
...
@@ -200,7 +237,9 @@ async function run() {
}]
});
assert
.
deepStrictEqual
(
noMedia
,
{
accepted
:
1
,
duplicates
:
0
,
noMedia
:
1
});
assert
.
deepStrictEqual
(
statusUpdates
,
[
'1001:2'
,
'1002:2'
,
'1003:3'
]);
const
emptyResult
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1003'
});
const
emptyResult
=
await
service
.
getFileResult
({
siteId
:
'doctest'
,
classId
:
'1003'
,
onlyHighlight
:
1
});
assert
.
strictEqual
(
emptyResult
.
fileExists
,
false
);
const
mappedNoMedia
=
await
service
.
acceptTasks
({
list
:
[{
...
...
请
注册
或
登录
后发表评论