Merge branch 'refs/heads/dev/前端页面修改'
50
README.md
@ -12,12 +12,13 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网
|
||||
|
||||
流媒体服务基于@夏楚 ZLMediaKit [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
|
||||
播放器使用@dexter jessibuca [https://github.com/langhuihui/jessibuca/tree/v3](https://github.com/langhuihui/jessibuca/tree/v3)
|
||||
前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改.
|
||||
播放器使用@Numberwolf-Yanlong h265web.js [https://github.com/numberwolf/h265web.js](https://github.com/numberwolf/h265web.js)
|
||||
前端页面基于vue-admin-template构建 [https://github.com/PanJiaChen/vue-admin-template?tab=readme-ov-file](https://github.com/PanJiaChen/vue-admin-template?tab=readme-ov-file)
|
||||
|
||||
# 应用场景:
|
||||
支持浏览器无插件播放摄像头视频。
|
||||
支持国标设备(摄像机、平台、NVR等)设备接入
|
||||
支持非国标(onvif, rtsp, rtmp,直播设备等等)设备接入,充分利旧。
|
||||
支持rtsp, rtmp,直播设备设备接入,充分利旧。
|
||||
支持国标级联。多平台级联。跨网视频预览。
|
||||
支持跨网网闸平台互联。
|
||||
|
||||
@ -29,19 +30,31 @@ ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZL
|
||||
# 付费社群
|
||||
[](https://t.zsxq.com/0d8VAD3Dm)
|
||||
> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接自行推出,星球会直接退款给大家。
|
||||
> 星球还提供了基于主线master分支的打包, 会随时更新。
|
||||
|
||||
# gitee同步仓库
|
||||
> 星球还提供了包括闭源的全功能试用包, 会随时更新。
|
||||
|
||||
# gitee仓库
|
||||
https://gitee.com/pan648540858/wvp-GB28181-pro.git
|
||||
|
||||
# 截图
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
<table>
|
||||
<tr>
|
||||
<td ><center><img src="doc/_media/1.png" >登录页面 </center></td>
|
||||
<td ><center><img src="doc/_media/2.png" >首页</center></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td ><center><img src="doc/_media/3.png" >分屏播放 </center></td>
|
||||
<td ><center><img src="doc/_media/4.png" >国标设备列表</center></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td ><center><img src="doc/_media/5.png" >行政区划管理 </center></td>
|
||||
<td ><center><img src="doc/_media/8.png" >业务分组管理</center></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td ><center><img src="doc/_media/6.png" >录制计划</center></td>
|
||||
<td ><center><img src="doc/_media/7.png" >平台信息</center></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# 功能特性
|
||||
- [X] 集成web界面
|
||||
@ -117,10 +130,10 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
|
||||
- [X] 支持国标信令集群
|
||||
|
||||
|
||||
# 非开源的内容
|
||||
- [X] ONVIF设备的接入,支持点播,云台控制,国标级联点播,自动点播。试用安装包以及使用教程: [知识星球](https://t.zsxq.com/10WAnH2MP),没有使用时间限制,需要源码可以星球私信我或者邮箱联系。
|
||||
# 闭源内容
|
||||
- [X] ONVIF设备的接入,支持点播,云台控制,国标级联点播,自动点播。
|
||||
- [X] 支持部标1078+808协议,支持点播,云台控制,录像回放,位置上报,自动点播。
|
||||
- [X] 支持国标28181-2022协议,支持巡航轨迹查询,PTZ精准控制,存储卡格式化,设备软件升级,OSD配置,h265+aac,支持辅码流,录像倒放等。具体的功能列表可在[知识星球](https://t.zsxq.com/18GXkpkqs)查看,试用安装包: [知识星球](https://t.zsxq.com/UJ6V3),没有使用时间限制,需要源码可以星球私信我或者邮箱联系。
|
||||
- [X] 支持国标28181-2022协议,支持巡航轨迹查询,PTZ精准控制,存储卡格式化,设备软件升级,OSD配置,h265+aac,支持辅码流,录像倒放等。
|
||||
|
||||
|
||||
# 授权协议
|
||||
@ -131,19 +144,14 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
|
||||
[知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表:,
|
||||
- [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp)
|
||||
|
||||
有偿技术支持,请发送邮件到648540858@qq.com
|
||||
有偿技术支持,一对一开发辅导,闭源内容合作请发送邮件到648540858@qq.com咨询
|
||||
|
||||
# 致谢
|
||||
感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。
|
||||
感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。
|
||||
感谢作者[Kyle](https://gitee.com/kkkkk5G) 开源了好用的前端页面
|
||||
感谢作者[dexter langhuihui](https://github.com/langhuihui)和[Numberwolf-Yanlong](https://github.com/numberwolf/h265web.js) 开源这么好用的WEB播放器。
|
||||
感谢各位大佬的赞助以及对项目的指正与帮助。包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后:
|
||||
[lawrencehj](https://github.com/lawrencehj) [Smallwhitepig](https://github.com/Smallwhitepig) [swwhaha](https://github.com/swwheihei)
|
||||
[hotcoffie](https://github.com/hotcoffie) [xiaomu](https://github.com/nikmu) [TristingChen](https://github.com/TristingChen)
|
||||
[chenparty](https://github.com/chenparty) [Hotleave](https://github.com/hotleave) [ydwxb](https://github.com/ydwxb)
|
||||
[ydpd](https://github.com/ydpd) [szy833](https://github.com/szy833) [ydwxb](https://github.com/ydwxb) [Albertzhu666](https://github.com/Albertzhu666)
|
||||
[mk1990](https://github.com/mk1990) [SaltFish001](https://github.com/SaltFish001)
|
||||
|
||||
同时感谢JetBrains对开源项目的支持,本项目使用IntelliJ IDEA开发与调试:
|
||||
|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 214 KiB |
@ -8,15 +8,18 @@
|
||||
|
||||
左侧树结构为行政区划结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备(
|
||||
可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除)
|
||||
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作, 添加状态选择"未添加"可以进行添加操作,选择"
|
||||
已添加"进行移除操作
|
||||

|
||||
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作。
|
||||
选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下,如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。
|
||||

|
||||
|
||||
## 业务分组
|
||||
|
||||
左侧树结构为业务分组结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备(
|
||||
可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除)
|
||||
业务分组下不能挂载设备,所以没有选择该节点的单选框.
|
||||
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作, 添加状态选择"未添加"可以进行添加操作,选择"
|
||||
已添加"进行移除操作
|
||||

|
||||
右侧为通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作。
|
||||
|
||||
选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下。
|
||||
如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。
|
||||
注意,根资源组下的那一级为业务分组类型不可以直接挂载设备,需要继续建立节点,后续的节点的都是虚拟组织类型, 就可以挂载通道了。
|
||||

|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
# 云端录像
|
||||
|
||||

|
||||

|
||||
云端录像是对录制在zlm服务下的录像文件的管理,录像的文件路径默认在ZLM/www/record下。
|
||||
|
||||
- 国标设备是否录像: 可以再WVP的配置中user-settings.record-sip设置为true那么每次点播以及录像回放都会录像;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
# 节点管理
|
||||
|
||||

|
||||

|
||||
|
||||
WVP支持单个WVP多个ZLM的方案来扩展WVP的视频并发能力,并发点播是因为带宽和性能的原因,单个ZLM节点能支持的路数有限,所以WVP增加了ZLM集群来扩展并发并且保证ZLM的高可用。
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 180 KiB |
@ -87,9 +87,9 @@ git clone https://github.com/648540858/wvp-GB28181-pro.git
|
||||
### 5.2 编译前端页面
|
||||
|
||||
```shell script
|
||||
cd wvp-GB28181-pro/web_src/
|
||||
cd wvp-GB28181-pro/web/
|
||||
npm --registry=https://registry.npmmirror.com install
|
||||
npm run build
|
||||
npm npm run build:prod
|
||||
```
|
||||
|
||||
编译如果报错, 一般都是网络问题, 导致的依赖包下载失败
|
||||
|
||||
@ -44,15 +44,12 @@ wvp支持多种数据库,包括Mysql,Postgresql,金仓等,配置任选
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
dynamic:
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
|
||||
username: root
|
||||
password: root123
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
|
||||
username: root
|
||||
password: root123
|
||||
```
|
||||
|
||||
#### 2.1.3 Postgresql数据库配置
|
||||
@ -61,14 +58,12 @@ spring:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
dynamic:
|
||||
primary: master
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 12345678
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 12345678
|
||||
|
||||
pagehelper:
|
||||
helper-dialect: postgresql
|
||||
@ -80,14 +75,13 @@ pagehelper:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
dynamic:
|
||||
primary: master
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.kingbase8.Driver
|
||||
url: jdbc:kingbase8://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=utf8
|
||||
username: root
|
||||
password: 12345678
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.kingbase8.Driver
|
||||
url: jdbc:kingbase8://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=utf8
|
||||
username: root
|
||||
password: 12345678
|
||||
|
||||
|
||||
pagehelper:
|
||||
helper-dialect: postgresql
|
||||
|
||||
@ -41,19 +41,7 @@ nohup ./MediaServer -d -m 3 &
|
||||
|
||||
### 前后端分离部署
|
||||
|
||||
前后端部署目前在最新的版本已经支持,请使用3月15日之后的版本部署
|
||||
前端编译后的文件在`src/main/resources/static`中,将此目录下的文件部署。
|
||||
WVP默认开启全部接口支持跨域。部署前端文件到WEB容器,并将访问的地址设置为WVP的地址即可。
|
||||
**配置前端服务器**
|
||||
|
||||
1. 在`src/main/resources/static/static/js/config.js`下配置服务器的地址,也就是wvp服务的地址
|
||||
|
||||
```javascript
|
||||
window.baseUrl = "http://xxx.com:18080"
|
||||
```
|
||||
|
||||
`这里的地址是需要客户电脑能访问到的,因为请求是客户端电脑发起,与代理不同`
|
||||
[接入设备](./_content/ability/device.md)
|
||||
前端基于 [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md) 构建, 参考这儿即可。
|
||||
|
||||
### 默认账号和密码
|
||||
|
||||
|
||||
BIN
doc/_media/1.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
doc/_media/2.png
|
Before Width: | Height: | Size: 938 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 920 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
BIN
doc/_media/3.png
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 1.0 MiB |
BIN
doc/_media/4.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
doc/_media/5.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
doc/_media/6.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
doc/_media/7.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
doc/_media/8.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 96 KiB |
@ -10,7 +10,7 @@
|
||||
* [推流列表](_content/ability/push.md)
|
||||
* [拉流代理](_content/ability/proxy.md)
|
||||
* [云端录像](_content/ability/cloud_record.md)
|
||||
* [节点管理](_content/ability/node_manger.md)
|
||||
* [节点管理](_content/ability/node_manager.md)
|
||||
* [通道管理](_content/ability/channel.md)
|
||||
* [国标级联](_content/ability/cascade2.md)
|
||||
* **流程与原理**
|
||||
|
||||
57
install.sh
Normal file
@ -0,0 +1,57 @@
|
||||
#! /bin/sh
|
||||
|
||||
WORD_DIR=$(cd $(dirname $0); pwd)
|
||||
SERVICE_NAME="wvp"
|
||||
|
||||
# 检查是否为 root 用户
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "提示: 建议使用 root 用户执行此脚本,否则可能权限不足!"
|
||||
read -p "继续?(y/n) " -n 1 -r
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
|
||||
# 当前目录直接搜索(不含子目录)
|
||||
jar_files=(*.jar)
|
||||
|
||||
if [ ${#jar_files[@]} -eq 0 ]; then
|
||||
echo "当前目录无 JAR 文件!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 遍历结果
|
||||
for jar in "${jar_files[@]}"; do
|
||||
echo "找到 JAR 文件: $jar"
|
||||
done
|
||||
|
||||
# 写文件
|
||||
# 生成 Systemd 服务文件内容
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null
|
||||
[Unit]
|
||||
Description=${SERVICE_NAME}
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
User=$USER
|
||||
WorkingDirectory=${WORD_DIR}
|
||||
ExecStart=java -jar ${jar_files}
|
||||
SuccessExitStatus=143
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
Environment=SPRING_PROFILES_ACTIVE=prod
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 重载 Systemd 并启动服务
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
sudo systemctl start "$SERVICE_NAME"
|
||||
|
||||
# 验证服务状态
|
||||
echo "服务已安装!执行以下命令查看状态:"
|
||||
echo "sudo systemctl status $SERVICE_NAME"
|
||||
3
pom.xml
@ -372,6 +372,7 @@
|
||||
<version>2.7.2</version>
|
||||
<configuration>
|
||||
<includeSystemScope>true</includeSystemScope>
|
||||
<executable>true</executable>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@ -422,6 +423,7 @@
|
||||
<exclude>**/application.yml</exclude>
|
||||
<exclude>**/application-*.yml</exclude>
|
||||
<exclude>**/local.jks</exclude>
|
||||
<exclude>**/install.sh</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
@ -441,6 +443,7 @@
|
||||
<includes>
|
||||
<include>application.yml</include>
|
||||
<include>application-*.yml</include>
|
||||
<include>install.sh</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package com.genersoft.iot.vmp.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 统计信息
|
||||
*/
|
||||
@Data
|
||||
public class StatisticsInfo {
|
||||
|
||||
private long id;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private String deviceId;
|
||||
|
||||
/**
|
||||
* 分支
|
||||
*/
|
||||
private String branch;
|
||||
|
||||
/**
|
||||
* git提交版本ID
|
||||
*/
|
||||
private String gitCommitId;
|
||||
|
||||
/**
|
||||
* git地址
|
||||
*/
|
||||
private String gitUrl;
|
||||
|
||||
/**
|
||||
* 构建版本
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 操作系统名称
|
||||
*/
|
||||
private String osName;
|
||||
|
||||
/**
|
||||
* 是否是docker环境
|
||||
*/
|
||||
private Boolean docker;
|
||||
|
||||
/**
|
||||
* 架构
|
||||
*/
|
||||
private String arch;
|
||||
|
||||
/**
|
||||
* jdk版本
|
||||
*/
|
||||
private String jdkVersion;
|
||||
|
||||
/**
|
||||
* redis版本
|
||||
*/
|
||||
private String redisVersion;
|
||||
|
||||
/**
|
||||
* sql数据库版本
|
||||
*/
|
||||
private String sqlVersion;
|
||||
|
||||
/**
|
||||
* sql数据库类型, mysql/postgresql/金仓等
|
||||
*/
|
||||
private String sqlType;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private String time;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatisticsInfo{" +
|
||||
"id=" + id +
|
||||
", deviceId='" + deviceId + '\'' +
|
||||
", branch='" + branch + '\'' +
|
||||
", gitCommitId='" + gitCommitId + '\'' +
|
||||
", gitUrl='" + gitUrl + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", osName='" + osName + '\'' +
|
||||
", docker=" + docker +
|
||||
", arch='" + arch + '\'' +
|
||||
", jdkVersion='" + jdkVersion + '\'' +
|
||||
", redisVersion='" + redisVersion + '\'' +
|
||||
", sqlVersion='" + sqlVersion + '\'' +
|
||||
", sqlType='" + sqlType + '\'' +
|
||||
", time='" + time + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@ import java.nio.file.Files;
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Order(value=14)
|
||||
@Order(value=15)
|
||||
public class CivilCodeFileConf implements CommandLineRunner {
|
||||
|
||||
@Autowired
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.genersoft.iot.vmp.conf;
|
||||
|
||||
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
@ -59,11 +60,14 @@ public class CloudRecordTimer {
|
||||
// TODO 后续可以删除空了的过期日期文件夹
|
||||
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
|
||||
String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
|
||||
boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
|
||||
cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
|
||||
if (deleteResult) {
|
||||
log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
|
||||
}
|
||||
try {
|
||||
boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
|
||||
cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
|
||||
if (deleteResult) {
|
||||
log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
|
||||
}
|
||||
}catch (ControllerException ignored) {}
|
||||
|
||||
}
|
||||
result += cloudRecordServiceMapper.deleteList(cloudRecordItemList);
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ public class SpringDocConfig {
|
||||
Contact contact = new Contact();
|
||||
contact.setName("pan");
|
||||
contact.setEmail("648540858@qq.com");
|
||||
|
||||
return new OpenAPI()
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme()
|
||||
@ -37,7 +38,11 @@ public class SpringDocConfig {
|
||||
.bearerFormat("JWT")))
|
||||
.info(new Info().title("WVP-PRO 接口文档")
|
||||
.contact(contact)
|
||||
.description("开箱即用的28181协议视频平台")
|
||||
.description("开箱即用的28181协议视频平台。 <br/>" +
|
||||
"1. 打开http://127.0.0.1:18080/doc.html#/1.%20全部/用户管理/login_1," +
|
||||
" 登录成功后返回AccessToken。 <br/>" +
|
||||
"2. 填写到AccessToken到参数值 http://127.0.0.1:18080/doc.html#/Authorize/1.%20全部 <br/>" +
|
||||
"后续接口就可以直接测试了")
|
||||
.version("v3.1.0")
|
||||
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
|
||||
}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
package com.genersoft.iot.vmp.conf;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.genersoft.iot.vmp.common.StatisticsInfo;
|
||||
import com.genersoft.iot.vmp.utils.DateUtil;
|
||||
import com.genersoft.iot.vmp.utils.GitUtil;
|
||||
import com.genersoft.iot.vmp.utils.SystemInfoUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
@Order(value=100)
|
||||
@Slf4j
|
||||
public class StatisticsInfoTask implements CommandLineRunner {
|
||||
|
||||
@Autowired
|
||||
private GitUtil gitUtil;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<Object, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
try {
|
||||
StatisticsInfo statisticsInfo = new StatisticsInfo();
|
||||
statisticsInfo.setDeviceId(SystemInfoUtils.getHardwareId());
|
||||
statisticsInfo.setBranch(gitUtil.getBranch());
|
||||
statisticsInfo.setGitCommitId(gitUtil.getGitCommitId());
|
||||
statisticsInfo.setGitUrl(gitUtil.getGitUrl());
|
||||
statisticsInfo.setVersion(gitUtil.getBuildVersion());
|
||||
|
||||
statisticsInfo.setOsName(System.getProperty("os.name"));
|
||||
statisticsInfo.setArch(System.getProperty("os.arch"));
|
||||
statisticsInfo.setJdkVersion(System.getProperty("java.version"));
|
||||
|
||||
statisticsInfo.setDocker(new File("/.dockerenv").exists());
|
||||
try {
|
||||
statisticsInfo.setRedisVersion(getRedisVersion());
|
||||
}catch (Exception ignored) {}
|
||||
try {
|
||||
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
|
||||
statisticsInfo.setSqlVersion(metaData.getDatabaseProductVersion());
|
||||
statisticsInfo.setSqlType(metaData.getDriverName());
|
||||
}catch (Exception ignored) {}
|
||||
statisticsInfo.setTime(DateUtil.getNow());
|
||||
sendPost(statisticsInfo);
|
||||
|
||||
|
||||
}catch (Exception e) {
|
||||
log.error("[获取信息失败] ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getRedisVersion() {
|
||||
if (redisTemplate.getConnectionFactory() == null) {
|
||||
return null;
|
||||
}
|
||||
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
|
||||
if (connection.info() == null) {
|
||||
return null;
|
||||
}
|
||||
return connection.info().getProperty("redis_version");
|
||||
}
|
||||
|
||||
public void sendPost(StatisticsInfo statisticsInfo) {
|
||||
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
|
||||
OkHttpClient client = httpClientBuilder.build();
|
||||
|
||||
RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JSON.toJSONString(statisticsInfo));
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.post(requestBodyJson)
|
||||
.url("http://api.wvp-pro.cn:136/api/statistics/ping")
|
||||
// .url("http://127.0.0.1:11236/api/statistics/ping")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build();
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
response.close();
|
||||
Objects.requireNonNull(response.body()).close();
|
||||
|
||||
}catch (Exception ignored){}
|
||||
}
|
||||
}
|
||||
@ -178,7 +178,7 @@ public class UserSetting {
|
||||
/**
|
||||
* 登录超时时间(分钟),
|
||||
*/
|
||||
private long loginTimeout = 30;
|
||||
private long loginTimeout = 60;
|
||||
|
||||
/**
|
||||
* jwk文件路径,若不指定则使用resources目录下的jwk.json
|
||||
|
||||
@ -97,7 +97,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
// return;
|
||||
default:
|
||||
}
|
||||
|
||||
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
|
||||
User user = new User();
|
||||
user.setId(jwtUser.getUserId());
|
||||
|
||||
@ -11,7 +11,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
@ -24,7 +23,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -58,33 +56,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
|
||||
/**
|
||||
* 描述: 静态资源放行,这里的放行,是不走 Spring Security 过滤器链
|
||||
**/
|
||||
@Override
|
||||
public void configure(WebSecurity web) {
|
||||
if (userSetting.getInterfaceAuthentication()) {
|
||||
ArrayList<String> matchers = new ArrayList<>();
|
||||
matchers.add("/");
|
||||
matchers.add("/#/**");
|
||||
matchers.add("/static/**");
|
||||
matchers.add("/swagger-ui.html");
|
||||
matchers.add("/swagger-ui/");
|
||||
matchers.add("/index.html");
|
||||
matchers.add("/doc.html");
|
||||
matchers.add("/webjars/**");
|
||||
matchers.add("/swagger-resources/**");
|
||||
matchers.add("/v3/api-docs/**");
|
||||
matchers.add("/js/**");
|
||||
matchers.add("/api/device/query/snap/**");
|
||||
matchers.add("/record_proxy/*/**");
|
||||
matchers.add("/api/emit");
|
||||
matchers.add("/favicon.ico");
|
||||
// 可以直接访问的静态数据
|
||||
web.ignoring().antMatchers(matchers.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置认证方式
|
||||
*
|
||||
@ -105,15 +76,36 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
List<String> defaultExcludes = new ArrayList<>();
|
||||
defaultExcludes.add("/");
|
||||
defaultExcludes.add("/#/**");
|
||||
defaultExcludes.add("/static/**");
|
||||
|
||||
List<String> defaultExcludes = userSetting.getInterfaceAuthenticationExcludes();
|
||||
defaultExcludes.add("/swagger-ui.html");
|
||||
defaultExcludes.add("/swagger-ui/**");
|
||||
defaultExcludes.add("/swagger-resources/**");
|
||||
defaultExcludes.add("/doc.html");
|
||||
defaultExcludes.add("/doc.html#/**");
|
||||
defaultExcludes.add("/v3/api-docs/**");
|
||||
|
||||
defaultExcludes.add("/index.html");
|
||||
defaultExcludes.add("/webjars/**");
|
||||
|
||||
defaultExcludes.add("/js/**");
|
||||
defaultExcludes.add("/api/device/query/snap/**");
|
||||
defaultExcludes.add("/record_proxy/*/**");
|
||||
defaultExcludes.add("/api/emit");
|
||||
defaultExcludes.add("/favicon.ico");
|
||||
defaultExcludes.add("/api/user/login");
|
||||
defaultExcludes.add("/index/hook/**");
|
||||
defaultExcludes.add("/api/device/query/snap/**");
|
||||
defaultExcludes.add("/index/hook/abl/**");
|
||||
defaultExcludes.add("/swagger-ui/**");
|
||||
defaultExcludes.add("/doc.html#/**");
|
||||
// defaultExcludes.add("/channel/log");
|
||||
|
||||
|
||||
|
||||
if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) {
|
||||
defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes());
|
||||
}
|
||||
|
||||
http.headers().contentTypeOptions().disable()
|
||||
.and().cors().configurationSource(configurationSource())
|
||||
|
||||
@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.conf.security.dto;
|
||||
|
||||
import com.genersoft.iot.vmp.storager.dao.dto.Role;
|
||||
import com.genersoft.iot.vmp.storager.dao.dto.User;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.security.core.CredentialsContainer;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
@ -19,8 +21,13 @@ public class LoginUser implements UserDetails, CredentialsContainer {
|
||||
*/
|
||||
private User user;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private String accessToken;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private String serverId;
|
||||
|
||||
/**
|
||||
* 登录时间
|
||||
@ -104,11 +111,4 @@ public class LoginUser implements UserDetails, CredentialsContainer {
|
||||
return user.getPushKey();
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ public class DeviceQuery {
|
||||
@Parameter(name = "deviceId", description = "设备国标编号", required = true)
|
||||
@GetMapping("/devices/{deviceId}")
|
||||
public Device devices(@PathVariable String deviceId){
|
||||
|
||||
|
||||
return deviceService.getDeviceByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
@ -224,16 +224,19 @@ public class DeviceQuery {
|
||||
"UDP(udp传输),TCP-ACTIVE(tcp主动模式),TCP-PASSIVE(tcp被动模式)", required = true)
|
||||
@PostMapping("/transport/{deviceId}/{streamMode}")
|
||||
public void updateTransport(@PathVariable String deviceId, @PathVariable String streamMode){
|
||||
Assert.isTrue(streamMode.equalsIgnoreCase("UDP")
|
||||
|| streamMode.equalsIgnoreCase("TCP-ACTIVE")
|
||||
|| streamMode.equalsIgnoreCase("TCP-PASSIVE"), "数据流传输模式, 取值:UDP/TCP-ACTIVE/TCP-PASSIVE");
|
||||
Device device = deviceService.getDeviceByDeviceId(deviceId);
|
||||
device.setStreamMode(streamMode);
|
||||
device.setStreamMode(streamMode.toUpperCase());
|
||||
deviceService.updateCustomDevice(device);
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "添加设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "device", description = "设备", required = true)
|
||||
@PostMapping("/device/add/")
|
||||
public void addDevice(Device device){
|
||||
@PostMapping("/device/add")
|
||||
public void addDevice(@RequestBody Device device){
|
||||
|
||||
if (device == null || device.getDeviceId() == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400);
|
||||
@ -250,8 +253,8 @@ public class DeviceQuery {
|
||||
|
||||
@Operation(summary = "更新设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "device", description = "设备", required = true)
|
||||
@PostMapping("/device/update/")
|
||||
public void updateDevice(Device device){
|
||||
@PostMapping("/device/update")
|
||||
public void updateDevice(@RequestBody Device device){
|
||||
if (device == null || device.getDeviceId() == null || device.getId() <= 0) {
|
||||
throw new ControllerException(ErrorCode.ERROR400);
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -89,7 +88,7 @@ public class MediaController {
|
||||
}
|
||||
|
||||
if (streamInfo != null){
|
||||
return new StreamContent(streamInfo);
|
||||
return new StreamContent(streamInfo);
|
||||
}else {
|
||||
//获取流失败,重启拉流后重试一次
|
||||
streamProxyService.stopByAppAndStream(app,stream);
|
||||
|
||||
@ -215,7 +215,7 @@ public class PlaybackController {
|
||||
|
||||
@Operation(summary = "回放倍速播放", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@Parameter(name = "streamId", description = "回放流ID", required = true)
|
||||
@Parameter(name = "speed", description = "倍速0.25 0.5 1、2、4", required = true)
|
||||
@Parameter(name = "speed", description = "倍速0.25 0.5 1、2、4、8", required = true)
|
||||
@GetMapping("/speed/{streamId}/{speed}")
|
||||
public void playSpeed(@PathVariable String streamId, @PathVariable Double speed) {
|
||||
log.info("playSpeed: "+streamId+", "+speed);
|
||||
@ -225,10 +225,6 @@ public class PlaybackController {
|
||||
log.warn("streamId不存在!");
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在");
|
||||
}
|
||||
if(speed != 0.25 && speed != 0.5 && speed != 1 && speed != 2.0 && speed != 4.0) {
|
||||
log.warn("不支持的speed: " + speed);
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持的speed(0.25 0.5 1、2、4)");
|
||||
}
|
||||
Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId());
|
||||
DeviceChannel channel = channelService.getOneById(inviteInfo.getChannelId());
|
||||
try {
|
||||
|
||||
@ -23,7 +23,6 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
|
||||
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
|
||||
import com.genersoft.iot.vmp.utils.DateUtil;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
@ -41,9 +40,6 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import javax.sip.InvalidArgumentException;
|
||||
import javax.sip.SipException;
|
||||
import java.text.ParseException;
|
||||
import javax.sip.InvalidArgumentException;
|
||||
import javax.sip.SipException;
|
||||
import javax.sip.message.Response;
|
||||
@ -442,7 +438,7 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
|
||||
deviceMobilePositionMapper.insertNewPosition(mobilePosition);
|
||||
}
|
||||
|
||||
if (deviceChannel.getDeviceId().equals(deviceChannel.getDeviceId())) {
|
||||
if (deviceChannel.getDeviceId().equals(device.getDeviceId())) {
|
||||
deviceChannel.setDeviceId(null);
|
||||
}
|
||||
if (deviceChannel.getGpsTime() == null) {
|
||||
|
||||
@ -28,6 +28,7 @@ import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent;
|
||||
import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent;
|
||||
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||
import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
|
||||
import com.genersoft.iot.vmp.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.service.IReceiveRtpServerService;
|
||||
import com.genersoft.iot.vmp.service.ISendRtpServerService;
|
||||
import com.genersoft.iot.vmp.service.bean.*;
|
||||
@ -1047,8 +1048,8 @@ public class PlayServiceImpl implements IPlayService {
|
||||
null);
|
||||
return;
|
||||
}
|
||||
log.info("[录像下载] deviceId: {}, channelId: {}, 下载速度:{}, 收流端口:{}, 收流模式:{}, SSRC: {}({}), SSRC校验:{}",
|
||||
device.getDeviceId(), channel.getDeviceId(), downloadSpeed, ssrcInfo.getPort(), device.getStreamMode(),
|
||||
log.info("[录像下载] deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}, 下载速度:{}, 收流端口:{}, 收流模式:{}, SSRC: {}({}), SSRC校验:{}",
|
||||
device.getDeviceId(), channel.getDeviceId(), startTime, endTime, downloadSpeed, ssrcInfo.getPort(), device.getStreamMode(),
|
||||
ssrcInfo.getSsrc(), String.format("%08x", Long.parseLong(ssrcInfo.getSsrc())).toUpperCase(),
|
||||
device.isSsrcCheck());
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ public class MediaInfo {
|
||||
private Integer audioChannels;
|
||||
@Schema(description = "音频采样率")
|
||||
private Integer audioSampleRate;
|
||||
@Schema(description = "音频采样率")
|
||||
@Schema(description = "时长")
|
||||
private Long duration;
|
||||
@Schema(description = "在线")
|
||||
private Boolean online;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.genersoft.iot.vmp.media.bean;
|
||||
|
||||
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RecordInfo {
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
@ -24,70 +26,6 @@ public class RecordInfo {
|
||||
return recordInfo;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getFolder() {
|
||||
return folder;
|
||||
}
|
||||
|
||||
public void setFolder(String folder) {
|
||||
this.folder = folder;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(long startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public double getTimeLen() {
|
||||
return timeLen;
|
||||
}
|
||||
|
||||
public void setTimeLen(double timeLen) {
|
||||
this.timeLen = timeLen;
|
||||
}
|
||||
|
||||
public String getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
public void setParams(String params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RecordInfo{" +
|
||||
|
||||
@ -70,4 +70,9 @@ public interface IMediaNodeServerService {
|
||||
|
||||
List<String> listRtpServer(MediaServer mediaServer);
|
||||
|
||||
void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
|
||||
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
|
||||
|
||||
void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema);
|
||||
}
|
||||
|
||||
@ -159,4 +159,10 @@ public interface IMediaServerService {
|
||||
int createRTPServer(MediaServer mediaServerItem, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode);
|
||||
|
||||
List<String> listRtpServer(MediaServer mediaServer);
|
||||
|
||||
StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
|
||||
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
|
||||
|
||||
void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema);
|
||||
}
|
||||
|
||||
@ -311,6 +311,10 @@ public class MediaServerServiceImpl implements IMediaServerService {
|
||||
if (mediaServerInRedis == null || !ssrcFactory.hasMediaServerSSRC(mediaServerInDataBase.getId())) {
|
||||
ssrcFactory.initMediaServerSSRC(mediaServerInDataBase.getId(),null);
|
||||
}
|
||||
if (mediaSerItem.getSecret() != null && !mediaServerInDataBase.getSecret().equals(mediaSerItem.getSecret())) {
|
||||
mediaServerInDataBase.setSecret(mediaSerItem.getSecret());
|
||||
}
|
||||
mediaServerInDataBase.setSecret(mediaSerItem.getSecret());
|
||||
String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId();
|
||||
redisTemplate.opsForHash().put(key, mediaServerInDataBase.getId(), mediaServerInDataBase);
|
||||
if (mediaServerInDataBase.isStatus()) {
|
||||
@ -966,4 +970,35 @@ public class MediaServerServiceImpl implements IMediaServerService {
|
||||
}
|
||||
mediaNodeServerService.stopProxy(mediaServer, streamKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
|
||||
if (mediaNodeServerService == null) {
|
||||
log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类");
|
||||
}
|
||||
mediaNodeServerService.loadMP4File(mediaServer, app, stream, datePath);
|
||||
return getStreamInfoByAppAndStream(mediaServer, app, stream, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) {
|
||||
IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
|
||||
if (mediaNodeServerService == null) {
|
||||
log.info("[seekRecordStamp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类");
|
||||
}
|
||||
mediaNodeServerService.seekRecordStamp(mediaServer, app, stream, stamp, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) {
|
||||
IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
|
||||
if (mediaNodeServerService == null) {
|
||||
log.info("[setRecordSpeed] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类");
|
||||
}
|
||||
mediaNodeServerService.setRecordSpeed(mediaServer, app, stream, speed, schema);
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +126,6 @@ public class ZLMHttpHookListener {
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
|
||||
public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
|
||||
|
||||
MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId());
|
||||
if (mediaServer == null) {
|
||||
return HookResult.SUCCESS();
|
||||
@ -190,6 +189,8 @@ public class ZLMHttpHookListener {
|
||||
|
||||
JSONObject ret = new JSONObject();
|
||||
boolean close = mediaService.closeStreamOnNoneReader(param.getMediaServerId(), param.getApp(), param.getStream(), param.getSchema());
|
||||
log.info("[ZLM HOOK]流无人观看是否触发关闭:{}, {}->{}->{}/{}", close, param.getMediaServerId(), param.getSchema(),
|
||||
param.getApp(), param.getStream());
|
||||
ret.put("code", 0);
|
||||
ret.put("close", close);
|
||||
return ret;
|
||||
|
||||
@ -169,7 +169,7 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
|
||||
return true;
|
||||
}else {
|
||||
log.info("[zlm-deleteRecordDirectory] 删除磁盘文件错误, server: {} {}:{}->{}/{}, 结果: {}", mediaServer.getId(), app, stream, date, fileName, jsonObject);
|
||||
return false;
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "删除磁盘文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
@ -549,4 +549,37 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
JSONObject jsonObject = zlmresTfulUtils.loadMP4File(mediaServer, app, stream, datePath);
|
||||
if (jsonObject == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
|
||||
}
|
||||
if (jsonObject.getInteger("code") != 0) {
|
||||
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) {
|
||||
JSONObject jsonObject = zlmresTfulUtils.seekRecordStamp(mediaServer, app, stream, stamp, schema);
|
||||
if (jsonObject == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
|
||||
}
|
||||
if (jsonObject.getInteger("code") != 0) {
|
||||
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) {
|
||||
JSONObject jsonObject = zlmresTfulUtils.setRecordSpeed(mediaServer, app, stream, speed, schema);
|
||||
if (jsonObject == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
|
||||
}
|
||||
if (jsonObject.getInteger("code") != 0) {
|
||||
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ public class ZLMRESTfulUtils {
|
||||
|
||||
private OkHttpClient client;
|
||||
|
||||
|
||||
public interface RequestCallback{
|
||||
void run(JSONObject response);
|
||||
}
|
||||
@ -415,4 +416,34 @@ public class ZLMRESTfulUtils {
|
||||
param.put("name", fileName);
|
||||
return sendPost(mediaServerItem, "deleteRecordDirectory",param, null);
|
||||
}
|
||||
|
||||
public JSONObject loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
Map<String, Object> param = new HashMap<>(1);
|
||||
param.put("vhost", "__defaultVhost__");
|
||||
param.put("app", app);
|
||||
param.put("stream", stream);
|
||||
param.put("file_path", datePath);
|
||||
param.put("file_repeat", "0");
|
||||
return sendPost(mediaServer, "loadMP4File",param, null);
|
||||
}
|
||||
|
||||
public JSONObject setRecordSpeed(MediaServer mediaServer, String app, String stream, int speed, String schema) {
|
||||
Map<String, Object> param = new HashMap<>(1);
|
||||
param.put("vhost", "__defaultVhost__");
|
||||
param.put("app", app);
|
||||
param.put("stream", stream);
|
||||
param.put("speed", speed);
|
||||
param.put("schema", schema);
|
||||
return sendPost(mediaServer, "setRecordSpeed",param, null);
|
||||
}
|
||||
|
||||
public JSONObject seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) {
|
||||
Map<String, Object> param = new HashMap<>(1);
|
||||
param.put("vhost", "__defaultVhost__");
|
||||
param.put("app", app);
|
||||
param.put("stream", stream);
|
||||
param.put("stamp", stamp);
|
||||
param.put("schema", schema);
|
||||
return sendPost(mediaServer, "seekRecordStamp",param, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
package com.genersoft.iot.vmp.gb28181.service;
|
||||
package com.genersoft.iot.vmp.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.genersoft.iot.vmp.common.StreamInfo;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 云端录像管理
|
||||
@ -17,7 +20,7 @@ public interface ICloudRecordService {
|
||||
/**
|
||||
* 分页回去云端录像列表
|
||||
*/
|
||||
PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId);
|
||||
PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId, Boolean ascOrder);
|
||||
|
||||
/**
|
||||
* 获取所有的日期
|
||||
@ -52,4 +55,15 @@ public interface ICloudRecordService {
|
||||
DownloadFileInfo getPlayUrlPath(Integer recordId);
|
||||
|
||||
List<CloudRecordItem> getAllList(String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId, List<Integer> ids);
|
||||
|
||||
/**
|
||||
* 加载录像文件,形成录像流
|
||||
*/
|
||||
void loadRecord(String app, String stream, String date, ErrorCallback<StreamInfo> callback);
|
||||
|
||||
void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema);
|
||||
|
||||
void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema);
|
||||
|
||||
void deleteFileByIds(Set<Integer> ids);
|
||||
}
|
||||
@ -15,52 +15,52 @@ public class CloudRecordItem {
|
||||
* 主键
|
||||
*/
|
||||
private int id;
|
||||
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
private String app;
|
||||
|
||||
|
||||
/**
|
||||
* 流
|
||||
*/
|
||||
private String stream;
|
||||
|
||||
|
||||
/**
|
||||
* 健全ID
|
||||
*/
|
||||
private String callId;
|
||||
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private long startTime;
|
||||
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
private long endTime;
|
||||
|
||||
|
||||
/**
|
||||
* ZLM Id
|
||||
*/
|
||||
private String mediaServerId;
|
||||
|
||||
|
||||
/**
|
||||
* 文件名称
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
private String filePath;
|
||||
|
||||
|
||||
/**
|
||||
* 文件夹
|
||||
*/
|
||||
private String folder;
|
||||
|
||||
|
||||
/**
|
||||
* 收藏,收藏的文件不移除
|
||||
*/
|
||||
@ -70,16 +70,16 @@ public class CloudRecordItem {
|
||||
* 保留,收藏的文件不移除
|
||||
*/
|
||||
private Boolean reserve;
|
||||
|
||||
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private long fileSize;
|
||||
|
||||
|
||||
/**
|
||||
* 文件时长
|
||||
*/
|
||||
private long timeLen;
|
||||
private double timeLen;
|
||||
|
||||
/**
|
||||
* 所属服务ID
|
||||
@ -96,7 +96,7 @@ public class CloudRecordItem {
|
||||
cloudRecordItem.setFileSize(param.getRecordInfo().getFileSize());
|
||||
cloudRecordItem.setFilePath(param.getRecordInfo().getFilePath());
|
||||
cloudRecordItem.setMediaServerId(param.getMediaServer().getId());
|
||||
cloudRecordItem.setTimeLen((long) param.getRecordInfo().getTimeLen() * 1000);
|
||||
cloudRecordItem.setTimeLen(param.getRecordInfo().getTimeLen() * 1000);
|
||||
cloudRecordItem.setEndTime((param.getRecordInfo().getStartTime() + (long)param.getRecordInfo().getTimeLen()) * 1000);
|
||||
Map<String, String> paramsMap = MediaServerUtils.urlParamToMap(param.getRecordInfo().getParams());
|
||||
if (paramsMap.get("callId") != null) {
|
||||
|
||||
@ -2,16 +2,22 @@ package com.genersoft.iot.vmp.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.genersoft.iot.vmp.common.StreamInfo;
|
||||
import com.genersoft.iot.vmp.conf.UserSetting;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaInfo;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.media.event.hook.Hook;
|
||||
import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
|
||||
import com.genersoft.iot.vmp.media.event.hook.HookType;
|
||||
import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event;
|
||||
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||
import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
|
||||
import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
|
||||
import com.genersoft.iot.vmp.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
|
||||
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
|
||||
import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
|
||||
@ -28,12 +34,10 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -57,9 +61,12 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
@Autowired
|
||||
private IRedisRpcPlayService redisRpcPlayService;
|
||||
|
||||
@Autowired
|
||||
private HookSubscribe subscribe;
|
||||
|
||||
@Override
|
||||
public PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime,
|
||||
String endTime, List<MediaServer> mediaServerItems, String callId) {
|
||||
String endTime, List<MediaServer> mediaServerItems, String callId, Boolean ascOrder) {
|
||||
// 开始时间和结束时间在数据库中都是以秒为单位的
|
||||
Long startTimeStamp = null;
|
||||
Long endTimeStamp = null;
|
||||
@ -84,7 +91,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
.replaceAll("_", "/_");
|
||||
}
|
||||
List<CloudRecordItem> all = cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp,
|
||||
callId, mediaServerItems, null);
|
||||
callId, mediaServerItems, null, ascOrder);
|
||||
return new PageInfo<>(all);
|
||||
}
|
||||
|
||||
@ -100,7 +107,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
long startTimeStamp = startDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
|
||||
long endTimeStamp = endDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
|
||||
List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp,
|
||||
endTimeStamp, null, mediaServerItems, null);
|
||||
endTimeStamp, null, mediaServerItems, null, null);
|
||||
if (cloudRecordItemList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
@ -213,7 +220,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
}
|
||||
|
||||
List<CloudRecordItem> all = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, endTimeStamp,
|
||||
callId, mediaServerItems, null);
|
||||
callId, mediaServerItems, null, null);
|
||||
if (all.isEmpty()) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到待收藏的视频");
|
||||
}
|
||||
@ -273,6 +280,96 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
|
||||
}
|
||||
return cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp,
|
||||
callId, mediaServerItems, ids);
|
||||
callId, mediaServerItems, ids, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadRecord(String app, String stream, String date, ErrorCallback<StreamInfo> callback) {
|
||||
long startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(date + " 00:00:00");
|
||||
long endTimestamp = startTimestamp + 24 * 60 * 60 * 1000;
|
||||
|
||||
List<CloudRecordItem> recordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimestamp, endTimestamp, null, null, null, false);
|
||||
if (recordItemList.isEmpty()) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "此时间无录像");
|
||||
}
|
||||
String mediaServerId = recordItemList.get(0).getMediaServerId();
|
||||
MediaServer mediaServer = mediaServerService.getOne(mediaServerId);
|
||||
if (mediaServer == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId);
|
||||
}
|
||||
String buildApp = "mp4_record";
|
||||
String buildStream = app + "_" + stream + "_" + date;
|
||||
MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, buildApp, buildStream);
|
||||
if (mediaInfo != null) {
|
||||
if (callback != null) {
|
||||
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, mediaInfo, null);
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServerId);
|
||||
subscribe.addSubscribe(hook, (hookData) -> {
|
||||
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null);
|
||||
if (callback != null) {
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
|
||||
}
|
||||
});
|
||||
String dateDir = recordItemList.get(0).getFilePath().substring(0, recordItemList.get(0).getFilePath().lastIndexOf("/"));
|
||||
mediaServerService.loadMP4File(mediaServer, buildApp, buildStream, dateDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema) {
|
||||
MediaServer mediaServer = mediaServerService.getOne(mediaServerId);
|
||||
if (mediaServer == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId);
|
||||
}
|
||||
mediaServerService.seekRecordStamp(mediaServer, app, stream, seek, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema) {
|
||||
MediaServer mediaServer = mediaServerService.getOne(mediaServerId);
|
||||
if (mediaServer == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId);
|
||||
}
|
||||
mediaServerService.setRecordSpeed(mediaServer, app, stream, speed, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFileByIds(Set<Integer> ids) {
|
||||
log.info("[删除录像文件] ids: {}", ids.toArray());
|
||||
List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.queryRecordByIds(ids);
|
||||
if (cloudRecordItemList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<CloudRecordItem> cloudRecordItemIdListForDelete = new ArrayList<>();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
|
||||
String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
|
||||
MediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId());
|
||||
try {
|
||||
boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServer, cloudRecordItem.getApp(),
|
||||
cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
|
||||
if (deleteResult) {
|
||||
log.warn("[录像文件] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
|
||||
cloudRecordItemIdListForDelete.add(cloudRecordItem);
|
||||
}
|
||||
}catch (ControllerException e) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
stringBuilder.append(", ");
|
||||
}
|
||||
stringBuilder.append(cloudRecordItem.getFileName());
|
||||
}
|
||||
|
||||
}
|
||||
if (!cloudRecordItemIdListForDelete.isEmpty()) {
|
||||
cloudRecordServiceMapper.deleteList(cloudRecordItemIdListForDelete);
|
||||
}
|
||||
if (stringBuilder.length() > 0) {
|
||||
stringBuilder.append(" 删除失败");
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), stringBuilder.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +79,8 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
|
||||
|
||||
Map<Integer, StreamInfo> recordStreamMap = new HashMap<>();
|
||||
|
||||
@Scheduled(fixedRate = 10, timeUnit = TimeUnit.MINUTES)
|
||||
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
||||
public void execution() {
|
||||
log.info("[录制计划] 执行");
|
||||
// 查询现在需要录像的通道Id
|
||||
List<Integer> startChannelIdList = queryCurrentChannelRecord();
|
||||
|
||||
@ -133,7 +132,7 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
|
||||
// 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int week = now.getDayOfWeek().getValue();
|
||||
int index = now.getHour() * 2 + (now.getMinute() > 30?1:0);
|
||||
int index = now.getHour() * 60 + now.getMinute();
|
||||
|
||||
// 查询现在需要录像的通道Id
|
||||
return recordPlanMapper.queryRecordIng(week, index);
|
||||
@ -221,7 +220,7 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
|
||||
}
|
||||
}
|
||||
// TODO 更新录像队列
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -6,7 +6,7 @@ import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
|
||||
import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
|
||||
import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
|
||||
import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
|
||||
import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
|
||||
import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
|
||||
|
||||
@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Mapper
|
||||
public interface CloudRecordServiceMapper {
|
||||
@ -55,12 +56,13 @@ public interface CloudRecordServiceMapper {
|
||||
" <if test= 'ids != null ' > and id in " +
|
||||
" <foreach collection='ids' item='item' open='(' separator=',' close=')' > #{item}</foreach>" +
|
||||
" </if>" +
|
||||
" order by start_time desc" +
|
||||
" <if test= 'ascOrder != null and ascOrder == true'> order by start_time asc</if>" +
|
||||
" <if test= 'ascOrder == null or ascOrder == false'> order by start_time desc</if>" +
|
||||
" </script>")
|
||||
List<CloudRecordItem> getList(@Param("query") String query, @Param("app") String app, @Param("stream") String stream,
|
||||
@Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp,
|
||||
@Param("callId")String callId, List<MediaServer> mediaServerItemList,
|
||||
List<Integer> ids);
|
||||
List<Integer> ids, @Param("ascOrder") Boolean ascOrder);
|
||||
|
||||
|
||||
@Select(" <script>" +
|
||||
@ -124,4 +126,26 @@ public interface CloudRecordServiceMapper {
|
||||
"where id = #{id}" +
|
||||
" </script>")
|
||||
CloudRecordItem queryOne(@Param("id") Integer id);
|
||||
|
||||
@Select(" <script>" +
|
||||
"select media_server_id " +
|
||||
" from wvp_cloud_record " +
|
||||
" where 0 = 0" +
|
||||
" <if test= 'app != null '> and app=#{app}</if>" +
|
||||
" <if test= 'stream != null '> and stream=#{stream}</if>" +
|
||||
" <if test= 'startTimeStamp != null '> and end_time >= #{startTimeStamp}</if>" +
|
||||
" <if test= 'endTimeStamp != null '> and start_time <= #{endTimeStamp}</if>" +
|
||||
" group by media_server_id" +
|
||||
" </script>")
|
||||
List<String> queryMediaServerId(@Param("app") String app,
|
||||
@Param("stream") String stream,
|
||||
@Param("startTimeStamp")Long startTimeStamp,
|
||||
@Param("endTimeStamp")Long endTimeStamp);
|
||||
|
||||
@Select(" <script>" +
|
||||
"select * " +
|
||||
" from wvp_cloud_record where id in " +
|
||||
" <foreach collection='ids' item='item' open='(' separator=',' close=')' > #{item}</foreach>" +
|
||||
" </script>")
|
||||
List<CloudRecordItem> queryRecordByIds(Set<Integer> ids);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.genersoft.iot.vmp.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.CentralProcessor;
|
||||
import oshi.hardware.GlobalMemory;
|
||||
@ -9,6 +10,7 @@ import oshi.hardware.NetworkIF;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -142,4 +144,19 @@ public class SystemInfoUtils {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getHardwareId(){
|
||||
SystemInfo systemInfo = new SystemInfo();
|
||||
HardwareAbstractionLayer hardware = systemInfo.getHardware();
|
||||
// CPU ID
|
||||
String cpuId = hardware.getProcessor().getProcessorIdentifier().getProcessorID();
|
||||
// 主板序号
|
||||
String serialNumber = hardware.getComputerSystem().getSerialNumber();
|
||||
|
||||
return DigestUtils.md5DigestAsHex(
|
||||
(
|
||||
DigestUtils.md5DigestAsHex(cpuId.getBytes(StandardCharsets.UTF_8)) +
|
||||
DigestUtils.md5DigestAsHex(serialNumber.getBytes(StandardCharsets.UTF_8))
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
package com.genersoft.iot.vmp.vmanager.cloudRecord;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.genersoft.iot.vmp.common.StreamInfo;
|
||||
import com.genersoft.iot.vmp.conf.UserSetting;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.conf.security.JwtUtils;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||
import com.genersoft.iot.vmp.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
|
||||
import com.genersoft.iot.vmp.streamPush.bean.BatchRemoveParam;
|
||||
import com.genersoft.iot.vmp.utils.DateUtil;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
|
||||
import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -20,12 +27,15 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
@ -46,6 +56,9 @@ public class CloudRecordController {
|
||||
@Autowired
|
||||
private IMediaServerService mediaServerService;
|
||||
|
||||
@Autowired
|
||||
private UserSetting userSetting;
|
||||
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/date/list")
|
||||
@ -101,6 +114,7 @@ public class CloudRecordController {
|
||||
@Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false)
|
||||
@Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false)
|
||||
@Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false)
|
||||
@Parameter(name = "ascOrder", description = "是否升序排序, 升序: true, 降序: false", required = false)
|
||||
public PageInfo<CloudRecordItem> openRtpServer(@RequestParam(required = false) String query,
|
||||
@RequestParam(required = false) String app,
|
||||
@RequestParam(required = false) String stream,
|
||||
@ -109,7 +123,8 @@ public class CloudRecordController {
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime,
|
||||
@RequestParam(required = false) String mediaServerId,
|
||||
@RequestParam(required = false) String callId
|
||||
@RequestParam(required = false) String callId,
|
||||
@RequestParam(required = false) Boolean ascOrder
|
||||
|
||||
) {
|
||||
log.info("[云端录像] 查询 app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId);
|
||||
@ -143,7 +158,7 @@ public class CloudRecordController {
|
||||
if (callId != null && ObjectUtils.isEmpty(callId.trim())) {
|
||||
callId = null;
|
||||
}
|
||||
return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId);
|
||||
return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, ascOrder);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@ -233,6 +248,113 @@ public class CloudRecordController {
|
||||
return cloudRecordService.getPlayUrlPath(recordId);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/loadRecord")
|
||||
@Operation(summary = "加载录像文件形成播放地址")
|
||||
@Parameter(name = "app", description = "应用名", required = true)
|
||||
@Parameter(name = "stream", description = "流ID", required = true)
|
||||
@Parameter(name = "date", description = "日期, 例如 2025-04-10", required = true)
|
||||
public DeferredResult<WVPResult<StreamContent>> loadRecord(
|
||||
HttpServletRequest request,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream,
|
||||
@RequestParam(required = true) String date
|
||||
) {
|
||||
DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>();
|
||||
|
||||
result.onTimeout(()->{
|
||||
log.info("[加载录像文件超时] app={}, stream={}, date={}", app, stream, date);
|
||||
WVPResult<StreamContent> wvpResult = new WVPResult<>();
|
||||
wvpResult.setCode(ErrorCode.ERROR100.getCode());
|
||||
wvpResult.setMsg("加载录像文件超时");
|
||||
result.setResult(wvpResult);
|
||||
});
|
||||
|
||||
ErrorCallback<StreamInfo> callback = (code, msg, streamInfo) -> {
|
||||
|
||||
WVPResult<StreamContent> wvpResult = new WVPResult<>();
|
||||
if (code == InviteErrorCode.SUCCESS.getCode()) {
|
||||
wvpResult.setCode(ErrorCode.SUCCESS.getCode());
|
||||
wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
|
||||
|
||||
if (streamInfo != null) {
|
||||
if (userSetting.getUseSourceIpAsStreamIp()) {
|
||||
streamInfo=streamInfo.clone();//深拷贝
|
||||
String host;
|
||||
try {
|
||||
URL url=new URL(request.getRequestURL().toString());
|
||||
host=url.getHost();
|
||||
} catch (MalformedURLException e) {
|
||||
host=request.getLocalAddr();
|
||||
}
|
||||
streamInfo.changeStreamIp(host);
|
||||
}
|
||||
if (!org.springframework.util.ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) {
|
||||
streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix());
|
||||
}
|
||||
wvpResult.setData(new StreamContent(streamInfo));
|
||||
}else {
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
}
|
||||
}else {
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
}
|
||||
result.setResult(wvpResult);
|
||||
};
|
||||
|
||||
cloudRecordService.loadRecord(app, stream, date, callback);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/seek")
|
||||
@Operation(summary = "定位录像播放到制定位置")
|
||||
@Parameter(name = "mediaServerId", description = "使用的节点Id", required = true)
|
||||
@Parameter(name = "stream", description = "流ID", required = true)
|
||||
@Parameter(name = "seek", description = "要定位的时间位置,从录像开始的时间算起", required = true)
|
||||
public void seekRecord(
|
||||
@RequestParam(required = true) String mediaServerId,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream,
|
||||
@RequestParam(required = true) Double seek,
|
||||
@RequestParam(required = false) String schema
|
||||
) {
|
||||
if (schema == null) {
|
||||
schema = "ts";
|
||||
}
|
||||
cloudRecordService.seekRecord(mediaServerId, app, stream, seek, schema);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/speed")
|
||||
@Operation(summary = "设置录像播放速度")
|
||||
@Parameter(name = "mediaServerId", description = "使用的节点Id", required = true)
|
||||
@Parameter(name = "stream", description = "流ID", required = true)
|
||||
@Parameter(name = "speed", description = "要设置的录像倍速", required = true)
|
||||
public void setRecordSpeed(
|
||||
@RequestParam(required = true) String mediaServerId,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream,
|
||||
@RequestParam(required = true) Integer speed,
|
||||
@RequestParam(required = false) String schema
|
||||
) {
|
||||
if (schema == null) {
|
||||
schema = "ts";
|
||||
}
|
||||
|
||||
cloudRecordService.setRecordSpeed(mediaServerId, app, stream, speed, schema);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除录像文件")
|
||||
@Parameter(name = "ids", description = "文件ID集合", required = true)
|
||||
public void deleteFileByIds(@RequestBody BatchRemoveParam ids) {
|
||||
cloudRecordService.deleteFileByIds(ids.getIds());
|
||||
}
|
||||
|
||||
/************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况,且wvp只有一个zlm节点的情况 ***************************************/
|
||||
|
||||
/**
|
||||
@ -295,7 +417,7 @@ public class CloudRecordController {
|
||||
try {
|
||||
ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());
|
||||
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
|
||||
zos.putNextEntry(new ZipEntry(DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(cloudRecordItem.getStartTime()) + ".mp4"));
|
||||
zos.putNextEntry(new ZipEntry(DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime()) + ".mp4"));
|
||||
File file = new File(cloudRecordItem.getFilePath());
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
continue;
|
||||
@ -379,7 +501,7 @@ public class CloudRecordController {
|
||||
if (remoteHost == null) {
|
||||
remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort());
|
||||
}
|
||||
PageInfo<CloudRecordItem> cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId);
|
||||
PageInfo<CloudRecordItem> cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null);
|
||||
PageInfo<CloudRecordUrl> cloudRecordUrlPageInfo = new PageInfo<>();
|
||||
if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) {
|
||||
cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum());
|
||||
@ -404,7 +526,7 @@ public class CloudRecordController {
|
||||
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
|
||||
CloudRecordUrl cloudRecordUrl = new CloudRecordUrl();
|
||||
cloudRecordUrl.setId(cloudRecordItem.getId());
|
||||
cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(cloudRecordItem.getStartTime()));
|
||||
cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime()));
|
||||
cloudRecordUrl.setPlayUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath());
|
||||
cloudRecordUrlList.add(cloudRecordUrl);
|
||||
}
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
package com.genersoft.iot.vmp.vmanager.log;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.conf.security.JwtUtils;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ICloudRecordService;
|
||||
import com.genersoft.iot.vmp.media.bean.MediaServer;
|
||||
import com.genersoft.iot.vmp.media.service.IMediaServerService;
|
||||
import com.genersoft.iot.vmp.service.ILogService;
|
||||
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
|
||||
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
|
||||
import com.genersoft.iot.vmp.service.bean.LogFileInfo;
|
||||
import com.genersoft.iot.vmp.utils.DateUtil;
|
||||
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
|
||||
import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
@ -23,24 +12,17 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Tag(name = "日志文件查询接口")
|
||||
|
||||
@ -28,6 +28,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -41,13 +42,14 @@ import oshi.hardware.HardwareAbstractionLayer;
|
||||
import oshi.hardware.NetworkIF;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Tag(name = "服务控制")
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/server")
|
||||
public class ServerController {
|
||||
@ -80,6 +82,7 @@ public class ServerController {
|
||||
@Value("${server.port}")
|
||||
private int serverPort;
|
||||
|
||||
|
||||
@Autowired
|
||||
private IRedisCatchStorage redisCatchStorage;
|
||||
|
||||
@ -176,33 +179,14 @@ public class ServerController {
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "重启服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping(value = "/restart")
|
||||
@Operation(summary = "关闭服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping(value = "/shutdown")
|
||||
@ResponseBody
|
||||
public void restart() {
|
||||
// taskExecutor.execute(()-> {
|
||||
// try {
|
||||
// Thread.sleep(3000);
|
||||
// SipProvider up = (SipProvider) SpringBeanFactory.getBean("udpSipProvider");
|
||||
// SipStackImpl stack = (SipStackImpl) up.getSipStack();
|
||||
// stack.stop();
|
||||
// Iterator listener = stack.getListeningPoints();
|
||||
// while (listener.hasNext()) {
|
||||
// stack.deleteListeningPoint((ListeningPoint) listener.next());
|
||||
// }
|
||||
// Iterator providers = stack.getSipProviders();
|
||||
// while (providers.hasNext()) {
|
||||
// stack.deleteSipProvider((SipProvider) providers.next());
|
||||
// }
|
||||
// VManageBootstrap.restart();
|
||||
// } catch (InterruptedException | ObjectInUseException e) {
|
||||
// throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
|
||||
// }
|
||||
// });
|
||||
public void shutdown() {
|
||||
log.info("正在关闭服务。。。");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
@Operation(summary = "获取系统配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
@GetMapping(value = "/system/configInfo")
|
||||
@ResponseBody
|
||||
@ -293,7 +277,7 @@ public class ServerController {
|
||||
@GetMapping(value = "/info")
|
||||
@ResponseBody
|
||||
@Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
|
||||
public Map<String, Map<String, String>> getInfo() {
|
||||
public Map<String, Map<String, String>> getInfo(HttpServletRequest request) {
|
||||
Map<String, Map<String, String>> result = new LinkedHashMap<>();
|
||||
Map<String, String> hardwareMap = new LinkedHashMap<>();
|
||||
result.put("硬件信息", hardwareMap);
|
||||
@ -341,6 +325,11 @@ public class ServerController {
|
||||
platformMap.put("GIT版本", version.getGIT_Revision_SHORT());
|
||||
platformMap.put("DOCKER环境", new File("/.dockerenv").exists()?"是":"否");
|
||||
|
||||
Map<String, String> docmap = new LinkedHashMap<>();
|
||||
result.put("文档地址", docmap);
|
||||
docmap.put("部署文档", String.format("%s://%s:%s/doc.html", request.getScheme(), request.getServerName(), request.getServerPort()));
|
||||
docmap.put("接口文档", "https://doc.wvp-pro.cn");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ -63,9 +63,9 @@ public class UserApiKeyController {
|
||||
|
||||
Long expirationTime = null;
|
||||
if (expiresAt != null) {
|
||||
long timestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt);
|
||||
expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000);
|
||||
if (expirationTime < 0) {
|
||||
expirationTime = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt);
|
||||
long difference = (expirationTime - System.currentTimeMillis()) / (60 * 1000);
|
||||
if (difference < 0) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "过期时间不能早于当前时间");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.genersoft.iot.vmp.vmanager.user;
|
||||
|
||||
import com.genersoft.iot.vmp.conf.UserSetting;
|
||||
import com.genersoft.iot.vmp.conf.exception.ControllerException;
|
||||
import com.genersoft.iot.vmp.conf.security.JwtUtils;
|
||||
import com.genersoft.iot.vmp.conf.security.SecurityUtils;
|
||||
@ -42,6 +43,9 @@ public class UserController {
|
||||
@Autowired
|
||||
private IRoleService roleService;
|
||||
|
||||
@Autowired
|
||||
private UserSetting userSetting;
|
||||
|
||||
@GetMapping("/login")
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," +
|
||||
@ -62,6 +66,7 @@ public class UserController {
|
||||
String jwt = JwtUtils.createToken(username);
|
||||
response.setHeader(JwtUtils.getHeader(), jwt);
|
||||
user.setAccessToken(jwt);
|
||||
user.setServerId(userSetting.getServerId());
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@ -2,4 +2,4 @@ spring:
|
||||
application:
|
||||
name: wvp
|
||||
profiles:
|
||||
active: dev
|
||||
active: 273
|
||||
|
||||
57
src/main/resources/install.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#! /bin/sh
|
||||
|
||||
WORD_DIR=$(cd $(dirname $0); pwd)
|
||||
SERVICE_NAME="wvp"
|
||||
|
||||
# 检查是否为 root 用户
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "提示: 建议使用 root 用户执行此脚本,否则可能权限不足!"
|
||||
read -p "继续?(y/n) " -n 1 -r
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
|
||||
# 当前目录直接搜索(不含子目录)
|
||||
jar_files=(*.jar)
|
||||
|
||||
if [ ${#jar_files[@]} -eq 0 ]; then
|
||||
echo "当前目录无 JAR 文件!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 遍历结果
|
||||
for jar in "${jar_files[@]}"; do
|
||||
echo "找到 JAR 文件: $jar"
|
||||
done
|
||||
|
||||
# 写文件
|
||||
# 生成 Systemd 服务文件内容
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null
|
||||
[Unit]
|
||||
Description=${SERVICE_NAME}
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
User=$USER
|
||||
WorkingDirectory=${WORD_DIR}
|
||||
ExecStart=java -jar ${jar_files}
|
||||
SuccessExitStatus=143
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
Environment=SPRING_PROFILES_ACTIVE=prod
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 重载 Systemd 并启动服务
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
sudo systemctl start "$SERVICE_NAME"
|
||||
|
||||
# 验证服务状态
|
||||
echo "服务已安装!执行以下命令查看状态:"
|
||||
echo "sudo systemctl status $SERVICE_NAME"
|
||||
@ -5,6 +5,10 @@
|
||||
|
||||
|
||||
spring:
|
||||
cache:
|
||||
type: redis
|
||||
thymeleaf:
|
||||
cache: false
|
||||
# 设置接口超时时间
|
||||
mvc:
|
||||
async:
|
||||
@ -113,8 +117,8 @@ sip:
|
||||
# keepalliveToOnline: false
|
||||
# 是否存储alarm信息
|
||||
alarm: false
|
||||
# 命令发送等待回复的超时时间, 单位:秒
|
||||
timeout: 15
|
||||
# 命令发送等待回复的超时时间, 单位:毫秒
|
||||
timeout: 1000
|
||||
|
||||
# 做为JT1078服务器的配置
|
||||
jt1078:
|
||||
|
||||
14
web/.editorconfig
Normal file
@ -0,0 +1,14 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
5
web/.env.development
Normal file
@ -0,0 +1,5 @@
|
||||
# just a flag
|
||||
ENV = 'development'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/dev-api'
|
||||
6
web/.env.production
Normal file
@ -0,0 +1,6 @@
|
||||
# just a flag
|
||||
ENV = 'production'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = ''
|
||||
|
||||
8
web/.env.staging
Normal file
@ -0,0 +1,8 @@
|
||||
NODE_ENV = production
|
||||
|
||||
# just a flag
|
||||
ENV = 'staging'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/stage-api'
|
||||
|
||||
4
web/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
build/*.js
|
||||
src/assets
|
||||
public
|
||||
dist
|
||||
198
web/.eslintrc.js
Normal file
@ -0,0 +1,198 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:vue/recommended', 'eslint:recommended'],
|
||||
|
||||
// add your custom rules here
|
||||
//it is base on https://github.com/vuejs/eslint-config-vue
|
||||
rules: {
|
||||
"vue/max-attributes-per-line": [2, {
|
||||
"singleline": 10,
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": false
|
||||
}
|
||||
}],
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/multiline-html-element-content-newline":"off",
|
||||
"vue/name-property-casing": ["error", "PascalCase"],
|
||||
"vue/no-v-html": "off",
|
||||
'accessor-pairs': 2,
|
||||
'arrow-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'block-spacing': [2, 'always'],
|
||||
'brace-style': [2, '1tbs', {
|
||||
'allowSingleLine': true
|
||||
}],
|
||||
'camelcase': [0, {
|
||||
'properties': 'always'
|
||||
}],
|
||||
'comma-dangle': [2, 'never'],
|
||||
'comma-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'comma-style': [2, 'last'],
|
||||
'constructor-super': 2,
|
||||
'curly': [2, 'multi-line'],
|
||||
'dot-location': [2, 'property'],
|
||||
'eol-last': 2,
|
||||
'eqeqeq': ["error", "always", {"null": "ignore"}],
|
||||
'generator-star-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'handle-callback-err': [2, '^(err|error)$'],
|
||||
'indent': [2, 2, {
|
||||
'SwitchCase': 1
|
||||
}],
|
||||
'jsx-quotes': [2, 'prefer-single'],
|
||||
'key-spacing': [2, {
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}],
|
||||
'keyword-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'new-cap': [2, {
|
||||
'newIsCap': true,
|
||||
'capIsNew': false
|
||||
}],
|
||||
'new-parens': 2,
|
||||
'no-array-constructor': 2,
|
||||
'no-caller': 2,
|
||||
'no-console': 'off',
|
||||
'no-class-assign': 2,
|
||||
'no-cond-assign': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-control-regex': 0,
|
||||
'no-delete-var': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-empty-character-class': 2,
|
||||
'no-empty-pattern': 2,
|
||||
'no-eval': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-parens': [2, 'functions'],
|
||||
'no-fallthrough': 2,
|
||||
'no-floating-decimal': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-implied-eval': 2,
|
||||
'no-inner-declarations': [2, 'functions'],
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-label-var': 2,
|
||||
'no-labels': [2, {
|
||||
'allowLoop': false,
|
||||
'allowSwitch': false
|
||||
}],
|
||||
'no-lone-blocks': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [2, {
|
||||
'max': 1
|
||||
}],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new-object': 2,
|
||||
'no-new-require': 2,
|
||||
'no-new-symbol': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-octal': 2,
|
||||
'no-octal-escape': 2,
|
||||
'no-path-concat': 2,
|
||||
'no-proto': 2,
|
||||
'no-redeclare': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-return-assign': [2, 'except-parens'],
|
||||
'no-self-assign': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-spaced-func': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-trailing-spaces': 2,
|
||||
'no-undef': 2,
|
||||
'no-undef-init': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unneeded-ternary': [2, {
|
||||
'defaultAssignment': false
|
||||
}],
|
||||
'no-unreachable': 2,
|
||||
'no-unsafe-finally': 2,
|
||||
'no-unused-vars': [2, {
|
||||
'vars': 'all',
|
||||
'args': 'none'
|
||||
}],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-constructor': 2,
|
||||
'no-useless-escape': 0,
|
||||
'no-whitespace-before-property': 2,
|
||||
'no-with': 2,
|
||||
'one-var': [2, {
|
||||
'initialized': 'never'
|
||||
}],
|
||||
'operator-linebreak': [2, 'after', {
|
||||
'overrides': {
|
||||
'?': 'before',
|
||||
':': 'before'
|
||||
}
|
||||
}],
|
||||
'padded-blocks': [2, 'never'],
|
||||
'quotes': [2, 'single', {
|
||||
'avoidEscape': true,
|
||||
'allowTemplateLiterals': true
|
||||
}],
|
||||
'semi': [2, 'never'],
|
||||
'semi-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [2, {
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}],
|
||||
'spaced-comment': [2, 'always', {
|
||||
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
|
||||
}],
|
||||
'template-curly-spacing': [2, 'never'],
|
||||
'use-isnan': 2,
|
||||
'valid-typeof': 2,
|
||||
'wrap-iife': [2, 'any'],
|
||||
'yield-star-spacing': [2, 'both'],
|
||||
'yoda': [2, 'never'],
|
||||
'prefer-const': 2,
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'object-curly-spacing': [2, 'always', {
|
||||
objectsInObjects: false
|
||||
}],
|
||||
'array-bracket-spacing': [2, 'never']
|
||||
}
|
||||
}
|
||||
16
web/.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
5
web/.travis.yml
Normal file
@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js: 10
|
||||
script: npm run test
|
||||
notifications:
|
||||
email: false
|
||||
21
web/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-present PanJiaChen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
111
web/README-zh.md
Normal file
@ -0,0 +1,111 @@
|
||||
# vue-admin-template
|
||||
|
||||
> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
|
||||
|
||||
[线上地址](http://panjiachen.github.io/vue-admin-template)
|
||||
|
||||
[国内访问](https://panjiachen.gitee.io/vue-admin-template)
|
||||
|
||||
目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
|
||||
|
||||
<p align="center">
|
||||
<b>SPONSORED BY</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
|
||||
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Extra
|
||||
|
||||
如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
|
||||
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
|
||||
|
||||
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
|
||||
|
||||
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
|
||||
|
||||
写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
|
||||
|
||||
- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
|
||||
- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
|
||||
- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
|
||||
- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
|
||||
- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
|
||||
|
||||
## Build Setup
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/PanJiaChen/vue-admin-template.git
|
||||
|
||||
# 进入项目目录
|
||||
cd vue-admin-template
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
```
|
||||
|
||||
浏览器访问 [http://localhost:9528](http://localhost:9528)
|
||||
|
||||
## 发布
|
||||
|
||||
```bash
|
||||
# 构建测试环境
|
||||
npm run build:stage
|
||||
|
||||
# 构建生产环境
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
## 其它
|
||||
|
||||
```bash
|
||||
# 预览发布环境效果
|
||||
npm run preview
|
||||
|
||||
# 预览发布环境效果 + 静态资源分析
|
||||
npm run preview -- --report
|
||||
|
||||
# 代码格式检查
|
||||
npm run lint
|
||||
|
||||
# 代码格式检查并自动修复
|
||||
npm run lint -- --fix
|
||||
```
|
||||
|
||||
更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
|
||||
|
||||
## 购买贴纸
|
||||
|
||||
你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
## Browsers support
|
||||
|
||||
Modern browsers and Internet Explorer 10+.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| --------- | --------- | --------- | --------- |
|
||||
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
|
||||
|
||||
Copyright (c) 2017-present PanJiaChen
|
||||
99
web/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# vue-admin-template
|
||||
|
||||
English | [简体中文](./README-zh.md)
|
||||
|
||||
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
|
||||
|
||||
**Live demo:** http://panjiachen.github.io/vue-admin-template
|
||||
|
||||
|
||||
**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
|
||||
|
||||
<p align="center">
|
||||
<b>SPONSORED BY</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
|
||||
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Build Setup
|
||||
|
||||
```bash
|
||||
# clone the project
|
||||
git clone https://github.com/PanJiaChen/vue-admin-template.git
|
||||
|
||||
# enter the project directory
|
||||
cd vue-admin-template
|
||||
|
||||
# install dependency
|
||||
npm install
|
||||
|
||||
# develop
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This will automatically open http://localhost:9528
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# build for test environment
|
||||
npm run build:stage
|
||||
|
||||
# build for production environment
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
```bash
|
||||
# preview the release environment effect
|
||||
npm run preview
|
||||
|
||||
# preview the release environment effect + static resource analysis
|
||||
npm run preview -- --report
|
||||
|
||||
# code format check
|
||||
npm run lint
|
||||
|
||||
# code format check and auto fix
|
||||
npm run lint -- --fix
|
||||
```
|
||||
|
||||
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
## Extra
|
||||
|
||||
If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
|
||||
|
||||
For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
|
||||
|
||||
## Related Project
|
||||
|
||||
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
|
||||
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
|
||||
|
||||
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
|
||||
|
||||
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
|
||||
|
||||
## Browsers support
|
||||
|
||||
Modern browsers and Internet Explorer 10+.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| --------- | --------- | --------- | --------- |
|
||||
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
|
||||
|
||||
Copyright (c) 2017-present PanJiaChen
|
||||
14
web/babel.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
'env': {
|
||||
'development': {
|
||||
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
|
||||
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
|
||||
// https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
|
||||
'plugins': ['dynamic-import-node']
|
||||
}
|
||||
}
|
||||
}
|
||||
35
web/build/index.js
Normal file
@ -0,0 +1,35 @@
|
||||
const { run } = require('runjs')
|
||||
const chalk = require('chalk')
|
||||
const config = require('../vue.config.js')
|
||||
const rawArgv = process.argv.slice(2)
|
||||
const args = rawArgv.join(' ')
|
||||
|
||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
||||
const report = rawArgv.includes('--report')
|
||||
|
||||
run(`vue-cli-service build ${args}`)
|
||||
|
||||
const port = 9526
|
||||
const publicPath = config.publicPath
|
||||
|
||||
var connect = require('connect')
|
||||
var serveStatic = require('serve-static')
|
||||
const app = connect()
|
||||
|
||||
app.use(
|
||||
publicPath,
|
||||
serveStatic('./src/main/resources/static', {
|
||||
index: ['index.html', '/']
|
||||
})
|
||||
)
|
||||
|
||||
app.listen(port, function () {
|
||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
||||
if (report) {
|
||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
run(`vue-cli-service build ${args}`)
|
||||
}
|
||||
24
web/jest.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest',
|
||||
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
|
||||
'jest-transform-stub',
|
||||
'^.+\\.jsx?$': 'babel-jest'
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
snapshotSerializers: ['jest-serializer-vue'],
|
||||
testMatch: [
|
||||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||
],
|
||||
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
|
||||
coverageDirectory: '<rootDir>/tests/unit/coverage',
|
||||
// 'collectCoverage': true,
|
||||
'coverageReporters': [
|
||||
'lcov',
|
||||
'text-summary'
|
||||
],
|
||||
testURL: 'http://localhost/'
|
||||
}
|
||||
9
web/jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
57
web/mock/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
const Mock = require('mockjs')
|
||||
const { param2Obj } = require('./utils')
|
||||
|
||||
const user = require('./user')
|
||||
const table = require('./table')
|
||||
|
||||
const mocks = [
|
||||
...user,
|
||||
...table
|
||||
]
|
||||
|
||||
// for front mock
|
||||
// please use it cautiously, it will redefine XMLHttpRequest,
|
||||
// which will cause many of your third-party libraries to be invalidated(like progress event).
|
||||
function mockXHR() {
|
||||
// mock patch
|
||||
// https://github.com/nuysoft/Mock/issues/300
|
||||
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
|
||||
Mock.XHR.prototype.send = function() {
|
||||
if (this.custom.xhr) {
|
||||
this.custom.xhr.withCredentials = this.withCredentials || false
|
||||
|
||||
if (this.responseType) {
|
||||
this.custom.xhr.responseType = this.responseType
|
||||
}
|
||||
}
|
||||
this.proxy_send(...arguments)
|
||||
}
|
||||
|
||||
function XHR2ExpressReqWrap(respond) {
|
||||
return function(options) {
|
||||
let result = null
|
||||
if (respond instanceof Function) {
|
||||
const { body, type, url } = options
|
||||
// https://expressjs.com/en/4x/api.html#req
|
||||
result = respond({
|
||||
method: type,
|
||||
body: JSON.parse(body),
|
||||
query: param2Obj(url)
|
||||
})
|
||||
} else {
|
||||
result = respond
|
||||
}
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
|
||||
for (const i of mocks) {
|
||||
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mocks,
|
||||
mockXHR
|
||||
}
|
||||
|
||||
81
web/mock/mock-server.js
Normal file
@ -0,0 +1,81 @@
|
||||
const chokidar = require('chokidar')
|
||||
const bodyParser = require('body-parser')
|
||||
const chalk = require('chalk')
|
||||
const path = require('path')
|
||||
const Mock = require('mockjs')
|
||||
|
||||
const mockDir = path.join(process.cwd(), 'mock')
|
||||
|
||||
function registerRoutes(app) {
|
||||
let mockLastIndex
|
||||
const { mocks } = require('./index.js')
|
||||
const mocksForServer = mocks.map(route => {
|
||||
return responseFake(route.url, route.type, route.response)
|
||||
})
|
||||
for (const mock of mocksForServer) {
|
||||
app[mock.type](mock.url, mock.response)
|
||||
mockLastIndex = app._router.stack.length
|
||||
}
|
||||
const mockRoutesLength = Object.keys(mocksForServer).length
|
||||
return {
|
||||
mockRoutesLength: mockRoutesLength,
|
||||
mockStartIndex: mockLastIndex - mockRoutesLength
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterRoutes() {
|
||||
Object.keys(require.cache).forEach(i => {
|
||||
if (i.includes(mockDir)) {
|
||||
delete require.cache[require.resolve(i)]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// for mock server
|
||||
const responseFake = (url, type, respond) => {
|
||||
return {
|
||||
url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
|
||||
type: type || 'get',
|
||||
response(req, res) {
|
||||
console.log('request invoke:' + req.path)
|
||||
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = app => {
|
||||
// parse app.body
|
||||
// https://expressjs.com/en/4x/api.html#req.body
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true
|
||||
}))
|
||||
|
||||
const mockRoutes = registerRoutes(app)
|
||||
var mockRoutesLength = mockRoutes.mockRoutesLength
|
||||
var mockStartIndex = mockRoutes.mockStartIndex
|
||||
|
||||
// watch files, hot reload mock server
|
||||
chokidar.watch(mockDir, {
|
||||
ignored: /mock-server/,
|
||||
ignoreInitial: true
|
||||
}).on('all', (event, path) => {
|
||||
if (event === 'change' || event === 'add') {
|
||||
try {
|
||||
// remove mock routes stack
|
||||
app._router.stack.splice(mockStartIndex, mockRoutesLength)
|
||||
|
||||
// clear routes cache
|
||||
unregisterRoutes()
|
||||
|
||||
const mockRoutes = registerRoutes(app)
|
||||
mockRoutesLength = mockRoutes.mockRoutesLength
|
||||
mockStartIndex = mockRoutes.mockStartIndex
|
||||
|
||||
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
|
||||
} catch (error) {
|
||||
console.log(chalk.redBright(error))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
29
web/mock/table.js
Normal file
@ -0,0 +1,29 @@
|
||||
const Mock = require('mockjs')
|
||||
|
||||
const data = Mock.mock({
|
||||
'items|30': [{
|
||||
id: '@id',
|
||||
title: '@sentence(10, 20)',
|
||||
'status|1': ['published', 'draft', 'deleted'],
|
||||
author: 'name',
|
||||
display_time: '@datetime',
|
||||
pageviews: '@integer(300, 5000)'
|
||||
}]
|
||||
})
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
url: '/vue-admin-template/table/list',
|
||||
type: 'get',
|
||||
response: config => {
|
||||
const items = data.items
|
||||
return {
|
||||
code: 20000,
|
||||
data: {
|
||||
total: items.length,
|
||||
items: items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
84
web/mock/user.js
Normal file
@ -0,0 +1,84 @@
|
||||
|
||||
const tokens = {
|
||||
admin: {
|
||||
token: 'admin-token'
|
||||
},
|
||||
editor: {
|
||||
token: 'editor-token'
|
||||
}
|
||||
}
|
||||
|
||||
const users = {
|
||||
'admin-token': {
|
||||
roles: ['admin'],
|
||||
introduction: 'I am a super administrator',
|
||||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
|
||||
name: 'Super Admin'
|
||||
},
|
||||
'editor-token': {
|
||||
roles: ['editor'],
|
||||
introduction: 'I am an editor',
|
||||
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
|
||||
name: 'Normal Editor'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
// user login
|
||||
{
|
||||
url: '/vue-admin-template/user/login',
|
||||
type: 'post',
|
||||
response: config => {
|
||||
const { username } = config.body
|
||||
const token = tokens[username]
|
||||
|
||||
// mock error
|
||||
if (!token) {
|
||||
return {
|
||||
code: 60204,
|
||||
message: 'Account and password are incorrect.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: token
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// get user info
|
||||
{
|
||||
url: '/vue-admin-template/user/info\.*',
|
||||
type: 'get',
|
||||
response: config => {
|
||||
const { token } = config.query
|
||||
const info = users[token]
|
||||
|
||||
// mock error
|
||||
if (!info) {
|
||||
return {
|
||||
code: 50008,
|
||||
message: 'Login failed, unable to get user details.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: info
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// user logout
|
||||
{
|
||||
url: '/vue-admin-template/user/logout',
|
||||
type: 'post',
|
||||
response: _ => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: 'success'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
25
web/mock/utils.js
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Object}
|
||||
*/
|
||||
function param2Obj(url) {
|
||||
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
|
||||
if (!search) {
|
||||
return {}
|
||||
}
|
||||
const obj = {}
|
||||
const searchArr = search.split('&')
|
||||
searchArr.forEach(v => {
|
||||
const index = v.indexOf('=')
|
||||
if (index !== -1) {
|
||||
const name = v.substring(0, index)
|
||||
const val = v.substring(index + 1, v.length)
|
||||
obj[name] = val
|
||||
}
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
param2Obj
|
||||
}
|
||||
74
web/package.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "vue-admin-template",
|
||||
"version": "4.4.0",
|
||||
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
|
||||
"author": "Pan <panfree23@gmail.com>",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve --host=0.0.0.0",
|
||||
"build:prod": "vue-cli-service build",
|
||||
"build:stage": "vue-cli-service build --mode staging",
|
||||
"preview": "node build/index.js --preview",
|
||||
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"test:unit": "jest --clearCache && vue-cli-service test:unit",
|
||||
"test:ci": "npm run lint && npm run test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wchbrad/vue-easy-tree": "^1.0.13",
|
||||
"@femessage/log-viewer": "^1.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"core-js": "3.6.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^4.9.0",
|
||||
"element-ui": "^2.15.14",
|
||||
"js-cookie": "2.2.0",
|
||||
"moment": "^2.29.1",
|
||||
"normalize.css": "7.0.0",
|
||||
"nprogress": "0.2.0",
|
||||
"ol": "^6.14.1",
|
||||
"path-to-regexp": "2.4.0",
|
||||
"screenfull": "5.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"v-charts": "^1.19.0",
|
||||
"vue": "2.6.10",
|
||||
"vue-clipboards": "^1.3.0",
|
||||
"vue-contextmenujs": "^1.4.11",
|
||||
"vue-router": "3.0.6",
|
||||
"vue-ztree-2.0": "^1.0.4",
|
||||
"vuex": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "4.4.4",
|
||||
"@vue/cli-plugin-eslint": "4.4.4",
|
||||
"@vue/cli-plugin-unit-jest": "4.4.4",
|
||||
"@vue/cli-service": "4.4.4",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"autoprefixer": "9.5.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "23.6.0",
|
||||
"babel-plugin-dynamic-import-node": "2.3.3",
|
||||
"chalk": "2.4.2",
|
||||
"connect": "3.6.6",
|
||||
"eslint": "6.7.2",
|
||||
"eslint-plugin-vue": "6.2.2",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"mockjs": "1.0.1-beta3",
|
||||
"runjs": "4.3.2",
|
||||
"sass": "1.26.8",
|
||||
"sass-loader": "8.0.2",
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"serve-static": "1.13.2",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"vue-template-compiler": "2.6.10"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8.9",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
8
web/postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
'plugins': {
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
'autoprefixer': {}
|
||||
}
|
||||
}
|
||||
BIN
web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
22
web/public/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="./static/js/jessibuca/jessibuca.js"></script>
|
||||
<script type="text/javascript" src="./static/js/ZLMRTCClient.js"></script>
|
||||
<script type="text/javascript" src="./static/js/config.js"></script>
|
||||
<script type="text/javascript" src="./static/js/h265web/h265webjs-v20221106.js"></script>
|
||||
<script type="text/javascript" src="./static/js/h265web/missile.js"></script>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/public/libDecoder.wasm
Normal file
BIN
web/public/static/images/abl-logo.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/public/static/images/arrow.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
web/public/static/images/bg13.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
web/public/static/images/bg14.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
web/public/static/images/bg17.png
Normal file
|
After Width: | Height: | Size: 876 KiB |
BIN
web/public/static/images/bg18.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
web/public/static/images/bg19.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
web/public/static/images/gis/camera-offline.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
web/public/static/images/gis/camera.png
Normal file
|
After Width: | Height: | Size: 10 KiB |