Merge pull request #403 from aaron7524/master

Java client 附件下载新功能实现
This commit is contained in:
Changhua 2025-05-04 21:48:57 +08:00 committed by GitHub
commit dbb7afad04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 355 additions and 13 deletions

View File

@ -1,5 +1,6 @@
package com.wechat.ferry.controller;
import com.alibaba.fastjson2.JSONObject;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
@ -14,6 +15,7 @@ import com.wechat.ferry.entity.vo.request.WxPpWcfAddFriendGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseSqlReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseTableReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDeleteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDownloadAttachReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfInviteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfPassFriendApplyReq;
@ -41,10 +43,12 @@ import com.wechat.ferry.entity.vo.response.WxPpWcfSendTextMsgResp;
import com.wechat.ferry.entity.vo.response.WxPpWcfSendXmlMsgResp;
import com.wechat.ferry.enums.ResponseCodeEnum;
import com.wechat.ferry.service.WeChatDllService;
import com.wechat.ferry.utils.PathUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/**
* 控制层-微信DLL处理
@ -258,16 +262,68 @@ public class WeChatDllController {
return TResponse.ok(ResponseCodeEnum.SUCCESS);
}
// @ApiOperation(value = "下载图片、视频、文件", notes = "queryMsgTypeList")
// @PostMapping(value = "/list/msgType")
// public TResponse<Object> queryMsgTypeList() {
// return TResponse.ok(ResponseCodeEnum.SUCCESS, list);
// }
//
// @ApiOperation(value = "解密图片", notes = "queryMsgTypeList")
// @PostMapping(value = "/list/msgType")
// public TResponse<Object> queryMsgTypeList() {
// return TResponse.ok(ResponseCodeEnum.SUCCESS, list);
// }
/**
* 下载视频 add by wmz 2025-05-01
*
* @param request
* @return
* @throws Exception
*/
@ApiOperation(value = "下载视频", notes = "download_video")
@PostMapping(value = "/download/video")
public TResponse<Object> downloadVideo(@Validated @RequestBody WxPpWcfDownloadAttachReq request) throws Exception {
String path = weChatDllService.downloadVideo(request);
if (path != null) {
JSONObject pathJson = new JSONObject();
pathJson.put("path", path);
return TResponse.ok(ResponseCodeEnum.SUCCESS, pathJson);
}
return TResponse.ok(ResponseCodeEnum.FAILED);
}
/**
* 下载图片 add by wmz 2025-05-02
*
* @param request
* @return
* @throws Exception
*/
@ApiOperation(value = "下载图片", notes = "download_picture")
@PostMapping(value = "/download/picture")
public TResponse<Object> downloadPicture(@Validated @RequestBody WxPpWcfDownloadAttachReq request) throws Exception {
//check parameter
String dir = request.getDir();
if (!StringUtils.hasText(dir)) {
log.info("需要指定图片的路径dir");
return TResponse.fail("需要指定图片的路径dir");
}
boolean res = PathUtils.createDir(dir);
if (!res) {
return TResponse.fail("图片路径创建失败" + dir);
}
String path = weChatDllService.downloadPicture(request);
if (path != null) {
JSONObject pathJson = new JSONObject();
pathJson.put("path", path);
return TResponse.ok(ResponseCodeEnum.SUCCESS, pathJson);
}
return TResponse.ok(ResponseCodeEnum.FAILED);
}
/**
* 暂未实现 add by mz 2025-05-01
*
* @param request
* @return
* @throws Exception
*/
@ApiOperation(value = "登陆二维码", notes = "loginQR")
@PostMapping(value = "/loginQR")
public TResponse<Object> loginQR(@Validated @RequestBody WxPpWcfDownloadAttachReq request) throws Exception {
String path = weChatDllService.loginQR();
return TResponse.ok(ResponseCodeEnum.SUCCESS, path);
}
}

View File

@ -0,0 +1,47 @@
package com.wechat.ferry.entity.vo.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 请求入参-下载附件信息
*
* @author wmz
* @date 2025-05-02
*/
@Data
@ApiModel(value = "wxPpWcfDownloadAttachReq", description = "个微WCF下载附件请求入参")
public class WxPpWcfDownloadAttachReq {
/**
* 消息接收人
* 消息接收人私聊为 wxidwxid_xxxxxxxxxxxxxx
* 群聊为 roomidxxxxxxxxxx@chatroom
*/
@NotBlank(message = "消息id不能为空")
@ApiModelProperty(value = "消息id")
private Long id;
/**
* 文件的extra
*/
@ApiModelProperty(value = "extra")
private String extra;
/**
* 缩略图thumb
*/
// @NotBlank(message = "thumb不能为空")
@ApiModelProperty(value = "缩略图thumb")
private String thumb;
/**
* dir (str): 存放图片的目录下载图片需要暂不支持视频
*/
@ApiModelProperty(value = "图片存放路径dir")
private String dir;
}

View File

@ -126,8 +126,12 @@ public class WeChatSocketClient {
public Response sendCmd(Request req) {
try {
// 设置发送 20 秒超时
cmdSocket.setSendTimeout(20000);
// 不知道之前设置20S有啥特殊情况这里设置超时时间 5s --> 参考Python版本
// 防止无响应的时候线程一直阻塞--ReceiveTimeout
// modify by wmz 2025-05-03
cmdSocket.setSendTimeout(5000);
cmdSocket.setReceiveTimeout(5000);
ByteBuffer bb = ByteBuffer.wrap(req.toByteArray());
cmdSocket.send(bb);
ByteBuffer ret = ByteBuffer.allocate(BUFFER_SIZE);
@ -655,7 +659,9 @@ public class WeChatSocketClient {
* @param src 加密的图片路径
* @param dir 保存图片的目录
* @return 解密图片的保存路径
* @see WeChatDllServiceImpl decryptImage
*/
@Deprecated
public String decryptImage(String src, String dir) {
Wcf.DecPath build = Wcf.DecPath.newBuilder().setSrc(src).setDst(dir).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_DECRYPT_IMAGE_VALUE).setDec(build).build();

View File

@ -6,6 +6,7 @@ import com.wechat.ferry.entity.vo.request.WxPpWcfAddFriendGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseSqlReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseTableReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDeleteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDownloadAttachReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfInviteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfPassFriendApplyReq;
@ -487,4 +488,39 @@ public interface WeChatDllService {
*/
String receiveTransfer(String weChatUid, String transferId, String transactionId);
/**
* 下载视频文件
*
* @param request 请求入参
* @return 文件路径
*
* @author wmz
* @throws java.lang.Exception
* @date 2025-05-02
*/
String downloadVideo(WxPpWcfDownloadAttachReq request) throws Exception;
/**
* 下载图片
*
* @param request 请求入参
* @return 文件路径
*
* @author wmz
* @throws java.lang.Exception
* @date 2025-05-02
*/
String downloadPicture(WxPpWcfDownloadAttachReq request) throws Exception;
/**
* 获取登录二维码
*
* @return 文件路径
*
* @author wmz
* @throws java.lang.Exception
* @date 2025-05-02
*/
String loginQR() throws Exception;
}

View File

@ -22,6 +22,7 @@ import com.wechat.ferry.entity.vo.request.WxPpWcfAddFriendGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseSqlReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDatabaseTableReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDeleteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfDownloadAttachReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfInviteGroupMemberReq;
import com.wechat.ferry.entity.vo.request.WxPpWcfPassFriendApplyReq;
@ -56,6 +57,8 @@ import com.wechat.ferry.exception.BizException;
import com.wechat.ferry.handle.WeChatSocketClient;
import com.wechat.ferry.service.WeChatDllService;
import com.wechat.ferry.utils.HttpClientUtil;
import com.wechat.ferry.utils.PathUtils;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
@ -762,5 +765,131 @@ public class WeChatDllServiceImpl implements WeChatDllService {
throw new BizException("微信客户端未登录或状态异常,请人工关闭本服务之后,退出微信客户端在重启本服务!");
}
}
@Override
public String loginQR() throws Exception {
long startTime = System.currentTimeMillis();
// 公共校验
checkClientStatus();
log.info("[登录]-[获取二维码]-入参打印:{}", "");
// # 强制等待 1 秒让数据入库避免那帮人总是嗷嗷叫超时
Thread.sleep(1000);
//第一步
Wcf.Request req = Wcf.Request.newBuilder().setFuncValue(Wcf.Functions.FUNC_REFRESH_QRCODE_VALUE)
.build();
Wcf.Response rsp = wechatSocketClient.sendCmd(req);
rsp.getStatus();
// RequestResponseBodyMethodProcessor s;
long endTime = System.currentTimeMillis();
log.info("[登录]-[获取二维码]-处理结束,耗时:{}ms", (endTime - startTime));
System.out.println(rsp.getStr());
return rsp.getStr();
}
@Override
public String downloadVideo(WxPpWcfDownloadAttachReq request) throws Exception {
long startTime = System.currentTimeMillis();
// 公共校验
checkClientStatus();
log.info("[下载]-[下载视频]-入参打印:{}", request);
int status = attachDownload(request.getId(), "", request.getThumb());
if (status != 0) {
log.info("{}:下载视频出错", request.getId());
return null;
}
//第二步检测文件,指定下载的目录
String base = PathUtils.removeExtension(request.getThumb());
String filePath = base + ".mp4";
String path = null;
for (int i = 0; i < 30; i++) {
if (new File(filePath).exists()) {
path = filePath;
log.info("视频下载完毕:{}", path);
break;
} else {
log.info("等待下载中:{}", i);
Thread.sleep(1000);
}
}
long endTime = System.currentTimeMillis();
log.info("[下载]-[下载视频]-处理结束,耗时:{}ms", (endTime - startTime));
return path;
}
@Override
public String downloadPicture(WxPpWcfDownloadAttachReq request) throws Exception {
long startTime = System.currentTimeMillis();
// 公共校验
checkClientStatus();
log.info("[下载]-[下载图片]-入参打印:{}", request);
int status = attachDownload(request.getId(), request.getExtra(), "");
if (status != 0) {
log.info("{}:下载出错", request.getId());
return null;
}
//第二步解密图片--下载图片
String filePath = decryptImage(request.getExtra(), request.getDir());
String path = null;
for (int i = 0; i < 15; i++) {
if (new File(filePath).exists()) {
log.info("图片下载完毕:{}", filePath);
path = filePath;
break;
} else {
log.info("等待下载中:{}", i);
Thread.sleep(1000);
}
}
long endTime = System.currentTimeMillis();
log.info("[下载]-[下载图片]-处理结束,耗时:{}ms", (endTime - startTime));
return path;
}
/**
* 下载附件图片视频文件这方法别直接调用
*
* @param id
* @param extra
* @param thumb
* @return int: 0 为成功, 其他失败
* @throws Exception
*/
private int attachDownload(long id, String extra, String thumb) {
try {
//# 强制等待 1 秒让数据入库避免那帮人总是嗷嗷叫超时
Thread.sleep(1000);
//第一步下载
Wcf.AttachMsg msg = Wcf.AttachMsg.newBuilder().setId(id)
.setExtra(extra).setThumb(thumb)
.build();
Wcf.Request req = Wcf.Request.newBuilder().setFuncValue(Wcf.Functions.FUNC_DOWNLOAD_ATTACH_VALUE)
.setAtt(msg)
.build();
Wcf.Response rsp = wechatSocketClient.sendCmd(req);
return rsp.getStatus();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
/**
* 解密图片
*
* @param srcPath 加密的图片路径
* @param dir 保存图片的目录
* @return 是否成功
*/
public String decryptImage(String srcPath, String dir) {
Wcf.DecPath build = Wcf.DecPath.newBuilder().setSrc(srcPath).setDst(dir).build();
Wcf.Request req = Wcf.Request.newBuilder().setFuncValue(Wcf.Functions.FUNC_DECRYPT_IMAGE_VALUE).setDec(build).build();
Wcf.Response rsp = wechatSocketClient.sendCmd(req);
if (rsp != null && rsp.getStatus() == 0) {
return rsp.getStr();
}
return null;
}
}

View File

@ -0,0 +1,68 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wechat.ferry.utils;
/**
* @date 2025-05-02
* @author zm
*/
import java.io.File;
public class PathUtils {
/**
* 分离文件路径获取不带扩展名的部分 等同于 Python os.path.splitext(thumb)[0]
*/
public static String removeExtension(String path) {
int dotIndex = path.lastIndexOf('.');
if (dotIndex == -1) {
return path;
}
return path.substring(0, dotIndex);
}
/**
* 获取文件名包含扩展名 等同于 Python os.path.basename(file_path)
*/
public static String getFileName(String path) {
File file = new File(path);
return file.getName();
}
/**
* 不存在则创建
* @param dirPath
* @return
*/
public static boolean createDir(String dirPath) {
File dir = new File(dirPath);
if (!dir.exists()) {
boolean created = dir.mkdirs(); // 创建目录包括任何不存在的父目录
if (created) {
System.out.println("目录创建成功: " + dirPath);
} else {
System.out.println("目录创建失败: " + dirPath);
return false;
}
} else {
//Nothing to do
}
return true;
}
// 示例用法
public static void main(String[] args) {
String thumb = "/tmp/video_cover.jpg";
String base = removeExtension(thumb); // /tmp/video_cover
String filePath = base + ".mp4"; // /tmp/video_cover.mp4
String fileName = getFileName(filePath); // video_cover.mp4
System.out.println("base: " + base);
System.out.println("filePath: " + filePath);
System.out.println("fileName: " + fileName);
}
}