新增抓拍接口。

This commit is contained in:
luoyw 2025-12-09 21:03:27 +08:00
parent e8c4af5521
commit 1c0733c915
18 changed files with 251 additions and 11 deletions

11
pom.xml
View File

@ -217,7 +217,18 @@
<artifactId>ruoyi-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.40</version>
</dependency>
<dependency>
<groupId>com.demo</groupId>
<artifactId>dahua-sdk-demo</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -2,16 +2,11 @@ package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.system.service.DahuaSnapService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@ -34,6 +29,8 @@ public class NtzyDeviceInfoController extends BaseController
@Autowired
private INtzyDeviceInfoService ntzyDeviceInfoService;
private final DahuaSnapService service =
new DahuaSnapService("D:/capture/", "http://192.168.1.96:8080/capture/");
/**
* 查询设备信息列表
*/
@ -101,4 +98,20 @@ public class NtzyDeviceInfoController extends BaseController
{
return toAjax(ntzyDeviceInfoService.deleteNtzyDeviceInfoByIds(ids));
}
/**
* 抓拍成功后保存图片文件
* 抓拍成功后
* 立即把 JPG 保存到本地
* 文件访问 URI返回给调用方相当于返回图片链接效果
*/
@GetMapping("/snap")
public AjaxResult snap(@RequestParam String ip,
@RequestParam(defaultValue = "37777") int port,
@RequestParam String user,
@RequestParam String pwd){
String url = service.snap(ip, port, user, pwd);
return success(url);
}
}

View File

@ -0,0 +1,103 @@
package com.ruoyi.web.controller.system;
import cn.hutool.core.io.IoUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.system.service.INtzyFileService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
/**
* 录像拍照文件下载Controller
*
* @author ruoyi
* @date 2025-12-09
*/
@RestController
@RequestMapping("/capture")
public class NtzyFileDownloadController extends BaseController
{
@Autowired
private INtzyFileService ntzyFileService;
/**
* 文件下载
*/
@GetMapping("/**")
@Operation(summary = "下载文件")
public void getFileContent(HttpServletRequest request, HttpServletResponse response) throws Exception
{
// 获取请求的路径
String path = StrUtil.subAfter(request.getRequestURI(), "/capture/", false);
if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递");
}
// 解码解决中文路径的问题
path = URLUtil.decode(path);
// 读取内容
byte[] content = readFileAsBytes("D:/capture/" + path);
if (content == null) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
writeAttachment(response, getFileName(path), content);
}
/**
* 返回附件
*
* @param response 响应
* @param filename 文件名
* @param content 附件内容
*/
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
// 设置 header contentType
response.setContentType("image/jpeg");
// 设置内容显示下载文件名https://www.cnblogs.com/wq-9/articles/12165056.html
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
response.setHeader("Content-Disposition", "inline;filename=" + encode(filename));
// 输出附件
IoUtil.write(response.getOutputStream(), false, content);
}
public static String encode(String raw) throws UnsupportedEncodingException {
return URLEncoder.encode(raw, "utf-8");
}
/**
* 从完整路径中提取文件名含扩展名
* @param path 路径字符串
* @return 文件名若路径为空或仅目录则返回 ""
*/
public static String getFileName(String path) {
if (path == null || path.isEmpty()) return "";
return Paths.get(path).getFileName().toString();
}
/**
* 读取文件并返回 byte[]
* @param path 文件路径相对或绝对
* @return 文件全部字节
* @throws IOException 读取失败时抛出
*/
public static byte[] readFileAsBytes(String path) throws IOException {
Path p = Paths.get(path);
return Files.readAllBytes(p);
}
}

View File

@ -90,6 +90,8 @@ spring:
# token配置
token:
enable: false
anonymous: true
# 令牌自定义标识
header: Authorization
# 令牌密钥
@ -97,6 +99,10 @@ token:
# 令牌有效期默认30分钟
expireTime: 30
security:
excludes:
- /capture/**
# MyBatis配置
mybatis:
# 搜索指定包别名

View File

@ -111,7 +111,7 @@ public class SecurityConfig
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
requests.antMatchers("/login", "/register", "/captchaImage", "/capture/**").permitAll()
// 静态资源可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
@ -120,7 +120,7 @@ public class SecurityConfig
})
// 添加Logout filter
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
// 添加JWT filter
// // 添加JWT filter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CORS filter
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)

View File

@ -22,7 +22,10 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>com.demo</groupId>
<artifactId>dahua-sdk-demo</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,104 @@
package com.ruoyi.system.service;
import com.netsdk.lib.NetSDKLib;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class DahuaSnapService {
// static {
// // 1. 加载大华 SDK
// System.loadLibrary("dhnetsdk");
// }
private final NetSDKLib netsdk = NetSDKLib.NETSDK_INSTANCE;
private final String saveRoot; // 根目录
private final String httpBaseUrl; // nginx 映射前缀
public DahuaSnapService(String saveRoot, String httpBaseUrl) {
this.saveRoot = saveRoot;
this.httpBaseUrl = httpBaseUrl;
netsdk.CLIENT_Init(null, null); // 初始化
}
/**
* 抓拍并返回图片链接
* @param ip 相机 IP
* @param port 37777
* @param user admin
* @param pwd 密码
* @return 可访问的 JPG 地址http
*/
public String snap(String ip, int port, String user, String pwd) {
// 2. 登录
NetSDKLib.NET_DEVICEINFO_Ex deviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();
IntByReference nError = new IntByReference(0);
NetSDKLib.LLong loginId = netsdk.CLIENT_LoginEx2(ip, port, user, pwd,
NetSDKLib.EM_LOGIN_SPAC_CAP_TYPE.EM_LOGIN_SPEC_CAP_TCP,
null, deviceInfo, nError);
if (loginId.longValue() == 0) {
throw new RuntimeException("登录失败, error=" + netsdk.CLIENT_GetLastError());
}
// 3. 准备保存路径
String cameraId = ip.replace(".", "-");
File dir = new File(saveRoot + cameraId);
if (!dir.exists()) dir.mkdirs();
String fileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".jpg";
String localPath = dir.getAbsolutePath() + File.separator + fileName;
// 4. 设置抓图回调一次性可放构造函数
SnapRevCallBack snapCb = new SnapRevCallBack(localPath);
netsdk.CLIENT_SetSnapRevCallBack(snapCb, null);
// 5. 请求抓拍
NetSDKLib.SNAP_PARAMS params = new NetSDKLib.SNAP_PARAMS();
params.Channel = 0; // 通道 0
params.Quality = 6; // 画质 1-6
params.ImageSize = 2; // 0-主码流 2-子码流
params.mode = 0;
if (!netsdk.CLIENT_SnapPictureEx(loginId, params, null)) {
netsdk.CLIENT_Logout(loginId);
throw new RuntimeException("抓拍指令失败, error=" + netsdk.CLIENT_GetLastError());
}
// 6. 等待回调写完简单 CountDownLatch 2s 超时
try { snapCb.latch.await(2, TimeUnit.SECONDS); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
// 7. 登出
netsdk.CLIENT_Logout(loginId);
// 8. 返回可访问的 http 链接
return httpBaseUrl + cameraId + "/" + fileName;
}
/* ===================== 回调 ===================== */
private static class SnapRevCallBack implements NetSDKLib.fSnapRev {
private final String savePath;
private final CountDownLatch latch = new CountDownLatch(1);
SnapRevCallBack(String savePath) { this.savePath = savePath; }
@Override
public void invoke(NetSDKLib.LLong lLoginID, Pointer pBuf, int revLen,
int encodeType, int cmdSerial, Pointer dwUser) {
if (pBuf == null || revLen <= 0) return;
// encodeType == 10 JPG
try (FileOutputStream fos = new FileOutputStream(savePath)) {
fos.write(pBuf.getByteArray(0, revLen));
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

Binary file not shown.

Binary file not shown.