diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/controller/WeChatDllController.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/controller/WeChatDllController.java index 1ae5a56..488d676 100644 --- a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/controller/WeChatDllController.java +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/controller/WeChatDllController.java @@ -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 queryMsgTypeList() { - // return TResponse.ok(ResponseCodeEnum.SUCCESS, list); - // } - // - // @ApiOperation(value = "解密图片", notes = "queryMsgTypeList") - // @PostMapping(value = "/list/msgType") - // public TResponse 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 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 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 loginQR(@Validated @RequestBody WxPpWcfDownloadAttachReq request) throws Exception { + String path = weChatDllService.loginQR(); + return TResponse.ok(ResponseCodeEnum.SUCCESS, path); + } + } diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/entity/vo/request/WxPpWcfDownloadAttachReq.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/entity/vo/request/WxPpWcfDownloadAttachReq.java new file mode 100644 index 0000000..be2fc17 --- /dev/null +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/entity/vo/request/WxPpWcfDownloadAttachReq.java @@ -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 { + + /** + * 消息接收人 + * 消息接收人,私聊为 wxid(wxid_xxxxxxxxxxxxxx) + * 群聊为 roomid(xxxxxxxxxx@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; + +} diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/handle/WeChatSocketClient.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/handle/WeChatSocketClient.java index 4e85961..558b24f 100644 --- a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/handle/WeChatSocketClient.java +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/handle/WeChatSocketClient.java @@ -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(); diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/WeChatDllService.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/WeChatDllService.java index 1c1cdca..9e80826 100644 --- a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/WeChatDllService.java +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/WeChatDllService.java @@ -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; + } diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/impl/WeChatDllServiceImpl.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/impl/WeChatDllServiceImpl.java index 1a7c31a..e3c8ab1 100644 --- a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/impl/WeChatDllServiceImpl.java +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/service/impl/WeChatDllServiceImpl.java @@ -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; + } } diff --git a/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/utils/PathUtils.java b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/utils/PathUtils.java new file mode 100644 index 0000000..0b707a3 --- /dev/null +++ b/clients/java/wcf-bmc/src/main/java/com/wechat/ferry/utils/PathUtils.java @@ -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); + } +}