diff --git a/README.md b/README.md index 0fb3464..d009bcb 100644 --- a/README.md +++ b/README.md @@ -1,195 +1,199 @@ -

- -

- -

- 🍬Java版微信聊天记录备份与管理工具 -

- -

- 👉 https://wx.xxccss.com/ 👈 -

- -

- Featured|HelloGitHub -

- -

- Stars Badge - Follow Badge - Fork Badge - Watchers Badge -

- -

- - - - - - - - - - - - -

- -------------------------------------------------------------------------------- - -## 📚 简介 - -wx-dump-4j是一款基于Java开发的微信数据分析工具。它准确显示好友数、群聊数和当日消息总量,提供过去15天每日消息统计,了解社交活跃度。识别展示最近一个月内互动频繁的前10位联系人。支持导出聊天记录、联系人、群聊信息,及查看**超过三天限制的朋友圈**历史记录和**找回微信好友**。 - -## 💡 主要功能 - -- 👤 **获取用户信息**:获取当前登录微信的详细信息,包括昵称、账号、手机号、邮箱、秘钥、微信Id。 -- 💬 **支持多种消息类型**:管理微信聊天对话中的文本、引用、图片、表情、卡片链接、系统消息等。 -- 📊 **综合管理**:提供微信会话、联系人、群聊与朋友圈的全面管理功能。 -- 📥 **记录导出**:支持导出微信聊天记录、联系人、已删除好友和群聊信息,便于备份和管理。 -- 📅 **查看历史朋友圈**:突破三日限制,查看更久以前的朋友圈历史记录,方便回顾和管理。 -- 📈 **微信统计功能**:展示微信好友数、群聊数及今日收发消息总量,了解社交活跃度。 -- 📊 **消息统计**:统计过去15天内每日微信消息数量,掌握长期消息交流情况。 -- 🔝 **互动联系人**:展示最近一个月互动最频繁的前10位联系人,了解重要社交联系。 -- 🧩 **消息类别占比**:展示微信消息类别占比图表,分析不同类型消息的占比情况。 -- ☁️ **关键字词云**:展示微信最近使用的关键字词云图,分析聊天内容重点。 -- 🔄 **找回已删除好友**:支持找回已删除的微信好友,恢复重要联系人。 -- 🖥️ **微信多开支持**:支持微信多开功能,方便管理多个账号,提高效率。 - -## 🚀 快速启动 - -本指南将帮助您快速启动并运行项目,无论是安装包部署还是本地部署。 - -### 环境准备 - -在开始之前,请确保您的开发环境满足以下要求: - -- 安装 [Java](https://repo.huaweicloud.com/java/jdk/11.0.2+9/jdk-11.0.2_windows-x64_bin.exe),版本为 JDK 11+。 -- 安装 [Node.js](https://nodejs.org/en/),版本为 18+。 -- 安装 [Maven](https://maven.apache.org/download.cgi),版本为 3.5.0+。 -- 选择一款开发工具,比如 IntelliJ IDEA。 - -### 二进制部署 - -- 点击下载最新版 [wx-dump-4j-bin.tar.gz](https://github.com/xuchengsheng/wx-dump-4j/releases/download/v1.1.0/wx-dump-4j-bin.tar.gz)。 - -- 解压缩 `wx-dump-4j-bin.tar.gz` 文件,并进入 `bin` 目录。 - -- 双击 `start.bat` 启动文件。 - -- 启动成功后,在浏览器中访问 [http://localhost:8080](http://localhost:8080) 以查看应用。 - -### 本地部署 - -- 下载源码: - ```bash - $ git clone https://github.com/xuchengsheng/wx-dump-4j.git - ``` -- 安装后端依赖: - ```bash - $ cd wx-dump-4j mvn clean install - ``` -- 使用开发工具(如 IntelliJ IDEA)启动 com.xcs.wx.WxDumpApplication。 -- 安装前端依赖: - ```bash - $ cd wx-dump-ui npm install - ``` -- 启动前端服务: - ```bash - $ npm run start - ``` -- 前端服务启动成功后,在浏览器中访问 http://localhost:8000 以查看应用。 - -## ⚡ 技术栈 -以下是本项目使用的技术栈: - -| 技术 | 描述 | 版本 | -|--------------|---------------------------|-----------| -| Spring Boot | Web 和 Thymeleaf 框架 | 2.7.15 | -| SQLite | 轻量级数据库 | 3.34.0 | -| Lombok | 简化 Java 代码 | 1.18.20 | -| MyBatis Plus | ORM 框架扩展 | 3.5.4.1 | -| Dynamic Datasource | 动态数据源管理 | 4.2.0 | -| Druid | 数据库连接池 | 1.2.20 | -| MapStruct | Java Bean 映射工具 | 1.4.2.Final | -| Hutool | Java 工具库 | 5.8.16 | -| JNA | Java 本地访问 | 5.8.0 | -| Protobuf | 序列化框架 | 3.25.1 | -| gRPC | RPC 框架 | 1.11.0 | -| EasyExcel | Excel 操作工具 | 3.3.3 | -| Commons Compress | 压缩和解压缩工具 | 1.19 | -| Jackson Dataformat XML | XML 解析工具 | 2.13.5 | -| Commons Lang3 | 常用工具类库 | 3.12.0 | - -## ⛔️️ 使用限制 -本软件仅适用于Windows操作系统。我们目前不支持macOS、Linux或其他操作系统。如果你在尝试在非Windows系统上运行本软件时可能遇到兼容性问题,这些问题可能导致软件无法正常运行或产生其他意外后果。 - -| 操作系统 | 支持情况 | -|:--------:|:----------:| -| Windows | 支持 | -| macOS | 不支持 | -| Linux | 不支持 | - -## ⚠️免责声明 - -本软件仅供技术研究和教育目的使用,旨在解密用户个人微信聊天记录。严禁将本软件用于任何非法目的,包括但不限于侵犯隐私权或非授权数据访问。作为软件开发者,我不对因使用或滥用本软件产生的任何形式的损失或损害承担责任。 - -## ⛵欢迎贡献! - -如果你发现任何错误🔍或者有改进建议🛠️,欢迎提交 issue 或者 pull request。你的反馈📢对于我非常宝贵💎! - -## 💻我的 GitHub 统计 - -[![Star History Chart](https://api.star-history.com/svg?repos=xuchengsheng/wx-dump-4j&type=Date)](https://star-history.com/#xuchengsheng/wx-dump-4j&Date) - -## 🎉Stargazers - -[![Stargazers123 repo roster for @xuchengsheng/wx-dump-4j](https://reporoster.com/stars/xuchengsheng/wx-dump-4j)](https://github.com/xuchengsheng/wx-dump-4j/stargazers) - -## 🎉Forkers - -[![Forkers repo roster for @xuchengsheng/wx-dump-4j](https://reporoster.com/forks/xuchengsheng/wx-dump-4j)](https://github.com/xuchengsheng/wx-dump-4j/network/members) - -## 🍱请我吃盒饭? - -作者晚上还要写博客✍️,平时还需要工作💼,如果帮到了你可以请作者吃个盒饭🥡 -
-logo -logo -
- -## ⭐️扫码关注微信公众号 - -关注后,回复关键字📲 **加群**📲,即可加入我们的技术交流群,与更多开发者一起交流学习。 - -在此,我们真诚地邀请您访问我们的GitHub项目页面,如果您觉得***wx-dump-4j***对您有帮助,请顺手点个⭐️**Star**⭐️!每一颗星星都是我们前进的动力,是对我们努力的最大肯定。非常感谢您的支持! - -
-logo> -
- -## 👀 演示图 - - - - - - - - - - - - - - - - - - - - - - -
\ No newline at end of file + +

+ +

+ +

+ 🍬Java版微信聊天记录备份与管理工具 +

+ +

+ 👉 https://wx.xxccss.com/ 👈 +

+ +

+ Featured|HelloGitHub +

+ +

+ Stars Badge + Follow Badge + Fork Badge + Watchers Badge +

+ +

+ + + + + + + + + + + + +

+ + +## 📚 简介 + +wx-dump-4j是一款基于Java开发的微信数据分析工具。它准确显示好友数、群聊数和当日消息总量,提供过去15天每日消息统计,了解社交活跃度。识别展示最近一个月内互动频繁的前10位联系人。支持导出聊天记录、联系人、群聊信息,及查看**超过三天限制的朋友圈**历史记录和**找回微信好友**。 + +## 💡 主要功能 + +- 👤 **获取用户信息**:获取当前登录微信的详细信息,包括昵称、账号、手机号、邮箱、秘钥、微信Id。 +- 💬 **支持多种消息类型**:管理微信聊天对话中的文本、引用、图片、表情、卡片链接、系统消息等。 +- 📊 **综合管理**:提供微信会话、联系人、群聊与朋友圈的全面管理功能。 +- 📥 **记录导出**:支持导出微信聊天记录、联系人、已删除好友和群聊信息,便于备份和管理。 +- 📅 **查看历史朋友圈**:突破三日限制,查看更久以前的朋友圈历史记录,方便回顾和管理。 +- 📈 **微信统计功能**:展示微信好友数、群聊数及今日收发消息总量,了解社交活跃度。 +- 📊 **消息统计**:统计过去15天内每日微信消息数量,掌握长期消息交流情况。 +- 🔝 **互动联系人**:展示最近一个月互动最频繁的前10位联系人,了解重要社交联系。 +- 🧩 **消息类别占比**:展示微信消息类别占比图表,分析不同类型消息的占比情况。 +- ☁️ **关键字词云**:展示微信最近使用的关键字词云图,分析聊天内容重点。 +- 🔄 **找回已删除好友**:支持找回已删除的微信好友,恢复重要联系人。 +- 🖥️ **微信多开支持**:支持微信多开功能,方便管理多个账号,提高效率。 + +## 🚀 快速启动 + +本指南将帮助您快速启动并运行项目,无论是安装包部署还是本地部署。 + +### 环境准备 + +在开始之前,请确保您的开发环境满足以下要求: + +- 安装 [Java](https://repo.huaweicloud.com/java/jdk/11.0.2+9/jdk-11.0.2_windows-x64_bin.exe),版本为 JDK 11+。 +- 安装 [Node.js](https://nodejs.org/en/),版本为 18+。 +- 安装 [Maven](https://maven.apache.org/download.cgi),版本为 3.5.0+。 +- 选择一款开发工具,比如 IntelliJ IDEA。 + +### 二进制部署 + +- 点击下载最新版 [wx-dump-4j-bin.tar.gz](https://github.com/xuchengsheng/wx-dump-4j/releases/download/v1.1.0/wx-dump-4j-bin.tar.gz)。 + +- 解压缩 `wx-dump-4j-bin.tar.gz` 文件,并进入 `bin` 目录。 + +- 双击 `start.bat` 启动文件。 + +- 启动成功后,在浏览器中访问 [http://localhost:8080](http://localhost:8080) 以查看应用。 + +### 本地部署 + +- 下载源码: + ```bash + $ git clone https://github.com/xuchengsheng/wx-dump-4j.git + ``` +- 安装后端依赖: + ```bash + $ cd wx-dump-4j mvn clean install + ``` +- 使用开发工具(如 IntelliJ IDEA)启动 com.xcs.wx.WxDumpApplication。 +- 安装前端依赖: + ```bash + $ cd wx-dump-ui npm install + ``` +- 启动前端服务: + ```bash + $ npm run start + ``` +- 前端服务启动成功后,在浏览器中访问 http://localhost:8000 以查看应用。 + +## ⚡ 技术栈 +以下是本项目使用的技术栈: + +| 技术 | 描述 | 版本 | +|--------------|---------------------------|-----------| +| Spring Boot | Web 和 Thymeleaf 框架 | 2.7.15 | +| SQLite | 轻量级数据库 | 3.34.0 | +| Lombok | 简化 Java 代码 | 1.18.20 | +| MyBatis Plus | ORM 框架扩展 | 3.5.4.1 | +| Dynamic Datasource | 动态数据源管理 | 4.2.0 | +| Druid | 数据库连接池 | 1.2.20 | +| MapStruct | Java Bean 映射工具 | 1.4.2.Final | +| Hutool | Java 工具库 | 5.8.16 | +| JNA | Java 本地访问 | 5.8.0 | +| Protobuf | 序列化框架 | 3.25.1 | +| gRPC | RPC 框架 | 1.11.0 | +| EasyExcel | Excel 操作工具 | 3.3.3 | +| Commons Compress | 压缩和解压缩工具 | 1.19 | +| Jackson Dataformat XML | XML 解析工具 | 2.13.5 | +| Commons Lang3 | 常用工具类库 | 3.12.0 | + +## ⛔️️ 使用限制 +本软件仅适用于Windows操作系统。我们目前不支持macOS、Linux或其他操作系统。如果你在尝试在非Windows系统上运行本软件时可能遇到兼容性问题,这些问题可能导致软件无法正常运行或产生其他意外后果。 + +| 操作系统 | 支持情况 | +|:--------:|:----------:| +| Windows | 支持 | +| macOS | 不支持 | +| Linux | 不支持 | + +## ⚠️免责声明 + +本软件仅供技术研究和教育目的使用,旨在解密用户个人微信聊天记录。严禁将本软件用于任何非法目的,包括但不限于侵犯隐私权或非授权数据访问。作为软件开发者,我不对因使用或滥用本软件产生的任何形式的损失或损害承担责任。 + +## ⛵欢迎贡献! + +如果你发现任何错误🔍或者有改进建议🛠️,欢迎提交 issue 或者 pull request。你的反馈📢对于我非常宝贵💎! + +## 💻我的 GitHub 统计 + +[![Star History Chart](https://api.star-history.com/svg?repos=xuchengsheng/wx-dump-4j&type=Date)](https://star-history.com/#xuchengsheng/wx-dump-4j&Date) + +## 🎉Stargazers + +[![Stargazers123 repo roster for @xuchengsheng/wx-dump-4j](https://reporoster.com/stars/xuchengsheng/wx-dump-4j)](https://github.com/xuchengsheng/wx-dump-4j/stargazers) + +## 🎉Forkers + +[![Forkers repo roster for @xuchengsheng/wx-dump-4j](https://reporoster.com/forks/xuchengsheng/wx-dump-4j)](https://github.com/xuchengsheng/wx-dump-4j/network/members) + +## 🍱请我吃盒饭? + +作者晚上还要写博客✍️,平时还需要工作💼,如果帮到了你可以请作者吃个盒饭🥡 +
+logo +logo +
+ +## ⭐️扫码关注微信公众号 + +关注后,回复关键字📲 **加群**📲,即可加入我们的技术交流群,与更多开发者一起交流学习。 + +在此,我们真诚地邀请您访问我们的GitHub项目页面,如果您觉得***wx-dump-4j***对您有帮助,请顺手点个⭐️**Star**⭐️!每一颗星星都是我们前进的动力,是对我们努力的最大肯定。非常感谢您的支持! + +
+logo> +
+ +## 👀 演示图 + + + + + + + + + + + + + + + + + + + + + + +
+======= +# wx-dump-4j + +>>>>>>> c4b6449ae2686772c4ab318ba75389b6a4df21ea diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/WxDumpApplication.java b/wx-dump-admin/src/main/java/com/xcs/wx/WxDumpApplication.java index 7d7fc9c..3d01fca 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/WxDumpApplication.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/WxDumpApplication.java @@ -12,33 +12,68 @@ import java.net.UnknownHostException; import java.util.Date; /** + * `WxDumpApplication` 类是整个 Spring Boot 应用的启动类,它承担着启动应用、配置相关功能以及在启动成功后向控制台输出应用启动相关信息的职责。 + * 通过 `@SpringBootApplication` 注解表明这是一个 Spring Boot 应用的主配置类,同时结合其他相关注解开启诸如事务管理、AOP 代理、定时任务等功能,方便在应用中使用对应的特性, + * 并且在 `main` 方法中完成应用上下文的创建、获取应用运行的相关配置信息(如端口、上下文路径等),最后将启动耗时、当前时间以及应用可访问的本地和网络地址等信息输出到控制台,便于开发者了解应用启动情况。 + * * @author xcs * @date 2023年12月21日 17时02分 **/ @SpringBootApplication +// 开启 Spring 框架中的事务管理功能,允许在应用中使用注解(如 `@Transactional`)来方便地管理数据库事务,确保数据操作的一致性和完整性,例如在涉及多个数据库操作的业务方法中,通过该注解可以控制这些操作要么全部成功提交,要么全部回滚。 @EnableTransactionManagement public class WxDumpApplication { public static void main(String[] args) throws UnknownHostException { + // 记录应用启动的开始时间,通过调用 `System.currentTimeMillis` 方法获取当前系统时间的毫秒数,作为应用启动的时间戳记录下来, + // 后续可以通过与应用启动完成后的时间戳做差值运算,得到应用启动所花费的时间,用于向用户展示启动耗时情况。 long startTime = System.currentTimeMillis(); + // 启动 Spring Boot 应用,调用 `SpringApplication.run` 方法,传入当前启动类(`WxDumpApplication.class`)以及启动参数(`args`)作为参数, + // 该方法会执行一系列的 Spring Boot 初始化操作,包括加载配置文件、创建 Spring 应用上下文、扫描并实例化各种组件(如 `Controller`、`Service`、`Repository` 等),最终返回一个可配置的应用上下文对象 `context`,用于后续获取应用的运行时配置等信息。 ConfigurableApplicationContext context = SpringApplication.run(WxDumpApplication.class, args); + // 记录应用启动的结束时间,同样通过调用 `System.currentTimeMillis` 方法获取当前系统时间的毫秒数,作为应用启动完成的时间戳,用于后续计算启动耗时。 long endTime = System.currentTimeMillis(); + // 获取应用运行的端口号,通过应用上下文对象(`context`)的 `getEnvironment` 方法获取应用的运行环境配置对象, + // 再调用其 `getProperty` 方法尝试获取名为 `"server.port"` 的配置属性值,如果不存在该属性,则使用默认值 `"8080"`, + // 这个端口号是应用对外提供服务所监听的端口,后续用于构建应用的访问地址信息。 String port = context.getEnvironment().getProperty("server.port", "8080"); + // 获取应用的上下文路径,通过应用上下文对象(`context`)的 `getEnvironment` 方法获取应用的运行环境配置对象, + // 再调用其 `getProperty` 方法尝试获取名为 `"server.servlet.context-path"` 的配置属性值,如果不存在该属性,则使用默认值 `""`(表示根路径), + // 上下文路径通常用于在应用部署到服务器时,作为应用的访问前缀,与端口号等信息一起构成完整的访问地址。 String contextPath = context.getEnvironment().getProperty("server.servlet.context-path", ""); + // 获取本地主机的 IP 地址,通过调用 `InetAddress.getLocalHost` 方法获取本地主机的网络地址信息对象,再调用其 `getHostAddress` 方法获取对应的 IP 地址字符串, + // 这个 IP 地址用于构建应用在网络环境下可访问的地址,区别于本地回环地址(`localhost`),方便其他设备在同一网络中访问该应用。 String localHostAddress = InetAddress.getLocalHost().getHostAddress(); + // 构建应用在本地访问的 URL 地址,按照 `http://localhost:` 的格式拼接字符串,其中 `` 是前面获取到的端口号,`` 是获取到的应用上下文路径, + // 这个地址方便开发者在本地通过浏览器等方式访问正在运行的应用,用于本地的调试和测试等操作。 String localUrl = "http://localhost:" + port + contextPath; + // 构建应用在网络环境下访问的 URL 地址,按照 `http://:` 的格式拼接字符串,其中 `` 是前面获取到的本地主机 IP 地址,`` 是端口号,`` 是应用上下文路径, + // 这个地址使得同一网络中的其他设备可以通过该地址访问到正在运行的应用,实现应用的网络访问功能。 String networkUrl = "http://" + localHostAddress + ":" + port + contextPath; + // 向控制台输出应用启动成功的提示信息以及启动耗时,通过 `System.out.println` 方法输出字符串 `"DONE successfully in "` 加上启动耗时(`endTime - startTime` 的差值,单位为毫秒)再加上 `"ms"`, + // 告知用户应用已经成功启动以及花费了多长时间完成启动过程,方便用户了解应用启动的效率情况。 System.out.println("DONE successfully in " + (endTime - startTime) + "ms"); + // 向控制台输出当前时间信息,通过创建一个 `Date` 类的实例(表示当前时间),并将其作为参数传递给 `System.out.println` 方法,输出当前的日期和时间信息, + // 让用户知晓应用启动完成时的具体时间点,方便记录和参考。 System.out.println("Time: " + new Date()); + // 输出一个用于装饰性的 ASCII 字符边框的上边框,通过 `System.out.println` 方法输出特定格式的字符串,形成一个可视化的边框效果,用于对后续输出的应用访问地址等重要信息进行视觉上的区分和美化,增强控制台输出信息的可读性。 System.out.println("╔════════════════════════════════════════════════════╗"); + // 输出应用访问地址相关的提示信息,通过 `System.out.println` 方法输出字符串 `"║ App listening at: ║"`,告知用户接下来将展示应用可以被访问的地址信息,起到引导性的作用。 System.out.println("║ App listening at: ║"); + // 输出应用在本地访问的地址信息,通过 `System.out.println` 方法输出字符串 `"║ > Local: "` 加上前面构建好的本地访问 URL 地址(`localUrl`)再加上空格进行对齐和美化, + // 向用户明确展示应用在本地可以通过什么地址进行访问,方便用户进行本地调试等操作。 System.out.println("║ > Local: " + localUrl + " "); + // 输出应用在网络环境下访问的地址信息,通过 `System.out.println` 方法输出字符串 `"║ > Network: "` 加上前面构建好的网络访问 URL 地址(`networkUrl`)再加上空格进行对齐和美化, + // 告知用户应用在网络中可以通过什么地址被其他设备访问,方便在多设备协同等场景下使用该应用。 System.out.println("║ > Network: " + networkUrl + " "); + // 输出一个空行,用于在地址信息和后续提示信息之间进行视觉上的分隔,通过 `System.out.println` 方法输出一个空字符串来实现,增强控制台输出信息的层次感和可读性。 System.out.println("║ ║"); + // 输出一个引导性的提示信息,告知用户现在可以使用上面展示的地址在浏览器中打开应用,通过 `System.out.println` 方法输出相应的字符串内容,方便用户快速了解如何进一步操作使用应用。 System.out.println("║ Now you can open browser with the above addresses↑ ║"); + // 输出一个用于装饰性的 ASCII 字符边框的下边框,通过 `System.out.println` 方法输出特定格式的字符串,与前面的上边框对应,形成一个完整的边框效果,对整个应用启动相关信息的输出进行视觉上的包裹,使其更加清晰和美观。 System.out.println("╚════════════════════════════════════════════════════╝"); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomInfoRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomInfoRepository.java index 25ffdb1..6c1382b 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomInfoRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomInfoRepository.java @@ -2,19 +2,12 @@ package com.xcs.wx.repository; import com.xcs.wx.domain.ChatRoomInfo; -/** - * 群聊详情 Repository - * - * @author xcs - * @date 2023年12月21日18:38:19 - */ +//@author xcs +// 定义ChatRoomInfoRepository接口,该接口通常用于处理与聊天群信息相关的数据访问操作 +// 遵循常见的Repository设计模式,将数据访问逻辑抽象出来,方便后续进行具体的实现与替换(比如切换不同的数据库实现等) public interface ChatRoomInfoRepository { - /** - * 查询群聊信息 - * - * @param chatRoomName 群聊名称 - * @return ChatRoomInfo - */ + //查询群聊信息 + ChatRoomInfo queryChatRoomInfo(String chatRoomName); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomRepository.java index f59bc3a..0a319c7 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ChatRoomRepository.java @@ -9,39 +9,35 @@ import com.xcs.wx.domain.vo.ExportChatRoomVO; import java.util.List; /** - * 群聊 Repository - * - * @author xcs - * @date 2023年12月21日18:38:19 + * 群聊 Repository接口,用于定义与群聊相关的数据访问操作方法。 + * 按照Repository设计模式,它将作为数据访问层与上层业务逻辑层进行交互的抽象层, + * 不同的数据库实现可以通过实现该接口来提供具体的数据操作逻辑。 */ public interface ChatRoomRepository { /** - * 查询群聊 - * + 查询群聊的方法,根据传入的查询条件来获取相应的群聊信息列表,并以分页形式返回。 + * 使用了MyBatis Plus提供的分页插件(Page类型体现)来方便地处理分页相关逻辑。 * @param chatRoomDTO 查询条件 * @return ChatRoom */ Page queryChatRoom(ChatRoomDTO chatRoomDTO); /** - * 查询群聊详情 - * + * 查询群聊详情的方法,通过传入群聊名称来获取特定群聊的详细信息。 * @param chatRoomName 群聊名称 * @return ChatRoom */ ChatRoom queryChatRoomDetail(String chatRoomName); /** - * 统计群聊数量 - * + * 统计群聊数量的方法,用于获取系统中总的群聊数量,可用于展示在后台管理页面等场景,帮助了解群聊数据的规模情况。 * @return 群聊总数 */ int countChatRoom(); /** - * 导出群聊 - * + * 导出群聊的方法,用于将群聊相关信息按照一定格式导出,比如导出为Excel文件等格式(具体取决于ExportChatRoomVO的设计以及后续实现逻辑)。 * @return ExportChatRoomVO */ List exportChatRoom(); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgRepository.java index 1dabec2..50e22a0 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgRepository.java @@ -1,18 +1,17 @@ package com.xcs.wx.repository; /** - * 联系人头像 Repository - * - * @author xcs - * @date 2024年6月18日15:31:54 + * 联系人头像 Repository接口,此接口主要用于定义与获取联系人头像相关的数据访问操作。 + * 它遵循了Repository设计模式,作为数据访问层的抽象,后续具体的数据库或存储相关实现会通过实现该接口来提供获取头像的实际逻辑。 */ public interface ContactHeadImgRepository { /** - * 获取联系人头像 - * + * 获取联系人头像的方法,通过传入用户名作为标识,来查找并获取对应的联系人头像数据。 + * 在实际应用场景中,可能是从数据库、文件系统或者其他存储介质中获取该联系人预先设置好的头像信息。 * @param usrName 用户名 - * @return 图片 + * @return byte[] + * 该字节数组代表了联系人头像对应的二进制数据,后续可以通过相应的图像处理库或框架将其转换为可视化的图片进行展示等操作。 */ byte[] getContactHeadImg(String usrName); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgUrlRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgUrlRepository.java index 5778f35..de765b8 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgUrlRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactHeadImgUrlRepository.java @@ -4,15 +4,16 @@ import java.util.List; import java.util.Map; /** - * 联系人头像 Repository - * - * @author xcs - * @date 2023年12月21日18:38:19 + * 联系人头像 Repository接口,主要用于定义获取联系人头像相关的操作方法, + * 为上层业务逻辑层提供了统一的获取联系人头像数据的抽象接口,便于后续根据具体需求实现不同的数据获取方式, + * 例如从数据库、文件服务器或者远程图片存储服务等获取头像信息。 */ public interface ContactHeadImgUrlRepository { /** - * 获取联系人头像 + * 获取联系人头像的方法,可批量获取多个用户名对应的联系人头像的URL地址信息。 + * 通过传入一个包含多个用户名的列表,查询并返回这些用户名对应的联系人头像的URL映射关系。 + * 这样可以方便一次性获取多个联系人的头像链接,适用于批量展示联系人头像等业务场景。 * * @param usrNames 用户名 * @return 头像列表 @@ -20,8 +21,8 @@ public interface ContactHeadImgUrlRepository { Map queryHeadImgUrl(List usrNames); /** - * 通过用户名查询联系人头像 - * + * 通过用户名查询联系人头像的方法,根据单个用户名来查找并获取对应的联系人头像的URL地址。 + * 在只需要获取特定某一个联系人头像的业务场景下使用该方法,更具针对性。 * @param userName 用户名 * @return 联系人头像 */ diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactLabelRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactLabelRepository.java index 40238da..6fb2c39 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactLabelRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactLabelRepository.java @@ -6,23 +6,26 @@ import java.util.List; import java.util.Map; /** - * 联系人标签 Repository - * - * @author xcs - * @date 2023年12月22日17:27:24 + * 联系人标签 Repository接口,用于定义针对联系人标签进行数据访问操作的相关方法。 + * 它在整个项目架构中处于数据访问层,为上层业务逻辑层提供了统一的获取联系人标签数据的抽象接口, + * 便于后续通过具体的数据库操作或者其他存储方式来实现这些方法,以满足业务中对联系人标签数据的使用需求。 */ public interface ContactLabelRepository { /** - * 查询联系人标签 - * + * 查询联系人标签的方法,该方法会以Map的形式返回联系人标签数据。 + * 通常情况下,Map中的键(Key)可以是联系人标签的某种唯一标识(比如标签ID或者自定义的唯一编码等), + * 值(Value)则对应着具体的标签内容描述等信息,通过这种键值对的形式方便根据标识快速查找对应的标签详情, + * 适用于一些需要根据特定标识来快速获取和操作标签的业务场景,比如通过标签ID来查找对应标签名称进行展示等。 * @return Map */ Map queryContactLabelAsMap(); /** - * 查询联系人标签 - * + * 查询联系人标签的方法,此方法将以列表(List)的形式返回联系人标签数据。 + * 列表中每个元素都是ContactLabel类型的对象,ContactLabel对象应该包含了联系人标签完整的详细信息, + * 比如标签名称、创建时间、所属分类等属性(具体属性取决于ContactLabel类的定义), + * 以列表形式返回便于进行遍历、批量操作等业务场景,例如展示所有联系人标签的列表信息给用户查看等。 * @return ContactLabel */ List queryContactLabelAsList(); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactRepository.java index a2c7006..4cbebd4 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/ContactRepository.java @@ -11,69 +11,75 @@ import java.util.Map; import java.util.Set; /** - * 联系人 Repository - * - * @author xcs - * @date 2023年12月22日 14时20分 + * 联系人 Repository接口,用于定义和联系人相关的数据访问操作方法, + * 遵循Repository设计模式,作为数据访问层与上层业务逻辑层交互的抽象接口, + * 方便后续根据不同的数据库实现或者业务需求来具体实现这些方法,从而获取相应的联系人相关信息。 **/ public interface ContactRepository { /** - * 查询联系人 - * + * 查询联系人的方法,根据传入的ContactDTO查询条件对象来获取满足条件的联系人信息, + * 并以分页形式返回结果。ContactDTO对象通常封装了各种查询条件,例如联系人姓名模糊匹配、 + * 所属分组、添加时间范围等,以此来灵活筛选出符合要求的联系人数据,返回的ContactVO对象则是用于展示给外部的视图对象, + * 包含了联系人部分关键信息(如昵称、头像链接等)且符合分页格式要求,便于前端展示等操作。 * @param contactDTO 查询条件 * @return ContactVO */ Page queryContact(ContactDTO contactDTO); /** - * 查询所有联系人 - * + * 查询所有联系人的方法,用于获取系统中全部的联系人信息, + * 返回的AllContactVO对象列表包含了所有联系人完整或部分关键的信息(具体由AllContactVO类定义决定), + * 便于在需要展示所有联系人概况等业务场景下使用,比如在联系人列表页面展示所有联系人的基本信息。 * @return AllContactVO */ List queryAllContact(); /** - * 获取联系人名称 - * + * 获取联系人名称(昵称)的方法,通过传入用户名来查找并获取对应的联系人昵称, + * 在业务场景中,用户名是唯一标识用户的关键信息,而昵称则是展示给其他用户看到的更友好的称呼, + * 该方法适用于单独获取某个特定用户的昵称情况,比如在聊天界面展示对方昵称等场景 * @param userName 用户名 * @return 昵称 */ String getContactNickname(String userName); /** - * 获取联系人名称 - * + * 获取联系人名称(昵称)的方法,和上面的getContactNickname(String userName)功能类似, + * 同样是通过传入用户名来查找对应的联系人昵称,此处可能是为了在不同业务逻辑中方便调用或者区分使用场景而定义的同名方法 * @param userName 用户名 * @return 昵称 */ String getNickName(String userName); /** - * 查询联系人与公众号的Id - * + * 查询联系人与公众号的Id的方法,用于获取满足一定条件(具体条件由业务逻辑和数据存储情况决定)的联系人与公众号相关的Id集合, + * 返回的Set集合中每个元素代表一个联系人或者公众号的唯一标识(Id), + * 该方法可能用于处理联系人与公众号之间关联关系的业务场景,比如查找同时关注了某些公众号的联系人等情况 * @return Contact */ Set getContactWithMp(); /** - * 获取联系人名称 - * + * 获取联系人名称(昵称)的方法,与前面单个用户名获取昵称的方法不同,此方法可以批量获取多个用户名对应的联系人昵称, + * 通过传入一个包含多个用户名的列表,返回一个Map来存储用户名和对应的昵称之间的映射关系, + * 便于在需要批量获取并处理多个联系人昵称的业务场景下使用,比如批量展示一组联系人的昵称等情况。 * @param userNames 用户名 * @return 昵称 */ Map getContactNickname(List userNames); /** - * 统计联系人数量 - * + * 统计联系人数量的方法,用于获取系统中联系人的总数, + * 该结果可以用于在后台管理页面展示联系人数据规模、进行数据统计分析等业务场景,帮助了解系统内联系人的总体情况。 * @return 联系人数量 */ int countContact(); /** - * 导出联系人 - * + * 导出联系人的方法,用于将联系人相关信息按照一定格式导出,例如导出为Excel文件等格式(具体取决于ExportContactVO的设计以及后续实现逻辑), + * 返回的List包含了经过整理和格式化后适合导出的联系人数据视图对象,每个对象包含了要导出的联系人关键信息, + * 如昵称、联系方式、备注等必要的展示字段,便于后续进行数据保存或者分享等操作 * @return ExportContactVO */ List exportContact(); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSContactContentRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSContactContentRepository.java index e8fd4d1..7aebc49 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSContactContentRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSContactContentRepository.java @@ -6,16 +6,18 @@ import com.xcs.wx.domain.dto.RecoverContactDTO; import java.util.List; /** - * 联系人内容 Repository - * - * @author xcs - * @date 2024年6月14日15:18:11 + * 联系人内容 Repository接口,主要用于定义针对联系人相关内容进行数据访问操作的方法, + * 在整个项目架构中处于数据访问层,为上层业务逻辑层提供统一的获取联系人内容数据的抽象接口, + * 后续可通过具体的数据库操作或者其他存储方式的实现来满足业务里对联系人内容信息获取的需求。 **/ public interface FTSContactContentRepository { /** - * 查询联系人 - * + * 查询联系人的方法,根据传入的RecoverContactDTO对象来查询并获取相应的联系人内容信息, + * 返回的结果是一个包含FTSContactContent对象的列表。RecoverContactDTO对象通常会封装一些用于筛选联系人内容的条件信息, + * 例如按照特定关键词、时间范围、联系人属性等来进行筛选,通过它可以灵活地查找出符合要求的联系人相关内容。 + * 而FTSContactContent对象应该包含了联系人内容的详细信息,比如聊天记录内容、备注信息、相关文件内容等(具体取决于FTSContactContent类的定义), + * 以列表形式返回便于进行遍历、批量处理等操作,适用于诸如展示符合特定条件的多个联系人内容等业务场景。 * @return FTSContactContent */ List queryContactContent(RecoverContactDTO recoverContactDTO); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSRecentUsedRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSRecentUsedRepository.java index 1352027..4497192 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSRecentUsedRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FTSRecentUsedRepository.java @@ -3,16 +3,17 @@ package com.xcs.wx.repository; import java.util.List; /** - * 最新使用关键字 Repository - * - * @author xcs - * @date 2024年1月23日11:20:56 + * 最新使用关键字 Repository接口,用于定义获取最新使用关键字相关数据访问操作的方法。 + * 在整个项目架构中,它处于数据访问层,旨在为上层业务逻辑层提供统一的查询最近使用关键字的抽象接口, + * 方便后续通过具体的数据库操作或者其他存储方式的实现,来获取相应的关键字数据,以满足业务中对最近使用关键字进行展示、分析等需求。 */ public interface FTSRecentUsedRepository { /** - * 查询最近使用的关键字 - * + * 查询最近使用的关键字的方法,该方法用于获取系统中最近被使用过的关键字信息, + * 返回的结果是一个包含字符串的列表,列表中的每个字符串元素代表一个关键字, + * 这些关键字是按照业务规则所定义的“最近使用”情况来筛选得出的,比如按照使用时间先后顺序,选取最近一段时间内使用过的关键字等, + * 可用于在诸如搜索历史展示、热门搜索推荐等业务场景下,为用户提供参考或者辅助操作。 * @return 返回关键字 */ List queryRecentUsedKeyWord(); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FeedsRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FeedsRepository.java index 6291363..d345085 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/FeedsRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/FeedsRepository.java @@ -5,18 +5,20 @@ import com.xcs.wx.domain.Feeds; import com.xcs.wx.domain.dto.FeedsDTO; /** - * 朋友圈 Repository - * - * @author xcs - * @date 2024年01月03日 16时56分 - **/ + * 朋友圈 Repository接口,用于定义与朋友圈相关的数据访问操作方法。 + * 在整个项目架构中,它处于数据访问层,遵循Repository设计模式,为上层业务逻辑层提供统一的获取朋友圈数据的抽象接口, + * 方便后续通过具体的数据库操作实现来获取相应的朋友圈信息,以满足业务中诸如查看朋友圈动态等需求。 + */ public interface FeedsRepository { /** - * 查询朋友圈 - * - * @param feedsDTO 分页参数 - * @return Feeds + * 查询朋友圈的方法,通过传入FeedsDTO对象作为分页参数来获取相应的朋友圈信息,并以分页形式返回结果。 + * FeedsDTO对象通常会封装一些用于筛选朋友圈动态的条件信息以及分页相关的设置(比如每页显示的条数、当前页码等), + * 借助这些条件可以灵活地从众多朋友圈动态中获取符合要求的部分进行展示等操作,返回的Page则是符合分页格式要求且包含了Feeds对象的结果集, + * Feeds对象应该包含了朋友圈动态完整的详细信息,比如发布者、发布内容、发布时间、配图等属性(具体属性取决于Feeds类的定义), + * 便于后续在前端页面等场景下进行展示以及其他业务逻辑处理。 + * @param feedsDTO 分页参数 + * @return Feeds 返回包含Feeds对象的分页结果集,方便在展示朋友圈动态时进行分页处理 */ Page queryFeeds(FeedsDTO feedsDTO); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkImageAttributeRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkImageAttributeRepository.java index 404909f..a7c2175 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkImageAttributeRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkImageAttributeRepository.java @@ -1,18 +1,18 @@ package com.xcs.wx.repository; /** - * 图片链接 Repository - * - * @author xcs - * @date 2024年01月03日 16时56分 + * 图片链接 Repository接口,主要用于定义与查询图片地址相关的数据访问操作方法。 + * 在项目架构中,它处于数据访问层,为上层业务逻辑层提供了统一获取图片地址信息的抽象接口, + * 后续可通过具体的数据库操作或者其他存储方式的实现来依据给定条件查找对应的图片地址,以满足业务中对图片展示、处理等相关需求。 **/ public interface HardLinkImageAttributeRepository { /** - * 查询图片地址 - * - * @param md5 md5 - * @return 图片 + * 查询图片地址的方法,通过传入图片的MD5值(以字节数组形式表示)作为参数,去查找并获取对应的图片地址信息。 + * MD5值通常可作为图片的一种唯一性标识,在存储和查找图片相关数据时,利用其来精准定位到特定的图片, + * 然后返回该图片对应的地址,返回的字符串类型地址可以用于后续在前端页面等场景下加载显示该图片,例如在HTML页面中通过 标签的src属性设置该地址来展示图片。 + * @param md5 md5 + * @return 图片 */ String queryHardLinkImage(byte[] md5); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkVideoAttributeRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkVideoAttributeRepository.java index 9c75d40..ad44b76 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkVideoAttributeRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/HardLinkVideoAttributeRepository.java @@ -1,18 +1,21 @@ package com.xcs.wx.repository; /** - * 视频链接 Repository - * + * 视频链接 Repository接口,其作用是定义与查询视频地址相关的数据访问操作方法。 + * 在整个项目的架构体系里,该接口处于数据访问层的位置,为上层业务逻辑层提供了统一的获取视频地址信息的抽象接口, + * 方便后续通过具体的数据库操作或者依托其他存储方式来实现根据给定条件查找对应的视频地址,以此满足业务当中涉及视频播放、管理等相关需求。 * @author xcs * @date 2024年01月03日 16时56分 **/ public interface HardLinkVideoAttributeRepository { /** - * 查询视频地址 - * + * 查询视频地址的方法,此方法接收一个以字节数组形式表示的MD5值作为参数,凭借该MD5值去查找并获取对应的视频地址信息。 + * 在实际应用场景中,MD5值常常被用作视频文件的一种唯一性标识,利用它能够精准地在存储系统中定位到特定的视频文件, + * 进而返回该视频对应的地址。而返回的视频地址是以字符串类型呈现的,这个字符串形式的地址可以应用于后续诸多业务场景中, + * 比如在前端页面中嵌入视频播放器并将该地址设置为视频源地址,从而实现视频的播放等操作。 * @param md5 md5值 - * @return 视频地址 + * @return 返回对应的视频地址 */ String queryHardLinkVideo(byte[] md5); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/MsgRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/MsgRepository.java index f45c712..336b775 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/MsgRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/MsgRepository.java @@ -8,61 +8,62 @@ import com.xcs.wx.domain.vo.TopContactsVO; import java.util.List; /** - * 消息 Repository - * + * 消息 Repository接口,用于定义和消息相关的数据访问操作方法, + * 在整个项目架构中处于数据访问层,为上层业务逻辑层提供统一的获取消息相关数据的抽象接口, + * 方便后续通过具体的数据库操作或者其他存储方式来实现这些方法,以满足业务中诸如查看聊天记录、消息统计分析等不同需求。 * @author xcs * @date 2023年12月25日15:31:37 */ public interface MsgRepository { - /** - * 根据talker与分页信息查询聊天记录 - * + * 根据talker与分页信息查询聊天记录的方法,通过传入对话者(talker)以及下一个序列号(nextSequence), + * 从存储中获取对应的聊天记录信息,并以消息(Msg)对象列表的形式返回。这里的talker通常代表参与对话的一方标识(比如用户名、联系人ID等), + * 而nextSequence可以用于实现分页查询聊天记录的功能,按照一定顺序(例如时间顺序等)定位到下一批要获取的消息, + * 返回的Msg对象列表包含了聊天记录中具体的消息内容、发送时间、消息类型等详细信息(具体取决于Msg类的定义), + * 便于在前端展示聊天记录或者进行其他与聊天记录相关的业务处理。 * @param talker 对话着 * @param nextSequence 下一个序列号 * @return Msg */ List queryMsgByTalker(String talker, Long nextSequence); - /** - * 导出数据 - * + * 导出数据的方法,根据传入的对话者(talker),将与之相关的消息数据进行导出操作,返回的同样是Msg对象列表。 + * 导出的消息数据可以按照一定格式进行处理(比如生成CSV文件、Excel文件等格式,具体取决于后续实现逻辑), + * 方便用户对特定联系人的聊天记录进行保存、分享或者备份等操作,Msg对象包含了消息的完整关键信息,确保导出的数据完整性。 * @param talker 对话着 * @return Msg */ List exportMsg(String talker); - /** - * 微信消息类型及其分布统计 - * + * 微信消息类型及其分布统计的方法,用于获取微信消息各种类型(比如文本消息、图片消息、语音消息等)以及它们在整体消息中的分布情况, + * 返回的是MsgTypeDistributionVO对象列表。MsgTypeDistributionVO对象应该包含了消息类型的名称以及对应的数量或者占比等统计信息(具体由该类定义决定), + * 此方法有助于了解微信消息的构成情况,可用于在后台管理页面展示消息类型统计图表或者进行数据分析等业务场景。 * @return MsgTypeDistributionVO */ List msgTypeDistribution(); - /** - * 统计过去 15 天每天的发送和接收消息数量 - * + * 统计过去15天每天的发送和接收消息数量的方法,返回的是CountRecentMsgsVO对象列表。 + * CountRecentMsgsVO对象应该包含了日期以及对应的发送和接收消息数量等信息(具体由该类定义决定), + * 通过该方法可以直观地了解在近期一段时间内消息的收发趋势情况,便于进行消息活跃度分析等业务场景,例如在数据分析报表中展示消息数量变化趋势。 * @return MsgTrendVO */ List countRecentMsgs(); - /** - * 最近一个月内微信互动最频繁的前10位联系人 - * + * 最近一个月内微信互动最频繁的前10位联系人的方法,用于找出在近期一个月时间里与当前用户互动最为频繁的10个联系人, + * 返回的是TopContactsVO对象列表。TopContactsVO对象应该包含了联系人相关信息以及互动频繁程度的衡量指标(比如消息交互次数等,具体由该类定义决定), + * 该方法有助于快速定位与自己联系紧密的联系人,可应用于联系人推荐、重要联系人提醒等业务场景 * @return MsgRankVO */ List topContacts(); - /** - * 统计发送消息数量 - * + * 统计发送消息数量的方法,用于获取当前用户总共发送的消息数量,返回的是一个整数,代表发送消息的具体条数, + * 该统计数据可以用于了解用户在微信上的消息输出情况,比如在用户个人数据统计页面展示发送消息总数等业务场景。 * @return 消息数量 */ int countSent(); - /** - * 统计接受消息数量 - * + * 统计接受消息数量的方法,用于获取当前用户总共接收的消息数量,同样返回一个整数,代表接收消息的具体条数, + * 此数据可用于衡量用户接收信息的情况,例如在与发送消息数量对比分析时使用,或者在用户数据统计页面展示接收消息总数等业务场景。 * @return 消息数量 */ int countReceived(); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/SessionRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/SessionRepository.java index 31f6d47..e7f363a 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/SessionRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/SessionRepository.java @@ -5,15 +5,19 @@ import com.xcs.wx.domain.vo.SessionVO; import java.util.List; /** - * 会话 Repository - * + * 会话 Repository接口,主要用于定义与会话相关的数据访问操作方法。 + * 在整个项目架构中,它处于数据访问层,为上层业务逻辑层提供了统一的获取会话信息的抽象接口, + * 便于后续通过具体的数据库操作或者其他存储方式来实现该方法,以满足业务中诸如展示所有会话列表等需求。 * @author xcs * @date 2023年12月21日17:33:19 */ public interface SessionRepository { /** - * 查询全部会话 + * 查询全部会话的方法,此方法用于获取系统中所有的会话信息, + * 返回的结果是一个包含SessionVO对象的列表。SessionVO对象通常包含了会话的关键信息, + * 比如会话对应的联系人名称或群聊名称、最后一条消息内容、最后消息发送时间等属性(具体属性取决于SessionVO类的定义), + * 以列表形式返回便于进行遍历、展示等操作,适用于在前端页面展示所有会话的列表情况,方便用户快速查看和选择进入相应会话等业务场景。 * * @return Session */ diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/repository/SqliteMasterRepository.java b/wx-dump-admin/src/main/java/com/xcs/wx/repository/SqliteMasterRepository.java index 2d0b1d0..feef96e 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/repository/SqliteMasterRepository.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/repository/SqliteMasterRepository.java @@ -1,16 +1,16 @@ package com.xcs.wx.repository; - /** - * SQLite 数据库中的一个系统表 Repository - * + * SQLite 数据库中的一个系统表 Repository接口,其主要作用是定义针对SQLite数据库系统表进行相关操作的方法。 + * 在整个项目架构里,该接口处于数据访问层,为上层业务逻辑层提供统一的与SQLite数据库系统表交互的抽象接口, + * 方便后续通过具体的数据库操作实现(例如使用JDBC等方式连接并操作SQLite数据库)来完成相应的功能,满足业务中对数据库表是否存在等情况进行判断的需求。 * @author xcs * @date 2024年6月13日09:19:24 */ public interface SqliteMasterRepository { - /** - * 查看表是否存在 - * + * 查看表是否存在的方法,通过传入要检查的表名(tableName)作为参数,去查询并判断在SQLite数据库中该表是否存在, + * 返回一个布尔值来表示结果。在实际的业务场景中,比如在进行数据库相关操作(如插入、查询数据等)前, + * 往往需要先确认目标表是否已经创建,以此避免因表不存在而导致的操作失败等问题,该方法就提供了这样一个判断机制。 * @param tableName 表名 * @return 是否存在 */ diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/ChatRoomService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/ChatRoomService.java index 8414eed..b356b1c 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/ChatRoomService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/ChatRoomService.java @@ -7,6 +7,8 @@ import com.xcs.wx.domain.vo.PageVO; /** * 群聊服务 + * 该接口定义了一系列与群聊相关的操作方法,为业务逻辑层提供了统一的服务接口规范, + * 具体的实现类需要实现这些方法来完成相应的群聊功能,例如查询群聊、获取群聊详情以及导出群聊等操作。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -15,24 +17,34 @@ public interface ChatRoomService { /** * 查询群聊 + * 根据传入的查询条件(ChatRoomDTO对象),从数据源(可能是数据库、缓存或者其他存储介质)中查询符合条件的群聊信息, + * 并以分页的形式返回查询结果,结果封装在PageVO对象中,其中ChatRoomVO用于表示单个群聊的相关视图信息, + * PageVO用于处理分页相关逻辑,包含了如总记录数、当前页数据等分页属性。 * - * @param chatRoomDTO 查询条件 - * @return ChatRoomVO + * @param chatRoomDTO 查询条件,该对象包含了各种用于筛选群聊的条件参数,例如群聊名称、创建时间范围、成员数量范围等, + * 根据具体业务需求来定义其内部的属性,通过传递该对象可以灵活地查询不同条件下的群聊信息。 + * @return ChatRoomVO,返回一个PageVO类型的对象,代表分页后的群聊信息列表,调用者可以通过该对象获取具体的群聊视图信息以及分页相关数据。 */ PageVO queryChatRoom(ChatRoomDTO chatRoomDTO); /** * 查询群聊详情 + * 根据传入的群聊名称(唯一标识一个群聊的关键信息,当然实际业务中也可能通过其他唯一标识来查询,这里以群聊名称为例), + * 从数据源中获取对应的群聊的详细信息,这些详细信息可能包含群聊的创建者、成员列表、聊天记录统计等丰富内容, + * 并将这些详细信息封装在ChatRoomDetailVO对象中返回给调用者,方便调用者展示或者进一步处理群聊的详细情况。 * - * @param chatRoomName 群聊名称 - * @return ChatRoomDetailVO + * @param chatRoomName 群聊名称,用于定位要查询详情的具体群聊,是一个表示群聊名称的字符串,在业务场景中应保证其唯一性, + * @return ChatRoomDetailVO,返回一个ChatRoomDetailVO类型的对象,其内部包含了所查询群聊的详细信息,供调用者使用。 */ ChatRoomDetailVO queryChatRoomDetail(String chatRoomName); /** * 导出群聊 + * 执行将群聊相关信息导出的操作,具体导出的格式为excel(当然也可以根据业务需求扩展为其他格式), + * 并返回导出的群聊excel文件在系统中的存储地址(文件路径),调用者可以通过该地址获取到导出的文件, + * 例如提供给用户下载或者进行其他后续处理操作。 * - * @return 群聊excel地址 + * @return 群聊excel地址,返回一个字符串类型的文件路径,指向存储导出的群聊excel文件的具体位置,方便后续进行相关操作。 */ String exportChatRoom(); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactHeadImgService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactHeadImgService.java index 64b7e0b..4d2c92f 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactHeadImgService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactHeadImgService.java @@ -2,6 +2,9 @@ package com.xcs.wx.service; /** * 联系人头像 + * 此接口用于定义与联系人头像相关的服务操作,它为具体的业务逻辑实现类提供了一个统一的规范, + * 表明需要实现获取联系人头像的功能,后续的实现类要按照这个接口定义来完成相应的业务逻辑处理, + * 例如从数据库、文件系统或者通过网络请求等方式获取指定联系人的头像数据。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -10,9 +13,13 @@ public interface ContactHeadImgService { /** * 头像 + * 该方法的主要作用是根据传入的用户名(用于唯一标识一个联系人),从相应的数据源(比如存储用户头像的数据库表、文件存储位置等) + * 获取对应的联系人头像数据,并以字节数组的形式返回。字节数组形式便于后续进行如展示头像(在前端进行字节数据解析展示图像)、 + * 保存头像到本地等各种操作。 * - * @param userName 用户名 - * @return 头像字节 + * @param userName 用户名,是一个字符串类型的参数,用于明确要获取头像的具体联系人,在业务场景中, + * 每个用户名应该唯一对应一个联系人,这样才能准确获取到相应联系人的头像信息。 + * @return 头像字节,返回一个字节数组,数组中的字节数据构成了对应联系人头像的图像信息,调用者可以利用这些字节数据进行进一步处理。 */ byte[] avatar(String userName); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactLabelService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactLabelService.java index c8f4aea..bcae49e 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactLabelService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactLabelService.java @@ -1,11 +1,13 @@ package com.xcs.wx.service; import com.xcs.wx.domain.vo.ContactLabelVO; - import java.util.List; /** * 联系人表情服务 + * 该接口定义了与联系人标签相关的服务操作规范,旨在为具体的业务逻辑实现类提供统一的接口标准, + * 后续实现类需依照此接口定义来实现相应的功能,比如从数据库、缓存或者其他存储介质中获取联系人标签信息等操作, + * 以此为整个系统提供查询联系人标签相关的服务。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -14,8 +16,12 @@ public interface ContactLabelService { /** * 查询联系人标签 + * 此方法用于从相关数据源(例如存储联系人标签信息的数据库表、配置文件等地方)中查询所有联系人的标签信息, + * 将查询到的多个联系人标签信息统一封装在ContactLabelVO对象中,然后以列表(List)的形式返回,方便调用者获取并处理这些标签数据, + * 例如可以用于在界面上展示联系人的标签分类情况、根据标签进行联系人筛选等操作。 * - * @return ContactLabel + * @return ContactLabel,返回一个List类型的列表,其中ContactLabelVO对象包含了单个联系人标签的详细信息, + * 如标签名称、标签描述、所属分组等(具体取决于业务中ContactLabelVO对象的定义属性),整个列表代表了所有查询到的联系人标签信息集合。 */ List queryContactLabel(); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactService.java index f47c01f..72388e0 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/ContactService.java @@ -5,11 +5,12 @@ import com.xcs.wx.domain.vo.AllContactVO; import com.xcs.wx.domain.vo.ContactLabelVO; import com.xcs.wx.domain.vo.ContactVO; import com.xcs.wx.domain.vo.PageVO; - import java.util.List; /** * 联系人服务 + * 该接口定义了一系列与联系人相关的操作服务,为业务逻辑层提供了统一的接口规范, + * 具体的实现类需要按照这些接口定义来实现相应功能,以满足系统中对联系人信息进行查询、导出等业务需求。 * * @author xcs * @date 2023年12月22日14:49:52 @@ -18,30 +19,43 @@ public interface ContactService { /** * 查询联系人 + * 根据传入的查询条件(ContactDTO对象),从对应的数据源(如数据库、缓存等存储介质)中查询符合条件的联系人信息, + * 并以分页的形式返回查询结果。返回结果封装在PageVO对象中,其中ContactVO用于表示单个联系人的相关视图信息, + * PageVO用于处理分页相关逻辑,包含了如总记录数、当前页数据等分页属性,方便调用者进行分页展示和数据处理。 * - * @param contactDTO 查询条件 - * @return ContactVO + * @param contactDTO 查询条件,该对象包含了各种用于筛选联系人的条件参数,例如联系人姓名、所属分组、添加时间范围等, + * 根据具体业务需求来定义其内部的属性,通过传递该对象可以灵活地查询不同条件下的联系人信息。 + * @return ContactVO,返回一个PageVO类型的对象,代表分页后的联系人信息列表,调用者可通过该对象获取具体的联系人视图信息以及分页相关数据。 */ PageVO queryContact(ContactDTO contactDTO); /** * 查询所有联系人 + * 此方法用于从数据源中获取所有联系人的信息,不进行任何筛选条件限制,将查询到的所有联系人信息分别封装在AllContactVO对象中, + * 然后以列表(List)的形式返回,方便调用者获取全部联系人数据,例如用于全量展示联系人列表或者进行整体数据统计等操作。 * - * @return AllContactVO + * @return AllContactVO,返回一个List类型的列表,其中AllContactVO对象包含了单个联系人的详细信息, + * 具体的详细信息内容取决于业务中AllContactVO对象的定义属性,整个列表代表了系统中所有联系人的信息集合。 */ List queryAllContact(); /** * 查询联系人标签 + * 从数据源(例如存储联系人标签信息的数据库表、配置文件等)中查询所有联系人的标签信息,将查询到的多个联系人标签信息统一封装在ContactLabelVO对象中, + * 然后以列表(List)的形式返回,便于调用者获取并处理这些标签数据,比如用于展示联系人的标签分类情况、根据标签进行联系人筛选等操作。 * - * @return ContactLabel + * @return ContactLabel,返回一个List类型的列表,其中ContactLabelVO对象包含了单个联系人标签的详细信息, + * 如标签名称、标签描述、所属分组等(具体取决于业务中ContactLabelVO对象的定义属性),整个列表代表了所有查询到的联系人标签信息集合。 */ List queryContactLabel(); /** * 导出联系人 + * 执行将联系人相关信息导出的操作,具体导出的格式为excel(当然也可根据业务需求扩展为其他格式), + * 并返回导出的联系人excel文件在系统中的存储地址(文件路径),调用者可通过该地址获取到导出的文件, + * 例如提供给用户下载或者进行其他后续处理操作。 * - * @return 联系人excel地址 + * @return 联系人excel地址,返回一个字符串类型的文件路径,指向存储导出的联系人excel文件的具体位置,方便后续进行相关操作。 */ String exportContact(); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/DashboardService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/DashboardService.java index f6419fc..b56f685 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/DashboardService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/DashboardService.java @@ -6,6 +6,8 @@ import java.util.List; /** * 仪表台服务 + * 该接口定义了一系列与仪表台相关的数据查询和统计功能服务,旨在为业务逻辑层提供统一的接口规范, + * 方便具体的实现类按照这些规范去实现相应功能,进而为上层应用或前端展示提供所需的仪表台相关数据,例如统计面板数据、消息分布情况等。 * * @author xcs * @date 2024年1月23日17:24:36 @@ -14,36 +16,56 @@ public interface DashboardService { /** * 统计面板 + * 此方法用于获取仪表台的统计面板相关数据,这些数据通常是对系统内一些关键指标进行综合统计后的结果, + * 例如总的消息数量、联系人数量、群聊数量等各类汇总信息,将这些统计信息封装在StatsPanelVO对象中返回, + * 方便调用者获取并展示在对应的统计面板界面上,让用户能够直观地了解系统的整体概况。 * - * @return StatsPanelVO + * @return StatsPanelVO,返回一个StatsPanelVO类型的对象,其内部包含了各种统计面板相关的数据属性(具体取决于业务中StatsPanelVO对象的定义), + * 调用者可以通过该对象获取相应的统计信息进行展示和后续处理。 */ StatsPanelVO statsPanel(); /** * 消息类型分布 + * 用于获取系统内消息类型的分布情况数据,比如文本消息、图片消息、语音消息等不同类型消息各自所占的比例或者数量等信息, + * 把这些关于消息类型分布的数据分别封装在MsgTypeDistributionVO对象中,然后以列表(List)的形式返回, + * 方便调用者对消息类型分布情况进行展示、分析或者基于此数据进行进一步的业务决策等操作。 * - * @return MsgTypeDistributionVO + * @return MsgTypeDistributionVO,返回一个List类型的列表,其中MsgTypeDistributionVO对象包含了单个消息类型相关的分布信息, + * 如消息类型名称、对应的数量或占比等(具体取决于业务中MsgTypeDistributionVO对象的定义属性),整个列表呈现了系统内消息类型的整体分布情况。 */ List msgTypeDistribution(); /** * 消息趋势 + * 主要负责获取系统内消息的趋势数据,例如近一段时间内(如近一周、近一个月等)消息数量的变化趋势情况, + * 把这些消息趋势相关的数据封装在CountRecentMsgsVO对象中,以列表(List)的形式返回,调用者可以利用这些数据来绘制消息趋势图表、 + * 分析消息量的增减变化规律等,便于对系统消息情况进行动态监测和分析。 * - * @return MsgTrendVO + * @return MsgTrendVO,返回一个List类型的列表,其中CountRecentMsgsVO对象包含了不同时间点对应的消息数量等趋势相关信息, + * 具体的信息内容根据业务需求而定,整个列表展示了消息数量随时间变化的趋势情况。 */ List countRecentMsgs(); /** * 消息排行 + * 该方法用于获取系统内消息相关的排行情况数据,例如按照联系人发送消息的数量进行排序,得到消息发送量最多的前几位联系人信息, + * 或者按照群聊的活跃程度(以消息数量衡量等方式)进行排行等,将这些排行相关的数据封装在TopContactsVO对象中,以列表(List)的形式返回, + * 方便调用者展示消息排行情况,例如在界面上展示消息最活跃的联系人榜单等操作。 * - * @return MsgTrendVO + * @return MsgTrendVO,返回一个List类型的列表,其中TopContactsVO对象包含了排行相关的具体信息, + * 如联系人名称、对应的消息数量、排名情况等(具体取决于业务中TopContactsVO对象的定义属性),整个列表呈现了系统内消息相关的排行情况。 */ List topContacts(); /** * 查询最近使用的关键字 + * 用于获取系统内用户最近使用过的关键字信息,这些关键字可能是在聊天过程中搜索、发送消息等场景下使用的关键词, + * 通过该方法可以收集并返回这些关键字,将它们封装在RecentUsedKeyWordVO对象中,以列表(List)的形式返回, + * 方便调用者进行展示或者基于关键字进行一些数据分析、功能优化等操作,例如根据热门关键字来推荐相关聊天内容等。 * - * @return 返回关键字 + * @return 返回关键字,返回一个List类型的列表,其中RecentUsedKeyWordVO对象包含了单个关键字相关的信息, + * 如关键字内容、使用频率、最后使用时间等(具体取决于业务中RecentUsedKeyWordVO对象的定义属性),整个列表呈现了系统内最近使用的关键字情况。 */ List queryRecentUsedKeyWord(); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/DatabaseService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/DatabaseService.java index 0ce9bfe..f3220dd 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/DatabaseService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/DatabaseService.java @@ -8,6 +8,9 @@ import java.util.List; /** * 注册数据源 + * 该接口主要定义了与数据源相关的操作服务,涉及数据库解密以及获取数据库列表等功能, + * 为具体的业务逻辑实现类提供统一的接口规范,以满足系统在处理数据源相关业务时的需求, + * 例如对加密的数据库进行解密操作、获取指定微信账号(wxId)对应的数据库信息等。 * * @author xcs * @date 2023年12月25日19:28:37 @@ -16,17 +19,27 @@ public interface DatabaseService { /** * 数据库解密 + * 此方法用于对数据库进行解密操作。它接收一个SseEmitter对象和一个DecryptDTO对象作为参数, + * SseEmitter对象是Spring框架中用于服务器推送事件(Server-Sent Events,简称SSE)的机制相关对象, + * 在这里可用于向客户端发送解密过程中的相关事件信息,比如解密进度、解密状态等,使得客户端能够实时了解解密情况。 + * 而DecryptDTO对象则包含了进行数据库解密所需的具体信息,例如加密密钥、数据库标识等(具体取决于业务中DecryptDTO对象的定义), + * 通过这些信息来执行相应的数据库解密逻辑。 * - * @param emitter sse发送事件对象 - * @param decryptDTO 解密信息 + * @param emitter sse发送事件对象,用于实现服务器向客户端推送解密相关事件信息,方便客户端实时知晓解密进度和状态等情况。 + * @param decryptDTO 解密信息,包含了解密数据库所需要的各项具体参数,是进行数据库解密操作的关键依据。 */ void decrypt(SseEmitter emitter, DecryptDTO decryptDTO); /** * 获取数据库列表 + * 该方法的功能是根据传入的微信账号标识(wxId),从相应的数据源(可能是配置文件、数据库存储等地方)中查询获取对应的数据库信息列表, + * 并将每个数据库的相关信息封装在DatabaseVO对象中,最后以列表(List)的形式返回。返回的列表包含了该微信账号下所有相关数据库的详细信息, + * 例如数据库名称、数据库版本、数据库存储路径等(具体取决于业务中DatabaseVO对象的定义属性),方便调用者获取并处理这些数据库相关数据, + * 比如展示给用户查看或者在其他业务逻辑中进一步使用这些数据库信息。 * - * @param wxId wxId - * @return DatabaseVO + * @param wxId wxId,用于唯一标识一个微信账号,通过该标识来定位和获取与之对应的数据库列表信息,确保获取到正确的数据库集合。 + * @return DatabaseVO,返回一个List类型的列表,其中每个DatabaseVO对象代表一个数据库的详细信息, + * 整个列表呈现了指定微信账号下的所有数据库情况。 */ List getDatabase(String wxId); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/DecryptService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/DecryptService.java index 921e596..8e25052 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/DecryptService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/DecryptService.java @@ -4,6 +4,8 @@ import com.xcs.wx.domain.bo.DecryptBO; /** * 解密服务 + * 该接口主要定义了针对特定内容进行解密操作的服务规范,旨在为具体的业务逻辑实现类提供统一的接口标准, + * 以便实现与解密相关的功能,比如根据给定的密钥对特定的输入文件等内容进行解密处理,满足系统在数据安全方面涉及解密的业务需求。 * * @author xcs * @date 2023年12月10日19:27:01 @@ -12,9 +14,10 @@ public interface DecryptService { /** * 解密 - * - * @param password 秘钥 - * @param decryptBO 输入文件 + * 此方法用于执行解密操作,它接收两个关键参数,一个是表示密钥的字符串 `password`,另一个是 `DecryptBO` 类型的对象 `decryptBO`。 + * `password` 参数作为解密的关键要素,相当于打开加密内容的“钥匙”,其具体格式和要求取决于所采用的加密解密算法以及业务规则设定, + * @param password 秘钥,是用于解密操作的关键字符串,其取值需符合对应加密算法要求的格式和规则,用于解开加密内容。 + * @param decryptBO 输入文件,实际上是一个包含了待解密内容相关关键信息的对象,为解密操作提供具体的操作对象和相关必要数据,例如文件相关的属性等情况。 */ void wechatDecrypt(String password, DecryptBO decryptBO); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/FeedsService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/FeedsService.java index 3bb33ba..7f27825 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/FeedsService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/FeedsService.java @@ -6,6 +6,8 @@ import com.xcs.wx.domain.vo.PageVO; /** * 朋友圈服务 + * 该接口定义了与朋友圈相关的操作服务,为业务逻辑层提供了统一的接口规范,具体的实现类需要按照此规范实现相应功能, + * 旨在满足系统中对朋友圈信息进行查询等业务需求,例如按照一定的分页规则查询朋友圈动态内容等操作。 * * @author xcs * @date 2024年1月3日17:25:26 @@ -14,9 +16,13 @@ public interface FeedsService { /** * 查询朋友圈 + * 依据传入的FeedsDTO对象(该对象包含了分页相关的参数,例如页码、每页显示的记录数等,也可能包含一些用于筛选朋友圈内容的其他条件参数, + * 具体取决于业务中对FeedsDTO对象的定义),从对应的数据源(如数据库、缓存等存储朋友圈数据的地方)中查询符合条件的朋友圈信息, + * 并以分页的形式返回查询结果。返回的结果会封装在PageVO对象中,其中FeedsVO用于表示单条朋友圈动态的相关视图信息, + * 比如朋友圈内容、发布者信息、发布时间等,PageVO则用于处理分页相关的逻辑,包含了如总记录数、当前页数据等分页属性,方便调用者进行分页展示和数据处理。 * - * @param feedsDTO 分页参数 - * @return FeedsVO + * @param feedsDTO 分页参数,是一个包含了各种用于查询朋友圈信息的条件参数的对象,重点包含分页相关设置,通过它可以灵活地指定查询的范围和筛选条件。 + * @return FeedsVO,返回一个PageVO类型的对象,代表分页后的朋友圈信息列表,调用者可以通过该对象获取具体的朋友圈动态视图信息以及分页相关数据。 */ PageVO queryFeeds(FeedsDTO feedsDTO); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/ImageService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/ImageService.java index 6d13906..a477dd1 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/ImageService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/ImageService.java @@ -5,6 +5,8 @@ import org.springframework.http.ResponseEntity; /** * 图片服务 + * 该接口定义了一系列与图片相关的下载操作服务,为业务逻辑层提供统一的接口规范,方便具体的实现类按照这些规范去实现相应功能, + * 以满足系统在获取图片资源时的不同需求,例如通过图片的MD5值、网络地址或者本地路径来下载对应的图片资源等操作。 * * @author xcs * @date 2024年1月18日22:06:46 @@ -13,25 +15,36 @@ public interface ImageService { /** * 通过Md5来下载图片 + * 此方法的功能是依据传入的图片的MD5值(md5参数),从相应的数据源(可能是图片存储服务器、缓存等地方,这些地方以MD5值作为图片的唯一标识来进行管理和检索) + * 查找并下载对应的图片资源。它返回一个ResponseEntity类型的对象,ResponseEntity用于封装整个HTTP响应的相关信息, + * 包括响应状态码、响应头以及响应体等,在这里可以通过它来向调用者返回下载图片操作的执行结果,例如是否成功下载、返回的图片资源情况等; + * Resource则代表实际的图片资源内容,其内部可以是对图片文件的各种抽象表示,方便后续进行如展示图片、保存图片到本地等进一步操作。 * - * @param md5 md5 - * @return ResponseEntity + * @param md5 md5,是一个字符串类型的参数,代表图片的MD5值,它在系统中作为图片的唯一标识,用于准确地定位并获取对应的图片资源,确保下载到正确的图片。 + * @return ResponseEntity,返回一个ResponseEntity类型的对象,通过该对象可以获取下载图片操作对应的HTTP响应信息以及实际的图片资源, + * 调用者可以根据响应状态码判断下载是否成功,并从Resource中获取图片内容进行后续处理。 */ ResponseEntity downloadImgMd5(String md5); /** * 下载图片 + * 根据传入的图片地址(path参数,通常是一个网络地址,比如URL形式的网络图片链接,指向图片在网络上的存储位置), + * 从该地址对应的位置下载图片资源。同样返回一个ResponseEntity对象,用于告知调用者下载操作的结果以及返回对应的图片资源, + * 方便调用者进行后续处理,例如在网页端展示图片或者将图片保存到本地等操作。 * - * @param path 图片地址 - * @return ResponseEntity + * @param path 图片地址,是一个字符串类型的参数,代表图片在网络上的存储位置链接,通过这个地址去获取对应的图片资源并下载下来。 + * @return ResponseEntity,返回一个ResponseEntity类型的对象,用于传递下载图片的结果和对应的图片资源,调用者据此进行后续操作。 */ ResponseEntity downloadImg(String path); /** * 下载图片 + * 按照传入的本地路径(localPath参数,即图片在本地文件系统中的存储路径,例如 "C:/images/myPic.jpg" 这样的形式), + * 从本地磁盘相应位置读取并获取图片资源,将其封装在ResponseEntity对象中返回,调用者可以通过该对象获取到本地图片资源, + * 用于在本地应用程序中展示、编辑或者进行其他相关操作。 * - * @param localPath 图片地址 - * @return ResponseEntity + * @param localPath 图片地址,是一个字符串类型的参数,指明图片在本地文件系统中的具体存储位置,以便准确地读取并获取对应的图片资源进行下载操作。 + * @return ResponseEntity,返回一个ResponseEntity类型的对象,用于返回从本地获取的图片资源以及对应的下载操作结果信息,方便后续使用。 */ ResponseEntity downloadImgFormLocal(String localPath); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/MsgService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/MsgService.java index f28390b..5def1a9 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/MsgService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/MsgService.java @@ -6,6 +6,8 @@ import java.util.List; /** * 消息服务 + * 该接口定义了与消息相关的操作服务,为业务逻辑层提供统一的接口规范,方便具体的实现类依据这些规范实现相应功能, + * 以此来满足系统中对消息查询、聊天记录导出等业务需求,例如获取指定聊天人的消息列表以及将和某聊天人的聊天记录导出为文件等操作。 * * @author xcs * @date 2023年12月25日15:05:09 @@ -14,18 +16,24 @@ public interface MsgService { /** * 查询消息 + * 此方法用于从对应的数据源(如数据库、缓存或者存储消息的其他介质)中,根据传入的参数查询符合条件的消息信息。 + * 将查询到的消息信息分别封装在 `MsgVO` 对象中,最后以列表(List)的形式返回,方便调用者获取并处理这些消息数据,例如在界面上展示聊天消息内容等操作。 * - * @param talker 聊天人的Id - * @param nextSequence 下一个序列号 - * @return MsgVO + * @param talker 聊天人的Id,是一个字符串类型的参数(具体类型也可能根据业务实际情况确定,这里假设为字符串便于唯一标识聊天对象),用于明确消息所属的聊天对象, + * 以此来筛选出对应的消息内容。 + * @param nextSequence 下一个序列号,通常为长整型(Long)数据,用于控制消息查询的起始位置或者顺序,辅助实现如分页查询、顺序获取等功能,按照业务规则来定位要查询的消息范围。 + * @return MsgVO,返回一个List类型的列表,其中 `MsgVO` 对象包含了单条消息的详细信息,如消息内容、发送时间、消息类型等(具体取决于业务中 `MsgVO` 对象的定义属性), + * 整个列表呈现了符合查询条件的所有消息情况。 */ List queryMsg(String talker, Long nextSequence); /** * 导出聊天记录 + * 根据传入的聊天人的Id(`talker` 参数),将与该聊天对象的聊天记录进行导出操作,具体导出的格式可能是文本文件、Excel文件或者其他适合记录聊天内容的格式(取决于业务实现), + * 并返回导出的聊天记录文件在系统中的存储地址(文件路径),调用者可以通过该地址获取到导出的文件,例如提供给用户下载或者进行其他后续处理操作。 * - * @param talker 聊天人的Id - * @return 文件地址 + * @param talker 聊天人的Id,用于唯一标识要导出聊天记录的具体聊天对象,确保准确地获取和导出与之相关的所有聊天记录信息。 + * @return 文件地址,返回一个字符串类型的文件路径,指向存储导出的聊天记录文件的具体位置,方便后续进行相关操作,如展示给用户下载链接等。 */ String exportMsg(String talker); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/RecoverContactService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/RecoverContactService.java index 76f63ce..aef66d2 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/RecoverContactService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/RecoverContactService.java @@ -2,11 +2,12 @@ package com.xcs.wx.service; import com.xcs.wx.domain.dto.RecoverContactDTO; import com.xcs.wx.domain.vo.RecoverContactVO; - import java.util.List; /** * 找回联系人服务 + * 该接口定义了与找回联系人相关的操作服务,为业务逻辑层提供统一的接口规范,具体的实现类需要按照这些规范来实现相应功能, + * 以满足系统中对找回已删除联系人以及相关数据处理的业务需求,例如查询可找回的联系人信息、导出已找回联系人相关数据等操作。 * * @author xcs * @date 2024年6月14日15:28:11 @@ -15,15 +16,21 @@ public interface RecoverContactService { /** * 找回好友 - * - * @return RecoverContactVO + * 根据传入的RecoverContactDTO对象(该对象包含了用于筛选和查找可找回好友的相关条件参数,例如好友删除时间范围、曾经的好友备注信息等, + * 具体取决于业务中RecoverContactDTO对象的定义),从对应的数据源(如数据库备份、缓存或者其他存储有联系人历史记录的地方)中查询符合条件的可找回好友信息, + * 将查询到的每个可找回好友的相关信息分别封装在RecoverContactVO对象中,最后以列表(List)的形式返回,方便调用者获取并处理这些可找回好友的数据, + * @return RecoverContactVO,返回一个List类型的列表,其中RecoverContactVO对象包含了单个可找回好友的详细信息, + * 如好友昵称、微信号、曾经的聊天记录概况等(具体取决于业务中RecoverContactVO对象的定义属性),整个列表呈现了所有符合条件的可找回好友情况。 */ List queryRecoverContact(RecoverContactDTO recoverContactDTO); /** * 删除已删除的好友 + * 执行将已找回的联系人(可能是之前误删除或者其他原因删除的好友)再次进行删除的操作,具体的删除逻辑可能涉及从数据库中移除相关记录、 + * 更新缓存等步骤(由具体实现类来完成这些具体操作细节),并返回一个地址(文件地址或者其他相关的标识地址,具体取决于业务实现方式), + * 这个地址可能用于记录此次删除操作的相关日志文件存储位置、备份文件地址等,方便后续进行查看、审计等操作,例如可以查看删除了哪些已找回的好友等情况。 * - * @return 地址 + * @return 地址,返回一个字符串类型的地址,其具体含义和用途根据业务中对该操作的设计而定,一般是与此次删除已找回好友操作相关的文件或记录的存储位置等信息。 */ String exportRecoverContact(); } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/SessionService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/SessionService.java index 60c188d..215685f 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/SessionService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/SessionService.java @@ -5,7 +5,8 @@ import com.xcs.wx.domain.vo.SessionVO; import java.util.List; /** - * 会话服务 + * 会话服务接口,定义了与会话相关的业务操作方法的规范,通常在面向接口编程的架构中, + * 具体的实现类会实现该接口并提供对应方法的实际逻辑,以此实现业务逻辑与调用方的解耦,方便进行不同实现方式的切换以及代码的可维护性和扩展性提升。 * * @author xcs * @date 2023年12月21日 17时16分 @@ -13,9 +14,12 @@ import java.util.List; public interface SessionService { /** - * 查询会话 + * 查询会话的方法,该方法用于获取会话相关信息,返回一个包含 `SessionVO` 类型对象的列表, + * 每个 `SessionVO` 对象应该封装了具体会话的详细信息(例如会话的参与方、会话的最后消息时间、未读消息数量等相关属性,具体取决于 `SessionVO` 类的定义), + * 供调用方进行展示、进一步处理或者与其他业务逻辑整合等操作。 * - * @return SessionVO + * @return List 返回一个列表,列表中的元素为 `SessionVO` 类型的对象,代表了查询到的各个会话的信息, + * 如果没有查询到任何会话,则返回一个空列表,调用方需要根据返回结果进行相应的判断和后续处理。 */ List querySession(); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/UserService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/UserService.java index 4525b8b..ac936a4 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/UserService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/UserService.java @@ -7,7 +7,9 @@ import com.xcs.wx.domain.vo.UserVO; import java.util.List; /** - * 用户服务 + * 用户服务接口,用于定义与用户相关的各类业务操作方法的规范。 + * 在分层架构中,该接口作为用户服务层对外提供的统一接口,具体的实现类会实现这些方法来完成实际的业务逻辑处理, + * 这样使得不同层次之间实现解耦,方便业务逻辑的替换、扩展以及代码的维护等操作。 * * @author xcs * @date 2023年12月21日 17时16分 @@ -15,58 +17,88 @@ import java.util.List; public interface UserService { /** - * 用户信息 + * 获取用户信息的方法。 + * 该方法用于获取当前用户(具体取决于业务逻辑中如何定义“当前用户”,可能是登录状态下的某个用户等情况)的详细信息, + * 返回的 `UserInfoVO` 对象应该封装了诸如用户的基本资料(如性别、年龄等基础信息)、权限信息、扩展信息等相关内容(具体取决于 `UserInfoVO` 类的定义), + * 供调用方进行展示、进一步分析或者与其他业务逻辑结合使用等操作。 * - * @return 用户信息 + * @return UserInfoVO 返回一个 `UserInfoVO` 类型的对象,代表了当前用户的详细信息,如果出现异常情况(比如获取信息失败等), + * 则具体的实现类需要按照相应的异常处理机制进行处理,调用方需要根据业务场景判断如何应对可能出现的异常返回情况。 */ UserInfoVO userInfo(); /** - * 用户头像 + * 获取用户头像的方法。 + * 用于获取当前用户的头像信息,返回的字符串通常应该是头像图片的存储路径(可以是本地文件路径或者网络路径等形式,具体取决于业务中头像的存储和引用方式), + * 或者是头像图片以某种编码格式(如Base64编码等)表示的字符串内容,以便调用方能够根据返回的信息展示用户头像,例如在界面上进行显示等操作。 * - * @return 头像 + * @return 头像,以字符串形式返回,代表用户头像相关的信息,如果获取头像过程中出现问题(如头像不存在、权限不足等情况), + * 实现类需要进行相应的异常处理,返回的内容应符合调用方预期的表示头像信息的格式,调用方根据返回结果进行展示等后续操作。 */ String avatar(); /** - * 用户昵称 + * 获取用户昵称的方法。 + * 其功能是获取当前用户的昵称信息,返回的字符串即为用户在系统中显示的昵称内容, + * 调用方可以将获取到的昵称用于界面展示、消息发送时的显示等与用户标识相关的各种业务场景中。 * - * @return 头像 + * @return 头像,此处方法名虽然为 `nickname`,但返回注释写的“头像”可能是笔误,正确的应该是返回当前用户的昵称信息,以字符串形式返回, + * 如果出现获取昵称失败等异常情况,由具体的实现类进行相应处理,调用方根据返回的字符串(正常情况下为有效的昵称内容)进行后续使用。 */ String nickname(); /** - * 所有用户 + * 获取所有用户信息的方法。 + * 该方法旨在获取系统中所有用户(同样具体范围取决于业务定义,可能是某个应用内的所有注册用户等情况)的相关信息, + * 返回一个包含 `UserVO` 类型对象的列表,每个 `UserVO` 对象封装了单个用户的关键信息(比如用户ID、用户名、用户状态等,具体由 `UserVO` 类的定义决定), + * 调用方可以利用这个列表进行遍历展示、统计用户数量、筛选特定用户等各种与用户集合相关的操作。 * - * @return wxIds + * @return List 返回一个列表,列表中的元素为 `UserVO` 类型的对象,代表了系统中各个用户的信息,如果没有任何用户(比如系统刚初始化还没有用户注册等情况), + * 则返回一个空列表,调用方需要根据返回的列表情况进行相应的后续判断和处理,例如判断列表是否为空来决定是否进行用户信息展示等操作。 */ List users(); /** - * 切换用户 + * 切换用户的方法。 + * 用于在多用户场景下(例如一个应用支持多个账号登录切换等情况),将当前操作的用户切换为指定的用户, + * 通过传入要切换到的用户的唯一标识(`wxId`)来确定具体要切换的目标用户,具体的切换逻辑(如更新当前用户上下文、保存当前用户状态等操作)由实现类来完成, + * 调用方只需传入正确的 `wxId` 参数即可发起切换用户的请求,方法无返回值,重点在于执行切换操作本身。 * - * @param wxId wxId + * @param wxId wxId,以字符串形式传入,代表要切换到的目标用户的唯一标识,该标识在系统中应该能够唯一确定一个用户, + * 实现类根据这个传入的 `wxId` 来查找并执行相应的用户切换操作,若传入的 `wxId` 不符合要求(比如不存在对应的用户等情况), + * 则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 */ void switchUser(String wxId); /** - * 当前用户 + * 获取当前用户标识的方法。 + * 用于返回当前正在操作或者处于活跃状态的用户的唯一标识(通常以 `wxId` 表示),这个标识可以用于在系统中区分不同用户、进行用户相关数据的关联等操作, + * 返回的字符串即为当前用户的 `wxId`,调用方可以根据这个返回值确定当前操作的用户是谁,进而进行后续与该用户相关的业务操作。 * - * @return wxId + * @return wxId,以字符串形式返回,代表当前用户的唯一标识,若出现无法确定当前用户(比如系统出现异常导致用户上下文丢失等情况), + * 则实现类需要按照相应的异常处理机制进行处理,调用方根据返回的字符串(正常情况下为有效的用户标识)进行后续使用。 */ String currentUser(); /** - * 保存用户 + * 保存用户信息的方法。 + * 用于将传入的用户相关信息(封装在 `UserBO` 对象中)保存到系统中,例如可以是新增用户时保存用户注册信息,或者修改用户信息后进行更新保存等操作, + * 具体的保存逻辑(如数据持久化到数据库、更新缓存中的用户信息等操作)由实现类根据业务需求和技术架构来完成, + * 传入的 `UserBO` 对象包含了要保存的用户详细信息(具体包含哪些属性由 `UserBO` 类的定义决定),方法无返回值,重点在于执行保存用户信息的操作本身。 * - * @param userBO 用户信息 + * @param userBO 用户信息,以 `UserBO` 类型的对象传入,该对象封装了要保存的用户的各类详细信息,实现类根据这个对象中的内容进行相应的保存操作, + * 若传入的 `UserBO` 对象不符合要求(比如缺少必要的关键信息等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 */ void saveUser(UserBO userBO); /** - * 获取微信存储路径 + * 获取微信存储路径的方法。 + * 根据传入的用户唯一标识(`wxId`)获取对应的微信相关文件(比如聊天记录文件、用户配置文件等,具体取决于业务中微信文件存储的相关定义)在系统中的存储路径, + * 返回的字符串即为该用户对应的微信存储路径,调用方可以利用这个路径去访问、操作相关的微信文件(前提是具有相应的权限),例如读取聊天记录等操作。 * - * @param wxId wxId + * @param wxId wxId,以字符串形式传入,代表用户的唯一标识,通过这个标识来查找并确定对应的微信存储路径, + * 若传入的 `wxId` 对应的用户不存在或者没有配置微信存储路径等情况,实现类需要进行相应的错误处理, + * 例如返回空字符串或者抛出异常提示调用方无法获取对应的存储路径等情况,具体处理方式根据业务要求而定。 */ String getBasePath(String wxId); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/WeChatService.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/WeChatService.java index 85831c0..b23eb0c 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/WeChatService.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/WeChatService.java @@ -5,7 +5,9 @@ import com.xcs.wx.domain.vo.WeChatConfigVO; import java.util.List; /** - * 微信服务 + * 微信服务接口,定义了一系列与微信相关的操作方法规范,旨在为外部调用者提供统一的接口来获取微信相关的各类信息、 + * 进行微信进程相关操作以及读取微信进程内存中的数据等,不同的实现类可以根据具体的业务逻辑和底层实现技术来实现这些方法, + * 以此实现微信服务功能与其他业务模块的解耦,方便代码的维护、扩展以及在不同场景下的复用。 * * @author xcs * @date 2023年12月25日09:37:30 @@ -13,58 +15,87 @@ import java.util.List; public interface WeChatService { /** - * 获取微信配置信息 + * 获取微信配置信息的方法。 + * 该方法用于获取微信应用相关的配置信息,返回一个包含 `WeChatConfigVO` 类型对象的列表, + * 每个 `WeChatConfigVO` 对象应该封装了微信配置的具体内容(例如微信的网络配置、消息提醒配置、界面显示配置等相关属性,具体取决于 `WeChatConfigVO` 类的定义), + * 调用方可以利用这些配置信息进行展示、根据配置进行相应的业务逻辑调整或者与其他模块协同工作等操作。 * - * @return WeChatDTO + * @return List 返回一个列表,列表中的元素为 `WeChatConfigVO` 类型的对象,代表了获取到的微信各项配置信息, + * 如果没有获取到任何配置信息(比如配置文件不存在、读取权限不足等情况),则返回一个空列表,调用方需要根据返回结果进行相应的判断和后续处理。 */ List readWeChatConfig(); /** - * 获取当前运行的微信进程的进程 ID。 - * - * @return 微信进程的进程 ID。如果未找到,返回空集合。 + * 获取当前运行的微信进程的进程 ID的方法。 + * 其功能是查找并返回当前正在运行的微信进程的唯一标识符(进程 ID),返回的是一个包含整数的列表, + + * @return 微信进程的进程 ID。如果未找到,返回空集合,以 `List` 形式返回,列表中的元素为微信进程的进程 ID, + * 若出现获取进程 ID 失败等异常情况(比如系统权限不足导致无法枚举进程等),实现类需要按照相应的异常处理机制进行处理, + * 调用方根据返回的列表情况进行后续使用(如遍历列表获取每个进程 ID 去做进一步操作等)。 */ List wechatPid(); /** - * 根据提供的进程 ID 找到相应进程的模块基地址。 + * 根据提供的进程 ID 找到相应进程的模块基地址的方法。 + * 此方法依据传入的微信进程的唯一标识符(进程 ID),在系统中查找该进程对应的模块(例如微信相关的动态链接库等模块)在内存中的基地址, + * 基地址是模块在进程内存空间中的起始位置,后续可以基于这个基地址结合偏移量等信息去准确访问模块内存储的各种微信相关数据(如账号信息、聊天记录等在内存中的具体位置), + * 如果找不到对应的模块基地址(比如进程不存在、模块加载异常等情况),则返回0,调用方可以根据返回值判断是否成功获取基地址,并进行后续相应的操作。 * - * @param pid 目标进程的 ID。 - * @return 进程的模块基地址,如果找不到则返回 0。 + * @param pid 目标进程的 ID,以整数形式传入,用于唯一标识要查找模块基地址的微信进程,实现类根据这个传入的进程 ID 在系统中定位到对应的进程及其相关模块信息, + * 若传入的 `pid` 不符合要求(比如不存在对应的进程等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @return 进程的模块基地址,如果找不到则返回 0,返回的长整型数值代表了微信相关模块在进程内存空间中的起始地址, + * 供调用方后续结合其他信息(如偏移量等)进一步操作使用,若出现查找基地址失败等异常情况,按照上述说明返回相应的值并由实现类进行异常处理。 */ long baseAddress(int pid); /** - * 获取指定进程ID的可执行文件版本。 - * - * @param pid 进程ID。 - * @return 文件的版本号,如果无法获取,则返回 null。 + * 获取指定进程ID的可执行文件版本的方法。 + * 用于获取指定微信进程对应的可执行文件(比如微信客户端的主执行文件)的版本号信息,返回的字符串即为版本号内容(例如 "8.0.1" 这样的格式,具体格式取决于微信版本号的定义方式), + * @param pid 进程ID,以整数形式传入,用于唯一标识要获取可执行文件版本的微信进程,实现类根据这个传入的进程 ID 定位到对应的微信可执行文件并尝试读取其版本号, + * 若传入的 `pid` 不符合要求(比如不存在对应的进程等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @return 文件的版本号,如果无法获取,则返回 null,以字符串形式返回版本号信息,若出现获取版本号失败等异常情况,按照上述说明返回相应的值并由实现类进行异常处理, + * 调用方根据返回的字符串(正常情况下为有效的版本号内容,若为 `null` 则表示获取失败)进行后续使用(如版本比较等操作)。 */ String getVersion(int pid); /** - * 根据进程ID获取微信ID。 + * 根据进程ID获取微信ID的方法。 + * 通过传入的微信进程的唯一标识符(进程 ID),获取对应的微信账号在系统中的唯一标识(微信ID),返回的字符串即为该微信账号的标识内容, + * 这个微信ID可以用于区分不同的微信账号,在涉及多账号相关的业务场景(如账号切换、消息推送定向到特定账号等操作)中发挥作用, + * 调用方根据返回的微信ID进行后续与特定微信账号相关的业务操作。 * - * @param pid 目标进程ID - * @return 微信ID + * @param pid 目标进程ID,以整数形式传入,用于唯一标识要获取微信ID的微信进程,实现类根据这个传入的进程 ID 查找并确定对应的微信账号的标识信息, + * 若传入的 `pid` 不符合要求(比如不存在对应的进程等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @return 微信ID,以字符串形式返回,代表对应微信进程的微信账号的唯一标识,若出现获取微信ID失败等异常情况,实现类需要按照相应的异常处理机制进行处理, + * 调用方根据返回的字符串(正常情况下为有效的微信ID内容)进行后续使用(如账号关联操作等)。 */ String getWxId(int pid); /** - * 从指定进程的指定内存地址读取信息。 + * 从指定进程的指定内存地址读取信息的方法。 + * 该方法尝试从给定的微信进程(通过进程 ID 标识)的特定内存地址中读取数据,返回的字符串即为从该内存地址读取到的信息内容(例如可能是微信聊天记录中的一条消息、用户昵称等信息), + * 如果读取操作失败(比如内存访问权限不足、指定内存地址无效等情况),则返回 `null`,调用方根据返回值判断是否成功读取信息并进行后续相应的处理(如展示读取到的内容或者处理读取失败的情况等)。 * - * @param pid 目标进程的 ID。 - * @param address 要读取的内存地址。 - * @return 读取到的数据,如果失败则返回 null。 + * @param pid 目标进程的 ID,以整数形式传入,用于唯一标识要从中读取内存信息的微信进程,实现类根据这个传入的进程 ID 在系统中定位到对应的进程,并基于此进行内存读取操作, + * 若传入的 `pid` 不符合要求(比如不存在对应的进程等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @param address 要读取的内存地址,以长整型形式传入,代表在目标进程内存空间中要读取数据的具体位置,实现类根据这个传入的地址在对应的微信进程内存中进行读取操作, + * 若传入的 `address` 不符合要求(比如地址超出进程内存范围等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @return 读取到的数据,如果失败则返回 null,以字符串形式返回从指定内存地址读取到的信息内容,若出现读取信息失败等异常情况,按照上述说明返回相应的值并由实现类进行异常处理, + * 调用方根据返回的字符串(正常情况下为有效的信息内容,若为 `null` 则表示读取失败)进行后续使用(如展示读取到的内容等操作)。 */ String getInfo(int pid, long address); /** - * 获取指定进程和数据库路径下的密钥 + * 获取指定进程和数据库路径下的密钥的方法。 + * 其目的是在给定的微信进程(通过进程 ID 标识)以及对应的数据库路径下,查找并获取特定的密钥信息,返回的字符串即为找到的密钥内容, + * 这个密钥可能用于后续对微信数据库(例如聊天记录数据库、用户配置数据库等)进行解密、访问控制等相关操作, + * 如果未找到符合要求的密钥(比如数据库不存在、密钥匹配失败等情况),则返回 `null`,调用方根据返回值判断是否获取到密钥并进行后续相应的处理(如使用密钥进行解密操作或者处理获取失败的情况等)。 * - * @param pid 目标进程的进程ID - * @param dbPath 数据库路径 - * @return 返回找到的密钥,如果未找到则返回null + * @param pid 目标进程的进程ID,以整数形式传入,用于唯一标识要获取密钥的微信进程,实现类根据这个传入的进程 ID 定位到对应的进程,并结合传入的数据库路径进行密钥查找操作, + * @param dbPath 数据库路径,以字符串形式传入,指定了期望获取密钥所对应的数据库所在的文件路径,实现类根据这个路径找到对应的数据库并在相关的进程内存等范围内查找密钥, + * 若传入的 `dbPath` 不符合要求(比如路径不存在、格式错误等情况),则实现类需要进行相应的错误处理,例如抛出异常提示调用方传入的参数有误等情况。 + * @return 返回找到的密钥,如果未找到则返回null,以字符串形式返回找到的密钥内容,若出现获取密钥失败等异常情况,按照上述说明返回相应的值并由实现类进行异常处理, + * 调用方根据返回的字符串(正常情况下为有效的密钥内容,若为 `null` 则表示获取失败)进行后续使用(如使用密钥解密数据库等操作)。 */ String getKey(int pid, String dbPath); -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ChatRoomServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ChatRoomServiceImpl.java index a7ee094..0e2f3aa 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ChatRoomServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ChatRoomServiceImpl.java @@ -30,7 +30,9 @@ import java.util.stream.Collectors; /** - * 群聊服务实现类 + * 群聊服务实现类,该类实现了ChatRoomService接口,用于提供具体的群聊相关业务逻辑处理方法。 + * 它依赖了多个与数据访问相关的仓库(Repository)以及映射(Mapping)类来完成数据的获取、转换等操作, + * 在整个项目架构中处于服务层,负责协调不同的数据访问操作以及业务逻辑处理,为上层的控制层(如Controller)提供服务。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -40,132 +42,184 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class ChatRoomServiceImpl implements ChatRoomService { + // 通过依赖注入获取群聊数据访问仓库,用于执行与群聊相关的数据库查询等操作 private final ChatRoomRepository chatRoomRepository; + // 群聊数据的映射类,用于在不同的领域对象(Domain Object)和视图对象(VO)之间进行数据转换 private final ChatRoomMapping chatRoomMapping; + // 联系人数据访问仓库,用于获取联系人相关信息,在处理群聊相关业务时可能会涉及到联系人的查询等操作 private final ContactRepository contactRepository; + // 群聊信息数据访问仓库,用于获取群聊特定的详细信息,比如群公告等信息 private final ChatRoomInfoRepository chatRoomInfoRepository; + // 联系人头像URL数据访问仓库,用于获取联系人头像的URL地址,在展示群聊成员头像等场景会用到 private final ContactHeadImgUrlRepository contactHeadImgUrlRepository; + /** + * 查询群聊的方法,根据传入的查询条件(ChatRoomDTO)来获取群聊信息,并进行一系列的处理后返回分页的群聊视图对象(PageVO)。 + * 处理过程包括设置群聊人数、处理头像为空的情况以及组装返回的分页数据格式等操作。 + * + * @param chatRoomDTO 查询条件,包含了诸如筛选条件、分页相关参数等信息,用于指定要查询的群聊范围和分页要求。 + * @return PageVO 返回包含ChatRoomVO对象的分页结果视图对象,其中ChatRoomVO包含了群聊的部分关键信息(如名称、头像等)用于展示, + * PageVO则封装了分页相关的信息(当前页、每页大小、总记录数等)。 + */ @Override public PageVO queryChatRoom(ChatRoomDTO chatRoomDTO) { - // 查询群聊 + // 查询群聊,使用Opt.ofNullable方法对chatRoomRepository.queryChatRoom(chatRoomDTO)的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // chatRoomRepository.queryChatRoom(chatRoomDTO)用于从数据存储(可能是数据库)中获取符合查询条件的群聊信息,以分页形式返回。 return Opt.ofNullable(chatRoomRepository.queryChatRoom(chatRoomDTO)) - // 设置群聊人数 + // 设置群聊人数,对获取到的分页群聊数据中的每一条群聊记录(ChatRoomVO对象)进行操作, + // 通过调用handleMembersCount方法,根据群聊记录中的RoomData来获取并设置群聊的成员数量。 .map(page -> { for (ChatRoomVO chatRoom : page.getRecords()) { chatRoom.setMemberCount(handleMembersCount(chatRoom.getRoomData())); } return page; }) - // 处理头像为空问题 + // 处理头像为空问题,再次遍历分页群聊数据中的每一条群聊记录, + // 如果群聊记录的头像URL(HeadImgUrl)为空字符串(即头像为空),则设置默认的联系人头像路径。 .map(page -> { for (ChatRoomVO chatRoom : page.getRecords()) { - // 如果有头像则不处理 + // 如果有头像则不处理,判断当前群聊记录的头像URL是否为空字符串,不为空则说明已有头像,直接跳过本次循环,继续处理下一个群聊记录。 if (!StrUtil.isBlank(chatRoom.getHeadImgUrl())) { continue; } - // 设置联系人头像路径 + // 设置联系人头像路径,根据群聊名称拼接出获取联系人头像的URL路径,赋值给群聊记录的头像URL属性, + // 这里假设/api/contact/headImg/avatar?userName=后面跟上群聊名称能获取到对应的联系人头像。 chatRoom.setHeadImgUrl("/api/contact/headImg/avatar?userName=" + chatRoom.getChatRoomName()); } return page; }) - // 返回分页数据 + // 返回分页数据,将处理后的分页数据(包含设置好成员数量和头像URL的群聊记录等信息)转换为自定义的PageVO格式返回, + // 构造PageVO对象时传入当前页、每页大小、总记录数以及群聊记录列表等参数。 .map(page -> new PageVO<>(page.getCurrent(), page.getSize(), page.getTotal(), page.getRecords())) - // 默认值 + // 默认值,如果前面的查询结果为空(即chatRoomRepository.queryChatRoom返回null),则创建一个默认的PageVO对象返回, + // 使用传入的ChatRoomDTO中的当前页和每页大小参数,并设置总记录数为0,记录列表为null。 .orElse(new PageVO<>(chatRoomDTO.getCurrent(), chatRoomDTO.getPageSize(), 0L, null)); } @Override public ChatRoomDetailVO queryChatRoomDetail(String chatRoomName) { - // 查询群聊详情 + // 查询群聊详情,使用Opt.ofNullable方法对chatRoomRepository.queryChatRoomDetail(chatRoomName)的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // chatRoomRepository.queryChatRoomDetail(chatRoomName)用于从数据存储中获取指定群聊名称对应的详细信息。 return Opt.ofNullable(chatRoomRepository.queryChatRoomDetail(chatRoomName)) - // 转换参数 + // 转换参数,对查询到的群聊详情数据调用chatRoomMapping的convert方法进行转换, + // 可能是将从数据存储获取到的原始数据格式转换为适合向外展示的ChatRoomDetailVO格式等操作。 .map(chatRoomMapping::convert) + // 填充其他参数,若前面的转换操作成功(即返回了非null的结果),则调用populateChatRoomDetails方法填充群聊详情的其他相关参数, + // 比如群标题、创建人、头像等信息。 // 填充其他参数 .ifPresent(this::populateChatRoomDetails) - // 填充群公告 + // 填充群公告,若前面填充其他参数操作成功,则调用populateChatRoomInfo方法填充群聊的公告相关信息, + // 比如公告发布时间、发布人等信息。 .ifPresent(this::populateChatRoomInfo) - // 填充群成员 + // 填充群成员,若前面填充群公告操作成功,则调用populateChatRoomMember方法填充群聊的成员相关信息, + // 比如成员昵称、头像等信息。 .ifPresent(this::populateChatRoomMember) - // 设置默认值 + // 设置默认值,如果前面的任何一步操作结果为空(比如查询不到群聊详情等情况),则返回null作为最终结果。 .orElse(null); } @Override public String exportChatRoom() { - // 文件路径 + // 文件路径,调用DirUtil的getExportDir方法获取导出文件的目录路径,并指定文件名(这里是"群聊.xlsx"), + // 该方法内部可能根据项目配置等确定最终的导出文件存储位置,并拼接文件名生成完整路径。 String filePath = DirUtil.getExportDir("群聊.xlsx"); - // 创建文件 + // 创建文件,获取文件路径对应的文件对象,并调用FileUtil.mkdir方法创建其父目录(如果不存在的话), + // 确保导出文件的目录存在,避免后续写入文件时因目录不存在而出现错误。 FileUtil.mkdir(new File(filePath).getParent()); - // 导出 + // 导出,使用EasyExcel框架进行Excel文件的写入操作,指定文件路径、写入的对象类型(ExportChatRoomVO)以及工作表名称("sheet1"), + // 并通过lambda表达式设置要写入的数据来源(即调用chatRoomRepository的exportChatRoom方法获取要导出的群聊数据列表,并对其进行群聊人数设置等处理)。 EasyExcel.write(filePath, ExportChatRoomVO.class) .sheet("sheet1") .doWrite(() -> { List exportChatRoomVOS = chatRoomRepository.exportChatRoom(); - // 设置群聊人数 + // 设置群聊人数,遍历要导出的每个群聊视图对象(ExportChatRoomVO), + // 调用handleMembersCount方法,根据群聊视图对象中的RoomData来获取并设置群聊的成员数量。 for (ExportChatRoomVO exportChatRoomVO : exportChatRoomVOS) { exportChatRoomVO.setMemberCount(handleMembersCount(exportChatRoomVO.getRoomData())); } return exportChatRoomVOS; }); - // 返回写入后的文件 + // 返回写入后的文件,将导出文件的完整路径返回,以便后续使用(如告知用户文件位置等)。 return filePath; } /** - * 填充群聊信息 - * - * @param chatRoomDetailVo 群聊详情VO + // 填充群聊信息的方法,用于为群聊详情视图对象(ChatRoomDetailVO)填充一些基本的群聊信息, + // 包括群标题、创建人、群头像等信息,通过调用相关的数据访问仓库(ContactRepository、ContactHeadImgUrlRepository)获取对应的数据进行填充。 + // @param chatRoomDetailVo 群聊详情VO对象,要填充信息的目标对象,传入的对象在方法内会被修改并填充相应的信息。 + */ private void populateChatRoomDetails(ChatRoomDetailVO chatRoomDetailVo) { - // 群标题 + // 群标题,调用contactRepository的getContactNickname方法,根据群聊详情视图对象中的群聊名称(ChatRoomName)获取对应的联系人昵称作为群标题, + // 赋值给群聊详情视图对象的ChatRoomTitle属性,这样就填充了群聊的标题信息。 chatRoomDetailVo.setChatRoomTitle(contactRepository.getContactNickname(chatRoomDetailVo.getChatRoomName())); - // 创建人 + // 创建人,调用contactRepository的getContactNickname方法,根据群聊详情视图对象中预留的创建人标识(reserved2字段,具体含义由业务决定)获取对应的联系人昵称作为创建人, + // 赋值给群聊详情视图对象的CreateBy属性,完成创建人信息的填充。 chatRoomDetailVo.setCreateBy(contactRepository.getContactNickname(chatRoomDetailVo.getReserved2())); - // 群头像 + // 群头像,调用contactHeadImgUrlRepository的queryHeadImgUrlByUserName方法,根据群聊详情视图对象中的群聊名称获取对应的联系人头像URL地址, + // 赋值给群聊详情视图对象的HeadImgUrl属性,实现群头像信息的填充。 chatRoomDetailVo.setHeadImgUrl(contactHeadImgUrlRepository.queryHeadImgUrlByUserName(chatRoomDetailVo.getChatRoomName())); } + /** - * 填充群公告 - * - * @param chatRoomDetailVo 群聊详情VO + // 填充群公告的方法,用于为群聊详情视图对象(ChatRoomDetailVO)填充群公告相关的信息, + // 包括查询群公告、转换参数、处理发布时间、设置发布人等操作,涉及到多个数据访问和数据转换的步骤。 + // @param chatRoomDetailVo 群聊详情VO对象,要填充公告信息的目标对象,传入的对象在方法内会被修改并填充相应的信息。 + */ private void populateChatRoomInfo(ChatRoomDetailVO chatRoomDetailVo) { - // 查询群公告 + // 查询群公告,调用chatRoomInfoRepository的queryChatRoomInfo方法,根据群聊详情视图对象中的群聊名称获取对应的群聊公告信息(ChatRoomInfo对象), + // 该对象包含了群公告的相关原始数据,比如公告内容、发布时间等信息(具体属性由ChatRoomInfo类定义)。 ChatRoomInfo chatRoomInfo = chatRoomInfoRepository.queryChatRoomInfo(chatRoomDetailVo.getChatRoomName()); - // 转换参数 + // 转换参数,调用chatRoomMapping的convert方法对查询到的群聊公告信息进行转换(可能转换为适合展示的视图对象格式等),得到ChatRoomInfoVO对象, + // ChatRoomInfoVO对象应该包含了更适合向外展示的公告相关属性,比如经过格式化后的发布时间等。 ChatRoomInfoVO chatRoomInfoVO = chatRoomMapping.convert(chatRoomInfo); - // 发布时间 + // 发布时间,获取转换后的群聊公告视图对象(ChatRoomInfoVO)中的公告发布时间属性(以时间戳形式存储,单位可能是秒,具体由业务决定), + // 后续会根据该时间戳进行时间格式的转换等操作。 Long announcementPublishTime = chatRoomInfoVO.getAnnouncementPublishTime(); - // 处理发布时间 + // 处理发布时间,如果公告发布时间不为空且大于0(表示有有效的发布时间),则将时间戳转换为日期时间格式的字符串, + // 通过调用DateUtil.formatDateTime方法,传入根据时间戳创建的Date对象,将其格式化为便于展示的日期时间字符串, + // 并赋值给ChatRoomInfoVO对象的strAnnouncementPublishTime属性,用于向外展示公告发布时间。 if (ObjUtil.isNotEmpty(announcementPublishTime) && announcementPublishTime > 0) { chatRoomInfoVO.setStrAnnouncementPublishTime(DateUtil.formatDateTime(new Date(announcementPublishTime * 1000L))); } - // 发布人 + // 发布人,调用contactRepository的getContactNickname方法,根据群聊公告中编辑人标识(announcementEditor字段,具体含义由业务决定)获取对应的联系人昵称作为发布人, + // 赋值给ChatRoomInfoVO对象的announcementPublisher属性,完成发布人信息的填充。 chatRoomInfoVO.setAnnouncementPublisher(contactRepository.getContactNickname(chatRoomInfoVO.getAnnouncementEditor())); - // 设置群聊公告 + // 设置群聊公告,将填充好信息的群聊公告视图对象(ChatRoomInfoVO)赋值给群聊详情视图对象(ChatRoomDetailVO)的ChatRoomInfo属性, + // 完成群公告信息在群聊详情视图对象中的填充,以便后续向外展示群公告相关内容。 chatRoomDetailVo.setChatRoomInfo(chatRoomInfoVO); } /** - * 填充群成员 - * - * @param chatRoomDetailVo 群聊详情VO + * 填充群成员的方法,用于为群聊详情视图对象(ChatRoomDetailVO)填充群成员相关的信息, + * 涉及到使用protobuf解析群聊数据、获取群成员列表、查询群成员头像和昵称等操作,并通过映射类将相关信息整合填充到群聊详情视图对象中。 + * @param chatRoomDetailVo 群聊详情VO对象,要填充成员信息的目标对象,传入的对象在方法内会被修改并填充相应的信息。 */ private void populateChatRoomMember(ChatRoomDetailVO chatRoomDetailVo) { try { - // 使用protobuf解析RoomData字段 + // 使用protobuf解析RoomData字段,调用ChatRoomProto.ChatRoom的parseFrom方法将群聊详情视图对象中的群聊数据(以字节数组形式存储)解析为ChatRoomProto.ChatRoom对象, + // ChatRoomProto.ChatRoom对象包含了群聊的详细结构信息,比如成员列表等,以便后续从中提取群成员相关数据。 ChatRoomProto.ChatRoom chatRoom = ChatRoomProto.ChatRoom.parseFrom(chatRoomDetailVo.getRoomData()); - // 获得群成员 + // 获得群成员,从解析后的ChatRoomProto.ChatRoom对象中获取群成员列表(成员信息以ChatRoomProto.Member对象表示), + // 后续可基于该列表进一步获取每个成员的相关属性,如微信Id等 List membersList = chatRoom.getMembersList(); - // 群成员的微信Id + // 群成员的微信Id,通过流操作提取群成员列表中每个成员的微信Id,使用map方法将ChatRoomProto.Member对象转换为对应的微信Id字符串, + // 最后通过collect方法将所有微信Id字符串收集到一个列表中,方便后续批量查询头像和昵称等操作。 List memberWxIds = membersList.stream().map(ChatRoomProto.Member::getWxId).collect(Collectors.toList()); - // 群成员头像 + // 群成员头像,调用contactHeadImgUrlRepository的queryHeadImgUrl方法,批量查询群成员微信Id对应的头像URL地址, + // 返回一个头像URL地址的映射关系(键为微信Id,值为头像URL),用于后续填充群成员的头像信息。 Map headImgUrlMap = contactHeadImgUrlRepository.queryHeadImgUrl(memberWxIds); - // 群成员昵称 + // 群成员昵称,调用contactRepository的getContactNickname方法,批量查询群成员微信Id对应的联系人昵称, + // 返回一个联系人昵称的映射关系(键为微信Id,值为昵称),用于后续填充群成员的昵称信息。 Map contactNicknameMap = contactRepository.getContactNickname(memberWxIds); - // 群成员 + // 调用chatRoomMapping的convert方法,将群成员列表(membersList,包含了群聊中各个成员的详细信息,以ChatRoomProto.Member对象表示)、 + // 头像URL映射关系(headImgUrlMap,通过微信Id能获取对应头像的URL地址)以及联系人昵称映射关系(contactNicknameMap,通过微信Id能获取对应联系人昵称)作为参数传入, + // 该convert方法内部会根据这些数据进行整合、转换等操作,最终将处理好的群成员信息填充到chatRoomDetailVo对象的Members属性中,完成群成员相关信息在群聊详情视图对象中的设置。 + chatRoomDetailVo.setMembers(chatRoomMapping.convert(membersList, headImgUrlMap, contactNicknameMap)); } catch (InvalidProtocolBufferException e) { log.error("Failed to parse RoomData", e); @@ -173,23 +227,30 @@ public class ChatRoomServiceImpl implements ChatRoomService { } /** - * 获取群聊人数 + * 获取群聊人数的方法,其功能是根据传入的群聊数据(roomData)来计算并返回该群聊包含的成员数量。 * - * @param roomData 群聊数据 - * @return 群聊人数 + * @param roomData 群聊数据,以字节数组形式传入,该字节数组中包含了群聊的详细信息(例如成员列表等内容),其格式遵循protobuf的相关定义,是从更底层存储中获取到的原始数据表示形式。 + * @return 群聊人数,返回值为Integer类型,表示该群聊中成员的个数,若解析群聊数据失败或者解析后获取不到成员列表信息,则返回0。 */ private Integer handleMembersCount(byte[] roomData) { - // 使用protobuf解析RoomData字段 + // 使用protobuf解析RoomData字段,先声明一个ChatRoomProto.ChatRoom类型的变量chatRoomProto,并初始化为null, + // 用于后续存储解析出来的ChatRoomProto.ChatRoom对象(若解析成功的话),该对象能够以结构化形式呈现群聊的详细内容,方便获取成员相关信息。 ChatRoomProto.ChatRoom chatRoomProto = null; try { + // 尝试调用ChatRoomProto.ChatRoom的parseFrom方法将传入的字节数组形式的roomData解析为ChatRoomProto.ChatRoom对象, + // 该方法是protobuf提供的用于将符合其定义格式的字节数据反序列化为对应Java对象的操作,若解析过程中数据格式不符合要求等则会抛出InvalidProtocolBufferException异常。 chatRoomProto = ChatRoomProto.ChatRoom.parseFrom(roomData); } catch (InvalidProtocolBufferException e) { log.error("parse roomData failed", e); } - // 空校验 + // 空校验,判断前面解析得到的chatRoomProto对象是否为null,若为null则说明群聊数据解析失败或者解析后无法得到有效的ChatRoomProto.ChatRoom对象, + // 在这种情况下,按照业务逻辑返回0作为群聊人数,表示无法获取到有效的成员数量信息。 if (chatRoomProto == null) { return 0; } + // 如果chatRoomProto对象不为null,说明群聊数据解析成功且能获取到有效的群聊结构信息, + // 通过调用chatRoomProto的getMembersList方法获取群聊中的成员列表(成员信息以ChatRoomProto.Member对象表示), + // 然后返回该成员列表的大小(即成员个数),以此作为群聊的人数返回给调用者。 return chatRoomProto.getMembersList().size(); } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactHeadImgServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactHeadImgServiceImpl.java index 7dc9295..f30bc14 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactHeadImgServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactHeadImgServiceImpl.java @@ -7,7 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** - * 联系人头像实现类 + * 联系人头像实现类,该类实现了ContactHeadImgService接口,用于提供具体的联系人头像相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调数据访问层(通过ContactHeadImgRepository)与上层调用者之间的交互,以获取联系人头像数据。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -15,12 +16,23 @@ import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class ContactHeadImgServiceImpl implements ContactHeadImgService { +public class ContactHeadImgServiceImpl implements ContactHeadImgService { +// 通过依赖注入获取联系人头像数据访问仓库(ContactHeadImgRepository), +// 后续将通过该仓库提供的方法来获取联系人头像相关的数据,比如从数据库或者其他存储介质中查询头像信息。 private final ContactHeadImgRepository contactHeadImgService; - + /** + * 获取联系人头像的方法,根据传入的用户名(userName)来获取对应的联系人头像数据。 + * 该方法内部直接调用了ContactHeadImgRepository中的getContactHeadImg方法来执行具体的数据获取操作, + * 并将获取到的结果(以字节数组形式表示的头像数据)返回给调用者。 + * + * @param userName 用户名,用于唯一确定要获取头像的具体联系人,不同的用户名对应不同联系人的头像数据。 + * @return byte[] 返回值为字节数组类型,代表联系人头像对应的二进制数据,调用者可以根据该数据进行后续的展示等操作,比如转换为图片并显示在界面上。 + */ @Override public byte[] avatar(String userName) { + // 调用contactHeadImgService(即ContactHeadImgRepository)的getContactHeadImg方法,传入用户名作为参数, + // 以获取对应的联系人头像数据,并将获取到的数据直接返回给该方法的调用者。 return contactHeadImgService.getContactHeadImg(userName); } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactLabelServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactLabelServiceImpl.java index a418397..21560eb 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactLabelServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactLabelServiceImpl.java @@ -12,7 +12,9 @@ import java.util.List; import java.util.Optional; /** - * 联系人表情服务实现 + * 联系人表情服务实现类,该类实现了ContactLabelService接口,用于提供具体的联系人表情相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调数据访问层(通过ContactLabelRepository)与上层调用者之间的交互, + * 并借助映射类(ContactLabelMapping)对获取到的数据进行转换,最终为上层提供处理后的联系人表情相关数据。 * * @author xcs * @date 2023年12月31日18:18:58 @@ -21,13 +23,31 @@ import java.util.Optional; @RequiredArgsConstructor public class ContactLabelServiceImpl implements ContactLabelService { + // 通过依赖注入获取联系人标签数据访问仓库(ContactLabelRepository), + // 后续将利用该仓库提供的方法从数据库或其他存储介质中获取联系人标签相关的数据。 private final ContactLabelRepository contactLabelRepository; + // 通过依赖注入获取联系人标签映射类(ContactLabelMapping), + // 该类用于将从数据访问层获取到的原始联系人标签数据转换为适合向外展示的ContactLabelVO对象形式,方便业务逻辑处理和展示。 private final ContactLabelMapping contactLabelMapping; + /** + * 查询联系人标签的方法,用于获取联系人标签相关信息,并经过数据转换后以ContactLabelVO对象列表的形式返回。 + * 首先从数据访问层获取联系人标签数据列表,然后利用映射类进行转换,如果获取的数据为空,则返回一个空的列表。 + * + * @return List 返回一个包含ContactLabelVO对象的列表,其中每个ContactLabelVO对象代表一个联系人标签的相关信息, + * 若没有查询到任何联系人标签数据,则返回一个空列表(即Collections.emptyList())。 + */ @Override public List queryContactLabel() { + // 使用Optional.ofNullable方法对contactLabelRepository.queryContactLabelAsList()的返回结果进行包装, + // 如果返回结果不为null,则进行后续的链式操作,若为null则直接返回默认值(由orElse指定的Collections.emptyList())。 + // contactLabelRepository.queryContactLabelAsList()用于从数据存储(如数据库)中获取联系人标签数据列表,其返回的列表元素类型取决于底层数据存储的具体格式。 return Optional.ofNullable(contactLabelRepository.queryContactLabelAsList()) + // 调用contactLabelMapping的convert方法对获取到的联系人标签数据列表进行转换, + // 将原始的联系人标签数据转换为ContactLabelVO对象列表,ContactLabelVO对象通常包含了更适合向外展示的联系人标签相关属性,如标签名称、描述等。 .map(contactLabelMapping::convert) + // 如果前面的操作(即获取数据并转换)结果为空(比如数据库中没有联系人标签数据,导致获取的列表为null或者转换后列表为空), + // 则返回一个空的列表(Collections.emptyList()),确保返回值始终是一个List类型的对象,避免出现空指针异常等情况。 .orElse(Collections.emptyList()); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactServiceImpl.java index 2c3a959..8efb4aa 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ContactServiceImpl.java @@ -18,7 +18,9 @@ import java.util.*; import java.util.stream.Collectors; /** - * 联系人服务实现类 + * 联系人服务实现类,该类实现了ContactService接口,用于提供具体的联系人相关业务逻辑处理方法。 + * 它依赖了多个与数据访问相关的仓库(Repository)以及映射(Mapping)类来完成数据的获取、转换等操作, + * 在整个项目架构中处于服务层,负责协调不同的数据访问操作以及业务逻辑处理,为上层的控制层(如Controller)提供服务。 * * @author xcs * @date 2023年12月22日 14时42分 @@ -27,58 +29,105 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class ContactServiceImpl implements ContactService { + // 通过依赖注入获取联系人数据访问仓库,用于执行与联系人相关的数据库查询等操作,比如获取联系人列表、详情等信息。 private final ContactRepository contactRepository; + // 通过依赖注入获取联系人标签数据访问仓库,用于查询联系人标签相关的数据,例如标签的名称、对应关系等信息。 private final ContactLabelRepository contactLabelRepository; + // 通过依赖注入获取联系人标签映射类,用于将从数据存储中获取到的原始联系人标签数据转换为适合向外展示的ContactLabelVO对象形式,方便后续业务逻辑处理和展示。 private final ContactLabelMapping contactLabelMapping; + /** + * 根据传入的查询条件(ContactDTO)分页查询联系人信息,并进行相关处理后返回分页的联系人视图对象(PageVO)。 + * 处理过程包括获取联系人标签映射关系,为每个联系人设置对应的标签名称等操作,若查询结果为空则返回默认的分页对象。 + * + * @param contactDTO 查询条件,包含了诸如筛选条件、分页相关参数等信息,用于指定要查询的联系人范围和分页要求。 + * @return PageVO 返回包含ContactVO对象的分页结果视图对象,其中ContactVO包含了联系人的部分关键信息(如姓名、头像等)用于展示, + * PageVO则封装了分页相关的信息(当前页、每页大小、总记录数等)。 + */ @Override public PageVO queryContact(ContactDTO contactDTO) { - // 分页查询联系人 + // 分页查询联系人,使用Optional.ofNullable方法对contactRepository.queryContact(contactDTO)的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // contactRepository.queryContact(contactDTO)用于从数据存储(可能是数据库)中获取符合查询条件的联系人信息,以分页形式返回。 return Optional.ofNullable(contactRepository.queryContact(contactDTO)) + // 对获取到的分页联系人数据进行处理,通过lambda表达式来实现具体的操作逻辑。 .map(page -> { + // 查询联系人标签映射关系,调用contactLabelRepository.queryContactLabelAsMap()方法从数据存储中获取联系人标签的映射关系, + // 返回一个以标签ID为键(String类型)、标签名称为值(String类型)的Map,方便后续根据标签ID查找对应的名称来设置联系人的标签信息。 Map contactLabelMap = contactLabelRepository.queryContactLabelAsMap(); for (ContactVO contactVO : page.getRecords()) { - // 分割当前联系人标签 + // 分割当前联系人标签,将联系人视图对象(ContactVO)中存储的标签ID列表(以逗号分隔的字符串形式)进行分割, + // 转换为标签ID字符串的流(Stream),然后通过map操作,根据contactLabelMap查找每个标签ID对应的标签名称, + // 再使用filter过滤掉为空字符串的标签名称(可能存在无效的标签ID情况),最后收集为一个包含有效标签名称的列表。 List labels = Arrays.stream(contactVO.getLabelIdList().split(",")) .map(contactLabelMap::get) .filter(StrUtil::isNotBlank) .collect(Collectors.toList()); - // 设置标签 + // 设置标签,将处理好的标签名称列表设置到联系人视图对象(ContactVO)的Labels属性中,完成联系人标签信息的设置。 contactVO.setLabels(labels); } - // 返回分页数据 + // 返回分页数据,将处理后的分页数据(包含设置好标签信息的联系人记录等信息)转换为自定义的PageVO格式返回, + // 构造PageVO对象时传入当前页、每页大小、总记录数以及联系人记录列表等参数。 return new PageVO<>(page.getCurrent(), page.getSize(), page.getTotal(), page.getRecords()); }) - // 默认值 + // 默认值,如果前面的查询结果为空(即contactRepository.queryContact返回null),则创建一个默认的PageVO对象返回, + // 使用传入的ContactDTO中的当前页和每页大小参数,并设置总记录数为0,记录列表为null。 .orElse(new PageVO<>(contactDTO.getCurrent(), contactDTO.getPageSize(), 0L, null)); } + /** + * 查询所有联系人的方法,直接调用contactRepository的queryAllContact方法从数据存储中获取所有联系人信息, + * 并以包含AllContactVO对象的列表形式返回,AllContactVO对象包含了联系人的详细或关键信息用于展示等操作。 + * + * @return List 返回一个包含AllContactVO对象的列表,其中的元素即为查询到的所有联系人信息, + * 列表长度取决于系统中实际存在的联系人数量。 + */ @Override public List queryAllContact() { return contactRepository.queryAllContact(); } + /** + * 查询联系人标签的方法,用于获取联系人标签相关信息,并经过数据转换后以ContactLabelVO对象列表的形式返回。 + * 首先从数据访问层获取联系人标签数据列表,然后利用映射类进行转换,如果获取的数据为空,则返回一个空的列表。 + * + * @return List 返回一个包含ContactLabelVO对象的列表,其中每个ContactLabelVO对象代表一个联系人标签的相关信息, + * 若没有查询到任何联系人标签数据,则返回一个空列表(即Collections.emptyList())。 + */ @Override public List queryContactLabel() { - // 查询标签 + // 查询标签,使用Optional.ofNullable方法对contactLabelRepository.queryContactLabelAsList()的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // contactLabelRepository.queryContactLabelAsList()用于从数据存储(如数据库)中获取联系人标签数据列表,其返回的列表元素类型取决于底层数据存储的具体格式。 return Optional.ofNullable(contactLabelRepository.queryContactLabelAsList()) - // 转换参数 + // 转换参数,调用contactLabelMapping的convert方法对获取到的联系人标签数据列表进行转换, + // 将原始的联系人标签数据转换为ContactLabelVO对象列表,ContactLabelVO对象通常包含了更适合向外展示的联系人标签相关属性,如标签名称、描述等。 .map(contactLabelMapping::convert) - // 设置默认值 + // 如果前面的操作(即获取数据并转换)结果为空(比如数据库中没有联系人标签数据,导致获取的列表为null或者转换后列表为空), + // 则返回一个空的列表(Collections.emptyList()),确保返回值始终是一个List类型的对象,避免出现空指针异常等情况。 .orElse(Collections.emptyList()); } + /** + * 导出联系人信息的方法,将联系人相关数据导出为Excel文件,设置好文件路径、创建必要的目录,并调用contactRepository的exportContact方法获取要导出的联系人数据后写入文件, + * 最后返回导出文件的路径。 + * + * @return String 返回导出的Excel文件的完整路径,可用于提示用户下载或者记录导出文件的位置等操作。 + */ @Override public String exportContact() { - // 文件路径 + // 文件路径,调用DirUtil的getExportDir方法获取导出文件的目录路径,并指定文件名(这里是"微信好友.xlsx"), + // 该方法内部可能根据项目配置等确定最终的导出文件存储位置,并拼接文件名生成完整路径。 String filePath = DirUtil.getExportDir("微信好友.xlsx"); - // 创建文件 + // 创建文件,获取文件路径对应的文件对象,并调用FileUtil.mkdir方法创建其父目录(如果不存在的话), + // 确保导出文件的目录存在,避免后续写入文件时因目录不存在而出现错误。 FileUtil.mkdir(new File(filePath).getParent()); - // 导出 + // 导出,使用EasyExcel框架进行Excel文件的写入操作,指定文件路径、写入的对象类型(ExportContactVO)以及工作表名称("sheet1"), + // 并通过方法引用(contactRepository::exportContact)调用contactRepository的exportContact方法获取要导出的联系人数据列表,然后将这些数据写入到Excel文件中。 EasyExcel.write(filePath, ExportContactVO.class) .sheet("sheet1") .doWrite(contactRepository::exportContact); - // 返回写入后的文件 + // 返回写入后的文件,将导出文件的完整路径返回,以便后续使用(如告知用户文件位置等)。 return filePath; } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DashboardServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DashboardServiceImpl.java index 074fdd6..c2decdc 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DashboardServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DashboardServiceImpl.java @@ -14,7 +14,9 @@ import java.util.Map; import java.util.stream.Collectors; /** - * 仪表台服务实现类 + * 仪表台服务实现类,该类实现了DashboardService接口,用于提供具体的仪表台相关业务逻辑处理方法。 + * 它依赖了多个与数据访问相关的仓库(Repository)来获取不同维度的数据,如消息、联系人、群聊等信息, + * 在整个项目架构中处于服务层,负责协调数据访问层与上层调用者之间的交互,将获取和处理后的数据提供给上层使用,例如展示在仪表台页面等场景。 * * @author xcs * @date 2024年01月23日 17时24分 @@ -24,73 +26,130 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class DashboardServiceImpl implements DashboardService { + // 通过依赖注入获取消息数据访问仓库,用于执行与消息相关的数据库查询等操作,比如获取消息数量、消息类型分布等信息。 private final MsgRepository msgRepository; + // 通过依赖注入获取联系人数据访问仓库,用于查询联系人相关的数据,例如联系人数量、联系人昵称等信息。 private final ContactRepository contactRepository; + // 通过依赖注入获取群聊数据访问仓库,用于获取群聊相关的数据,像群聊数量等信息。 private final ChatRoomRepository chatRoomRepository; + // 通过依赖注入获取联系人头像URL数据访问仓库,用于查询联系人头像的URL地址,在展示联系人头像等场景会用到。 private final ContactHeadImgUrlRepository contactHeadImgUrlRepository; + // 通过依赖注入获取最新使用关键字数据访问仓库,用于获取最近使用过的关键字相关数据。 private final FTSRecentUsedRepository recentUsedRepository; + /** + * 获取统计面板数据的方法,用于收集并返回一些关键数据,包括联系人数量、群聊数量、今日发送和接收消息数量等, + * 将这些数据封装到StatsPanelVO对象中返回,以便在仪表台的统计面板等地方展示这些基础统计信息。 + * + * @return StatsPanelVO 返回包含联系人数量、群聊数量、今日发送消息数量以及今日接收消息数量的StatsPanelVO对象, + * 这些数据可直观展示系统中相关信息的基本情况。 + */ @Override public StatsPanelVO statsPanel() { - // 联系人数量 + // 联系人数量,调用contactRepository的countContact方法从数据存储(如数据库)中获取系统中联系人的总数,返回一个整数表示联系人的数量。 int contact = contactRepository.countContact(); - // 群聊数量 + // 群聊数量,调用chatRoomRepository的countChatRoom方法从数据存储中获取系统中群聊的总数,同样返回一个整数代表群聊的数量。 int chatRoom = chatRoomRepository.countChatRoom(); - // 今日发送消息数量 + // 今日发送消息数量,调用msgRepository的countSent方法从数据存储中获取今日发送的消息总数,返回的整数表示发送消息的具体条数。 int sent = msgRepository.countSent(); - // 今日接收消息数量 + // 今日接收消息数量,调用msgRepository的countReceived方法从数据存储中获取今日接收的消息总数,返回的整数表示接收消息的具体条数。 int received = msgRepository.countReceived(); - // 返回数据 + // 返回数据,使用获取到的联系人数量、群聊数量、今日发送消息数量以及今日接收消息数量创建一个StatsPanelVO对象并返回, + // 该对象将这些关键数据进行了封装,方便后续在展示层等进行统一展示和使用。 return new StatsPanelVO(contact, chatRoom, sent, received); } + /** + * 获取微信消息类型及其分布统计数据的方法,先从消息数据访问仓库获取消息类型分布数据,然后对其进行重新分组、聚合等处理, + * 最终以MsgTypeDistributionVO对象列表的形式返回处理后的消息类型分布情况,若获取的数据为空则返回一个空列表。 + * + * @return List 返回一个包含MsgTypeDistributionVO对象的列表,每个对象包含消息类型及其对应的数量信息, + * 展示了微信消息的类型分布情况,若没有获取到相关数据则返回一个空列表(即Collections.emptyList())。 + */ @Override public List msgTypeDistribution() { - // 微信消息类型及其分布统计 + // 微信消息类型及其分布统计,使用Opt.ofNullable方法对msgRepository.msgTypeDistribution()的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // msgRepository.msgTypeDistribution()用于从数据存储中获取微信消息各种类型(比如文本消息、图片消息等)以及它们在整体消息中的分布情况, + // 返回的是MsgTypeDistributionVO对象列表,其中包含了消息类型名称以及对应的数量等统计信息。 return Opt.ofNullable(msgRepository.msgTypeDistribution()) - // 重新分组一次 + // 重新分组一次,对获取到的MsgTypeDistributionVO对象列表进行流处理,通过Collectors.groupingBy方法按照消息类型(MsgTypeDistributionVO::getType)进行重新分组, + // 然后使用Collectors.summingInt方法对每个类型对应的数量(MsgTypeDistributionVO::getValue)进行求和,得到一个以消息类型为键、该类型消息总数为值的新Map。 .map(msgTypes -> msgTypes.stream().collect(Collectors.groupingBy(MsgTypeDistributionVO::getType, Collectors.summingInt(MsgTypeDistributionVO::getValue)))) - // 聚合并返回List + // 聚合并返回List,对上一步得到的汇总后的Map进行流处理,将每个键值对(代表一个消息类型及其总数)转换为MsgTypeDistributionVO对象, + // 再通过collect方法收集为一个包含MsgTypeDistributionVO对象的列表,这样就得到了重新整理后的消息类型分布数据列表。 .map(summedMap -> summedMap.entrySet().stream().map(entry -> new MsgTypeDistributionVO(entry.getKey(), entry.getValue())).collect(Collectors.toList())) - // 默认值 + // 默认值,如果前面的操作中获取的原始消息类型分布数据为空(即msgRepository.msgTypeDistribution()返回null), + // 则返回一个空的列表(Collections.emptyList()),确保返回值始终是一个List类型的对象,避免出现空指针异常等情况。 .orElse(Collections.emptyList()); } + /** + * 获取最近消息数量统计数据的方法,直接调用msgRepository的countRecentMsgs方法从数据存储中获取过去15天每天的发送和接收消息数量统计信息, + * 并以CountRecentMsgsVO对象列表的形式返回,该列表中的每个对象包含了某一天的消息数量相关数据。 + * + * @return List 返回一个包含CountRecentMsgsVO对象的列表,其中每个元素记录了某一天的发送和接收消息数量相关数据, + * 可用于展示消息数量在近期一段时间内的变化趋势等情况。 + */ @Override public List countRecentMsgs() { return msgRepository.countRecentMsgs(); } + /** + * 获取最近一个月内微信互动最频繁的前10位联系人数据的方法,先从消息数据访问仓库获取互动频繁的联系人原始数据, + * 然后查询这些联系人的昵称和头像URL地址,为每个联系人设置昵称和头像信息,若获取的数据为空则返回一个空列表。 + * + * @return List 返回一个包含TopContactsVO对象的列表,其中的元素代表了互动最频繁的前10位联系人相关信息, + * 包含了联系人的用户名、昵称、头像等信息,若没有获取到相关数据则返回一个空列表(即Collections.emptyList())。 + */ @Override public List topContacts() { - // 最近一个月内微信互动最频繁的前10位联系人 + // 最近一个月内微信互动最频繁的前10位联系人,使用Opt.ofNullable方法对msgRepository.topContacts()的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // msgRepository.topContacts()用于从数据存储中获取在最近一个月时间里与当前用户互动最为频繁的10个联系人相关信息,返回的是TopContactsVO对象列表。 return Opt.ofNullable(msgRepository.topContacts()) - // 处理昵称&头像 + // 处理昵称&头像,对获取到的TopContactsVO对象列表进行处理,通过lambda表达式实现具体的处理逻辑,主要是查询并设置联系人的昵称和头像信息。 .map(topContacts -> { - // 获取所有的用户名 + // 获取所有的用户名,通过流操作提取TopContactsVO对象列表中每个联系人对应的用户名,收集为一个字符串列表, + // 以便后续批量查询联系人的昵称和头像URL地址。 List userNames = topContacts.stream().map(TopContactsVO::getUserName).collect(Collectors.toList()); - // 联系人昵称 + // 联系人昵称,调用contactRepository的getContactNickname方法,批量查询用户列表对应的联系人昵称, + // 返回一个以用户名为键(String类型)、对应联系人昵称为值(String类型)的Map,方便后续根据用户名查找昵称来设置联系人的昵称信息。 Map nicknameMap = contactRepository.getContactNickname(userNames); - // 联系人头像 + // 联系人头像,调用contactHeadImgUrlRepository的queryHeadImgUrl方法,批量查询用户列表对应的联系人头像URL地址, + // 返回一个以用户名为键(String类型)、对应头像URL为值(String类型)的Map,用于后续根据用户名查找头像URL来设置联系人的头像信息。 Map headImgUrlMap = contactHeadImgUrlRepository.queryHeadImgUrl(userNames); - // 遍历处理 + // 遍历处理,遍历TopContactsVO对象列表中的每个联系人对象,为其设置昵称和头像信息。 for (TopContactsVO topContact : topContacts) { - // 设置昵称 + // 设置昵称,根据当前联系人的用户名从nicknameMap中获取对应的昵称,并设置到TopContactsVO对象的NickName属性中,完成昵称信息的设置。 topContact.setNickName(nicknameMap.get(topContact.getUserName())); - // 设置头像 + // 设置头像,根据当前联系人的用户名从headImgUrlMap中获取对应的头像URL,并设置到TopContactsVO对象的HeadImgUrl属性中,完成头像信息的设置。 topContact.setHeadImgUrl(headImgUrlMap.get(topContact.getUserName())); } return topContacts; }) - // 默认值 + // 默认值,如果前面获取的原始互动最频繁联系人数据为空(即msgRepository.topContacts()返回null), + // 则返回一个空的列表(Collections.emptyList()),确保返回值始终是一个List类型的对象,避免出现空指针异常等情况。 .orElse(Collections.emptyList()); } + /** + * 获取最近使用关键字数据的方法,先从最新使用关键字数据访问仓库获取最近使用过的关键字字符串列表, + * 然后将每个关键字字符串转换为RecentUsedKeyWordVO对象,最后收集为一个包含RecentUsedKeyWordVO对象的列表并返回。 + * + * @return List 返回一个包含RecentUsedKeyWordVO对象的列表,其中每个对象代表一个最近使用过的关键字相关信息, + * 可用于展示最近使用关键字的情况,比如在搜索历史等场景中展示给用户查看。 + */ @Override public List queryRecentUsedKeyWord() { return recentUsedRepository.queryRecentUsedKeyWord() + // 将获取到的关键字字符串列表转换为流,方便后续进行操作。 .stream() + // 对每个关键字字符串调用RecentUsedKeyWordVO的构造方法创建一个RecentUsedKeyWordVO对象, + // 这样就将原始的关键字字符串转换为包含更多相关属性(如果类中有定义的话)的对象形式。 .map(RecentUsedKeyWordVO::new) + // 将转换后的RecentUsedKeyWordVO对象收集为一个列表,最终返回该列表,其包含了所有最近使用过的关键字相关信息。 .collect(Collectors.toList()); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DatabaseServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DatabaseServiceImpl.java index ac6f868..2528e17 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DatabaseServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DatabaseServiceImpl.java @@ -41,7 +41,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; /** - * 注册数据源实现类 + * 注册数据源实现类,该类实现了DatabaseService接口以及ApplicationRunner接口。 + * 实现DatabaseService接口用于提供与数据库相关的业务逻辑操作,比如解密数据库、注册数据源等功能。 + * 实现ApplicationRunner接口可以在Spring Boot应用启动后执行特定的初始化或业务逻辑代码。 + * 在整个项目架构中处于服务层,负责协调不同服务(如解密服务、微信服务、用户服务等)之间的交互,完成复杂的业务流程,例如数据库解密及后续数据源注册等操作。 * * @author xcs * @date 2023年12月25日19:30:26 @@ -51,45 +54,58 @@ import java.util.stream.Stream; @RequiredArgsConstructor public class DatabaseServiceImpl implements DatabaseService, ApplicationRunner { + // 通过依赖注入获取解密服务,用于执行数据库解密相关的具体操作,比如对微信数据库文件进行解密处理。 private final DecryptService decryptService; + // 通过依赖注入获取微信服务,可能用于获取微信相关的配置信息、密钥等内容,辅助数据库解密等操作的进行。 private final WeChatService weChatService; + // 通过依赖注入获取用户服务,用于执行与用户相关的操作,例如保存用户信息等,在数据库解密完成后可能需要记录相关用户信息。 private final UserService userService; + @Override public void decrypt(SseEmitter emitter, DecryptDTO decryptDTO) { - // 文件分隔符 + // 文件分隔符,通过获取默认文件系统的分隔符,用于后续拼接文件路径,确保在不同操作系统下路径的正确性(不同系统文件分隔符不同,如Windows是'\', Linux是'/')。 String separator = FileSystems.getDefault().getSeparator(); - // 微信目录 + // 微信目录,根据传入的解密参数(DecryptDTO)中的基础路径(basePath)和微信用户ID(wxId)拼接出微信数据库文件所在的完整目录路径。 String dbPath = decryptDTO.getBasePath() + separator + decryptDTO.getWxId(); - // 秘钥 + // 秘钥,调用weChatService的getKey方法,传入进程ID(pid)和微信数据库文件目录路径(dbPath)作为参数,尝试获取微信数据库的解密密钥, + // 该密钥用于后续对数据库文件的解密操作。 String key = weChatService.getKey(decryptDTO.getPid(), dbPath); - // 获取微信秘钥失败 + // 获取微信秘钥失败,判断获取到的密钥是否为空字符串,如果为空则表示获取微信密钥失败,进入以下处理流程。 if (StrUtil.isBlank(key)) { try { + // 向前端发送错误响应信息,通过emitter发送一个ResponseVO对象,其中包含错误码(-1)和错误消息("获取微信秘钥失败,请稍后再试。"), + // 并指定消息的媒体类型为APPLICATION_JSON,告知前端获取密钥出现问题。 emitter.send(ResponseVO.error(-1, "获取微信秘钥失败,请稍后再试。"), MediaType.APPLICATION_JSON); } catch (IOException e) { + // 如果在发送响应信息过程中出现IO异常,将该异常包装为运行时异常抛出,以便在更上层进行统一的异常处理。 throw new RuntimeException(e); } finally { + // 无论是否发送成功,都标记SseEmitter完成,释放相关资源,结束与前端的此次通信。 emitter.complete(); } return; } - // 扫描目录 + // 扫描目录,在微信数据库文件所在目录下,拼接出要扫描的具体子目录路径(这里是MSG目录),后续将遍历该目录下的文件进行解密等操作。 String scanPath = dbPath + separator + "MSG"; - // 输出目录 + // 输出目录,调用DirUtil的getDbDir方法,传入微信用户ID(wxId)作为参数,获取解密后数据库文件的输出目录路径,用于存放解密后的文件。 String outputPath = DirUtil.getDbDir(decryptDTO.getWxId()); - // 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录 + // 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录,通过Paths.get方法将字符串形式的扫描路径转换为Path对象, + // 然后利用Files.walk方法获取一个包含该路径下所有文件和子目录的Stream,方便后续对其进行过滤和处理操作。 try (Stream stream = Files.walk(Paths.get(scanPath))) { - // 过滤出非目录的文件 + // 过滤出非目录的文件,调用getWeChatDb方法,传入文件流(stream)和输出目录路径(outputPath)作为参数, + // 对遍历到的文件和目录进行筛选,只保留文件(非目录)信息,并转换为DecryptBO对象列表,该列表包含了要解密的文件相关信息。 List decryptBOList = getWeChatDb(stream, outputPath); - // 遍历解密 + // 遍历解密,对获取到的要解密的文件列表进行循环遍历,逐个处理每个文件的解密及相关后续操作。 for (int i = 0; i < decryptBOList.size(); i++) { DecryptBO decryptBO = decryptBOList.get(i); - // 计算进度百分比 + // 计算进度百分比,根据当前处理的文件索引(i)和文件列表总大小(decryptBOList.size())计算出当前的解密进度百分比, + // 用于后续向前端发送进度信息展示给用户。 int currentProgress = ((i + 1) * 100) / decryptBOList.size(); - // 当前要处理的文件 + // 当前要处理的文件,根据DecryptBO对象中的输入文件路径信息创建一个File对象,代表当前正在处理的数据库文件实体,方便后续获取文件相关属性。 File currentFile = new File(decryptBO.getInput()); - // 响应给前端的对象 + // 响应给前端的对象,使用建造者模式(Builder Pattern)创建一个DecryptVO对象,设置该对象的文件名、文件大小、文件总数以及当前进度等属性, + // 用于将解密进度相关信息封装后发送给前端展示。 DecryptVO decryptVO = DecryptVO.builder() .fileName(FileUtil.getName(currentFile)) .fileSize(FileUtil.readableFileSize(currentFile)) @@ -97,15 +113,21 @@ public class DatabaseServiceImpl implements DatabaseService, ApplicationRunner { .currentProgress(currentProgress) .build(); try { + // 向前端发送解密进度信息,通过emitter将包含解密进度信息的ResponseVO对象发送给前端,告知前端当前文件的解密进度情况, + // 消息的媒体类型同样指定为APPLICATION_JSON。 emitter.send(ResponseVO.ok(decryptVO), MediaType.APPLICATION_JSON); } catch (IOException ignore) { + // 如果发送过程中出现IO异常,暂时忽略该异常(这里可能是考虑到不影响整体的解密流程继续进行,只是前端可能无法及时获取到这一次的进度信息)。 } - // 解密 + // 解密,调用decryptService的wechatDecrypt方法,传入获取到的解密密钥(key)和当前要解密的文件信息对象(decryptBO), + // 执行对当前数据库文件的解密操作。 decryptService.wechatDecrypt(key, decryptBO); - // 注册数据源 + // 注册数据源,调用registerDataSource方法,传入解密后文件的输出路径(decryptBO.getOutput()), + // 将解密后的数据库文件注册为应用中的数据源,以便后续可以通过该数据源进行数据库相关操作。 registerDataSource(decryptBO.getOutput()); } - // 保存用户 + // 保存用户,调用userService的saveUser方法,使用建造者模式创建一个UserBO对象并传入相关用户信息(如基础路径、账号、手机号等), + // 将解密相关的用户信息保存到系统中,可能用于记录哪些用户的数据库文件已经完成了解密等操作。 userService.saveUser(UserBO.builder() .basePath(decryptDTO.getBasePath()) .account(decryptDTO.getAccount()) @@ -115,21 +137,36 @@ public class DatabaseServiceImpl implements DatabaseService, ApplicationRunner { .wxId(decryptDTO.getWxId()) .build()); } catch (Exception e) { + // 如果在整个解密、发送进度信息、注册数据源或保存用户信息等操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"Sqlite database decryption failed"表示是SQLite数据库解密失败的提示内容,e则是捕获到的具体异常对象。 log.error("Sqlite database decryption failed", e); } finally { + // 无论是否出现异常,都标记SseEmitter完成,释放相关资源,结束与前端的此次通信。 emitter.complete(); } } - + /** + * 根据微信用户ID(wxId)获取对应的数据库信息列表,具体是查找指定微信用户目录下的所有.db后缀的数据库文件, + * 将其相关信息(文件路径、文件大小等)封装到DatabaseVO对象中,并以列表形式返回。 + * 如果对应的微信用户数据库目录不存在,则直接返回一个空列表。 + * + * @param wxId 微信用户ID,用于唯一确定要获取数据库信息的对应微信用户,不同的wxId对应不同用户的数据库相关资源。 + * @return List 返回一个包含DatabaseVO对象的列表,每个DatabaseVO对象包含了数据库文件的路径和大小等信息, + * 若指定微信用户的数据库目录不存在则返回一个空列表(即Collections.emptyList())。 + */ @Override public List getDatabase(String wxId) { + // 根据微信用户ID获取对应的数据库目录路径,调用DirUtil的getDbDir方法传入wxId作为参数,得到存放该微信用户数据库文件的目录路径。 String dbPath = DirUtil.getDbDir(wxId); - // 不存在目录直接返回 + // 不存在目录直接返回,判断获取到的数据库目录路径是否存在,如果不存在(即对应的微信用户数据库目录不存在),则直接返回一个空列表,避免后续不必要的操作。 if (!FileUtil.exist(dbPath)) { return Collections.emptyList(); } - // 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录 + // 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录,通过Paths.get方法将字符串形式的数据库目录路径转换为Path对象, + // 然后利用Files.walk方法获取一个包含该路径下所有文件和子目录的Stream,方便后续对其进行过滤和处理操作。 try (Stream stream = Files.walk(Paths.get(dbPath))) { + // 对文件流进行过滤、映射等操作,先过滤出文件名以".db"结尾的文件(即筛选出数据库文件), + // 然后对每个符合条件的文件创建一个DatabaseVO对象,并设置其文件路径和文件大小属性,最后收集为一个包含DatabaseVO对象的列表返回。 return stream.filter(file -> file.toString().endsWith(".db")) .map(file -> { DatabaseVO databaseVO = new DatabaseVO(); @@ -139,76 +176,126 @@ public class DatabaseServiceImpl implements DatabaseService, ApplicationRunner { }) .collect(Collectors.toList()); } catch (Exception e) { + // 如果在遍历目录、获取数据库文件信息等操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"get database failed"表示获取数据库信息失败的提示内容,e则是捕获到的具体异常对象。 log.error("get database failed", e); } + // 如果出现异常或者前面的操作没有正常返回结果,则返回一个空列表,确保方法的返回值始终符合预期的List类型。 return Collections.emptyList(); } /** - * 获取微信的db文件 + * 获取微信的db文件的方法,用于从给定的文件流(代表一个目录下的所有文件和目录信息)中筛选出符合条件的微信数据库文件, + * 并将其转换为DecryptBO对象列表返回。筛选条件包括文件不是目录且文件名以".db"结尾,同时设置每个DecryptBO对象的输入输出路径等属性。 * - * @param stream 目录 - * @param outputPath 输出目录 - * @return DecryptBO + * @param stream 目录,以Stream形式传入,代表要筛选的文件和目录信息的流,通常是通过Files.walk方法获取的某个目录下的所有文件和子目录的路径流。 + * @param outputPath 输出目录,以字符串形式传入,用于指定解密后数据库文件的输出存放目录路径,在创建DecryptBO对象时会结合文件名等信息设置输出路径属性。 + * @return List 返回一个包含DecryptBO对象的列表,每个DecryptBO对象包含了要解密的数据库文件的输入路径(原始文件路径)和输出路径(解密后存放路径)等信息, + * 该列表包含了筛选出来的所有符合条件的微信数据库文件相关信息。 */ private List getWeChatDb(Stream stream, String outputPath) { + // 对传入的文件流进行过滤操作,先过滤掉代表目录的Path对象,只保留代表文件的Path对象, + // 这样后续处理的都是文件相关信息,便于进一步筛选出数据库文件。 return stream.filter(file -> !Files.isDirectory(file)) - // 过滤出文件名以.db结尾的文件 + // 过滤出文件名以.db结尾的文件,在剩下的文件中再次筛选,只保留文件名以".db"结尾的文件,即筛选出微信数据库文件。 .filter(file -> file.toString().endsWith(".db")) - // 将每个符合条件的文件路径映射为DecryptDTO对象 + // 将每个符合条件的文件路径映射为DecryptDTO对象,对每个筛选出来的数据库文件Path对象,创建一个DecryptBO对象, + // 传入文件的原始路径(toString()方法获取)作为输入路径,结合输出目录路径(outputPath)和文件名创建输出路径,设置到DecryptBO对象中。 .map(item -> new DecryptBO(item.toString(), outputPath + FileUtil.getName(item.toString()))) - // 转换成List + // 转换成List,将经过上述处理后得到的所有DecryptBO对象收集为一个列表,最终返回该列表,其包含了所有符合条件的微信数据库文件相关信息。 .collect(Collectors.toList()); } + /** + * Spring Boot应用启动后执行的方法,实现了ApplicationRunner接口的run方法。 + * 该方法主要用于在应用启动时,遍历当前工作目录下的db目录中的所有.db后缀的数据库文件, + * 对每个符合条件的文件调用registerDataSource方法进行数据源的动态注册操作,若db目录不存在则直接返回不进行任何操作。 + * + * @param args 应用启动时传入的参数,由Spring Boot传递进来,不过在当前方法中未对其进行使用,通常可用于接收命令行参数等信息来影响应用启动时的一些行为。 + */ @Override public void run(ApplicationArguments args) { - // 获取当前工作目录下的 db 目录 + // 获取当前工作目录下的db目录,调用DirUtil的getDbDir方法(无参数形式,可能获取默认配置下的db目录路径)获取当前工作目录下存放数据库文件的db目录路径。 String dbPath = DirUtil.getDbDir(); - // 获得目录 + // 获得目录,通过Paths.get方法将字符串形式的db目录路径转换为Path对象,方便后续进行文件相关的操作,比如判断目录是否存在等。 Path dbDirectory = Paths.get(dbPath); - // 检查目录是否存在 + // 检查目录是否存在,判断获取到的db目录对应的Path对象所代表的目录是否真实存在,如果不存在则直接返回,不进行后续遍历文件等操作。 if (!Files.exists(dbDirectory)) { return; } - // 使用 Files.walk 创建一个 Stream 来遍历给定路径下的所有文件和目录 + // 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录,通过Files.walk方法获取一个包含db目录下所有文件和子目录的Stream, + // 用于后续筛选数据库文件并进行数据源注册操作。 try (Stream stream = Files.walk(dbDirectory)) { - // 处理文件流 + // 处理文件流,对获取到的文件流进行一系列过滤、遍历操作,先过滤掉代表目录的Path对象,再筛选出文件名以".db"结尾的文件, + // 然后对每个符合条件的数据库文件调用registerDataSource方法进行数据源的动态注册操作。 stream.filter(file -> !Files.isDirectory(file)) // 过滤出文件名以 .db 结尾的文件 .filter(file -> file.toString().endsWith(".db")) // 将每个符合条件的文件创建 DataSourceProperty 对象 .forEach(dbFile -> registerDataSource(dbFile.toString())); } catch (Exception e) { + // 如果在遍历目录、注册数据源等操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"Failed to register the data source"表示数据源注册失败的提示内容,e则是捕获到的具体异常对象。 log.error("Failed to register the data source", e); } } /** - * 动态注册数据源 + * 动态注册数据源的方法,用于将指定路径的数据库文件注册为应用中的数据源,以便后续可以通过该数据源进行数据库相关操作。 + * 该方法会根据数据库文件路径获取对应的微信用户ID、数据库名称等信息,配置数据源相关属性(如连接池参数等), + * 然后通过Spring相关工具类获取数据源创建对象和动态路由数据源对象,最终将创建好的数据源添加到动态路由数据源中完成注册。 * - * @param dbPath 数据库路径 + * @param dbPath 数据库路径,以字符串形式传入,代表要注册为数据源的数据库文件的完整路径,通过该路径可以获取到数据库的相关信息以及确定其唯一性。 */ private void registerDataSource(String dbPath) { + // 从数据库文件路径中获取微信用户ID,先获取数据库文件路径的父目录(向上一级),再获取该父目录的名称作为微信用户ID, + // 这里假设数据库文件存放目录的上一级目录名称就是对应的微信用户ID,用于后续区分不同用户的数据源等操作。 String wxId = FileUtil.getName(FileUtil.getParent(dbPath, 1)); + // 获取数据库名称,直接获取数据库文件路径中的文件名作为数据库名称,用于在数据源配置等过程中标识该数据源对应的具体数据库。 String dbName = FileUtil.getName(dbPath); + // 创建DruidConfig对象,用于配置数据源的连接池相关参数,Druid是常用的数据库连接池组件,这里对其进行一些参数设置。 DruidConfig druidConfig = new DruidConfig(); + // 设置连接池初始大小,指定连接池初始化时创建的连接数量为5个,即应用启动时就会预先创建5个数据库连接,方便后续使用时直接获取,提高效率。 druidConfig.setInitialSize(5); + // 设置连接池最小空闲连接数,保证连接池中最少有5个空闲连接,避免连接资源过度释放导致后续获取连接时需要重新创建,影响性能。 druidConfig.setMinIdle(5); + // 设置连接池最大活动连接数,限制连接池中同时最多能有20个活动连接,防止过多的连接占用过多系统资源,造成资源浪费或性能问题。 druidConfig.setMaxActive(20); + // 设置获取连接的最大等待时间(单位为毫秒),当连接池中没有可用连接时,请求获取连接的线程最多等待60000毫秒(即60秒), + // 超过这个时间如果还没有获取到连接则会抛出异常,避免线程长时间阻塞等待连接。 druidConfig.setMaxWait(60000); + // 设置验证查询语句,用于定期检测连接是否有效,这里设置为"SELECT 1",即通过执行这个简单的查询语句来验证数据库连接是否可用,确保连接的有效性。 druidConfig.setValidationQuery("SELECT 1"); + // 设置在空闲时检测连接是否有效,当连接处于空闲状态时,会按照一定的规则(由连接池内部机制决定)周期性地执行验证查询语句来检测连接是否还能正常使用, + // 如果无效则会自动回收该连接并重新创建新的连接补充到连接池中。 druidConfig.setTestWhileIdle(true); + // 设置在从连接池获取连接时不进行有效性检测,即当应用从连接池中获取连接时,不会执行验证查询语句来再次验证连接是否可用, + // 依赖于连接池在空闲时的检测机制来保证连接的有效性,这样可以提高获取连接的效率,避免每次获取都进行验证带来的性能损耗。 druidConfig.setTestOnBorrow(false); + // 设置在归还连接到连接池时不进行有效性检测,当应用使用完连接并归还到连接池时,不会再次验证连接是否可用,同样依赖空闲时的检测机制来管理连接有效性。 druidConfig.setTestOnReturn(false); + // 设置是否对预编译语句进行缓存,开启该功能后,对于相同的预编译语句可以复用缓存中的结果,提高执行效率,减少编译语句的开销。 druidConfig.setPoolPreparedStatements(true); + // 创建DataSourceProperty对象,用于设置数据源的基本属性,比如数据库连接URL、驱动类名、连接池名称等信息,是配置数据源的重要对象。 DataSourceProperty sourceProperty = new DataSourceProperty(); + // 设置数据库连接URL,按照项目中定义的常量(SqliteConstant.URL_PREFIX)加上传入的数据库文件路径(dbPath)来拼接出完整的数据库连接URL, + // 用于指定要连接的数据库的具体位置和相关配置信息,不同类型的数据库其URL格式有所不同,这里针对SQLite数据库进行相应的拼接。 sourceProperty.setUrl(SqliteConstant.URL_PREFIX + dbPath); + // 设置驱动类名,使用项目中定义的常量(SqliteConstant.DRIVER_CLASS_NAME)来指定SQLite数据库的驱动类名称, + // 驱动类用于在Java程序与数据库之间建立通信连接,告诉程序如何与特定类型的数据库进行交互。 sourceProperty.setDriverClassName(SqliteConstant.DRIVER_CLASS_NAME); + // 设置连接池名称,通过调用DSNameUtil的getDSName方法,传入微信用户ID(wxId)和数据库名称(dbName)作为参数,生成一个唯一的连接池名称, + // 用于在动态路由数据源中区分不同的数据源,方便管理和使用。 sourceProperty.setPoolName(DSNameUtil.getDSName(wxId, dbName)); + // 通过Spring相关工具类(SpringUtil)获取动态路由数据源对象,该对象在应用中负责管理多个数据源,根据不同的需求动态切换使用不同的数据源进行数据库操作。 DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtil.getBean(DynamicRoutingDataSource.class); + // 通过Spring相关工具类(SpringUtil)获取默认数据源创建对象,用于根据前面配置好的DataSourceProperty对象来创建实际的数据源对象(如基于配置创建Druid数据源等)。 DefaultDataSourceCreator dataSourceCreator = SpringUtil.getBean(DefaultDataSourceCreator.class); + // 使用数据源创建对象(dataSourceCreator)根据配置好的数据源属性(sourceProperty)创建实际的数据源对象, + // 该数据源对象就是可以与数据库进行交互的具体实例,包含了连接数据库的各种配置信息和连接资源等。 DataSource dataSource = dataSourceCreator.createDataSource(sourceProperty); + // 将创建好的数据源添加到动态路由数据源中,通过动态路由数据源对象(dynamicRoutingDataSource)的addDataSource方法, + // 传入连接池名称(sourceProperty.getPoolName())和数据源对象(dataSource),完成数据源的动态注册,使得应用可以使用该数据源进行数据库操作。 dynamicRoutingDataSource.addDataSource(sourceProperty.getPoolName(), dataSource); } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DecryptServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DecryptServiceImpl.java index 01ac8eb..cfaa3de 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DecryptServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/DecryptServiceImpl.java @@ -20,7 +20,9 @@ import java.security.GeneralSecurityException; import java.util.Arrays; /** - * 解密服务实现类 + * 解密服务实现类,该类实现了DecryptService接口,用于提供具体的解密相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责执行对特定数据(如微信数据库文件等)的解密操作,通常会依赖一些加密工具类以及Java的加密相关API来完成具体的解密功能, + * 为上层应用提供数据解密服务,比如在需要访问加密的数据库内容等场景下使用。 * * @author xcs * @date 2023年12月25日11:09:07 @@ -31,71 +33,101 @@ import java.util.Arrays; public class DecryptServiceImpl implements DecryptService { /** - * SQLite数据库的文件头 + * SQLite数据库的文件头,定义了一个常量字符串,用于标识SQLite数据库文件的格式版本等信息, + * 在后续解密操作中可能用于验证文件格式或者在重新构建解密后的文件时写入头部信息等操作。 */ private static final String SQLITE_FILE_HEADER = "SQLite format 3\u0000"; /** - * 一页的大小 + * 一页的大小,定义了一个常量表示数据库文件分页的大小,通常数据库文件在存储时会按照一定的页大小进行划分, + * 这里指定为4096字节,在后续读取、解密和处理数据库文件数据时会按照这个页大小为单位进行相关操作。 */ private static final int DEFAULT_PAGESIZE = 4096; /** - * 迭代次数 + * 迭代次数,用于在生成密钥等加密相关操作中的一个参数,指定了密码扩展函数(如PBKDF2等)进行计算时的迭代次数, + * 迭代次数越多,生成的密钥安全性相对越高,但也会消耗更多的计算资源,这里设置为64000次迭代。 */ private static final int ITERATIONS = 64000; /** - * Key长度 + * Key长度,定义了生成的密钥的长度为32字节,明确了在加密和解密操作中所使用密钥的长度要求, + * 符合特定加密算法(如后续可能使用的基于HMAC等相关算法)对密钥长度的规范。 */ private static final int HASH_KEY_LENGTH = 32; + /** + * 微信数据库解密的方法,根据传入的密码(password)和包含解密相关输入输出路径等信息的DecryptBO对象(decryptBO), + * 对微信数据库文件进行解密操作,包括读取文件内容、提取关键信息、验证密钥、解密数据并写入到新的文件中。 + * + * @param password 用于解密的密码,通常是通过其他途径获取到的能够解开数据库加密的关键字符串信息。 + * @param decryptBO 包含了数据库文件的输入路径(原始加密文件路径)和输出路径(解密后文件的存放路径)等信息的对象, + * 同时也可作为在整个解密过程中传递文件相关数据和配置的载体。 + */ @Override public void wechatDecrypt(String password, DecryptBO decryptBO) { - // 创建File文件 + // 创建File文件,根据DecryptBO对象中存储的输入文件路径信息创建一个File对象,代表要进行解密操作的原始数据库文件实体, + // 方便后续通过Java的文件操作相关API对其进行读取等操作。 File file = new File(decryptBO.getInput()); try (FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) { - // 文件大小 + // 文件大小,获取要解密的数据库文件的大小(以字节为单位),通过File对象的length方法获取文件长度, + // 该长度信息在后续读取文件内容、分配缓冲区等操作中会作为边界条件使用。 long fileSize = file.length(); + // 将文件内容映射到内存缓冲区,通过FileChannel的map方法,以只读模式(FileChannel.MapMode.READ_ONLY)将整个文件内容映射到内存中, + // 返回一个MappedByteBuffer对象,方便后续直接在内存中对文件数据进行读取操作,提高读取效率,其映射范围是从文件开头(偏移量为0)到文件末尾(文件大小fileSize指定的长度)。 MappedByteBuffer fileContent = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize); - // 提取盐值 + // 提取盐值,创建一个长度为16字节的字节数组用于存储从文件中提取的盐值信息, + // 通过MappedByteBuffer的get方法从文件内容缓冲区中读取相应长度的数据作为盐值,盐值通常在加密过程中与密码一起参与密钥生成等操作,用于增加加密的安全性和随机性。 byte[] salt = new byte[16]; fileContent.get(salt); - // 提取第一页 + // 提取第一页,创建一个长度为DEFAULT_PAGESIZE - 16字节的字节数组,用于存储从文件中提取的第一页除去盐值部分的数据, + // 同样通过MappedByteBuffer的get方法读取相应长度的数据,这里减去16字节是因为前面已经提取了盐值部分,第一页剩余的数据才是后续要处理的主体内容(包含IV、正文、哈希等信息)。 byte[] firstPage = new byte[DEFAULT_PAGESIZE - 16]; fileContent.get(firstPage); - // 提取第一页的内容与IV + // 提取第一页的内容与IV,从第一页数据中提取出包含正文和初始化向量(IV)的部分内容, + // 通过Arrays.copyOfRange方法复制字节数组中指定范围的内容,这里是从第一页数据的开头到倒数第32个字节之前的部分,作为正文和IV的数据, + // 后续会根据这些数据进行解密等操作。 byte[] firstPageBodyAndIv = Arrays.copyOfRange(firstPage, 0, firstPage.length - 32); - // 提取第一页的内容 + // 提取第一页的内容,从第一页数据中提取出正文部分内容,复制字节数组中从开头到倒数第48个字节之前的部分作为正文数据, + // 这部分是真正需要解密的核心数据内容,与前面提取的firstPageBodyAndIv相比,范围更精确地定位到了正文数据本身。 byte[] firstPageBody = Arrays.copyOfRange(firstPage, 0, firstPage.length - 48); - // 提取第一页IV + // 提取第一页IV,从第一页数据中提取出初始化向量(IV)部分内容,复制字节数组中从倒数第48个字节到倒数第32个字节之间的部分作为IV数据, + // IV在加密算法(如AES的CBC模式)中用于在加密或解密过程中对每个数据块进行初始化操作,保证加密的随机性和安全性。 byte[] firstPageIv = Arrays.copyOfRange(firstPage, firstPage.length - 48, firstPage.length - 32); - // 提取第一页的hashMac + // 提取第一页的hashMac,从第一页数据中提取出哈希消息认证码(hashMac)部分内容,复制字节数组中从倒数第32个字节到倒数第12个字节之间的部分作为hashMac数据, + // hashMac通常用于验证数据的完整性以及验证密钥是否正确等操作,通过对数据进行哈希计算并与存储的hashMac进行对比来判断数据是否被篡改或者密钥是否匹配。 byte[] firstPageHashMac = Arrays.copyOfRange(firstPage, firstPage.length - 32, firstPage.length - 12); - // 提取第一页的保留字段 + // 提取第一页的保留字段,从第一页数据中提取出保留字段部分内容,复制字节数组中从倒数第48个字节到末尾的部分作为保留字段数据, + // 保留字段可能包含一些特定用途的数据(具体由文件格式或加密相关规范定义),在后续处理(如写入解密后的文件等)过程中需要保留这些数据的完整性。 byte[] firstPageReservedSegment = Arrays.copyOfRange(firstPage, firstPage.length - 48, firstPage.length); - // 生成key + // 生成key,调用Pbkdf2HmacUtil的pbkdf2Hmac方法,传入密码(先通过HexUtil.decodeHex方法将十六进制格式的密码字符串转换为字节数组)、盐值、迭代次数以及密钥长度等参数, + // 通过基于密码的密钥派生函数(PBKDF2-HMAC)生成用于解密的实际密钥,该密钥生成过程综合考虑了密码、盐值以及指定的迭代次数等因素,增强了密钥的安全性。 byte[] key = Pbkdf2HmacUtil.pbkdf2Hmac(HexUtil.decodeHex(password), salt, ITERATIONS, HASH_KEY_LENGTH); byte[] macSalt = new byte[salt.length]; for (int i = 0; i < salt.length; i++) { + // 对盐值的每个字节进行异或操作,通过与固定值58(十六进制为0x3A)进行异或运算,生成用于后续密钥验证等操作的另一种盐值形式(macSalt), + // 这种变换可能是特定加密验证机制所要求的,用于在验证密钥是否正确时与其他数据一起参与计算。 macSalt[i] = (byte) (salt[i] ^ 58); } - // 秘钥匹配成功 + // 秘钥匹配成功,调用Pbkdf2HmacUtil的checkKey方法,传入生成的密钥(key)、变换后的盐值(macSalt)、提取的第一页哈希消息认证码(firstPageHashMac)以及包含正文和IV的第一页部分数据(firstPageBodyAndIv)作为参数, + // 验证生成的密钥是否与文件中存储的相关验证信息匹配,如果匹配成功(即返回true),则说明密钥正确,可以继续进行后续的解密和文件写入操作。 if (Pbkdf2HmacUtil.checkKey(key, macSalt, firstPageHashMac, firstPageBodyAndIv)) { File outputFile = new File(decryptBO.getOutput()); File parentDir = outputFile.getParentFile(); - // 检查父目录是否存在,如果不存在,则创建 + // 检查父目录是否存在,如果不存在,则创建,判断解密后文件的输出路径的父目录是否存在,如果不存在则通过mkdirs方法创建整个目录结构, + // 确保后续能够将解密后的文件正确写入到指定的输出路径中,避免因目录不存在而导致写入失败。 if (!parentDir.exists()) { parentDir.mkdirs(); } - // 解密并写入新文件 + // 解密并写入新文件,创建一个用于写入解密后数据的FileChannel,通过FileChannel.open方法打开输出文件(以创建、写入模式,即StandardOpenOption.CREATE和StandardOpenOption.WRITE), + // 用于后续将解密后的数据库文件内容写入到新的文件中,实现解密并生成新文件的操作。 try (FileChannel outChannel = FileChannel.open(outputFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { ByteBuffer headerBuffer = ByteBuffer.wrap(SQLITE_FILE_HEADER.getBytes()); outChannel.write(headerBuffer); @@ -103,19 +135,23 @@ public class DecryptServiceImpl implements DecryptService { ByteBuffer decryptedFirstPage = ByteBuffer.wrap(doDecrypt(key, firstPageIv, firstPageBody)); ByteBuffer reservedSegmentBuffer = ByteBuffer.wrap(firstPageReservedSegment); - // 创建暂存缓冲区 + // 创建暂存缓冲区,创建一个大小为文件大小(fileSize)的ByteBuffer作为临时缓冲区,用于暂存解密后的文件数据以及保留字段等信息, + // 方便后续一次性将所有数据写入到输出文件中,提高写入效率,减少频繁的文件写入操作次数。 ByteBuffer tempBuffer = ByteBuffer.allocate((int) fileSize); - // 写入第一页的解密内容和保留字段到暂存缓冲区 + // 写入第一页的解密内容和保留字段到暂存缓冲区,将解密后的第一页正文数据(通过doDecrypt方法解密得到)和提取的第一页保留字段数据分别写入到临时缓冲区中, + // 按照文件格式要求的顺序依次放入缓冲区,准备后续将整个缓冲区的数据写入到输出文件中。 tempBuffer.put(decryptedFirstPage); tempBuffer.put(reservedSegmentBuffer); - // 解密后续数据块 + // 解密后续数据块,通过循环读取文件内容缓冲区中剩余的数据(只要还有未读取的数据,即fileContent.hasRemaining()为true), + // 按照每页DEFAULT_PAGESIZE的大小依次读取并处理每一页数据,进行解密和相关数据整理操作,直到处理完所有数据或者遇到填充页面(通过isPaddingPage方法判断)为止。 while (fileContent.hasRemaining()) { byte[] page = new byte[DEFAULT_PAGESIZE]; fileContent.get(page); - // 判断是否是填充页面,如果是则跳过后续处理 + // 判断是否是填充页面,如果是则跳过后续处理,调用isPaddingPage方法判断当前读取的页面数据是否全部为0字节(即填充页面), + // 如果是填充页面则说明已经到达文件末尾的填充部分,不需要再进行解密等操作,直接跳出循环结束数据处理过程。 if (isPaddingPage(page)) { break; } @@ -127,30 +163,35 @@ public class DecryptServiceImpl implements DecryptService { ByteBuffer decryptedBody = ByteBuffer.wrap(doDecrypt(key, iv, body)); ByteBuffer reservedSegmentBuf = ByteBuffer.wrap(reservedSegment); - // 将解密内容和保留字段写入暂存缓冲区 + // 将解密内容和保留字段写入暂存缓冲区,将解密后的当前页正文数据和保留字段数据分别写入到临时缓冲区中,与前面处理第一页数据类似, + // 按照顺序将每一页解密后的有效数据和保留字段依次放入缓冲区,为最终写入输出文件做准备。 tempBuffer.put(decryptedBody); tempBuffer.put(reservedSegmentBuf); } - // 将暂存缓冲区内容写入到输出文件 + // 将暂存缓冲区内容写入到输出文件,通过flip方法将临时缓冲区的指针位置调整到初始位置,准备从缓冲区开头开始读取数据, + // 然后通过FileChannel的write方法将缓冲区中的所有数据一次性写入到输出文件中,完成解密后的数据写入操作,生成最终的解密后的数据库文件。 tempBuffer.flip(); outChannel.write(tempBuffer); } } } catch (Exception e) { + // 如果在读取文件、生成密钥、解密数据、写入文件等整个解密操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"WeChat decryption failed"表示微信数据库解密失败的提示内容,e则是捕获到的具体异常对象。 log.error("WeChat decryption failed", e); } } /** - * 检查页面是否为填充页面 + * 检查页面是否为填充页面的方法,通过遍历页面数据中的每个字节,判断是否所有字节都为0, + * 如果是则认为该页面是填充页面,常用于在解密数据库文件等操作中判断是否已经到达文件末尾的填充部分,从而决定是否继续后续的数据处理操作。 * - * @param page 页面数据 - * @return 如果是填充页面返回true,否则返回false + * @param page 页面数据,以字节数组形式传入,代表从数据库文件中读取的某一页数据内容,用于判断该页面是否为全部字节都为0的填充页面。 + * @return 如果是填充页面返回true,否则返回false,表示该页面包含有效数据,需要继续进行解密等相关处理操作。 */ private boolean isPaddingPage(byte[] page) { for (byte b : page) { - if (b != 0) { + if (b!= 0) { return false; } } @@ -158,13 +199,15 @@ public class DecryptServiceImpl implements DecryptService { } /** - * 使用AES/CBC/NoPadding模式进行解密 + * 使用AES/CBC/NoPadding模式进行解密的方法,根据传入的密钥(key)、初始化向量(iv)以及待解密的数据(input), + * 利用Java的加密相关API(javax.crypto包下的Cipher类等)创建相应的解密器对象,配置解密模式和参数,然后执行解密操作,返回解密后的数据。 * - * @param key 密钥 - * @param iv 初始化向量 - * @param input 待解密的数据 - * @return 解密后的数据 - * @throws GeneralSecurityException 抛出异常 + * @param key 密钥,以字节数组形式传入,是经过前面密钥生成等操作得到的用于解密的关键数据,长度和内容符合特定加密算法要求。 + * @param iv 初始化向量,以字节数组形式传入,用于在AES加密算法的CBC模式下对每个数据块进行初始化,保证解密的正确性和安全性,其长度等要求由加密算法规范确定。 + * @param input 待解密的数据,以字节数组形式传入,代表从加密的数据库文件中读取出来的需要进行解密的原始数据内容。 + * @return 解密后的数据,返回字节数组形式的数据,即经过解密操作后得到的原始明文数据,可用于后续的业务逻辑处理或者写入到解密后的文件中等操作。 + * @throws GeneralSecurityException 抛出异常,如果在创建解密器、配置解密参数或者执行解密操作过程中出现与安全相关的异常情况(如算法不支持、密钥错误等), + * 则会抛出该异常,需要在调用该方法的上层代码中进行相应的异常处理。 */ private byte[] doDecrypt(byte[] key, byte[] iv, byte[] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); @@ -173,4 +216,4 @@ public class DecryptServiceImpl implements DecryptService { cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); return cipher.doFinal(input); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/FeedsServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/FeedsServiceImpl.java index 3d79f3a..3449580 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/FeedsServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/FeedsServiceImpl.java @@ -24,7 +24,9 @@ import java.util.Optional; import java.util.stream.Collectors; /** - * 朋友圈服务实现 + * 朋友圈服务实现类,该类实现了FeedsService接口,用于提供具体的朋友圈相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调不同的数据访问操作(通过多个Repository)以及数据转换(通过FeedsMapping)等工作, + * 例如查询朋友圈数据、解析朋友圈内容中的关键信息(如媒体内容、位置信息等)并进行相应的格式转换和数据填充,最终为上层提供处理好的朋友圈相关数据,以便展示或进一步使用。 * * @author xcs * @date 2024年1月3日17:25:26 @@ -34,80 +36,113 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class FeedsServiceImpl implements FeedsService { + // 通过依赖注入获取朋友圈数据访问仓库,用于执行与朋友圈相关的数据库查询等操作,比如根据特定条件查询朋友圈记录等信息。 private final FeedsRepository feedsRepository; + // 通过依赖注入获取朋友圈数据映射类,用于将从数据存储中获取到的原始朋友圈数据转换为适合向外展示的FeedsVO对象形式,方便业务逻辑处理和展示。 private final FeedsMapping feedsMapping; + // 通过依赖注入获取联系人数据访问仓库,用于查询联系人相关的数据,例如根据用户名获取联系人昵称等信息,在朋友圈数据处理中用于设置发布者的昵称。 private final ContactRepository contactRepository; + // 通过依赖注入获取联系人头像URL数据访问仓库,用于查询联系人头像的URL地址,以便在朋友圈数据中设置发布者的头像链接信息。 private final ContactHeadImgUrlRepository contactHeadImgUrlRepository; + // 通过依赖注入获取硬链接视频属性数据访问仓库,可能用于获取朋友圈中视频相关的额外属性信息(虽然代码中暂未体现其具体使用)。 private final HardLinkVideoAttributeRepository hardLinkVideoAttributeRepository; + // 通过依赖注入获取硬链接图片属性数据访问仓库,可能用于获取朋友圈中图片相关的额外属性信息(同样代码中暂未体现其具体使用)。 private final HardLinkImageAttributeRepository hardLinkImageAttributeRepository; + /** + * 根据传入的查询条件(FeedsDTO)分页查询朋友圈信息,并进行一系列数据处理后返回分页的朋友圈视图对象(PageVO)。 + * 数据处理包括解析朋友圈内容中的XML获取详细信息、转换日期格式、设置联系人昵称和头像等操作,若查询结果为空则返回默认的分页对象。 + * + * @param feedsDTO 查询条件,包含了诸如筛选条件、分页相关参数等信息,用于指定要查询的朋友圈记录范围和分页要求。 + * @return PageVO 返回包含FeedsVO对象的分页结果视图对象,其中FeedsVO包含了朋友圈的各种关键信息(如发布者、内容、时间等)用于展示, + * PageVO则封装了分页相关的信息(当前页、每页大小、总记录数等)。 + */ @Override public PageVO queryFeeds(FeedsDTO feedsDTO) { - // 查询朋友圈 + // 查询朋友圈,使用Optional.ofNullable方法对feedsRepository.queryFeeds(feedsDTO)的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // feedsRepository.queryFeeds(feedsDTO)用于从数据存储(可能是数据库)中获取符合查询条件的朋友圈信息,以分页形式返回。 return Optional.ofNullable(feedsRepository.queryFeeds(feedsDTO)) - // 处理头像并转换参数 + // 处理头像并转换参数,对获取到的分页朋友圈数据进行处理,通过lambda表达式来实现具体的操作逻辑,主要涉及数据转换、解析XML内容以及设置各种相关属性等操作。 .map(pageResult -> { - // 转换参数 + // 转换参数,调用feedsMapping的convert方法对获取到的分页结果中的朋友圈记录列表(pageResult.getRecords())进行转换, + // 将原始的朋友圈数据转换为FeedsVO对象列表,FeedsVO对象通常包含了更适合向外展示的朋友圈相关属性,如发布者用户名、发布时间、内容等。 List feedsVos = feedsMapping.convert(pageResult.getRecords()) - // 转换成流 + // 转换成流,将转换后的FeedsVO对象列表转换为流(Stream),方便后续使用流的相关操作(如过滤、映射、遍历等)对每个FeedsVO对象进行进一步处理。 .stream() - // 解析Content里面的XML + // 解析Content里面的XML,对每个FeedsVO对象中的朋友圈内容(feedsVO.getContent())进行XML解析操作, + // 通过调用parseXmlToObj方法将XML内容转换为TimelineObjectBO对象,以便提取其中更详细的信息(如媒体内容、位置信息等)用于后续设置到FeedsVO对象中。 .map(feedsVO -> { TimelineObjectBO timelineObjectBO = parseXmlToObj(feedsVO.getContent()); - // 空校验 + // 空校验,判断解析XML得到的TimelineObjectBO对象是否为null,如果为null说明XML解析失败或者内容为空等情况, + // 此时直接返回原FeedsVO对象,不进行后续基于解析结果的属性设置操作。 if (timelineObjectBO == null) { return feedsVO; } - // 设置内容描述 + // 设置内容描述,将解析得到的TimelineObjectBO对象中的内容描述信息(timelineObjectBO.getContentDesc())设置到FeedsVO对象的ContentDesc属性中, + // 完善朋友圈内容的展示信息。 feedsVO.setContentDesc(timelineObjectBO.getContentDesc()); - // 设置媒体内容 + // 设置媒体内容,调用getMedia方法,传入TimelineObjectBO对象作为参数,获取其中的媒体相关信息(如图片、视频的链接等), + // 并将获取到的媒体信息设置到FeedsVO对象的Medias属性中,方便后续展示朋友圈中的媒体内容。 feedsVO.setMedias(getMedia(timelineObjectBO)); - // 设置地址 + // 设置地址,调用getLocation方法,传入TimelineObjectBO对象作为参数,获取其中的位置相关信息(如所在城市、具体地址等), + // 并将获取到的位置信息设置到FeedsVO对象的Location属性中,完善朋友圈发布位置相关的展示信息。 feedsVO.setLocation(getLocation(timelineObjectBO)); return feedsVO; }) - // 处理日期 + // 处理日期,对每个FeedsVO对象进行日期格式转换操作,通过peek方法对流中的每个元素(即FeedsVO对象)进行操作, + // 该操作不会改变流中的元素数量和顺序,只是对元素进行特定的处理(这里是处理日期格式)。 .peek(feedsVO -> { - // 转换日期 + // 转换日期,调用DateUtil的formatDateTime方法,将朋友圈发布时间(feedsVO.getCreateTime(),这里可能是以时间戳形式存储,先乘以1000L转换为毫秒数)转换为指定格式的日期时间字符串, + // 以便更友好地展示给用户查看。 String strCreateTime = DateUtil.formatDateTime(new Date(feedsVO.getCreateTime() * 1000L)); - // 设置日期 + // 设置日期,将转换后的日期时间字符串设置到FeedsVO对象的StrCreateTime属性中,完成日期格式的转换和设置操作。 feedsVO.setStrCreateTime(strCreateTime); }) - // 处理联系人名称 + // 处理联系人名称,同样通过peek方法对每个FeedsVO对象进行操作,用于查询并设置朋友圈发布者的联系人昵称信息。 .peek(feedsVO -> { - // 查询用户名 + // 查询用户名,调用contactRepository的getContactNickname方法,传入FeedsVO对象中的发布者用户名(feedsVO.getUserName())作为参数, + // 从数据存储中查询对应的联系人昵称信息。 String nickname = contactRepository.getContactNickname(feedsVO.getUserName()); - // 设置用户名 + // 设置用户名,将查询到的联系人昵称设置到FeedsVO对象的NickName属性中,完成发布者昵称信息的设置操作。 feedsVO.setNickName(nickname); }) - // 处理联系人头像 + // 处理联系人头像,再次通过peek方法对每个FeedsVO对象进行操作,用于查询并设置朋友圈发布者的联系人头像URL信息。 .peek(feedsVO -> { - // 联系人头像 + // 联系人头像,调用contactHeadImgUrlRepository的queryHeadImgUrlByUserName方法,传入FeedsVO对象中的发布者用户名(feedsVO.getUserName())作为参数, + // 从数据存储中查询对应的联系人头像URL地址信息。 String headImgUrl = contactHeadImgUrlRepository.queryHeadImgUrlByUserName(feedsVO.getUserName()); - // 设置联系人头像 + // 设置联系人头像,将查询到的联系人头像URL地址设置到FeedsVO对象的HeadImgUrl属性中,完成发布者头像信息的设置操作。 feedsVO.setHeadImgUrl(headImgUrl); }) - // 转换成List + // 转换成List,将经过上述一系列处理后的流中的FeedsVO对象收集为一个列表,方便后续将处理好的朋友圈数据封装到PageVO对象中返回。 .collect(Collectors.toList()); - // 返回分页数据 + // 返回分页数据,将处理后的分页数据(包含设置好各种属性信息的朋友圈记录列表等信息)转换为自定义的PageVO格式返回, + // 构造PageVO对象时传入当前页、每页大小、总记录数以及处理后的朋友圈记录列表等参数。 return new PageVO<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal(), feedsVos); }) - // 默认值 + // 默认值,如果前面的查询结果为空(即feedsRepository.queryFeeds返回null),则创建一个默认的PageVO对象返回, + // 使用传入的FeedsDTO中的当前页和每页大小参数,并设置总记录数为0,记录列表为null。 .orElse(new PageVO<>(feedsDTO.getCurrent(), feedsDTO.getPageSize(), 0L, null)); } /** - * 获取媒体内容 + * 获取媒体内容的方法,用于从给定的TimelineObjectBO对象中提取媒体相关信息(如图片、视频的链接等), + * 并将其转换为FeedsMediaVO对象列表返回,若不存在媒体信息则返回一个空列表。 * - * @param timelineObjectBO 参数 - * @return FeedsMediaVO + * @param timelineObjectBO 参数,以TimelineObjectBO对象形式传入,该对象包含了朋友圈内容解析后得到的详细信息,其中包含媒体相关的结构信息, + * 通过它可以获取到朋友圈中发布的媒体内容的具体属性(如链接、缩略图等)。 + * @return FeedsMediaVO 返回一个包含FeedsMediaVO对象的列表,每个FeedsMediaVO对象包含了媒体内容的URL和缩略图URL等信息, + * 用于展示朋友圈中的图片、视频等媒体相关内容,若没有媒体信息则返回一个空列表。 */ private List getMedia(TimelineObjectBO timelineObjectBO) { List feedsMediaVos = new ArrayList<>(); - // 获取媒体 + // 获取媒体,从TimelineObjectBO对象中获取媒体列表信息,通过调用getContentObject方法获取其中的内容对象,再获取该内容对象中的媒体列表(mediaList), + // 该媒体列表包含了朋友圈中发布的所有媒体(如图片、视频等)的详细信息结构(每个元素包含URL、缩略图等属性)。 List mediaList = timelineObjectBO.getContentObject().getMediaList(); + // 判断媒体列表是否为空,如果为空(即不存在媒体信息),则直接返回空的FeedsMediaVO对象列表,避免后续不必要的操作。 if (CollUtil.isEmpty(mediaList)) { return feedsMediaVos; } @@ -122,14 +157,18 @@ public class FeedsServiceImpl implements FeedsService { } /** - * 获取地址 + * 获取地址的方法,用于从给定的TimelineObjectBO对象中提取位置相关信息(如所在城市、具体地址等), + * 并将其转换为FeedsLocationVO对象返回,若不存在位置信息则返回null。 * - * @param timelineObjectBO 参数 - * @return FeedsLocationVO + * @param timelineObjectBO 参数,以TimelineObjectBO对象形式传入,该对象包含了朋友圈内容解析后得到的详细信息,其中包含位置相关的结构信息, + * 通过它可以获取到朋友圈发布时标注的位置相关属性(如城市、具体地址、分类等)。 + * @return FeedsLocationVO 返回一个FeedsLocationVO对象,该对象包含了朋友圈发布位置的相关信息(如城市、具体地址、分类等), + * 用于展示朋友圈的发布位置情况,若没有位置信息则返回null。 */ private FeedsLocationVO getLocation(TimelineObjectBO timelineObjectBO) { TimelineObjectBO.Location location = timelineObjectBO.getLocation(); - // 空校验 + // 空校验,通过ObjUtil.isNotEmpty方法判断获取到的位置信息对象(location)是否不为空,即是否存在位置相关信息, + // 如果不为空则进行后续的创建并设置FeedsLocationVO对象属性的操作,若为空则直接返回null,表示没有位置信息。 if (ObjUtil.isNotEmpty(location)) { FeedsLocationVO feedsLocationVO = new FeedsLocationVO(); feedsLocationVO.setCity(location.getCity()); @@ -143,10 +182,13 @@ public class FeedsServiceImpl implements FeedsService { } /** - * 将xml转换成对象 + * 将xml转换成对象的方法,用于将给定的XML格式的字符串(xml)转换为TimelineObjectBO对象, + * 在转换过程中会先对XML字符串进行一些预处理(替换特定字符),然后使用XmlUtil工具类进行解析操作, + * 若解析过程中出现异常则记录错误信息并返回null。 * - * @param xml xml内容 - * @return TimelineObjectBO + * @param xml xml内容,以字符串形式传入,代表朋友圈内容中包含详细信息的XML格式数据,需要将其转换为对象形式以便提取其中的各种属性信息。 + * @return TimelineObjectBO 返回解析后的TimelineObjectBO对象,该对象包含了从XML中提取的朋友圈内容详细信息(如媒体、位置、内容描述等), + * 若解析失败则返回null。 */ private TimelineObjectBO parseXmlToObj(String xml) { try { @@ -154,6 +196,8 @@ public class FeedsServiceImpl implements FeedsService { xml = xml.replace("", ""); return XmlUtil.parseXml(xml, TimelineObjectBO.class); } catch (Exception e) { + // 如果在XML替换字符、解析操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"parse xml to obj fail"表示将XML转换为对象失败的提示内容,e则是捕获到的具体异常对象。 log.error("parse xml to obj fail", e); } return null; diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ImageServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ImageServiceImpl.java index 493ec57..1ac42e9 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ImageServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/ImageServiceImpl.java @@ -22,7 +22,9 @@ import java.nio.file.Files; import java.nio.file.Paths; /** - * 图片服务实现 + * 图片服务实现类,该类实现了ImageService接口,用于提供具体的图片相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调图片数据的查询(通过HardLinkImageAttributeRepository)、用户信息获取(通过UserService)以及图片的下载、解密和返回等操作, + * 为上层应用提供获取图片资源的服务,例如根据图片的MD5值、路径等信息来获取对应的图片内容并以合适的方式返回给客户端等场景使用。 * * @author xcs * @date 2024年1月18日22:06:46 @@ -32,96 +34,152 @@ import java.nio.file.Paths; @RequiredArgsConstructor public class ImageServiceImpl implements ImageService { + // 通过依赖注入获取硬链接图片属性数据访问仓库,用于查询图片相关的数据,例如根据图片的MD5值查找对应的图片URL等信息,辅助图片获取操作。 private final HardLinkImageAttributeRepository hardLinkImageAttributeRepository; + // 通过依赖注入获取用户服务,用于获取与当前用户相关的信息,比如用户的微信ID、基础路径等,在确定图片的存储位置、归属用户等方面起到关键作用。 private final UserService userService; + /** + * 根据图片的MD5值下载图片的方法,首先从数据库中查询对应MD5值的图片URL,然后基于用户信息和图片URL等确定图片文件的路径, + * 对图片文件(如果需要可能进行解密操作)进行处理后,以合适的格式(如设置为IMAGE_JPEG媒体类型)将图片资源作为响应返回给客户端, + * 如果在过程中出现找不到图片等情况则返回404响应。 + * + * @param md5 图片的MD5值,以字符串形式传入,用于唯一标识图片,通过该MD5值可以在数据库中查找对应的图片相关信息(如URL等),进而获取图片文件。 + * @return ResponseEntity 返回一个包含图片资源(Resource类型)的响应实体(ResponseEntity类型), + * 如果成功获取并处理图片则返回包含图片内容且设置了正确媒体类型(IMAGE_JPEG)的响应,若出现异常或找不到图片则返回404状态的响应实体,表示资源未找到。 + */ @Override public ResponseEntity downloadImgMd5(String md5) { try { - // 查询数据库 + // 查询数据库,调用hardLinkImageAttributeRepository的queryHardLinkImage方法,传入将十六进制格式的MD5值字符串转换为字节数组后的参数(通过HexUtil.decodeHex方法转换), + // 从数据库中查找对应的图片URL,该URL是获取图片文件的关键信息之一。 String imgUrl = hardLinkImageAttributeRepository.queryHardLinkImage(HexUtil.decodeHex(md5)); - // 查询结果为空,返回404 + // 查询结果为空,返回404,如果从数据库中查询到的图片URL为空字符串(即没有找到对应MD5值的图片信息),则直接返回一个404状态的响应实体, + // 告知客户端请求的图片资源不存在。 if (StrUtil.isBlank(imgUrl)) { return ResponseEntity.notFound().build(); } - // 获取用户信息 + // 获取用户信息,调用userService的currentUser方法获取当前用户的微信ID(wxId),用于后续确定图片文件所在的目录路径等信息, + // 基于用户来定位图片资源的存储位置,确保不同用户的图片数据不会混淆。 String wxId = userService.currentUser(); - // 获得文件目录 + // 获得文件目录,调用DirUtil的相关方法(这里是getDir方法,传入用户的基础路径、微信ID以及图片URL等参数)获取图片文件所在的完整目录路径, + // 为后续检查文件是否存在以及读取文件等操作做准备。 String filePath = DirUtil.getDir(userService.getBasePath(wxId), wxId, imgUrl); - // 检查文件是否存在 + // 检查文件是否存在,通过FileUtil的exist方法判断获取到的图片文件路径对应的文件是否真实存在,如果不存在则返回404状态的响应实体, + // 表示无法找到要下载的图片文件。 if (!FileUtil.exist(filePath)) { return ResponseEntity.notFound().build(); } - // 获取图片文件夹地址 + // 获取图片文件夹地址,调用DirUtil的getImgDir方法,传入微信ID(wxId)作为参数,获取用于存放处理后(可能经过解密等操作)图片的文件夹路径, + // 方便后续将解密或处理后的图片放置到该文件夹中进行统一管理。 String outPath = DirUtil.getImgDir(wxId); - // 解密并返回 + // 解密并返回,调用ImgDecoderUtil的decodeDat方法,传入图片文件路径(filePath)和输出文件夹路径(outPath)作为参数, + // 对图片文件进行解密等相关处理操作,并返回处理后的图片文件路径(imgPath),如果处理后的图片路径为空则表示出现问题,同样返回404响应。 String imgPath = ImgDecoderUtil.decodeDat(filePath, outPath); - // 如果图片地址为空 if (imgPath == null) { return ResponseEntity.notFound().build(); } - // 返回图片 + // 返回图片,构建一个成功状态(200 OK)的响应实体,设置响应的媒体类型为IMAGE_JPEG(表示返回的是JPEG格式的图片资源), + // 并将图片文件对应的输入流资源(通过Files.newInputStream方法获取图片文件的输入流,再包装为InputStreamResource对象)设置到响应实体的主体内容中, + // 这样客户端接收到该响应后就可以正确解析并显示图片内容了。 return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .body(new InputStreamResource(Files.newInputStream(Paths.get(imgPath)))); } catch (Exception e) { + // 如果在查询数据库、获取用户信息、检查文件存在性、图片解密以及构建响应等整个操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"downloadImgMd5 error"表示根据MD5值下载图片出现错误的提示内容,e则是捕获到的具体异常对象。 log.error("downloadImgMd5 error", e); } - // 默认返回404 + // 默认返回404,如果前面的操作出现异常或者最终没有成功获取到图片资源,则返回一个404状态的响应实体,表示资源未找到,作为兜底的返回结果。 return ResponseEntity.notFound().build(); } + /** + * 根据图片路径下载图片的方法,首先获取当前用户信息,然后生成一个默认的目标图片路径(以随机生成的文件名保存为GIF格式), + * 通过HTTP工具尝试从给定的路径下载图片到目标路径,若下载成功则以合适的格式(如设置为IMAGE_JPEG媒体类型)将图片资源作为响应返回给客户端, + * 若出现异常则默认返回404响应。 + * + * @param path 图片的路径,以字符串形式传入,该路径可以是网络URL或者本地文件路径等,用于指定要下载的图片的来源位置, + * 通过该路径可以获取到对应的图片内容并保存到本地指定位置后再返回给客户端。 + * @return ResponseEntity 返回一个包含图片资源(Resource类型)的响应实体(ResponseEntity类型), + * 如果成功下载并处理图片则返回包含图片内容且设置了正确媒体类型(IMAGE_JPEG)的响应,若出现异常则返回404状态的响应实体,表示资源未找到。 + */ @Override public ResponseEntity downloadImg(String path) { - // 获取用户信息 + // 获取用户信息,调用userService的currentUser方法获取当前用户的微信ID(wxId),用于后续确定图片保存的目录路径等信息, + // 确保图片按照用户相关的规则进行存储和管理。 String wxId = userService.currentUser(); - // 返回默认图片 + // 返回默认图片,调用DirUtil的getImgDirWithName方法,传入微信ID(wxId)和一个随机生成的文件名(通过IdUtil.fastSimpleUUID方法生成UUID并添加.gif后缀)作为参数, + // 获取一个用于保存下载图片的目标文件路径,该路径采用了随机文件名,方便在一些场景下避免文件名冲突等问题。 String destPath = DirUtil.getImgDirWithName(wxId, IdUtil.fastSimpleUUID() + ".gif"); try { - // 下载图片 + // 下载图片,调用HttpUtil的downloadFile方法,传入图片的来源路径(path)和目标保存路径(destPath)作为参数, + // 通过HTTP相关的下载机制将图片从指定的来源路径下载到本地的目标路径中,完成图片获取操作。 HttpUtil.downloadFile(path, destPath); - // 返回图片 + // 返回图片,构建一个成功状态(200 OK)的响应实体,设置响应的媒体类型为IMAGE_JPEG(表示返回的是JPEG格式的图片资源), + // 并将图片文件对应的输入流资源(通过Files.newInputStream方法获取图片文件的输入流,再包装为InputStreamResource对象)设置到响应实体的主体内容中, + // 这样客户端接收到该响应后就可以正确解析并显示图片内容了。 return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .body(new InputStreamResource(Files.newInputStream(Paths.get(destPath)))); } catch (Exception ignore) { - // 忽略异常 + // 忽略异常,如果在下载图片过程中出现异常,这里暂时选择忽略该异常(可能是考虑到不影响整体的流程继续执行,只是无法成功返回图片资源给客户端), + // 后续会返回404响应表示资源未找到。 } - // 默认返回404 + // 默认返回404,如果前面的下载操作出现异常或者没有成功下载到图片资源,则返回一个404状态的响应实体,表示资源未找到,作为兜底的返回结果。 return ResponseEntity.notFound().build(); } + /** + * 从本地路径下载图片的方法,首先获取当前用户信息,然后根据用户基础路径、微信ID以及给定的本地图片路径确定图片文件的实际路径, + * 检查图片文件是否存在,若存在则对图片(如果需要可能进行解密操作)进行处理后,以合适的格式(如设置为IMAGE_JPEG媒体类型)将图片资源作为响应返回给客户端, + * 若出现找不到图片等情况则返回404响应。 + * + * @param localPath 本地图片的路径,以字符串形式传入,该路径是基于本地文件系统的图片文件路径,通过结合用户相关信息可以确定其完整的实际路径, + * 进而对其进行存在性检查、解密(如果有需要)以及返回等操作。 + * @return ResponseEntity 返回一个包含图片资源(Resource类型)的响应实体(ResponseEntity类型), + * 如果成功获取并处理图片则返回包含图片内容且设置了正确媒体类型(IMAGE_JPEG)的响应,若出现异常或找不到图片则返回404状态的响应实体,表示资源未找到。 + */ @Override public ResponseEntity downloadImgFormLocal(String localPath) { try { - // 获取用户信息 + // 获取用户信息,调用userService的currentUser方法获取当前用户的微信ID(wxId),用于后续结合其他信息确定图片文件的实际路径以及相关操作, + // 确保针对的是当前用户对应的本地图片资源。 String wxId = userService.currentUser(); - // 获得文件目录 + // 获得文件目录,调用DirUtil的相关方法(这里是getDir方法,传入用户的基础路径、微信ID以及本地图片路径等参数)获取本地图片文件所在的完整目录路径, + // 以便后续检查文件是否存在以及进行其他操作。 String filePath = DirUtil.getDir(userService.getBasePath(wxId), wxId, localPath); - // 检查文件是否存在,返回404 + // 检查文件是否存在,返回404,通过FileUtil的exist方法判断获取到的图片文件路径对应的文件是否真实存在,如果不存在则返回404状态的响应实体, + // 表示无法找到要下载的本地图片文件。 if (!FileUtil.exist(filePath)) { return ResponseEntity.notFound().build(); } - // 获取图片文件夹地址 + // 获取图片文件夹地址,调用DirUtil的getImgDir方法,传入微信ID(wxId)作为参数,获取用于存放处理后(可能经过解密等操作)图片的文件夹路径, + // 方便后续将解密或处理后的图片放置到该文件夹中进行统一管理。 String outPath = DirUtil.getImgDir(wxId); - // 检查文件是否存在 + // 检查文件是否存在,通过FileUtil的exist方法再次判断获取到的图片输出文件夹路径对应的文件夹是否存在, + // 如果不存在则通过mkdir方法创建该文件夹,确保后续有合适的位置来存放处理后的图片文件。 if (!FileUtil.exist(outPath)) { FileUtil.mkdir(outPath); } - // 解密并返回 + // 解密并返回,调用ImgDecoderUtil的decodeDat方法,传入图片文件路径(filePath)和输出文件夹路径(outPath)作为参数, + // 对图片文件进行解密等相关处理操作,并返回处理后的图片文件路径(imgPath),如果处理后的图片路径为空则表示出现问题,同样返回404响应。 String imgPath = ImgDecoderUtil.decodeDat(filePath, outPath); - // 如果图片地址为空 if (imgPath == null) { return ResponseEntity.notFound().build(); } - // 返回图片 + // 返回图片,构建一个成功状态(200 OK)的响应实体,设置响应的媒体类型为IMAGE_JPEG(表示返回的是JPEG格式的图片资源), + // 并将图片文件对应的输入流资源(通过Files.newInputStream方法获取图片文件的输入流,再包装为InputStreamResource对象)设置到响应实体的主体内容中, + // 这样客户端接收到该响应后就可以正确解析并显示图片内容了。 return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .body(new InputStreamResource(Files.newInputStream(Paths.get(imgPath)))); } catch (Exception e) { + // 如果在获取用户信息、检查文件存在性、创建文件夹、图片解密以及构建响应等整个操作过程中出现任何异常,使用log.error记录错误信息, + // 方便后续排查问题,其中"downloadImgFormLocal error"表示从本地路径下载图片出现错误的提示内容,e则是捕获到的具体异常对象。 log.error("downloadImgFormLocal error", e); } - // 默认返回404 + // 默认返回404,如果前面的操作出现异常或者最终没有成功获取到图片资源,则返回一个404状态的响应实体,表示资源未找到,作为兜底的返回结果。 return ResponseEntity.notFound().build(); } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/MsgServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/MsgServiceImpl.java index 27a2c57..d427dde 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/MsgServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/MsgServiceImpl.java @@ -29,7 +29,9 @@ import java.util.List; import java.util.stream.Collectors; /** - * 消息服务实现类 + * 消息服务实现类,该类实现了MsgService接口,用于提供具体的消息相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调消息数据的查询(通过MsgRepository)、数据转换(通过MsgMapping)以及消息相关的额外处理(如根据消息类型应用不同策略等)、消息导出等操作, + * 为上层应用提供查询和管理消息的服务,例如获取指定聊天对象的消息列表、将消息数据导出为Excel文件等场景使用。 * * @author xcs * @date 2023年12月25日 17时04分 @@ -39,80 +41,124 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class MsgServiceImpl implements MsgService { + // 通过依赖注入获取消息数据访问仓库,用于执行与消息相关的数据库查询等操作,比如根据聊天对象和消息序列号查询消息记录等信息。 private final MsgRepository msgRepository; + // 通过依赖注入获取消息数据映射类,用于将从数据存储中获取到的原始消息数据转换为适合向外展示的MsgVO对象形式,方便业务逻辑处理和展示。 private final MsgMapping msgMapping; + // 通过依赖注入获取联系人头像URL数据访问仓库,用于查询联系人头像的URL地址,以便在消息相关处理中设置聊天对象的头像链接信息。 private final ContactHeadImgUrlRepository contactHeadImgUrlRepository; + // 通过依赖注入获取联系人数据访问仓库,用于查询联系人相关的数据,例如根据用户名获取联系人昵称等信息,在消息处理中可用于获取聊天对象的昵称等操作。 private final ContactRepository contactRepository; + /** + * 根据聊天对象(talker)和下一条消息的序列号(nextSequence)查询消息列表,并进行一系列数据处理后返回处理后的MsgVO对象列表。 + * 数据处理包括按照时间排序、设置聊天对象ID、格式化日期、设置聊天头像以及根据消息类型应用相应的处理策略等操作。 + * + * @param talker 聊天对话者,以字符串形式传入,用于指定要查询消息的聊天对象,通过该参数可以从数据库中筛选出与该聊天对象相关的消息记录。 + * @param nextSequence 下一条消息的序列号,以Long类型传入,用于在分页查询等场景下指定从哪条消息开始继续查询(比如实现消息的分页加载功能), + * 可以根据业务需求传入合适的序列号值来获取相应部分的消息列表。 + * @return List 返回一个包含MsgVO对象的列表,每个MsgVO对象包含了消息的各种关键信息(如发送者、发送时间、消息内容、消息类型等),经过了一系列处理后更适合展示和后续使用, + * 列表按照消息的创建时间进行了排序。 + */ @Override public List queryMsg(String talker, Long nextSequence) { + // 根据聊天对象和下一条消息序列号查询消息,调用msgRepository的queryMsgByTalker方法,传入聊天对象(talker)和下一条消息序列号(nextSequence)作为参数, + // 从数据库中获取与该聊天对象相关的所有消息记录,返回一个包含Msg对象的列表,Msg对象可能是数据库中存储消息的原始数据结构形式。 List allData = msgRepository.queryMsgByTalker(talker, nextSequence); - // 根据时间排序 + // 根据时间排序,对转换后的MsgVO对象列表进行排序操作,通过stream将列表转换为流,然后使用sorted方法结合Comparator.comparing(MsgVO::getCreateTime)按照消息的创建时间属性进行排序, + // 使得返回的消息列表按照时间先后顺序展示,方便查看消息的时间线顺序。 return msgMapping.convert(allData).stream().sorted(Comparator.comparing(MsgVO::getCreateTime)) - // 遍历数据 + // 遍历数据,通过peek方法对流中的每个MsgVO对象进行一系列操作,peek方法不会改变流中的元素数量和顺序,只是对每个元素进行特定的处理。 .peek(msgVO -> { msgVO.setWxId(getChatWxId(talker, msgVO)); - // 设置处理日期 + // 设置处理日期,调用DateUtil的formatDateTime方法,将消息的创建时间(msgVO.getCreateTime(),这里可能是以时间戳形式存储,先乘以1000转换为毫秒数)转换为指定格式的日期时间字符串, + // 以便更友好地展示给用户查看,将转换后的日期时间字符串设置到MsgVO对象的StrCreateTime属性中。 msgVO.setStrCreateTime(DateUtil.formatDateTime(new Date(msgVO.getCreateTime() * 1000))); - // 设置聊天头像 + // 设置聊天头像,调用getChatAvatar方法,传入消息对应的聊天对象ID(msgVO.getWxId())作为参数,获取对应的聊天头像URL地址, + // 并将获取到的头像URL设置到MsgVO对象的Avatar属性中,完善消息展示时的头像信息。 msgVO.setAvatar(getChatAvatar(msgVO.getWxId())); - // 读取消息类型策略 + // 读取消息类型策略,调用MsgStrategyFactory的getStrategy方法,传入消息的类型(msgVO.getType())和子类型(msgVO.getSubType())作为参数, + // 根据消息的类型和子类型获取对应的消息处理策略对象(MsgStrategy),不同类型的消息可能需要不同的处理方式,通过这种策略模式来灵活处理各种消息情况。 MsgStrategy strategy = MsgStrategyFactory.getStrategy(msgVO.getType(), msgVO.getSubType()); - // 根据对应的策略进行处理 - if (strategy != null) { + // 根据对应的策略进行处理,如果获取到的消息处理策略对象不为null,说明存在针对该类型消息的处理逻辑, + // 则调用该策略对象的process方法,传入MsgVO对象作为参数,对消息进行相应的特定处理(具体处理逻辑由不同的策略实现类决定)。 + if (strategy!= null) { strategy.process(msgVO); } }).collect(Collectors.toList()); } + /** + * 导出指定聊天对象(talker)的消息数据为Excel文件的方法,首先查询该聊天对象的所有消息记录,进行一系列数据处理后, + * 使用EasyExcel库将处理后的消息数据写入到指定的Excel文件中,并返回文件的完整路径,若在过程中出现异常会进行相应的错误记录。 + * + * @param talker 聊天对话者,以字符串形式传入,用于指定要导出消息的聊天对象,通过该参数可以从数据库中获取与之相关的所有消息记录进行导出操作。 + * @return String 返回一个字符串,表示导出后的Excel文件的完整路径,上层应用可以根据该路径对导出的文件进行进一步操作(如提示用户下载等)。 + */ @Override public String exportMsg(String talker) { + // 查询要导出的消息列表,调用msgRepository的exportMsg方法,传入聊天对象(talker)作为参数,从数据库中获取该聊天对象的所有消息记录, + // 返回一个包含Msg对象的列表,这些消息记录将作为后续导出操作的数据来源。 List msgList = msgRepository.exportMsg(talker); - // 根据时间排序 + // 根据时间排序,对转换后的MsgVO对象列表进行排序操作,通过stream将列表转换为流,然后使用sorted方法结合Comparator.comparing(MsgVO::getCreateTime)按照消息的创建时间属性进行排序, + // 使得导出的消息数据在Excel文件中按照时间先后顺序展示,方便查看消息的时间线顺序。 List msgVOList = msgMapping.convert(msgList).stream().sorted(Comparator.comparing(MsgVO::getCreateTime)) - // 遍历数据 + // 遍历数据,通过peek方法对流中的每个MsgVO对象进行一系列操作,peek方法不会改变流中的元素数量和顺序,只是对每个元素进行特定的处理。 .peek(msgVO -> { msgVO.setWxId(getChatWxId(talker, msgVO)); - // 设置处理日期 + // 设置处理日期,调用DateUtil的formatDateTime方法,将消息的创建时间(msgVO.getCreateTime(),这里可能是以时间戳形式存储,先乘以1000转换为毫秒数)转换为指定格式的日期时间字符串, + // 以便更友好地展示给用户查看,将转换后的日期时间字符串设置到MsgVO对象的StrCreateTime属性中。 msgVO.setStrCreateTime(DateUtil.formatDateTime(new Date(msgVO.getCreateTime() * 1000))); - // 读取消息类型策略 + // 读取消息类型策略,调用MsgStrategyFactory的getStrategy方法,传入消息的类型(msgVO.getType())和子类型(msgVO.getSubType())作为参数, + // 根据消息的类型和子类型获取对应的消息处理策略对象(MsgStrategy),不同类型的消息可能需要不同的处理方式,通过这种策略模式来灵活处理各种消息情况。 MsgStrategy strategy = MsgStrategyFactory.getStrategy(msgVO.getType(), msgVO.getSubType()); - // 根据对应的策略进行处理 - if (strategy != null) { + // 根据对应的策略进行处理,如果获取到的消息处理策略对象不为null,说明存在针对该类型消息的处理逻辑, + // 则调用该策略对象的process方法,传入MsgVO对象作为参数,对消息进行相应的特定处理(具体处理逻辑由不同的策略实现类决定)。 + if (strategy!= null) { strategy.process(msgVO); } }).collect(Collectors.toList()); - // 聊天人的昵称 + // 聊天人的昵称,调用contactRepository的getContactNickname方法,传入聊天对象(talker)作为参数,从数据库中查询获取该聊天对象对应的联系人昵称信息, + // 用于后续生成导出的Excel文件的文件名等操作,使得文件名更具可读性,能直观反映是哪个聊天对象的消息数据。 String nickname = contactRepository.getContactNickname(talker); - // 分隔符 + // 分隔符,通过获取默认文件系统的分隔符,用于后续拼接文件路径,确保在不同操作系统下路径的正确性(不同系统文件分隔符不同,如Windows是'\', Linux是'/')。 String separator = FileSystems.getDefault().getSeparator(); - // 文件路径 + // 文件路径,获取当前用户目录(通过System.getProperty("user.dir")获取),并拼接上"data"和"export"文件夹名称,构建出用于存放导出Excel文件的基础目录路径, + // 后续会在该目录下创建具体的文件并写入消息数据。 String filePath = System.getProperty("user.dir") + separator + "data" + separator + "export"; - // 创建文件 + // 创建文件,调用FileUtil的mkdir方法,传入构建好的文件路径(filePath)作为参数,创建用于存放导出文件的目录,如果目录已存在则不会重复创建, + // 确保有合适的文件夹来存放后续生成的Excel文件。 FileUtil.mkdir(filePath); - // 文件路径+文件名 + // 文件路径+文件名,在前面构建的文件路径基础上,拼接上聊天对象的昵称(作为文件名主体)和".xlsx"后缀,形成完整的导出Excel文件的路径和文件名, + // 后续将使用该路径来创建并写入消息数据到Excel文件中。 String pathName = filePath + separator + nickname + ".xlsx"; - // 导出 + // 导出,使用EasyExcel的write方法开始构建Excel文件写入操作,传入文件路径(pathName)和要写入的数据类型(ExportMsgVO.class)作为参数, + // 指定工作表名称为"sheet1",然后通过doWrite方法并传入一个lambda表达式作为数据源提供方式,在lambda表达式中调用msgMapping的convertToExportMsgVO方法, + // 将处理后的MsgVO对象列表转换为适合写入Excel的ExportMsgVO对象列表,最终将消息数据写入到Excel文件中完成导出操作。 EasyExcel.write(pathName, ExportMsgVO.class).sheet("sheet1").doWrite(() -> msgMapping.convertToExportMsgVO(msgVOList)); - // 返回写入后的文件 + // 返回写入后的文件,将生成的Excel文件的完整路径返回给调用者,方便上层应用进行后续操作(如提示用户下载、记录文件位置等)。 return pathName; } /** - * 获取对话人Id + * 获取对话人Id的方法,根据消息的发送情况(是我发送还是接收)以及消息中的额外字节信息(在群聊场景下)等因素来确定聊天对象的微信ID(wxId)。 + * 如果是我发送的消息则返回当前用户的微信ID,如果是接收的消息且是群聊场景则从消息的额外字节信息中解析出对应的聊天对象ID,若解析失败则返回聊天对象的原始名称。 * - * @param talker 聊天对话者 - * @param msgVO 消息VO - * @return wxId + * @param talker 聊天对话者,以字符串形式传入,代表当前消息对应的聊天对象名称,用于判断消息的发送方向以及在群聊场景下作为解析相关信息的基础参数。 + * @param msgVO 消息VO,以MsgVO对象形式传入,包含了消息的各种详细信息(如是否是发送者、额外字节信息等),通过该对象可以获取到判断聊天对象ID所需的关键数据。 + * @return wxId 返回一个字符串,表示聊天对象的微信ID,用于在其他地方(如设置头像、关联用户等操作)准确识别聊天对象的身份。 */ private String getChatWxId(String talker, MsgVO msgVO) { - // 我发送的消息 + // 我发送的消息,通过判断MsgVO对象中的是否是发送者属性(msgVO.getIsSender())是否等于1来确定当前消息是否是由我发送的, + // 如果是则调用SpringUtil获取UserService的实例,再调用其currentUser方法获取当前用户的微信ID作为聊天对象的微信ID返回, + // 因为我发送的消息对应的聊天对象就是我自己,所以返回当前用户的ID。 if (msgVO.getIsSender() == 1) { return SpringUtil.getBean(UserService.class).currentUser(); } - // 我接受的消息 + // 我接受的消息,当消息是我接收的情况时,进入以下逻辑进行处理,先判断是否是群聊场景。 try { - // 群聊 + // 群聊,通过判断聊天对象名称(talker)是否以特定的群聊后缀(ChatRoomConstant.CHATROOM_SUFFIX)结尾来确定是否是群聊场景, + // 如果是群聊则需要从消息的额外字节信息(msgVO.getBytesExtra())中解析出具体的聊天对象ID信息。 if (talker.endsWith(ChatRoomConstant.CHATROOM_SUFFIX)) { MsgProto.MessageBytesExtra messageBytesExtra = MsgProto.MessageBytesExtra.parseFrom(msgVO.getBytesExtra()); List message2List = messageBytesExtra.getMessage2List(); @@ -123,15 +169,20 @@ public class MsgServiceImpl implements MsgService { } } } catch (InvalidProtocolBufferException e) { + // 如果在解析群聊消息的额外字节信息过程中出现协议缓冲区无效的异常(InvalidProtocolBufferException), + // 使用log.error记录错误信息,方便后续排查问题,其中"Failed to obtain the conversationalist Id"表示获取对话人ID失败的提示内容,e则是捕获到的具体异常对象。 log.error("Failed to obtain the conversationalist Id", e); } + // 如果不是群聊场景或者在群聊场景下解析聊天对象ID失败,则直接返回聊天对象的原始名称(talker)作为聊天对象的微信ID, + // 这可能是在一些简单的一对一聊天等场景下直接使用聊天对象的名称来代表其身份标识。 return talker; } /** - * 聊天头像 + * 获取聊天头像的方法,根据给定的聊天对象的微信ID(wxId),查询并返回对应的聊天头像的URL地址, + * 通过调用contactHeadImgUrlRepository的相关查询方法来获取头像URL信息,用于在消息展示等场景中设置聊天对象的头像链接。 * - * @param wxId 聊天对话者 + * @param wxId 聊天对话者,以字符串形式传入,代表聊天对象的微信ID,通过该ID可以从数据存储中查找对应的联系人头像URL地址信息。 */ private String getChatAvatar(String wxId) { return contactHeadImgUrlRepository.queryHeadImgUrlByUserName(wxId); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/RecoverContactServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/RecoverContactServiceImpl.java index 58957f4..a81f49c 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/RecoverContactServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/RecoverContactServiceImpl.java @@ -20,7 +20,9 @@ import java.util.Set; import java.util.stream.Collectors; /** - * 找回联系人服务 实现类 + * 找回联系人服务实现类,该类实现了RecoverContactService接口,用于提供具体的找回联系人相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调联系人相关数据的查询(通过FTSContactContentRepository和ContactRepository)、数据转换(通过RecoverContactMapping)以及联系人数据的导出操作, + * 为上层应用提供找回联系人相关的查询和数据导出服务,比如根据特定条件查询可能找回的联系人信息,并能将这些信息导出为Excel文件等功能。 * * @author xcs * @date 2024年6月14日15:32:10 @@ -30,31 +32,61 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class RecoverContactServiceImpl implements RecoverContactService { + // 通过依赖注入获取全文搜索联系人内容数据访问仓库,用于执行与联系人相关的全文搜索等查询操作,比如根据特定条件查询联系人相关的内容信息,辅助找回联系人的功能实现。 private final FTSContactContentRepository ftsContactContentRepository; + // 通过依赖注入获取联系人数据访问仓库,用于查询联系人相关的其他数据,例如获取具有公众号属性的联系人集合等信息,在找回联系人的筛选等操作中起到辅助作用。 private final ContactRepository contactRepository; + // 通过依赖注入获取找回联系人数据映射类,用于将从数据存储中获取到的原始联系人相关数据转换为适合向外展示的RecoverContactVO对象形式,方便业务逻辑处理和展示。 private final RecoverContactMapping recoverContactMapping; + /** + * 根据传入的查询条件(RecoverContactDTO)查询可能找回的联系人信息,并进行相应的过滤和转换后返回RecoverContactVO对象列表。 + * 过滤逻辑是排除掉已经存在于特定联系人集合(具有公众号属性的联系人集合)中的联系人,然后将剩余的联系人数据转换为RecoverContactVO对象形式返回。 + * + * @param recoverContactDTO 查询条件,以RecoverContactDTO对象形式传入,包含了诸如筛选条件等信息,用于指定要查询的可能找回的联系人范围等要求。 + * @return List 返回一个包含RecoverContactVO对象的列表,每个RecoverContactVO对象包含了找回联系人相关的关键信息(如联系人别名等), + * 经过了筛选和转换操作后更适合展示和后续使用。 + */ @Override public List queryRecoverContact(RecoverContactDTO recoverContactDTO) { + // 根据查询条件查询联系人内容信息,调用ftsContactContentRepository的queryContactContent方法,传入RecoverContactDTO对象作为参数, + // 从数据库中获取符合查询条件的联系人相关内容信息,返回一个包含FTSContactContent对象的列表,这些对象包含了联系人的部分关键数据(如别名等),用于后续处理。 List ftsContactContents = ftsContactContentRepository.queryContactContent(recoverContactDTO); + // 获取具有公众号属性的联系人集合,调用contactRepository的getContactWithMp方法,获取一个包含具有公众号属性的联系人别名的集合(Set), + // 用于后续在查询到的联系人内容信息中进行筛选,排除掉已经是公众号的联系人,重点关注可能真正需要找回的联系人。 Set set = contactRepository.getContactWithMp(); + // 对查询到的联系人内容信息进行过滤和转换操作,先通过stream将联系人内容信息列表转换为流,然后使用filter方法进行过滤, + // 过滤条件是联系人的别名不在具有公众号属性的联系人集合中(即!set.contains(ftsContent.getAlias())),排除掉公众号类的联系人。 return ftsContactContents.stream() - .filter(ftsContent -> !set.contains(ftsContent.getAlias())) + .filter(ftsContent ->!set.contains(ftsContent.getAlias())) + // 对过滤后的联系人内容信息进行转换,通过map方法调用recoverContactMapping的convert方法,将每个FTSContactContent对象转换为RecoverContactVO对象, + // 使得数据格式更适合向外展示和后续的业务逻辑处理。 .map(recoverContactMapping::convert) + // 将转换后的RecoverContactVO对象收集为一个列表,最终返回该列表作为查询结果。 .collect(Collectors.toList()); } + /** + * 将可能找回的联系人信息导出为Excel文件的方法,首先确定导出文件的路径,创建相应的文件目录(如果不存在), + * 然后使用EasyExcel库将查询到的可能找回的联系人信息写入到指定的Excel文件中,并返回文件的完整路径,若在过程中出现异常会按照相应的默认逻辑处理。 + * + * @return String 返回一个字符串,表示导出后的Excel文件的完整路径,上层应用可以根据该路径对导出的文件进行进一步操作(如提示用户下载等)。 + */ @Override public String exportRecoverContact() { - // 文件路径 + // 文件路径,调用DirUtil的getExportDir方法,传入文件名("已删除好友.xlsx")作为参数,获取用于存放导出的Excel文件的完整路径, + // 该方法内部可能会根据项目的配置或者默认规则来生成合适的文件路径,确保文件能正确存储到指定位置。 String filePath = DirUtil.getExportDir("已删除好友.xlsx"); - // 创建文件 + // 创建文件,调用FileUtil的mkdir方法,传入根据文件路径创建的File对象的父目录(即new File(filePath).getParent())作为参数, + // 创建用于存放导出文件的目录,如果目录已存在则不会重复创建,确保有合适的文件夹来存放后续生成的Excel文件。 FileUtil.mkdir(new File(filePath).getParent()); - // 导出 + // 导出,使用EasyExcel的write方法开始构建Excel文件写入操作,传入文件路径(filePath)和要写入的数据类型(RecoverContactVO.class)作为参数, + // 指定工作表名称为"sheet1",然后通过doWrite方法并传入一个lambda表达式作为数据源提供方式,在lambda表达式中调用queryRecoverContact方法(传入一个默认的RecoverContactDTO对象作为参数), + // 来获取要写入Excel文件的可能找回的联系人信息数据(以RecoverContactVO对象列表形式返回),最终将这些数据写入到Excel文件中完成导出操作。 EasyExcel.write(filePath, RecoverContactVO.class) .sheet("sheet1") .doWrite(() -> queryRecoverContact(new RecoverContactDTO())); - // 返回写入后的文件 + // 返回写入后的文件,将生成的Excel文件的完整路径返回给调用者,方便上层应用进行后续操作(如提示用户下载、记录文件位置等)。 return filePath; } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/SessionServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/SessionServiceImpl.java index 6ce033d..bd6c94b 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/SessionServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/SessionServiceImpl.java @@ -13,7 +13,9 @@ import java.util.Collections; import java.util.List; /** - * 会话服务实现类 + * 会话服务实现类,该类实现了SessionService接口,用于提供具体的会话相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调会话数据的查询(通过SessionRepository)以及对查询到的会话数据进行后续处理, + * 比如处理会话中联系人头像路径为空的情况以及格式化会话的时间信息等,最终为上层提供处理好的会话相关数据,以便展示或进一步使用。 * * @author xcs * @date 2023年12月21日 17时17分 @@ -22,27 +24,43 @@ import java.util.List; @RequiredArgsConstructor public class SessionServiceImpl implements SessionService { + // 通过依赖注入获取会话数据访问仓库,用于执行与会话相关的数据库查询等操作,比如查询所有的会话信息记录等,为后续处理提供原始数据来源。 private final SessionRepository sessionRepository; + /** + * 查询会话信息的方法,首先通过会话数据访问仓库分页查询会话信息,然后对查询到的会话数据进行处理, + * 处理内容包括当联系人头像路径为空时设置默认的头像路径,以及将会话时间戳格式化为指定的日期时间格式, + * 如果查询结果为空则返回一个空列表。 + * + * @return List 返回一个包含SessionVO对象的列表,每个SessionVO对象包含了会话相关的关键信息(如联系人用户名、头像路径、时间等), + * 经过了相应的数据处理后更适合展示和后续使用,若没有查询到会话信息则返回一个空列表。 + */ @Override public List querySession() { - // 分页查询会话信息 + // 分页查询会话信息,使用Opt.ofNullable方法对sessionRepository.querySession()的返回结果进行包装, + // 如果结果不为null则进行后续的链式操作,若为null则直接返回默认值(由orElse指定)。 + // sessionRepository.querySession()用于从数据存储(可能是数据库)中获取所有的会话信息记录,以列表形式返回(可能是分页后的结果,具体取决于该方法内部实现)。 return Opt.ofNullable(sessionRepository.querySession()) - // 处理头像为空问题 + // 处理头像为空问题,对获取到的会话信息列表进行处理,通过lambda表达式来实现具体的操作逻辑,主要是遍历列表中的每个SessionVO对象,处理头像路径为空的情况。 .map(sessions -> { for (SessionVO session : sessions) { - // 如果有头像则不处理 + // 如果有头像则不处理,判断当前SessionVO对象的头像路径(session.getHeadImgUrl())是否不为空字符串, + // 如果不为空说明已经有头像路径了,直接跳过本次循环,不进行后续设置默认头像路径的操作。 if (!StrUtil.isBlank(session.getHeadImgUrl())) { continue; } - // 设置联系人头像路径 + // 设置联系人头像路径,当头像路径为空时,设置一个默认的头像路径,通过拼接字符串的方式构造出一个URL形式的头像路径, + // 其中包含了联系人的用户名(session.getUserName())作为参数,用于后续获取联系人头像的相关操作,该路径可能指向一个默认头像或者根据用户名获取头像的接口地址等。 session.setHeadImgUrl("/api/contact/headImg/avatar?userName=" + session.getUserName()); } return sessions; }) - // 处理日期 + // 处理日期,对处理完头像路径后的会话信息列表进行操作,通过ifPresent方法,当列表不为空时(即前面查询到了会话信息), + // 对列表中的每个SessionVO对象执行lambda表达式中的操作,这里是调用DateFormatUtil的formatTimestamp方法将会话的时间戳(sessionVo.getTime())格式化为指定的日期时间格式, + // 并将格式化后的结果设置到SessionVO对象的ShortTime属性中,方便后续展示更友好的时间信息。 .ifPresent(sessionVos -> sessionVos.forEach(sessionVo -> sessionVo.setShortTime(DateFormatUtil.formatTimestamp(sessionVo.getTime())))) - // 默认值 + // 默认值,如果前面的查询结果为空(即sessionRepository.querySession返回null),则返回一个空列表(通过Collections.emptyList()获取), + // 作为兜底的返回结果,确保方法始终返回符合预期类型(List)的结果。 .orElse(Collections.emptyList()); } } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/UserServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/UserServiceImpl.java index a7fb76f..35129d2 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/UserServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/UserServiceImpl.java @@ -28,7 +28,9 @@ import java.util.List; import java.util.Optional; /** - * UserService 实现类 + * UserService实现类,该类实现了UserService接口,用于提供具体的用户相关业务逻辑处理方法。 + * 在整个项目架构中处于服务层,负责协调用户数据的获取、保存、切换等操作,涉及从文件系统读取用户配置信息、与不同数据源交互获取用户相关属性(如头像、昵称等)以及更新用户信息等功能, + * 为上层应用提供全面的用户管理相关服务,例如获取当前用户信息、切换登录用户、获取所有用户列表等场景使用。 * * @author xcs * @date 2024年6月15日16:06:37 @@ -38,85 +40,138 @@ import java.util.Optional; @RequiredArgsConstructor public class UserServiceImpl implements UserService { + // 通过依赖注入获取联系人头像URL数据访问仓库,用于查询联系人头像的URL地址,在获取用户头像信息时会调用该仓库的相关方法,从对应数据源获取头像数据。 private final ContactHeadImgUrlRepository contactHeadImgUrlRepository; + // 通过依赖注入获取联系人数据访问仓库,用于查询联系人相关的数据,例如根据用户名获取联系人昵称等信息,在获取用户昵称等操作中会使用该仓库的方法与数据源交互。 private final ContactRepository contactRepository; + // 通过依赖注入获取用户数据映射类,用于将从文件系统读取到的原始用户数据(以UserBO形式存在)转换为适合向外展示的UserInfoVO或UserVO等对象形式,方便业务逻辑处理和展示。 private final UserMapping userMapping; + /** + * 获取当前用户的详细信息(以UserInfoVO形式返回)的方法,首先获取当前选中的用户账号(微信ID),进行空校验后获取该用户对应的目录路径, + * 接着从目录下的文件中解析出用户数据(UserBO对象),如果昵称缺失则从数据源获取并补全,最后将UserBO对象转换为UserInfoVO对象返回,若过程中出现关键数据为空则返回null。 + * + * @return UserInfoVO 返回一个包含当前用户详细信息的UserInfoVO对象,其中包含了诸如昵称、微信ID、其他相关配置等信息,方便展示和在业务中使用当前用户的完整信息, + * 若无法获取到有效信息(如未找到当前用户账号或对应目录不存在等情况)则返回null。 + */ @Override public UserInfoVO userInfo() { - // 当前选中账号 + // 当前选中账号,调用currentUser方法获取当前选中的用户账号(微信ID),用于后续定位该用户相关的文件、数据等信息, + // 这是获取当前用户详细信息的基础,后续操作都围绕该账号对应的资源展开。 String wxId = currentUser(); - // 空校验 + // 空校验,判断获取到的微信ID是否为null,如果为null说明没有获取到当前有效的用户账号,直接返回null,避免后续出现空指针等异常情况。 if (wxId == null) { return null; } - // 当前账号目录 + // 当前账号目录,调用DirUtil的getUserDir方法,传入微信ID(wxId)作为参数,获取该用户在文件系统中对应的目录路径, + // 该目录下可能存放着与该用户相关的配置文件等数据,是后续读取用户详细信息的来源位置。 String userDir = DirUtil.getUserDir(wxId); - // 空校验 + // 空校验,通过FileUtil的exist方法判断获取到的用户目录是否真实存在,如果不存在则返回null,因为无法从不存在的目录中获取用户信息, + // 确保后续操作的目录是有效的。 if (!FileUtil.exist(userDir)) { return null; } - // 解析并返回 + // 解析并返回,使用JSONUtil的toBean方法,将从用户目录下读取的UTF-8编码的字符串内容(通过FileUtil.readUtf8String方法读取文件内容)转换为UserBO对象, + // 该对象包含了从文件中解析出的用户详细信息(如昵称、微信ID、基础路径等),作为后续处理和转换的基础数据。 UserBO userBO = JSONUtil.toBean(FileUtil.readUtf8String(userDir), UserBO.class); - // 补全昵称 + // 补全昵称,判断解析出的UserBO对象中的昵称属性(userBO.getNickname())是否为空字符串(通过StrUtil.NULL常量判断), + // 如果为空说明昵称缺失,需要调用getNickName方法根据微信ID(userBO.getWxId())从数据源获取昵称并设置到UserBO对象中进行补全。 if (StrUtil.NULL.equals(userBO.getNickname())) { userBO.setNickname(getNickName(userBO.getWxId())); } return userMapping.convert(userBO); } + /** + * 获取当前用户头像URL的方法,首先获取当前选中的用户账号(微信ID),进行空校验后调用getAvatar方法根据微信ID获取对应的头像URL地址并返回, + * 若获取到的微信ID为空则返回null。 + * + * @return String 返回一个字符串,表示当前用户的头像URL地址,用于在展示用户信息等场景中设置用户头像的链接,若无法获取到有效微信ID则返回null。 + */ @Override public String avatar() { String wxId = currentUser(); - // 空校验 + // 空校验,判断获取到的微信ID是否为null,如果为null说明没有获取到当前有效的用户账号,直接返回null,避免后续调用获取头像方法时出现空指针等异常情况。 if (wxId == null) { return null; } return getAvatar(wxId); } + /** + * 获取当前用户昵称的方法,首先获取当前选中的用户账号(微信ID),进行空校验后调用getNickName方法根据微信ID获取对应的昵称并返回, + * 若获取到的微信ID为空则返回null。 + * + * @return String 返回一个字符串,表示当前用户的昵称,用于在展示用户信息等场景中显示用户的称呼,若无法获取到有效微信ID则返回null。 + */ @Override public String nickname() { String wxId = currentUser(); - // 空校验 + // 空校验,判断获取到的微信ID是否为null,如果为null说明没有获取到当前有效的用户账号,直接返回null,避免后续调用获取昵称方法时出现空指针等异常情况。 if (wxId == null) { return null; } return getNickName(wxId); } + /** + * 获取所有用户信息(以UserVO列表形式返回)的方法,首先创建一个空的UserVO对象列表用于存储所有用户信息, + * 然后获取所有的微信ID列表,遍历每个微信ID,获取对应的头像、昵称以及判断是否为当前用户等信息,将这些信息封装为UserVO对象添加到列表中,最后返回该列表。 + * + * @return List 返回一个包含UserVO对象的列表,每个UserVO对象包含了用户的微信ID、昵称、头像URL以及是否为当前用户等关键信息, + * 用于展示所有用户的基本情况,方便在多用户相关的业务场景中进行操作和展示。 + */ @Override public List users() { - // 用户信息 + // 用户信息,创建一个空的UserVO对象列表,用于后续存储所有用户的信息,通过逐个添加UserVO对象来构建完整的用户列表数据。 List users = new ArrayList<>(); - // 获取微信Id + // 获取微信Id,调用getWxIds方法获取所有的微信ID列表,该列表包含了系统中所有用户对应的微信账号标识,是后续遍历构建每个用户信息的基础数据来源。 List wxIds = getWxIds(); - // 遍历 + // 遍历,通过循环遍历获取到的微信ID列表,对每个微信ID进行相关信息的获取和封装操作,构建对应的UserVO对象并添加到用户列表中。 for (String wxId : wxIds) { - // 当前选中账号 + // 当前选中账号,通过比较当前微信ID(wxId)和当前用户的微信ID(通过currentUser方法获取)是否相等,判断该用户是否为当前选中的用户, + // 返回一个布尔值,用于后续在UserVO对象中标识该用户的当前选中状态。 boolean current = wxId.equals(currentUser()); - // 头像 + // 头像,调用getAvatar方法,传入当前微信ID(wxId)作为参数,从数据源获取该用户对应的头像URL地址,用于在构建UserVO对象时设置头像信息。 String avatar = getAvatar(wxId); - // 昵称 + // 昵称,调用getNickName方法,传入当前微信ID(wxId)作为参数,从数据源获取该用户对应的昵称,用于在构建UserVO对象时设置昵称信息。 String nickName = getNickName(wxId); - // 用户信息 + // 用户信息,创建一个新的UserVO对象,将当前微信ID(wxId)、获取到的昵称(nickName)、头像URL(avatar)以及是否为当前用户(current)等信息作为参数传入构造函数, + // 构建一个完整的UserVO对象,并添加到用户列表(users)中,完成一个用户信息的封装和添加操作。 users.add(new UserVO(wxId, nickName, avatar, current)); } return users; } + /** + * 切换当前用户的方法,将传入的微信ID(wxId)写入到用户切换配置目录对应的文件中,通过这种方式记录当前选中的用户账号, + * 以便后续在获取当前用户等相关操作中能正确识别当前使用的是哪个用户的信息。 + * + * @param wxId 要切换到的用户的微信ID,以字符串形式传入,指定了需要设置为当前用户的账号标识,会将该微信ID保存到相应的配置文件中实现用户切换功能。 + */ @Override public void switchUser(String wxId) { FileUtil.writeString(wxId, DirUtil.getSwitchUserDir(), "UTF-8"); } + /** + * 获取当前用户的微信ID的方法,首先获取用户切换配置目录对应的路径,判断该目录是否存在, + * 如果不存在则从所有微信ID列表中获取第一个微信ID作为默认的当前用户ID(若列表为空则返回null), + * 如果目录存在则直接读取该目录下文件中的内容作为当前用户的微信ID并返回。 + * + * @return String 返回一个字符串,表示当前用户的微信ID,用于在其他与当前用户相关的操作(如获取当前用户信息、头像、昵称等)中确定操作的对象, + * 若无法获取到有效信息则返回null。 + */ @Override public String currentUser() { - // 获取用户切换配置目录 + // 获取用户切换配置目录,调用DirUtil的getSwitchUserDir方法获取用于存储用户切换相关配置的目录路径, + // 后续会根据该目录是否存在来决定如何获取当前用户的微信ID。 String switchUserDir = DirUtil.getSwitchUserDir(); - // 不存在的情况下,默认读取第一个 + // 不存在的情况下,默认读取第一个,通过FileUtil的exist方法判断获取到的用户切换配置目录是否存在, + // 如果不存在,说明没有明确的用户切换配置记录,需要从所有微信ID列表中获取第一个微信ID作为默认的当前用户ID(若列表为空则返回null)。 if (!FileUtil.exist(switchUserDir)) { - // 获取微信Id + // 获取微信Id,使用Optional对获取到的微信ID列表(通过getWxIds方法获取)进行包装,先通过filter方法过滤掉空列表情况(确保列表不为空), + // 再通过map方法获取列表中的第一个元素作为默认的当前用户微信ID,若整个过程出现异常或者列表为空则返回null。 return Optional.of(getWxIds()) .filter(items -> !items.isEmpty()).map(items -> items.get(0)) .orElse(null); @@ -124,40 +179,68 @@ public class UserServiceImpl implements UserService { return FileUtil.readUtf8String(switchUserDir); } + /** + * 保存用户信息的方法,将传入的UserBO对象转换为JSON字符串格式,然后写入到该用户对应的目录下的文件中, + * 通过这种方式实现用户信息的持久化存储,方便后续读取和使用用户相关的配置等数据。 + * + * @param userBO 要保存的用户信息对象,以UserBO对象形式传入,包含了用户的各种详细信息(如昵称、微信ID、基础路径等), + * 这些信息会被转换为JSON字符串并保存到对应的文件中,用于更新或新增用户的配置信息。 + */ @Override public void saveUser(UserBO userBO) { FileUtil.writeString(JSONUtil.toJsonStr(userBO), DirUtil.getUserDir(userBO.getWxId()), "UTF-8"); } + /** + * 获取用户的基础路径的方法,首先根据传入的微信ID(wxId)获取该用户在文件系统中对应的目录路径,进行空校验后读取该目录下文件中的内容, + * 将内容解析为UserBO对象,再从该对象中获取基础路径属性并返回,若目录不存在则返回null。 + * + * @param wxId 要获取基础路径的用户的微信ID,以字符串形式传入,用于定位该用户对应的目录以及从中解析出相关信息, + * 通过该微信ID可以准确获取到特定用户配置文件中存储的基础路径信息。 + * @return String 返回一个字符串,表示用户的基础路径,可用于后续涉及到该用户相关文件操作等场景下确定基础的文件路径范围,若无法获取到有效目录则返回null。 + */ @Override public String getBasePath(String wxId) { String userDir = DirUtil.getUserDir(wxId); - // 空校验 + // 空校验,通过FileUtil的exist方法判断获取到的用户目录是否真实存在,如果不存在则返回null,因为无法从不存在的目录中获取用户的基础路径信息, + // 确保后续操作的目录是有效的。 if (!FileUtil.exist(userDir)) { return null; } String userJson = FileUtil.readUtf8String(userDir); - // 转换json并获取basePath参数 + // 转换json并获取basePath参数,使用JSONUtil的toBean方法将读取的JSON字符串(userJson)转换为UserBO对象, + // 再从该对象中获取基础路径(basePath)属性并返回,该基础路径信息可用于后续与该用户相关的文件操作等业务场景。 return JSONUtil.toBean(userJson, UserBO.class).getBasePath(); } + /** + * 获取所有微信ID的方法,首先创建一个空的字符串列表用于存储微信ID,然后获取数据库目录对应的路径,判断该目录是否存在, + * 如果不存在则直接返回空列表,若存在则通过遍历该目录下的所有条目,筛选出文件夹类型的条目,将其名称(即微信ID)添加到列表中,最后返回该列表, + * 若在遍历过程中出现IOException异常则记录错误信息。 + * + * @return List 返回一个包含所有微信ID的字符串列表,该列表包含了系统中所有用户对应的微信账号标识,用于后续在多用户相关的业务操作中进行遍历、查询等操作, + * 若数据库目录不存在或者遍历出现异常则可能返回空列表。 + */ /** * 获取微信Id * * @return wxIds */ private List getWxIds() { - // 用户信息 + // 用户信息,创建一个空的字符串列表,用于后续存储所有用户对应的微信ID,通过逐个添加微信ID来构建完整的列表数据。 List userVOList = new ArrayList<>(); - // 目录 + // 目录,通过Paths.get方法根据DirUtil的getDbDir方法获取的数据库目录路径构建一个Path对象,用于后续判断目录是否存在以及遍历目录下的条目等操作, + // 该目录可能存放着与各个用户相关的数据或者配置信息,是获取微信ID的基础位置。 Path path = Paths.get(DirUtil.getDbDir()); - // 查看目录是否存在 + // 查看目录是否存在,通过FileUtil的exist方法判断构建的Path对象对应的目录是否真实存在,如果不存在则直接返回空的微信ID列表, + // 因为没有可遍历的目录也就无法获取到微信ID信息了。 if (!FileUtil.exist(path.toFile())) { return userVOList; } - // 指定要扫描的目录 + // 指定要扫描的目录,通过Files的newDirectoryStream方法创建一个DirectoryStream对象,用于遍历指定Path对象(即数据库目录)下的所有条目, + // 可以方便地对目录下的文件和文件夹等进行逐一处理,这里准备开始遍历目录下的内容来查找微信ID相关信息。 try (DirectoryStream stream = Files.newDirectoryStream(path)) { - // 遍历 + // 遍历,通过循环遍历Directory for (Path entry : stream) { // 判断是否为文件夹 if (FileUtil.isDirectory(entry)) { @@ -171,27 +254,51 @@ public class UserServiceImpl implements UserService { } /** - * 根据wxId获取头像 + * 根据wxId获取头像的方法。 + * 此方法的主要功能是从特定的数据源中获取指定微信ID(wxId)对应的用户头像的URL地址。 + * 它借助了动态数据源切换机制,先将数据源切换到对应微信ID所属的正确数据源(这里涉及到与微信消息相关的数据源,即 DataSourceType.MICRO_MSG_DB), + * 然后通过数据访问层的方法查询头像URL,最后再清除数据源切换的设置,返回获取到的头像URL地址。整个过程保证了在多数据源环境下能准确获取到所需用户头像信息。 * - * @param wxId wxId - * @return 头像 + * @param wxId 微信ID,以字符串形式传入,用于唯一标识要获取头像的用户,通过这个ID可以在相应数据源中定位到该用户的头像相关记录,进而获取头像的URL地址。 + * @return 头像 返回一个字符串,该字符串代表了对应微信ID用户的头像URL地址。这个URL可以在前端展示页面等地方使用,用于加载并显示用户的头像图片。 + * 如果在对应的数据源中找不到该微信ID的头像信息,或者在查询过程中出现其他异常情况,返回的结果可能为空字符串或者null(具体取决于相关数据访问层的实现)。 */ private String getAvatar(String wxId) { + // 使用DynamicDataSourceContextHolder的push方法来设置当前使用的数据源。 + // 它会根据传入的微信ID(wxId)以及指定的数据源类型(DataSourceType.MICRO_MSG_DB),通过DSNameUtil的getDSName方法来获取具体的数据源名称, + // 并将其“推”入到当前的数据源上下文环境中,使得后续的数据访问操作(如下面的查询头像URL操作)会在这个指定的数据源上进行,以此确保能从正确的数据源获取数据。 DynamicDataSourceContextHolder.push(DSNameUtil.getDSName(wxId, DataSourceType.MICRO_MSG_DB)); + // 调用contactHeadImgUrlRepository(这应该是一个数据访问层的接口实现类,用于与存储头像URL相关的数据存储进行交互)的queryHeadImgUrlByUserName方法, + // 传入微信ID(wxId)作为参数,从当前设定好的数据源中查询该用户名(也就是微信ID)对应的头像URL地址。 + // 这个查询操作可能涉及到数据库的查询语句执行等具体操作,由contactHeadImgUrlRepository的具体实现类来负责完成与底层数据存储的交互逻辑,最终获取到头像URL并赋值给avatar变量。 String avatar = contactHeadImgUrlRepository.queryHeadImgUrlByUserName(wxId); + // 在获取完头像URL地址后,使用DynamicDataSourceContextHolder的clear方法来清除之前设置的数据源上下文。 + // 这样做是为了避免对后续其他可能涉及数据源操作的代码产生影响,将数据源的状态恢复到默认或者之前的状态,保持整个应用的数据源使用环境的正确和有序。 DynamicDataSourceContextHolder.clear(); return avatar; } /** - * 根据wxId获取昵称 + * 根据wxId获取昵称的方法。 + * 本方法旨在从特定的数据源中,依据给定的微信ID(wxId)查找并获取对应的用户昵称信息。 + * 同样运用了动态数据源切换机制,先切换到正确的数据源(与微信消息相关的 DataSourceType.MICRO_MSG_DB), + * 再通过相应的数据访问层方法获取昵称,最后清除数据源切换设置,返回获取到的用户昵称,以此保障在多数据源架构下准确获取用户昵称。 * - * @param wxId wxId - * @return 昵称 + * @param wxId 微信ID,以字符串形式传入,作为唯一标识要获取昵称的用户的依据,通过这个ID可以在对应的数据源中找到该用户的昵称记录,从而获取昵称信息。 + * @return 昵称 返回一个字符串,代表对应微信ID用户的昵称,这个昵称可以用于在展示用户信息的界面等场景中,明确地显示出用户的称呼,方便用户识别。 + * 若在数据源中不存在与该微信ID对应的昵称信息,或者在获取昵称的过程中发生异常,返回的结果可能为空字符串或者null(具体取决于相关数据访问层实现)。 */ private String getNickName(String wxId) { + // 借助DynamicDataSourceContextHolder的push方法来设定当前要使用的数据源。 + // 依据传入的微信ID(wxId)以及指定的数据源类型(DataSourceType.MICRO_MSG_DB),利用DSNameUtil的getDSName方法确定具体的数据源名称, + // 然后将这个数据源名称“推”入当前的数据源上下文,使得后续的数据访问操作会在该指定数据源上执行,确保能从正确的数据源获取用户昵称数据。 DynamicDataSourceContextHolder.push(DSNameUtil.getDSName(wxId, DataSourceType.MICRO_MSG_DB)); + // 调用contactRepository(这通常是一个用于与存储联系人相关数据的存储进行交互的数据访问层接口实现类)的getNickName方法, + // 把微信ID(wxId)作为参数传入,从当前设定好的数据源中查询获取对应的用户昵称。 + // 具体的查询实现是由contactRepository的具体实现类负责的,可能会涉及到数据库表的查询、数据提取等操作,最终获取到的用户昵称赋值给nickName变量。 String nickName = contactRepository.getNickName(wxId); + // 在获取完用户昵称后,使用DynamicDataSourceContextHolder的clear方法清除之前设置的数据源上下文, + // 目的是恢复数据源状态,防止对后续其他数据源相关操作造成不必要的干扰,确保整个应用在数据源使用方面的正常和规范。 DynamicDataSourceContextHolder.clear(); return nickName; } diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/WeChatServiceImpl.java b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/WeChatServiceImpl.java index 3d5b7f9..f7f804c 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/WeChatServiceImpl.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/service/impl/WeChatServiceImpl.java @@ -29,7 +29,9 @@ import java.nio.file.Paths; import java.util.*; /** - * 微信服务实现类 + * 微信服务实现类,该类实现了WeChatService接口,用于提供与微信相关的特定业务逻辑处理方法。 + * 主要涉及到读取微信配置信息的功能,包括获取微信进程ID、校验微信版本、根据内存地址偏移量读取微信相关信息(如昵称、账号、手机号等)以及获取微信文件存储路径等操作, + * 在整个项目架构中处于服务层,为上层应用提供微信相关配置数据的查询和获取服务,以便进行后续的展示或者其他相关业务处理。 * * @author xcs * @date 2023年12月25日 09时37分 @@ -40,335 +42,476 @@ import java.util.*; public class WeChatServiceImpl implements WeChatService { /** - * 微信进程 + * 微信进程名称,定义为常量字符串,用于在后续查找微信进程相关操作中作为标识来确定目标进程, + * 通常操作系统中的微信可执行文件名为"WeChat.exe",通过这个名称可以在进程列表中准确识别微信的进程。 */ private static final String EXE_NAME = "WeChat.exe"; /** - * 微信程序dll文件 + * 微信程序的动态链接库(dll)文件名称,定义为常量字符串,在涉及到与微信底层功能交互(可能通过调用该dll中的函数实现一些特定功能)时, + * 以此名称来定位和加载对应的dll文件,在Windows系统下微信相关的一些功能实现可能依赖于"WeChatWin.dll"这个文件。 */ private static final String MODULE_NAME = "WeChatWin.dll"; /** - * 文档目录 + * 文档目录的标识字符串,定义为常量,在后续涉及到文件路径相关操作中,如果需要定位到系统的文档目录相关位置时, + * 可以通过该标识来进行相应的判断或者拼接完整的文件路径,不同操作系统可能有不同的表示方式,这里采用了一种特定的表示形式(可能针对Windows系统)。 */ private static final String MY_DOCUMENT = "MyDocument:"; /** - * 注册表中 WeChat 的路径 + * 注册表中微信相关配置的路径,定义为常量字符串,在Windows操作系统中,应用程序通常会将一些配置信息存储在注册表中, + * 这里指定了微信在注册表中的存储路径,后续可以通过该路径去读取微信相关的配置键值,例如文件保存路径等信息。 */ private static final String WECHAT_REG_PATH = "Software\\Tencent\\WeChat"; /** - * 注册表键值名称 + * 注册表中用于存储微信文件保存路径的键值名称,定义为常量字符串,通过该名称可以在注册表中找到微信具体设置的文件保存路径信息, + * 用于后续获取微信相关文件存储位置等操作。 */ private static final String FILE_SAVE_PATH = "FileSavePath"; /** - * WeChat 文件夹名称 + * 微信文件所在文件夹的名称,定义为常量字符串,在构建微信文件相关的完整路径时,会使用到这个文件夹名称来确定具体的位置, + * 通常微信的各类数据文件会存放在以"WeChat Files"命名的文件夹下。 */ private static final String WECHAT_FILES_DIR = "\\WeChat Files"; /** - * WeChat 配置文件的路径 + * 微信配置文件的具体路径,定义为常量字符串,明确指出了微信配置文件在系统中的存放位置, + * 后续可能会通过读取该配置文件来获取更多微信相关的配置参数等信息(虽然当前代码中未体现对该文件的读取操作)。 */ private static final String CONFIG_FILE_PATH = "\\AppData\\Roaming\\Tencent\\WeChat\\All Users\\config\\3ebffe94.ini"; /** - * 微信内存地址偏移量配置 + * 微信内存地址偏移量配置对象,通过依赖注入获取,用于存储不同微信版本对应的内存地址偏移量信息, + * 在读取微信相关信息(如昵称、账号等)时,需要依据当前微信版本对应的偏移量来准确获取内存中的数据,不同版本可能偏移量不同。 */ private final WeChatOffsetProperties weChatOffsetConfig; + /** + * 读取微信配置信息的方法,整体流程是先获取微信的进程ID列表,若微信未启动(进程ID列表为空)则抛出异常, + * 然后遍历每个进程ID,进行版本校验、获取对应版本的偏移量,接着依据偏移量从内存中读取微信的各项信息(昵称、账号、手机号等)以及获取微信文件目录和微信ID等, + * 最后将这些信息封装为WeChatConfigVO对象添加到列表中并返回,若在过程中出现如版本不支持、未读取到偏移量等问题则抛出相应异常。 + * + * @return List 返回一个包含WeChatConfigVO对象的列表,每个WeChatConfigVO对象包含了微信的进程ID、基址、版本号以及从内存中读取的昵称、账号、手机号、文件目录、微信ID等配置信息, + * 用于展示微信的相关配置详情,方便在其他业务逻辑中使用这些信息进行进一步操作(如展示给用户查看、与其他系统集成等)。 + */ @Override public List readWeChatConfig() { - // 获取微信进程Id + // 获取微信进程Id,调用wechatPid方法来查找并获取正在运行的微信进程的ID列表,该方法内部可能通过与操作系统相关的进程查找机制(比如Windows系统的相关API调用)来实现, + // 获取到的进程ID列表将作为后续操作的基础,用于定位不同的微信运行实例。 List pidList = wechatPid(); - // 微信未登录 + // 微信未登录,通过判断获取到的微信进程ID列表是否为空来确定微信是否已经启动并运行, + // 如果为空表示没有找到正在运行的微信进程,说明微信尚未启动,此时抛出BizException异常,提示用户需要先打开微信才能继续后续操作。 if (pidList.isEmpty()) { throw new BizException(-1, "检测到微信尚未启动,请先打开微信以继续进行操作。"); } List weChatConfigVOList = new ArrayList<>(); - // 遍历微信Id列表 + // 遍历微信Id列表,通过循环遍历获取到的微信进程ID列表,对每个微信进程进行相关配置信息的读取和处理操作,构建对应的WeChatConfigVO对象并添加到列表中。 for (Integer pid : pidList) { - // 获取版本号 + // 获取版本号,调用getVersion方法并传入当前微信进程的ID(pid)作为参数,从对应的微信进程中获取其版本号信息, + // 该版本号后续会用于版本校验以及确定对应的内存地址偏移量等操作,获取版本号的具体实现可能涉及到读取微信进程内存中的特定数据或者其他相关机制。 String version = getVersion(pid); - // 支持最小的版本号 + // 支持最小的版本号,通过对weChatOffsetConfig对象(存储微信内存地址偏移量配置信息)中版本相关配置的键值集合进行操作, + // 使用stream流找到第一个键(也就是最早支持的最小版本号),若集合为空则默认使用"0.0.0.0"作为最小版本号,这个最小版本号用于后续和当前微信版本号进行比较校验。 String supportMinVersion = weChatOffsetConfig.getVersion().keySet().stream().findFirst().orElse("0.0.0.0"); - // 校验版本号 + // 校验版本号,调用compareVersions方法,传入最小支持版本号(supportMinVersion)和当前获取到的微信版本号(version)作为参数, + // 对微信版本进行校验,如果当前版本号小于最小支持版本号,说明当前微信版本不满足要求,抛出BizException异常,提示用户需要升级微信到最新版本,并告知当前的微信版本号。 if (compareVersions(supportMinVersion, version)) { throw new BizException(-1, "当前微信版本不支持,请升级微信最新版本,当前微信版本号:" + version); } - // 读取微信版本对应的偏移量 + // 读取微信版本对应的偏移量,调用getVersionConfig方法并传入当前微信版本号(version)作为参数,从weChatOffsetConfig对象中获取对应版本的内存地址偏移量配置信息, + // 这些偏移量对于准确从微信进程内存中读取相关信息(如昵称、账号等)至关重要,不同版本的微信可能在内存布局上有所不同,需要对应的偏移量来定位具体数据位置。 WeChatOffsetProperties.VersionConfig versionConfig = getVersionConfig(version); - // 未读取到偏移量 + // 未读取到偏移量,判断获取到的版本对应的偏移量配置对象是否为null,如果是null表示没有找到当前微信版本对应的偏移量信息, + // 可能是配置缺失等原因,此时抛出BizException异常,提示用户需要从Github获取最新代码,并告知当前的微信版本号。 if (versionConfig == null) { throw new BizException(-1, "未读取到偏移量配置,请从Github获取最新代码,当前微信版本号:" + version); } - // 获取微信的基址 + // 获取微信的基址,调用baseAddress方法并传入当前微信进程的ID(pid)作为参数,获取微信进程在内存中的基址信息, + // 基址是后续根据偏移量计算具体数据内存地址的基础,有了基址和偏移量就能准确找到需要读取的微信相关信息在内存中的位置。 long baseAddress = baseAddress(pid); - // 获取微信昵称 + // 获取微信昵称,调用getInfo方法,传入当前微信进程的ID(pid)以及计算得到的微信昵称在内存中的地址(通过基址加上对应版本的昵称偏移量,即baseAddress + versionConfig.getNickname())作为参数, + // 从微信进程内存中读取并获取微信昵称信息,读取内存数据的具体实现可能涉及到与操作系统底层内存交互的相关操作(比如通过JNA库调用系统API等方式)。 String nickname = getInfo(pid, (baseAddress + versionConfig.getNickname())); - // 获取微信账号 + // 获取微信账号,同样调用getInfo方法,传入微信进程的ID(pid)以及计算出的微信账号在内存中的地址(baseAddress + versionConfig.getAccount())作为参数, + // 从微信进程内存中读取并获取微信账号信息,原理和获取昵称类似,都是依据内存地址来定位和读取相应的数据。 String account = getInfo(pid, (baseAddress + versionConfig.getAccount())); - // 获取微信手机号 + // 获取微信手机号,再次调用getInfo方法,传入微信进程的ID(pid)以及微信手机号在内存中的地址(baseAddress + versionConfig.getMobile())作为参数, + // 从微信进程内存中读取并获取微信手机号信息,以获取微信用户更多的身份标识相关信息。 String mobile = getInfo(pid, (baseAddress + versionConfig.getMobile())); - // 获取微信文件目录 + // 获取微信文件目录,调用getBasePath方法来获取微信相关文件在系统中的存储目录路径,该路径可能用于后续访问微信聊天记录、文件等相关数据的操作, + // 获取路径的具体实现可能涉及到读取注册表信息、解析配置文件或者其他系统相关的文件路径查找机制。 String basePath = getBasePath(); - // 获取微信Id + // 获取微信Id,调用getWxId方法并传入当前微信进程的ID(pid)作为参数,获取微信的唯一标识ID信息, + // 这个ID可能是在微信系统内部用于区分不同用户或者账号的标识,用于后续在业务逻辑中准确识别不同的微信实例。 String wxId = getWxId(pid); - // 返回配置信息 + // 返回配置信息,将获取到的微信进程ID(pid)、基址(baseAddress)、版本号(version)以及昵称(nickname)、账号(account)、手机号(mobile)、文件目录(basePath)、微信ID(wxId)等信息, + // 封装为一个WeChatConfigVO对象,并添加到weChatConfigVOList列表中,完成一个微信实例的配置信息收集,循环结束后将返回包含所有微信实例配置信息的列表。 weChatConfigVOList.add(new WeChatConfigVO(pid, baseAddress, version, nickname, account, mobile, basePath, wxId)); } return weChatConfigVOList; } /** - * 根据版本号查找匹配的内容 + * 根据版本号查找匹配的内容的方法。 + * 该方法的主要目的是在存储微信内存地址偏移量配置信息的集合中,依据给定的微信版本号查找对应的版本配置内容。 + * 首先尝试进行精准匹配,如果找不到精准匹配的版本号,则会查找版本号前三位相同的最新版本对应的配置内容,若都未找到则返回null。 * - * @param version 要查找的版本号 - * @return 返回匹配的版本内容,如果未找到则返回 null + * @param version 要查找的版本号,以字符串形式传入,代表需要获取其对应配置信息的微信版本标识,通过这个版本号在配置集合中进行匹配查找操作。 + * @return 返回匹配的版本内容,如果未找到则返回 null,返回的WeChatOffsetProperties.VersionConfig对象包含了对应版本的微信内存地址偏移量等配置信息, + * 这些信息可用于后续从微信进程内存中准确读取如昵称、账号等相关数据,若查找失败则返回null表示无法获取到对应版本的配置内容。 */ public WeChatOffsetProperties.VersionConfig getVersionConfig(String version) { + // 获取存储微信不同版本配置信息的映射表,通过weChatOffsetConfig对象(该对象在类中通过依赖注入获取,用于管理微信内存地址偏移量配置)的getVersion方法, + // 得到一个以版本号为键,WeChatOffsetProperties.VersionConfig对象为值的映射表,其中WeChatOffsetProperties.VersionConfig对象包含了对应版本的各种偏移量等详细配置信息。 Map versionConfigMap = weChatOffsetConfig.getVersion(); - // 尝试精准匹配 + // 尝试精准匹配,通过判断versionConfigMap映射表中是否包含传入的版本号(version)作为键,来确定是否存在精准匹配的版本配置信息。 if (versionConfigMap.containsKey(version)) { + // 如果存在精准匹配的版本号,直接从映射表中获取对应的WeChatOffsetProperties.VersionConfig对象并返回,该对象包含了此版本的相关偏移量等配置内容。 return versionConfigMap.get(version); } String matchingVersion = null; - // 遍历版本数据的所有版本号,找到匹配前三位相同的最新版本 + // 遍历版本数据的所有版本号,找到匹配前三位相同的最新版本。通过循环遍历versionConfigMap映射表中的所有键(即所有可用的版本号), + // 对每个版本号进行前缀匹配判断以及版本新旧比较,目的是找到与传入版本号前三位相同且是这些匹配的版本号中最新的那个版本号。 for (String availableVersion : versionConfigMap.keySet()) { + // 调用isMatchingPrefix方法判断当前遍历到的可用版本号(availableVersion)与传入的版本号(version)前三位是否相同, + // 并且同时判断当前找到的匹配版本号(matchingVersion)是否为null(首次匹配或者之前没有找到更新的匹配版本号时为null), + // 或者当前可用版本号(availableVersion)比已记录的匹配版本号(matchingVersion)更新(通过compareTo方法比较版本号大小,大于表示更新), + // 如果满足这些条件,则将当前可用版本号记录为新的匹配版本号(matchingVersion)。 if (isMatchingPrefix(version, availableVersion) && (matchingVersion == null || availableVersion.compareTo(matchingVersion) > 0)) { matchingVersion = availableVersion; } } - // 返回匹配的版本内容,如果都没有匹配到则返回 null + // 返回匹配的版本内容,如果都没有匹配到则返回 null。判断最终找到的匹配版本号(matchingVersion)是否不为null, + // 如果不为null说明找到了合适的匹配版本(前三位相同的较新版本),则从versionConfigMap映射表中获取对应的WeChatOffsetProperties.VersionConfig对象并返回, + // 若仍然为null则表示没有找到任何匹配的版本配置信息,直接返回null。 return (matchingVersion != null) ? versionConfigMap.get(matchingVersion) : null; } /** - * 判断两个版本号前三位是否相同 + * 判断两个版本号前三位是否相同的方法。 + * 通过截取两个版本号的前三位字符并进行比较,来确定它们是否具有相同的前缀部分,以此判断是否满足某种匹配条件(可能用于版本相关的模糊匹配等场景)。 * - * @param version1 版本号1 - * @param version2 版本号2 - * @return 如果前三位相同返回 true,否则返回 false + * @param version1 版本号1,以字符串形式传入,代表要比较的第一个版本号,用于和另一个版本号对比其前缀部分是否相同。 + * @param version2 版本号2,以字符串形式传入,代表要比较的第二个版本号,与version1进行前缀部分的相等性判断操作。 + * @return 如果前三位相同返回 true,否则返回 false,返回的布尔值用于在调用处(如在查找版本配置的方法中)根据结果进行相应的逻辑处理, + * 例如决定是否将某个版本号作为匹配的版本号来获取对应的配置信息等。 */ private boolean isMatchingPrefix(String version1, String version2) { + // 通过调用substring方法分别截取version1和version2的前三位字符(从索引0开始,截取长度为5,这里可能是考虑到版本号格式中前三位比较关键且可能包含小数点等情况), + // 然后使用equals方法比较这两个截取后的字符串是否相等,相等则表示前三位相同,返回true,否则返回false。 return version1.substring(0, 5).equals(version2.substring(0, 5)); } /** - * 获取当前运行的微信进程的进程 ID。 + * 获取当前运行的微信进程的进程 ID的方法。 + * 此方法借助Windows系统相关的API(通过JNA库进行调用)来枚举系统中的所有进程,从中筛选出微信(WeChat.exe)进程,并返回其进程ID列表, + * 如果没有找到微信进程则返回一个空列表(在调用处判断列表长度为0则表示未找到微信进程)。 * - * @return 微信进程的进程 ID。如果未找到,返回 0。 + * @return 微信进程的进程 ID。如果未找到,返回 0,实际上是返回一个包含整数(进程ID)的列表,列表中的每个元素对应一个正在运行的微信进程的ID, + * 这些进程ID可用于后续针对微信进程进行如读取内存数据、获取相关配置等特定操作,若没有找到微信进程则返回空列表(调用处可根据列表情况判断是否找到)。 */ @Override public List wechatPid() { + // 创建一个空的整数列表,用于存储后续查找到的微信进程的进程ID,初始时列表为空,随着遍历进程发现微信进程会逐个添加其进程ID进去。 List pidList = new ArrayList<>(); - // 实例化Kernel32库接口 + // 实例化Kernel32库接口,Kernel32是JNA库中用于与Windows系统的Kernel32.dll(包含了许多系统底层功能相关的函数,如进程管理等)进行交互的接口, + // 通过INSTANCE属性获取其单例实例,后续可以利用这个实例调用Kernel32.dll中提供的相关函数来实现进程相关的操作,比如创建进程快照、遍历进程等。 Kernel32 kernel32 = Kernel32.INSTANCE; - // 创建系统快照,用来枚举所有进程 + // 创建系统快照,用来枚举所有进程,调用Kernel32实例的CreateToolhelp32Snapshot函数,传入参数Tlhelp32.TH32CS_SNAPPROCESS表示创建一个用于枚举所有正在运行进程的快照, + // 第二个参数是一个WinDef.DWORD类型的0值(表示对所有进程进行快照,无特定筛选条件),该函数返回一个句柄(WinNT.HANDLE类型),代表创建的系统快照,通过这个快照可以遍历系统中的所有进程。 WinNT.HANDLE hSnapshot = kernel32.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0)); - // 初始化进程入口结构体 + // 初始化进程入口结构体,通过创建Tlhelp32.PROCESSENTRY32.ByReference类型的对象pe32来初始化一个用于存储进程相关信息(如进程ID、进程名等)的结构体, + // 这个结构体将在后续遍历进程时用于获取每个进程的详细信息,ByReference表示这个结构体是按引用传递的,方便在相关函数调用中对其内部成员进行赋值操作。 Tlhelp32.PROCESSENTRY32.ByReference pe32 = new Tlhelp32.PROCESSENTRY32.ByReference(); - // 遍历所有进程 + // 遍历所有进程,通过调用Kernel32实例的Process32First函数,传入创建的系统快照句柄(hSnapshot)和初始化的进程入口结构体(pe32)作为参数, + // 尝试获取第一个进程的信息,如果获取成功(函数返回true),则进入循环开始遍历所有进程,在循环中不断调用Process32Next函数获取下一个进程的信息,直到遍历完所有进程。 if (kernel32.Process32First(hSnapshot, pe32)) { do { - // 获取进程名 + // 获取进程名,通过调用Native.toString函数将pe32结构体中的szExeFile成员(存储进程的可执行文件名,以字节数组形式存储)转换为字符串形式, + // 得到当前遍历到的进程的名称,用于后续与目标进程名(微信进程名)进行比较判断。 String exeName = Native.toString(pe32.szExeFile); - // 检查进程名是否为目标进程名 + // 检查进程名是否为目标进程名,通过比较获取到的进程名(exeName)与定义的微信进程名常量(EXE_NAME,即"WeChat.exe")是否相等(忽略大小写比较), + // 来确定当前遍历到的进程是否为微信进程。 if (exeName.equalsIgnoreCase(EXE_NAME)) { - // 返回找到的进程ID + // 返回找到的进程ID,如果当前进程是微信进程,将pe32结构体中的th32ProcessID成员(存储进程的ID,是一个DWORD类型,需要转换为整数类型)转换为整数, + // 并添加到pidList列表中,完成一个微信进程ID的收集操作,后续继续遍历其他进程重复此操作。 pidList.add(pe32.th32ProcessID.intValue()); } - // 继续遍历下一个进程 + // 继续遍历下一个进程,通过调用Process32Next函数继续获取下一个进程的信息,进行下一轮的进程名判断和相关操作,直到所有进程都遍历完毕。 } while (kernel32.Process32Next(hSnapshot, pe32)); } - // 关闭快照句柄 + // 关闭快照句柄,在遍历完所有进程后,调用Kernel32实例的CloseHandle函数,传入创建的系统快照句柄(hSnapshot)作为参数,关闭系统快照,释放相关资源。 kernel32.CloseHandle(hSnapshot); - // 如果没有找到进程,返回0 + // 如果没有找到进程,返回0,实际上是返回前面创建的pidList列表,若在遍历过程中没有找到微信进程,列表为空,在调用处可以根据列表的长度等情况判断是否找到微信进程。 return pidList; } /** - * 根据提供的进程 ID 找到相应进程的模块基地址。 + * 根据提供的进程 ID 找到相应进程的模块基地址的方法。 + * 此方法借助Windows系统相关的API(通过JNA库调用)来创建进程模块的快照,遍历该进程下的所有模块, + * 从中查找微信相关的动态链接库(DLL)模块,并返回其基地址,若找不到对应的微信DLL模块则返回0。 * - * @param pid 目标进程的 ID。 - * @return 进程的模块基地址,如果找不到则返回 0。 + * @param pid 目标进程的 ID,以整数形式传入,用于唯一标识要查找模块基地址的微信进程,通过这个ID可以在系统中定位到对应的进程及其相关模块信息。 + * @return 进程的模块基地址,如果找不到则返回 0,返回的长整型数值代表了微信相关DLL在进程内存空间中的起始地址, + * 该基地址后续可结合偏移量等信息用于准确访问DLL内部存储的各种微信相关数据(如昵称、账号等信息在内存中的具体位置计算)。 */ @Override public long baseAddress(int pid) { + // 实例化Kernel32库接口,Kernel32是JNA库中用于与Windows系统的Kernel32.dll(包含了许多系统底层功能相关的函数,如进程和模块管理等)进行交互的接口, + // 通过INSTANCE属性获取其单例实例,后续将利用这个实例调用Kernel32.dll中提供的相关函数来实现查找模块基地址的操作。 Kernel32 kernel32 = Kernel32.INSTANCE; - // 创建模块快照 + // 创建模块快照,调用Kernel32实例的CreateToolhelp32Snapshot函数,传入参数Tlhelp32.TH32CS_SNAPMODULE表示创建一个用于枚举指定进程中所有模块的快照, + // 第二个参数是一个WinDef.DWORD类型,将传入的进程ID(pid)转换为该类型后传入,表示针对此特定进程创建模块快照,该函数返回一个句柄(WinNT.HANDLE类型),代表创建的模块快照, + // 通过这个快照可以遍历该进程下的所有模块信息。 WinNT.HANDLE hModuleSnapshot = kernel32.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPMODULE, new WinDef.DWORD(pid)); - // 确保快照创建成功 + // 确保快照创建成功,通过判断创建的模块快照句柄(hModuleSnapshot)是否不等于WinNT.INVALID_HANDLE_VALUE(无效句柄值,表示创建快照失败), + // 如果不等于则说明快照创建成功,可以继续进行后续的模块遍历查找操作,否则直接返回0表示无法获取模块基地址(因为没有有效的快照来查找模块信息)。 if (!WinNT.INVALID_HANDLE_VALUE.equals(hModuleSnapshot)) { - // 初始化 MODULEENTRY32W 结构体的引用 + // 初始化 MODULEENTRY32W 结构体的引用,通过创建Tlhelp32.MODULEENTRY32W.ByReference类型的对象me32来初始化一个用于存储模块相关信息(如模块名、基地址等)的结构体, + // 这个结构体将在后续遍历模块时用于获取每个模块的详细信息,ByReference表示这个结构体是按引用传递的,方便在相关函数调用中对其内部成员进行赋值操作。 Tlhelp32.MODULEENTRY32W.ByReference me32 = new Tlhelp32.MODULEENTRY32W.ByReference(); - // 遍历所有模块 + // 遍历所有模块,通过调用Kernel32实例的Module32FirstW函数,传入创建的模块快照句柄(hModuleSnapshot)和初始化的模块入口结构体(me32)作为参数, + // 尝试获取第一个模块的信息,如果获取成功(函数返回true),则进入循环开始遍历所有模块,在循环中不断调用Module32NextW函数获取下一个模块的信息,直到遍历完该进程下的所有模块。 if (kernel32.Module32FirstW(hModuleSnapshot, me32)) { do { - // 从模块入口结构中读取模块名 + // 从模块入口结构中读取模块名,通过调用Native.toString函数将me32结构体中的szModule成员(存储模块的名称,以字节数组形式存储)转换为字符串形式, + // 得到当前遍历到的模块的名称,用于后续与目标微信DLL模块名进行比较判断。 String moduleName = Native.toString(me32.szModule); - // 检查是否为微信 DLL + // 检查是否为微信 DLL,通过比较获取到的模块名(moduleName)与定义的微信程序DLL文件名称常量(MODULE_NAME,即"WeChatWin.dll")是否相等(忽略大小写比较), + // 来确定当前遍历到的模块是否为微信相关的DLL模块。 if (MODULE_NAME.equalsIgnoreCase(moduleName)) { - // 返回微信 DLL 的基地址 + // 返回微信 DLL 的基地址,如果当前模块是微信相关的DLL模块,首先调用kernel32.CloseHandle函数关闭模块快照句柄(hModuleSnapshot)以释放相关资源, + // 然后通过Pointer.nativeValue函数获取me32结构体中modBaseAddr成员(存储模块的基地址,是一个Pointer类型)对应的原生指针值(转换为长整型)并返回, + // 这个值就是微信DLL在进程内存中的基地址,后续可基于此进行进一步的数据读取操作。 kernel32.CloseHandle(hModuleSnapshot); return Pointer.nativeValue(me32.modBaseAddr); } } while (kernel32.Module32NextW(hModuleSnapshot, me32)); } - // 关闭快照句柄 + // 关闭快照句柄,无论是否找到微信DLL模块,在遍历完模块后(或者在遍历开始前如果Module32FirstW返回false),都需要调用kernel32.CloseHandle函数, + // 传入模块快照句柄(hModuleSnapshot)作为参数,关闭模块快照,释放相关资源。 kernel32.CloseHandle(hModuleSnapshot); } - // 如果未找到微信 DLL,返回 0 + // 如果未找到微信 DLL,返回 0,若在遍历模块过程中没有找到对应的微信DLL模块,直接返回0表示无法获取到模块基地址, + // 调用该方法的地方可以根据返回值判断是否成功获取以及进行相应的后续处理。 return 0; } /** - * 从指定进程的指定内存地址读取信息。 + * 从指定进程的指定内存地址读取信息的方法。 + * 该方法先打开目标进程以获取读取内存的权限,然后分配一块内存缓冲区,尝试从指定的内存地址读取数据到缓冲区, + * 若读取成功则将缓冲区中的字节数据转换为字符串,并进行一些字符串处理(如截取到第一个空字符为止),最后返回处理后的字符串信息, + * 若读取失败或者字符串为空则返回null。 * - * @param pid 目标进程的 ID。 - * @param address 要读取的内存地址。 - * @return 读取到的数据,如果失败则返回 null。 + * @param pid 目标进程的 ID,以整数形式传入,用于唯一标识要从中读取内存信息的微信进程,通过这个ID可以在系统中定位到对应的进程并获取相应的内存访问权限等操作。 + * @param address 要读取的内存地址,以长整型形式传入,代表在目标进程内存空间中要读取数据的具体位置,该地址通常是基于进程的模块基地址加上相应的偏移量计算得到的。 + * @return 读取到的数据,如果失败则返回 null,返回的字符串包含了从指定内存地址读取到的信息内容(经过处理后,去除了可能多余的空字符等), + * 例如可能是微信的昵称、账号等相关信息,若读取过程出现问题(如读取内存失败、没有有效数据等)则返回null。 */ @Override public String getInfo(int pid, long address) { + // 实例化Kernel32库接口,同前面的方法一样,Kernel32是用于与Windows系统的Kernel32.dll进行交互的接口,通过获取其单例实例, + // 后续可以调用其中相关函数来实现打开进程、读取内存等操作,为从指定进程内存地址读取信息做准备。 Kernel32 kernel32 = Kernel32.INSTANCE; - // 打开目标进程 + // 打开目标进程,调用Kernel32实例的OpenProcess函数,传入参数WinNT.PROCESS_VM_READ表示以读取进程内存的权限打开目标进程, + // 第二个参数false表示不继承句柄(在父子进程关系等场景下相关的句柄继承特性,这里不需要继承所以设为false),第三个参数传入进程ID(pid), + // 该函数返回一个句柄(WinNT.HANDLE类型),代表打开的目标进程,后续可以通过这个句柄对进程内存进行读取操作。 WinNT.HANDLE process = kernel32.OpenProcess(WinNT.PROCESS_VM_READ, false, pid); - // 分配内存作为缓冲区读取数据 + // 分配内存作为缓冲区读取数据,首先定义一个缓冲区大小为64字节(这里的大小可以根据预期要读取的数据长度等因素进行调整,64字节只是一个初始设定值), + // 通过创建Memory类的实例buffer来分配指定大小的内存空间作为缓冲区,用于存储从进程内存中读取的数据,同时创建一个IntByReference类型的对象read, + // 用于存储实际读取的字节数(后续读取内存操作后会对其进行赋值)。 int bufferSize = 64; Memory buffer = new Memory(bufferSize); IntByReference read = new IntByReference(); - // 从指定地址读取内存 + // 从指定地址读取内存,调用Kernel32实例的ReadProcessMemory函数,传入打开的进程句柄(process)、表示要读取的内存地址的Pointer类型对象(通过将长整型地址转换为Pointer类型)、 + // 分配好的缓冲区buffer、缓冲区大小bufferSize以及用于存储实际读取字节数的read对象作为参数,尝试从指定的内存地址读取数据到缓冲区中, + // 函数执行后会返回一个布尔值表示读取操作是否成功,将这个布尔值存储在success变量中用于后续判断。 boolean success = kernel32.ReadProcessMemory(process, new Pointer(address), buffer, bufferSize, read); if (success) { - // 将读取的字节转换为字符串 + // 将读取的字节转换为字符串,首先通过buffer.getByteArray方法从缓冲区中获取实际读取到的字节数组,参数0表示从缓冲区起始位置开始获取, + // read.getValue()表示获取的字节长度(即实际读取到的字节数),然后使用String的构造函数将字节数组转换为字符串,指定字符编码为默认编码(通常取决于系统设置), + // 得到从内存中读取的数据内容,存储在data变量中。 byte[] dataBytes = buffer.getByteArray(0, read.getValue()); String data = new String(dataBytes, 0, read.getValue()); - // 查找第一个空字符(字符串结束标志) + // 查找第一个空字符(字符串结束标志),调用String的indexOf方法查找字符串data中第一个出现的空字符('\0')的位置索引, + // 如果找到了(返回值不为 -1),则说明字符串中存在表示结束的空字符,需要进行截取操作,去除后面多余的部分(可能是未初始化的内存数据等)。 int nullPos = data.indexOf('\0'); if (nullPos != -1) { - // 截取到第一个空字符为止的字符串 + // 截取到第一个空字符为止的字符串,通过调用String的substring方法,传入起始索引0和结束索引nullPos(即到第一个空字符位置), + // 对data字符串进行截取操作,得到一个只包含有效数据(到空字符为止)的字符串,更新data变量的值。 data = data.substring(0, nullPos); } - // 返回读取的字符串,如果为空则返回 null + // 返回读取的字符串,如果为空则返回 null,先调用kernel32.CloseHandle函数关闭打开的进程句柄(process)以释放相关资源, + // 然后判断处理后的data字符串是否为空(通过isEmpty方法判断),如果为空则返回null,否则返回处理后的字符串内容,表示成功从指定内存地址读取到的有效信息。 kernel32.CloseHandle(process); return data.isEmpty() ? null : data; } - // 关闭进程句柄 + // 关闭进程句柄,若读取内存操作失败(success为false),同样需要调用kernel32.CloseHandle函数关闭打开的进程句柄(process),释放相关资源,然后直接返回null, + // 表示无法从指定内存地址获取到有效信息。 kernel32.CloseHandle(process); return null; } /** - * 获取指定进程和数据库路径下的密钥 + * 获取指定进程和数据库路径下的密钥的方法。 + * 该方法主要功能是在给定的目标进程(通过进程ID标识)以及对应的数据库路径下,尝试查找并获取符合特定验证条件的密钥。 + * 其过程涉及打开目标进程、根据不同平台标识扫描模块地址、选择合适的模块地址列表,然后在选定的地址范围内倒序遍历查找密钥, + * 若找到符合验证条件(通过 `verifyKey` 方法验证)的密钥则返回,若遍历完都未找到则返回 `null`。 * - * @param pid 目标进程的进程ID - * @param dbPath 数据库路径 - * @return 返回找到的密钥,如果未找到则返回null + * @param pid 目标进程的进程ID,以整数形式传入,用于唯一标识要从中查找密钥的目标进程,系统会依据这个ID来定位并操作对应的进程,例如打开进程获取内存访问权限等操作。 + * @param dbPath 数据库路径,以字符串形式传入,指定了期望获取密钥所对应的数据库所在的文件路径,后续会基于此路径构建完整的数据库文件名(如 `MicroMsg.db`)用于密钥验证等操作。 + * @return 返回找到的密钥,如果未找到则返回 `null`,返回的字符串表示找到的符合条件的密钥内容,若整个查找和验证过程都未发现合适的密钥,则返回 `null` 表示获取密钥失败。 */ @Override public String getKey(int pid, String dbPath) { - // 打开目标进程 + // 打开目标进程,调用Kernel32实例(通过JNA库与Windows系统的Kernel32.dll交互的接口实例)的OpenProcess函数来打开目标进程。 + // 传入的参数 `0x1F0FFF` 表示请求的访问权限,这个十六进制数值代表了一系列权限组合(具体对应着如进程的读取、写入、执行等相关权限,这里是满足后续操作所需的权限设置), + // 第二个参数 `false` 表示不继承句柄(即该进程句柄不会被其他相关进程继承,在一些父子进程关系场景下涉及的句柄继承特性,这里不需要继承所以设为 `false`), + // 第三个参数传入进程ID(pid),该函数执行后返回一个句柄(WinNT.HANDLE类型),代表打开的目标进程,后续可以通过这个句柄对进程内存进行相关操作,比如读取内存数据等,以此来查找密钥。 WinNT.HANDLE process = Kernel32.INSTANCE.OpenProcess(0x1F0FFF, false, pid); - // 定义不同平台对应的字节数组 + // 定义不同平台对应的字节数组,分别创建表示 "iphone\0"、"android\0"、"ipad\0" 的字节数组,这里添加的末尾 `\0` 可能是为了符合某种特定的字符串格式要求(比如在内存中存储或比较时的格式要求), + // 这些字节数组后续会用于扫描进程模块中与不同平台相关的特定标识,以确定合适的模块地址范围来查找密钥,不同平台在内存中的存储模式或标识可能有所不同,通过这种方式进行区分。 byte[] iphoneByteArray = "iphone\0".getBytes(); byte[] androidByteArray = "android\0".getBytes(); byte[] ipadByteArray = "ipad\0".getBytes(); - // 用于存储不同平台的模块扫描结果 + // 用于存储不同平台的模块扫描结果,创建一个空的 `Pointer` 类型列表,用于存储通过 `patternScanModule` 方法扫描不同平台相关字节数组在进程模块中找到的地址信息, + // 这个列表后续会根据不同平台扫描结果的情况进行筛选和赋值,最终确定要在哪些模块地址范围内查找密钥。 List typeAddress = new ArrayList<>(); + // 调用 `patternScanModule` 方法,传入打开的进程句柄(process)和表示 "iphone" 平台标识的字节数组(iphoneByteArray)作为参数, + // 该方法会在进程的模块中扫描匹配此字节数组模式的地址信息,并返回一个包含 `Pointer` 类型地址的列表(表示找到的相关模块地址),将结果存储在 `typeAddress1` 变量中。 List typeAddress1 = patternScanModule(process, iphoneByteArray); + // 同样地,调用 `patternScanModule` 方法,传入进程句柄和表示 "android" 平台标识的字节数组(androidByteArray),扫描并获取对应的模块地址列表,存储在 `typeAddress2` 变量中。 List typeAddress2 = patternScanModule(process, androidByteArray); + // 再次调用 `patternScanModule` 方法,传入进程句柄和表示 "ipad" 平台标识的字节数组(ipadByteArray),获取 "ipad" 平台相关的模块地址列表,存储在 `typeAddress3` 变量中。 List typeAddress3 = patternScanModule(process, ipadByteArray); - // 优先选择长度至少为2的模块地址列表 + // 优先选择长度至少为2的模块地址列表,通过一系列条件判断来确定最终要使用的模块地址列表(存储在 `typeAddress` 变量中)。 + // 首先判断 `typeAddress1` 列表的大小是否大于等于2,如果满足则将 `typeAddress` 赋值为 `typeAddress1`,表示优先选择 "iphone" 平台相关且地址数量足够的模块地址范围来查找密钥。 if (typeAddress1.size() >= 2) { typeAddress = typeAddress1; } else if (typeAddress2.size() >= 2) { + // 如果 "iphone" 平台的地址列表不符合条件,再判断 "android" 平台的 `typeAddress2` 列表大小是否大于等于2,若满足则选择 "android" 平台相关的模块地址范围。 typeAddress = typeAddress2; } else if (typeAddress3.size() >= 2) { + // 若 "android" 平台的地址列表也不符合条件,接着判断 "ipad" 平台的 `typeAddress3` 列表大小是否大于等于2,满足则选择 "ipad" 平台相关的模块地址范围。 typeAddress = typeAddress3; } else if (!typeAddress1.isEmpty()) { + // 如果前面三个平台的地址列表长度都小于2,但 "iphone" 平台的地址列表不为空,说明虽然数量不足但也可以尝试使用,将 `typeAddress` 赋值为 `typeAddress1`。 typeAddress = typeAddress1; } else if (!typeAddress2.isEmpty()) { + // 若 "iphone" 平台地址列表为空,而 "android" 平台的地址列表不为空,将 `typeAddress` 赋值为 `typeAddress2`,选择 "android" 平台相关的模块地址范围(即使数量不足)。 typeAddress = typeAddress2; } else if (!typeAddress3.isEmpty()) { + // 同理,若前两个平台地址列表都为空,而 "ipad" 平台的地址列表不为空,将 `typeAddress` 赋值为 `typeAddress3`,选择 "ipad" 平台相关的模块地址范围来查找密钥。 typeAddress = typeAddress3; } - // 拼接MicroMsg数据库路径 + // 拼接MicroMsg数据库路径,将传入的数据库路径(dbPath)与 "\\Msg\\MicroMsg.db" 进行字符串拼接,构建出完整的微信消息数据库(MicroMsg.db)的文件路径, + // 这个完整路径后续会用于验证获取到的密钥是否与该数据库相关且有效(通过 `verifyKey` 方法进行验证),确保获取的密钥是对应此数据库的正确密钥。 String microMsg = dbPath + "\\Msg\\MicroMsg.db"; - // 获取倒序迭代器 + // 获取倒序迭代器,通过调用 `typeAddress` 列表的 `listIterator` 方法,并传入 `typeAddress.size()` 作为参数,获取一个指向列表末尾的列表迭代器, + // 这个迭代器可以用于从后往前(倒序)遍历 `typeAddress` 列表中的元素(即模块地址指针),方便后续按照特定顺序(倒序)在模块地址范围内查找密钥。 ListIterator pointerListIterator = typeAddress.listIterator(typeAddress.size()); - // 倒序遍历模块地址列表 + // 倒序遍历模块地址列表,通过 `while` 循环结合迭代器的 `hasPrevious` 方法,只要迭代器还有前一个元素(即还没遍历到列表开头),就持续进行循环操作, + // 在循环内获取上一个元素(模块地址指针),并将其转换为长整型的内存地址值,用于后续在该地址范围内查找密钥的操作。 while (pointerListIterator.hasPrevious()) { Pointer addressPointer = pointerListIterator.previous(); long address = Pointer.nativeValue(addressPointer); - // 在地址范围内以步长8递减遍历,读取密钥 + // 在地址范围内以步长8递减遍历,读取密钥,从当前模块地址(address)开始,以步长为8(这里的步长8可能与内存中数据存储结构或读取规则相关,比如按8字节对齐等情况)递减, + // 直到地址减少到不小于 `address - 2000` 的范围,在每个地址位置尝试读取密钥(通过调用 `readKey` 方法),并进行后续的验证操作。 for (long i = address; i >= (address - 2000); i -= 8) { - // 读取key + // 读取key,调用 `readKey` 方法,传入打开的进程句柄(process)和当前遍历到的内存地址(i)作为参数,尝试从该内存地址读取密钥内容, + // 读取到的密钥以字符串形式返回(如果读取成功的话),存储在 `key` 变量中,用于后续的验证判断。 String key = readKey(process, i); - // 如果密钥不为空且验证通过,则返回密钥 + // 如果密钥不为空且验证通过,则返回密钥,通过判断 `key` 变量是否不为 `null` 并且调用 `verifyKey` 方法验证 `key` 与构建好的数据库路径(microMsg)是否匹配成功, + // 如果两者条件都满足,说明找到了符合要求的密钥,直接将其返回,结束整个 `getKey` 方法的执行,表示成功获取到了密钥。 if (key != null && verifyKey(key, microMsg)) { return key; } } } - // 未找到匹配的密钥,返回null + // 未找到匹配的密钥,返回null,如果在整个倒序遍历模块地址列表以及在各个地址范围内查找和验证的过程中,都没有找到符合条件的密钥, + // 则最终返回 `null`,表示此次获取密钥的操作失败,调用该方法的地方可以根据返回值进行相应的处理,比如提示用户或者进行其他补救逻辑等。 return null; } /** - * 获取秘钥 + * 获取秘钥的方法,其功能是从指定的目标进程的内存地址中读取密钥信息。 + * 该方法先尝试从给定内存地址读取一个地址值,再依据这个读取到的地址去获取真正的密钥内容, + * 然后将密钥内容转换为十六进制字符串形式返回,如果在读取内存等操作过程中出现失败情况则返回 `null`。 * - * @param address 要读取的内存地址 - * @return 读取到的秘钥,如果失败则返回 null。 + * @param process 目标进程的句柄,通过 `WinNT.HANDLE` 类型传入,代表已经打开且具有相应访问权限的目标进程, + * 此句柄用于后续对该进程内存进行读取操作,是与目标进程交互的关键标识,由外部调用时传入(通常是先通过 `OpenProcess` 等方法打开进程获取到的)。 + * @param address 要读取的内存地址,以长整型形式传入,表示在目标进程内存空间中期望获取密钥相关信息的具体位置, + * 方法会从这个地址开始进行相应的内存读取和密钥提取操作。 + * @return 读取到的秘钥,如果失败则返回 `null`,返回的字符串为密钥内容以十六进制表示的形式, + * 若在读取内存、转换数据等操作过程中出现问题(例如无法读取指定内存地址的数据等情况)则返回 `null`,表示获取密钥失败。 */ private String readKey(WinNT.HANDLE process, long address) { + // 实例化Kernel32库接口,Kernel32是JNA库中用于与Windows系统的Kernel32.dll进行交互的接口, + // 通过 `INSTANCE` 属性获取其单例实例,后续利用这个实例调用Kernel32.dll中提供的相关函数来实现读取目标进程内存等操作, + // 这是与Windows底层系统进行内存交互操作的基础,以此来完成密钥读取相关的功能。 Kernel32 kernel32 = Kernel32.INSTANCE; - // 准备缓冲区用于读取内存 + // 准备缓冲区用于读取内存,创建一个 `Memory` 类型的对象 `buffer`,分配大小为8字节的内存空间作为缓冲区, + // 这个缓冲区用于第一次读取内存操作,目的是从指定的内存地址(`address`)读取一个地址值(可能是后续获取真正密钥所在位置的相关指针信息), + // 同时创建一个 `IntByReference` 类型的对象 `bytesRead`,用于记录实际读取到的字节数,方便后续判断读取操作是否完整执行。 Memory buffer = new Memory(8); IntByReference bytesRead = new IntByReference(); - // 读取目标进程的内存地址 + + // 读取目标进程的内存地址,调用 `kernel32` 实例的 `ReadProcessMemory` 函数,传入打开的进程句柄(`process`)、 + // 表示要读取的内存地址的 `Pointer` 类型对象(通过将长整型地址 `address` 转换为 `Pointer` 类型)、准备好的缓冲区 `buffer`、 + // 缓冲区大小(强制转换为 `int` 类型,这里就是8字节)以及用于记录实际读取字节数的 `bytesRead` 对象作为参数, + // 尝试从指定的内存地址读取数据到缓冲区中,若读取操作失败(函数返回 `false`),则直接返回 `null`,表示无法获取到有效的数据用于后续操作。 if (!kernel32.ReadProcessMemory(process, new Pointer(address), buffer, (int) buffer.size(), bytesRead)) { return null; } - // 将缓冲区中的字节转换为长整型值 + // 将缓冲区中的字节转换为长整型值,首先通过 `ByteBuffer` 的 `wrap` 方法将缓冲区 `buffer` 中的字节数组(从索引0开始,长度为8字节)包装为一个 `ByteBuffer` 对象, + // 并通过 `order` 方法指定字节顺序为小端序(`ByteOrder.LITTLE_ENDIAN`),这是为了确保在多平台环境下数据解析的一致性,符合特定内存数据存储和解析的要求, + // 然后调用 `getLong` 方法从这个 `ByteBuffer` 对象中读取一个长整型值,这个值被认为是后续获取真正密钥所在的内存地址,存储在 `keyAddress` 变量中,为下一步读取密钥做准备。 ByteBuffer bufferWrap = ByteBuffer.wrap(buffer.getByteArray(0, 8)).order(ByteOrder.LITTLE_ENDIAN); long keyAddress = bufferWrap.getLong(); - // 准备一个缓冲区用于存放密钥 + // 准备一个缓冲区用于存放密钥,创建一个新的 `Memory` 类型对象 `keyBuffer`,分配大小为32字节的内存空间作为缓冲区, + // 这个缓冲区用于从前面计算得到的密钥内存地址(`keyAddress`)读取真正的密钥内容,同样大小32字节是根据密钥可能的存储长度等预先设定的一个合适大小。 Memory keyBuffer = new Memory(32); - // 从计算出的密钥地址读取密钥 + + // 从计算出的密钥地址读取密钥,再次调用 `kernel32` 实例的 `ReadProcessMemory` 函数,传入进程句柄(`process`)、 + // 表示密钥地址的 `Pointer` 类型对象(通过将长整型的 `keyAddress` 转换为 `Pointer` 类型)、准备好的密钥缓冲区 `keyBuffer`、 + // 密钥缓冲区大小(强制转换为 `int` 类型,即32字节)以及前面的 `bytesRead` 对象作为参数,尝试从计算出的密钥地址读取密钥数据到 `keyBuffer` 缓冲区中, + // 若读取操作失败(函数返回 `false`),则直接返回 `null`,表示无法获取到有效的密钥内容。 if (!kernel32.ReadProcessMemory(process, new Pointer(keyAddress), keyBuffer, (int) keyBuffer.size(), bytesRead)) { return null; } - // 将密钥转换为十六进制字符串 + // 将密钥转换为十六进制字符串,创建一个 `StringBuilder` 对象 `sb`,用于逐步构建密钥的十六进制表示字符串。 + // 首先获取 `keyBuffer` 缓冲区中的字节数组(从索引0开始,长度为32字节),存储在 `key` 变量中,这个字节数组就是读取到的密钥内容(以字节形式存在), + // 然后通过 `Formatter` 结合 `try-with-resources` 语句来遍历字节数组中的每个字节,将其格式化为十六进制的字符串形式,并追加到 `sb` 对象中,最终形成完整的密钥十六进制表示字符串。 StringBuilder sb = new StringBuilder(); byte[] key = keyBuffer.getByteArray(0, 32); try (Formatter formatter = new Formatter(sb)) { @@ -376,7 +519,8 @@ public class WeChatServiceImpl implements WeChatService { formatter.format("%02x", b & 0xff); } } - // 返回密钥的十六进制表示 + // 返回密钥的十六进制表示,将构建好的密钥十六进制表示字符串(存储在 `sb` 对象中)通过 `toString` 方法转换为普通字符串并返回, + // 这个字符串就是最终获取到的密钥信息的表示形式,供调用该方法的地方使用(例如用于后续的密钥验证等操作)。 return sb.toString(); } @@ -389,110 +533,174 @@ public class WeChatServiceImpl implements WeChatService { */ private boolean verifyKey(String password, String dbFile) { // 创建File文件 + // 这个对象代表了磁盘上对应的实际文件,后续可以基于它进行文件读取等相关操作,用于获取文件内部的数据来进行密钥验证。 File file = new File(dbFile); try (FileInputStream fis = new FileInputStream(file)) { - // 文件大小 + // 文件大小,创建一个长度为5000字节的字节数组 `fileContent`,用于存储从文件中读取的内容, + // 这里设定5000字节是根据预期要读取的文件相关部分数据量大小来确定的,后续会从文件中读取相应字节数的数据到这个数组中,方便提取验证所需的关键信息。 byte[] fileContent = new byte[5000]; - // 读取内容 + // 读取内容,调用 `FileInputStream` 的 `read` 方法,将文件中的数据读取到 `fileContent` 字节数组中, + // 实际读取的字节数可能小于数组长度(取决于文件大小等情况),读取操作会从文件开头按顺序读取数据,后续将基于这些读取到的数据进行进一步的处理和验证。 fis.read(fileContent); - // 提取盐值 + // 提取盐值,通过 `Arrays.copyOfRange` 方法从 `fileContent` 字节数组中提取一部分数据作为盐值, + // 具体是从索引0开始,长度为16字节的子数组,在加密相关操作中,盐值通常用于增加密码安全性、防止彩虹表攻击等,提取出来的盐值将用于后续生成密钥等验证步骤。 byte[] salt = Arrays.copyOfRange(fileContent, 0, 16); - // 提取第一页 + // 提取第一页,同样使用 `Arrays.copyOfRange` 方法,从 `fileContent` 字节数组中提取代表文件“第一页”内容的部分数据, + // 提取范围是从索引16到4096(长度为4096 - 16 = 4080字节),这部分数据可能包含了与加密、验证相关的重要信息,后续会进一步从中提取更具体的内容用于验证操作。 byte[] firstPage = Arrays.copyOfRange(fileContent, 16, 4096); - // 提取第一页的内容与IV + // 提取第一页的内容与IV,再次使用 `Arrays.copyOfRange` 方法,从前面提取的“第一页”数据(`firstPage` 字节数组)中进一步提取其主体内容与初始向量(IV,Initialization Vector,在加密算法中常作为随机化因素使用)部分, + // 提取范围是从索引0到 `firstPage.length - 32`,即去除了末尾32字节的部分,这部分数据将作为后续验证计算的一部分依据。 byte[] firstPageBodyAndIv = Arrays.copyOfRange(firstPage, 0, firstPage.length - 32); - // 提取第一页的hashMac + // 提取第一页的hashMac,还是通过 `Arrays.copyOfRange` 方法,从“第一页”数据(`firstPage` 字节数组)中提取末尾部分的哈希消息认证码(hashMac), + // 提取范围是从 `firstPage.length - 32` 到 `firstPage.length - 12`(长度为20字节),这个哈希值通常是对前面提取的主体内容等进行特定哈希计算得到的结果,用于验证数据的完整性和密钥的正确性。 byte[] firstPageHashMac = Arrays.copyOfRange(firstPage, firstPage.length - 32, firstPage.length - 12); - // 生成key + // 生成key,调用 `Pbkdf2HmacUtil` 工具类的 `pbkdf2Hmac` 方法,传入将密钥字符串(`password`)转换后的十六进制字节数组(通过 `HexUtil.decodeHex` 方法转换)、 + // 前面提取的盐值(`salt`)、迭代次数64000以及期望生成的密钥长度32字节作为参数,基于密码和盐值按照PBKDF2-HMAC算法生成一个新的密钥字节数组 `key`, + // 这个生成的密钥将用于后续与文件中提取的数据进行匹配验证操作,PBKDF2-HMAC算法常用于通过密码和盐值安全地生成加密密钥。 byte[] key = Pbkdf2HmacUtil.pbkdf2Hmac(HexUtil.decodeHex(password), salt, 64000, 32); byte[] macSalt = new byte[salt.length]; for (int i = 0; i < salt.length; i++) { + // 对盐值的每个字节进行异或操作,创建一个与盐值长度相同的新字节数组 `macSalt`,通过循环遍历盐值(`salt`)的每个字节, + // 将其与固定值58(十六进制为 `0x3A`)进行异或操作,得到新的字节值并存储到 `macSalt` 数组中,这个新的字节数组 `macSalt` 将在后续验证操作中使用, + // 异或操作在这里可能是某种特定加密验证逻辑中的一部分处理步骤,用于进一步处理盐值以适配验证过程。 macSalt[i] = (byte) (salt[i] ^ 58); } - // 秘钥匹配成功 + // 秘钥匹配成功,调用 `Pbkdf2HmacUtil` 工具类的 `checkKey` 方法,传入生成的密钥(`key`)、处理后的盐值(`macSalt`)、 + // 提取的第一页哈希消息认证码(`firstPageHashMac`)以及第一页主体内容与初始向量(`firstPageBodyAndIv`)作为参数, + // 该方法会基于这些数据进行特定的验证计算,若计算结果表明密钥与文件中的数据匹配,则返回 `true`,表示密钥验证成功;否则返回 `false`。 return Pbkdf2HmacUtil.checkKey(key, macSalt, firstPageHashMac, firstPageBodyAndIv); } catch (Exception e) { + // 如果在文件读取、数据提取或者密钥验证等过程中出现了任何异常(比如文件不存在、读取文件权限不足、验证计算过程出现错误等情况), + // 通过 `log.error` 方法记录错误信息,"Verification key failed" 作为错误描述的标识,方便后续查看日志排查问题,`e` 是捕获到的具体异常对象, + // 然后直接返回 `false`,表示验证失败。 log.error("Verification key failed", e); } return false; } /** - * 列出并检索进程中指定已加载模块 + * 列出并检索进程中指定已加载模块的方法。 + * 该方法的主要功能是获取目标进程中已加载的各个模块的详细信息,通过调用Windows系统相关的API(借助JNA库)来枚举模块, + * 尝试获取每个模块的信息并存储到列表中,如果在枚举或获取模块信息过程中出现失败情况会记录相应警告日志,最终返回包含模块信息的列表。 * - * @param process 目标进程 + * @param process 目标进程,以 `WinNT.HANDLE` 类型传入,代表已经打开且具有相应访问权限的目标进程, + * 此句柄用于后续与该进程交互,如枚举其加载的模块等操作,是整个方法操作针对的具体进程对象,由外部调用时传入(通常是先通过 `OpenProcess` 等方法打开进程获取到的)。 + * @return 返回一个包含 `Psapi.MODULEINFO` 类型对象的列表,列表中的每个元素代表目标进程中一个已加载模块的详细信息, + * 若在枚举或获取模块信息过程中出现问题,可能返回一个空列表,表示无法成功获取到完整的模块信息。 */ private List enumProcessModule(WinNT.HANDLE process) { + // 创建一个空的列表,用于存储后续获取到的各个模块的详细信息,初始时列表为空,随着成功获取每个模块的信息会将其对应的 `Psapi.MODULEINFO` 对象添加到这个列表中。 List moduleInfos = new ArrayList<>(); - // 获取模块句柄列表 + + // 获取模块句柄列表,创建一个长度为1024的 `WinNT.HMODULE` 类型数组 `hModules`,用于存储目标进程中各个已加载模块的句柄信息, + // 这里设定数组长度为1024是一种初始的预估大小,期望能够容纳下进程中加载的模块句柄,实际情况中如果模块数量超过这个值可能需要进一步处理(不过通常是有一定合理性的预估大小)。 WinNT.HMODULE[] hModules = new WinNT.HMODULE[1024]; - // 指针大小 + + // 指针大小,计算存储所有模块句柄所需的字节数,通过将 `hModules` 数组的长度乘以 `Native.POINTER_SIZE`(这是通过JNA库获取的表示指针大小的常量值,通常在不同平台下有对应的正确字节数), + // 得到理论上存储所有模块句柄应该需要的字节长度,这个值将作为后续调用相关函数时传入的参数,用于告知系统期望读取或写入的数据量大小等信息。 int cbNeeded = hModules.length * Native.POINTER_SIZE; - // 存储所有模块句柄所需的字节数 + + // 存储所有模块句柄所需的字节数,创建一个 `IntByReference` 类型的对象 `read`,用于存储实际读取或写入的字节数相关信息, + // 在后续调用相关系统函数(如 `EnumProcessModules` 等)时,函数会根据实际操作情况更新这个对象的值,以便后续判断操作是否完整执行以及获取实际操作的数据量等情况。 IntByReference read = new IntByReference(); - // 调用 EnumProcessModules 方法 + + // 调用 EnumProcessModules 方法,通过 `Psapi.INSTANCE`(这是JNA库中用于与Windows系统的 `Psapi.dll` 进行交互的接口实例,用于获取进程模块相关的功能函数)调用 `EnumProcessModules` 函数, + // 传入打开的目标进程句柄(`process`)、用于存储模块句柄的数组 `hModules`、前面计算得到的期望字节数 `cbNeeded` 以及用于记录实际操作字节数的 `read` 对象作为参数, + // 尝试枚举目标进程中已加载的所有模块,将模块句柄信息存储到 `hModules` 数组中,函数执行后返回一个布尔值 `success`,用于表示枚举操作是否成功。 boolean success = Psapi.INSTANCE.EnumProcessModules(process, hModules, cbNeeded, read); - // 读取模块失败 + + // 读取模块失败,如果枚举模块操作失败(即 `success` 为 `false`),通过 `log.warn` 方法记录警告信息,信息内容包含 "EnumProcessModules failed." 表示枚举模块失败的提示以及通过 `Native.getLastError` 方法获取的系统错误代码, + // 方便后续查看日志排查问题,然后直接返回当前的空 `moduleInfos` 列表,表示无法获取到模块信息,方法执行结束。 if (!success) { log.warn("EnumProcessModules failed. Error code: " + Native.getLastError()); return moduleInfos; } - // 遍历加载到的所有模块 + + // 遍历加载到的所有模块,通过循环遍历 `hModules` 数组中的每个元素(每个元素对应一个模块的句柄,可能为 `null` 表示无效或未获取到对应模块信息), + // 对每个非 `null` 的模块句柄对应的模块进行详细信息获取操作,以此来收集所有已加载模块的完整信息。 for (WinDef.HMODULE hModule : hModules) { - // 模块不等于空的情况下 + // 模块不等于空的情况下,判断当前遍历到的模块句柄 `hModule` 是否不为 `null`,如果不为 `null` 则表示存在对应的有效模块,需要进一步获取其详细信息,进入下面的操作流程。 if (hModule != null) { - // 获取模块信息 + // 获取模块信息,创建一个 `Psapi.MODULEINFO` 类型的对象 `moduleInfo`,这个对象用于存储即将获取的模块详细信息, + // 包括模块的基地址、大小、入口点等相关信息,后续会通过调用系统函数来填充这个对象的各个成员变量。 Psapi.MODULEINFO moduleInfo = new Psapi.MODULEINFO(); - // 加载模块的详细信息 + + // 加载模块的详细信息,调用 `Psapi.INSTANCE` 的 `GetModuleInformation` 函数,传入目标进程句柄(`process`)、当前模块的句柄(`hModule`)、 + // 用于存储模块信息的 `moduleInfo` 对象以及 `moduleInfo` 对象的大小(通过 `moduleInfo.size()` 获取)作为参数,尝试获取指定模块的详细信息并填充到 `moduleInfo` 对象中, + // 函数执行后返回一个布尔值 `moduleInformationSuccess`,用于表示获取模块信息操作是否成功。 boolean moduleInformationSuccess = Psapi.INSTANCE.GetModuleInformation(process, hModule, moduleInfo, moduleInfo.size()); - // 读取失败 + + // 读取失败,如果获取模块详细信息的操作失败(即 `moduleInformationSuccess` 为 `false`),通过 `log.warn` 方法记录警告信息, + // 信息内容包含 "GetModuleInformation failed." 表示获取模块信息失败的提示以及通过 `Native.getLastError` 方法获取的系统错误代码,方便后续查看日志排查问题, + // 然后使用 `continue` 语句跳过当前循环,继续处理下一个模块(即继续遍历 `hModules` 数组中的下一个元素),不将当前失败的模块信息添加到列表中。 if (!moduleInformationSuccess) { log.warn("GetModuleInformation failed. Error code: " + Native.getLastError()); continue; } + + // 将获取到详细信息的模块对应的 `moduleInfo` 对象添加到 `moduleInfos` 列表中,完成一个模块信息的收集操作,后续继续遍历其他模块重复此过程, + // 直到遍历完 `hModules` 数组中所有有效的模块句柄对应的模块信息收集操作,最终 `moduleInfos` 列表将包含所有成功获取到详细信息的模块对象。 moduleInfos.add(moduleInfo); } } + return moduleInfos; } /** - * 通过模块名检索指定进程加载的模块信息 + * 通过模块名检索指定进程加载的模块信息的方法。 + * 该方法首先调用 `enumProcessModule` 方法获取指定进程中所有已加载模块的信息列表,然后遍历这个列表, + * 针对每个模块获取其文件名并判断是否以特定的模块名(`WeChatServiceImpl.MODULE_NAME`)结尾, + * 如果找到匹配的模块则返回其对应的 `Psapi.MODULEINFO` 对象,若遍历完所有模块都未找到匹配的则返回 `null`。 * - * @param process 指定的进程句柄 - * @return 包含模块信息的 Psapi.MODULEINFO 对象,如果未找到,则返回 null + * @param process 指定的进程句柄,以 `WinNT.HANDLE` 类型传入,代表已经打开且具有相应访问权限的目标进程, + * 此句柄用于后续与该进程交互,例如获取其加载的模块信息等操作,是整个方法操作针对的具体进程对象,由外部调用时传入(通常是先通过 `OpenProcess` 等方法打开进程获取到的)。 + * @return 包含模块信息的 `Psapi.MODULEINFO` 对象,如果未找到,则返回 `null`,返回的 `Psapi.MODULEINFO` 对象包含了匹配模块的详细信息,如模块的基地址、大小、入口点等, + * 若在遍历所有已加载模块后都没有找到文件名以指定模块名结尾的模块,则返回 `null`,表示未检索到符合要求的模块信息。 */ private Psapi.MODULEINFO moduleFromName(WinNT.HANDLE process) { - // 列出并检索进程中指定已加载模块 + // 列出并检索进程中指定已加载模块,调用 `enumProcessModule` 方法,传入目标进程句柄(`process`)作为参数, + // 该方法会返回一个包含 `Psapi.MODULEINFO` 类型对象的列表,列表中的每个元素代表目标进程中一个已加载模块的详细信息,将这个列表存储在 `moduleInfos` 变量中, + // 作为后续遍历查找特定模块的基础数据来源。 List moduleInfos = enumProcessModule(process); - // 遍历模块列表 + // 遍历模块列表,通过循环遍历 `moduleInfos` 列表中的每个 `Psapi.MODULEINFO` 类型的元素(每个元素对应一个已加载模块的详细信息), + // 对每个模块进行相关操作,以查找文件名符合要求的特定模块。 for (Psapi.MODULEINFO moduleInfo : moduleInfos) { - // 创建用于存储模块文件名的字节数组 + // 创建用于存储模块文件名的字节数组,创建一个长度为 `WinNT.MAX_PATH`(这是一个预定义的常量,表示系统中文件路径的最大长度,通常足够存储模块文件名)的字节数组 `buffer`, + // 用于后续存储获取到的模块文件名信息,这个数组初始化为全0字节,等待被填充有效的文件名数据。 byte[] buffer = new byte[WinNT.MAX_PATH]; - // 根据模块的基址创建模块句柄 + // 根据模块的基址创建模块句柄,通过将当前遍历到的模块信息(`moduleInfo`)中的模块基址(`lpBaseOfDll` 成员变量,表示模块在进程内存空间中的起始地址)转换为 `WinNT.HANDLE` 类型, + // 创建一个新的模块句柄对象 `handle`,这个句柄后续会用于获取模块文件名的操作,它基于模块在内存中的基址来标识对应的模块实体,以便与系统函数交互获取相关信息。 WinNT.HANDLE handle = new WinNT.HANDLE(moduleInfo.lpBaseOfDll); - // 获取模块文件名并存储在 buffer 中 + // 获取模块文件名并存储在 buffer 中,调用 `Psapi.INSTANCE`(这是JNA库中用于与Windows系统的 `Psapi.dll` 进行交互的接口实例,用于获取进程模块相关的功能函数)的 `GetModuleFileNameExA` 函数, + // 传入目标进程句柄(`process`)、刚创建的模块句柄(`handle`)、用于存储文件名的字节数组 `buffer` 以及字节数组的长度(`buffer.length`)作为参数, + // 尝试获取指定模块的文件名信息,并将其存储到 `buffer` 字节数组中,函数执行后会按照系统的字符编码格式将文件名填充到数组中(可能包含结尾的空字符等情况)。 Psapi.INSTANCE.GetModuleFileNameExA(process, handle, buffer, buffer.length); - // 截取字符串,去除多余的字节,得到模块文件名 + // 截取字符串,去除多余的字节,得到模块文件名,调用 `ArrayUtil.sub` 方法(这应该是某个工具类中用于操作字节数组的方法,可能来自于如 `hutool` 等工具库), + // 传入字节数组 `buffer`、起始索引0以及通过 `ArrayUtil.indexOf` 方法(同样来自相关工具库,用于查找字节数组中特定字节第一次出现的索引位置)找到的字节数组中第一个空字符(字节值为0,表示字符串结束)的索引位置作为参数, + // 从 `buffer` 字节数组中截取从起始到第一个空字符之前的部分,去除了可能多余的空字符及后面无效的字节,得到真正有效的模块文件名字节数组 `trimmedBytes`。 byte[] trimmedBytes = ArrayUtil.sub(buffer, 0, ArrayUtil.indexOf(buffer, (byte) 0)); - // 获得文件名 + // 获得文件名,通过使用 `String` 的构造函数,传入截取后的有效字节数组 `trimmedBytes` 以及默认的字符编码(通过 `Charset.defaultCharset()` 获取,通常是系统默认的字符编码方式)作为参数, + // 将字节数组转换为字符串形式,得到模块的文件名,存储在 `fileName` 变量中,方便后续进行文件名的匹配判断操作。 String fileName = new String(trimmedBytes, Charset.defaultCharset()); - // 判断模块文件名是否以指定的模块名结尾 + // 判断模块文件名是否以指定的模块名结尾,通过调用 `fileName` 字符串的 `endsWith` 方法,传入要匹配的模块名(`WeChatServiceImpl.MODULE_NAME`,这是在类中定义的常量,表示特定的模块名称)作为参数, + // 判断当前模块的文件名是否以这个指定的模块名结尾,如果满足这个条件,则说明找到了符合要求的模块,直接返回当前模块对应的 `moduleInfo` 对象,包含了该模块的详细信息,方法执行结束。 if (fileName.endsWith(WeChatServiceImpl.MODULE_NAME)) { return moduleInfo; } } - // 未找到匹配的模块,返回 null + // 未找到匹配的模块,返回 null,如果在遍历完 `moduleInfos` 列表中所有模块后,都没有找到文件名以指定模块名结尾的模块, + // 则表示未检索到符合要求的模块信息,此时返回 `null`,供调用该方法的地方根据返回值进行相应的处理,比如提示用户或者进行其他逻辑判断等操作。 return null; } @@ -503,35 +711,60 @@ public class WeChatServiceImpl implements WeChatService { * @param pattern 要扫描的模式字节数组 * @return 包含匹配的内存地址的列表 */ + /** + * 在指定进程的模块中按照特定模式扫描内存地址的方法。 + * 该方法首先获取指定进程中特定模块的信息,确定模块的基址和最大地址范围,然后在这个模块的内存范围内逐页进行模式扫描, + * 将扫描到符合给定模式的内存地址添加到列表中,最后返回包含所有找到的内存地址的列表,若模块不存在或者未扫描到匹配地址则返回空列表。 + * + * @param process 指定的进程句柄,以 `WinNT.HANDLE` 类型传入,代表已经打开且具有相应访问权限的目标进程, + * 此句柄用于后续与该进程交互,例如读取其模块内存等操作,是整个方法操作针对的具体进程对象,由外部调用时传入(通常是先通过 `OpenProcess` 等方法打开进程获取到的)。 + * @param pattern 要扫描的字节模式,以字节数组形式传入,代表在模块内存中期望匹配的特定字节序列模式, + * 方法会在模块的内存范围内查找出现这个字节模式的位置,将其对应的内存地址记录下来并返回。 + * @return 返回一个包含 `Pointer` 类型元素的列表,列表中的每个 `Pointer` 指向在模块内存中扫描到符合给定模式的内存地址, + * 若在扫描过程中没有找到任何匹配的内存地址(比如模块不存在或者模块内存中确实不存在符合模式的情况),则返回一个空列表,表示未发现符合要求的内存地址。 + */ private List patternScanModule(WinNT.HANDLE process, byte[] pattern) { - // 用于存储找到的内存地址的列表 + // 用于存储找到的内存地址的列表,创建一个空的 `List` 类型列表 `foundPointers`,用于后续存储在模块内存中扫描到符合给定模式的内存地址, + // 初始时列表为空,随着扫描过程中发现匹配的地址会将对应的 `Pointer` 对象添加到这个列表中。 List foundPointers = new ArrayList<>(); - // 通过模块名检索指定进程加载的模块信息 + // 通过模块名检索指定进程加载的模块信息,调用 `moduleFromName` 方法,传入目标进程句柄(`process`)作为参数, + // 该方法会尝试在进程已加载的模块中查找文件名以特定名称(在 `moduleFromName` 方法中定义的相关模块名,例如微信相关的特定模块名等)结尾的模块, + // 并返回对应的 `Psapi.MODULEINFO` 对象,这个对象包含了模块的详细信息(如基址、大小等),将返回的模块信息存储在 `moduleInfo` 变量中,作为后续扫描操作的基础。 Psapi.MODULEINFO moduleInfo = moduleFromName(process); - // 空校验 + // 空校验,判断通过 `moduleFromName` 方法获取到的模块信息是否为 `null`,如果是 `null` 则表示没有找到符合要求的模块, + // 此时直接返回当前的空 `foundPointers` 列表,表示无法进行内存地址扫描操作,方法执行结束,因为没有对应的模块就不存在可扫描的内存范围了。 if (moduleInfo == null) { return foundPointers; } - // 获取模块基址和模块最大地址 + // 获取模块基址和模块最大地址,通过 `Pointer.nativeValue` 方法将模块信息(`moduleInfo`)中的模块基址(`lpBaseOfDll` 成员变量,表示模块在进程内存空间中的起始地址)转换为长整型数值, + // 得到模块的基址 `baseAddress`,然后通过计算模块基址加上模块的镜像大小(`moduleInfo.SizeOfImage` 表示模块在内存中的占用空间大小)得到模块内存范围的最大地址 `maxAddress`, + // 同时将模块基址赋值给 `pageAddress`,用于后续在模块内存范围内逐页进行扫描操作时作为起始扫描地址,`pageAddress` 会随着扫描的推进不断更新。 long baseAddress = Pointer.nativeValue(moduleInfo.lpBaseOfDll); long maxAddress = Pointer.nativeValue(moduleInfo.lpBaseOfDll) + moduleInfo.SizeOfImage; long pageAddress = baseAddress; - // 循环扫描模块内存页 + // 循环扫描模块内存页,通过 `while` 循环,只要当前扫描的页面地址(`pageAddress`)小于模块内存的最大地址(`maxAddress`),就持续进行循环扫描操作, + // 在每次循环中对当前内存页面进行模式扫描,获取下一次扫描的起始地址以及该页面内匹配模式的内存地址列表,然后更新 `pageAddress` 继续下一轮扫描,直到扫描完整个模块内存范围。 while (pageAddress < maxAddress) { - // 扫描当前页的模式,并返回下一个扫描的起始地址和匹配的内存地址列表 + // 扫描当前页的模式,并返回下一个扫描的起始地址和匹配的内存地址列表,调用 `scanPatternPage` 方法,传入目标进程句柄(`process`)、当前扫描的页面起始地址(`pageAddress`)以及要扫描的字节模式(`pattern`)作为参数, + // 该方法会在当前内存页面内按照给定的字节模式进行扫描,返回一个包含两个元素的 `Pair` 对象,其中第一个元素是下一次扫描应该从哪个地址开始(可能是本页扫描完后下一页的起始地址等情况), + // 第二个元素是一个包含 `Pointer` 类型的列表,表示在当前页面内扫描到符合给定模式的内存地址列表,将返回的 `Pair` 对象存储在 `pair` 变量中,方便后续提取相关信息进行操作。 Pair> pair = scanPatternPage(process, pageAddress, pattern); pageAddress = pair.getLeft(); - // 如果找到了匹配的内存地址,则添加到列表中 + // 如果找到了匹配的内存地址,则添加到列表中,判断 `pair` 对象中获取到的匹配内存地址列表(`pair.getRight()`)是否不为 `null`, + // 如果不为 `null` 说明在当前页面内找到了符合模式的内存地址,通过调用 `foundPointers.addAll` 方法将这些找到的内存地址全部添加到 `foundPointers` 列表中,完成一次页面扫描的地址收集操作, + // 后续继续循环扫描下一个页面,重复此过程,直到扫描完整个模块内存范围。 if (pair.getRight() != null) { foundPointers.addAll(pair.getRight()); } } - // 返回找到的内存地址列表 + + // 返回找到的内存地址列表,在扫描完整个模块内存范围后,将包含所有找到的符合给定模式的内存地址的 `foundPointers` 列表返回, + // 调用该方法的地方可以根据返回的列表进行后续操作,比如基于这些地址进一步读取内存数据或者进行其他相关逻辑处理等。 return foundPointers; } @@ -541,66 +774,66 @@ public class WeChatServiceImpl implements WeChatService { * @return 微信文件目录 */ public String getBasePath() { - // 默认微信文件路径设为 "MyDocument:" + // 默认微信文件路径设为 "MyDocument:",这里的MY_DOCUMENT应该是一个预定义的常量,表示默认路径 String wechatDir = MY_DOCUMENT; - // 是否读取成功的标记 + // 是否读取成功的标记,初始化为false,表示尚未成功读取到微信文件路径 boolean readSuccess = false; + // 尝试从注册表读取WeChat的文件保存路径 try { - // 尝试从注册表读取 WeChat 的文件保存路径 + // 检查注册表中是否存在指定的WeChat相关键值,WinReg.HKEY_CURRENT_USER表示当前用户根键,WECHAT_REG_PATH是WeChat在注册表中的路径 if (Advapi32Util.registryKeyExists(WinReg.HKEY_CURRENT_USER, WECHAT_REG_PATH)) { - // 如果成功读取,设置 wechatDir 为读取的路径 + // 如果成功读取,设置wechatDir为读取的路径,通过Advapi32Util工具类从注册表获取指定键对应的值,FILE_SAVE_PATH可能是注册表中保存文件路径的键名 wechatDir = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, WECHAT_REG_PATH, FILE_SAVE_PATH); // 标记为读取成功 readSuccess = true; } } catch (Exception ignore) { - // 如果读取失败,设路径为 "MyDocument:" + // 如果读取注册表过程中出现异常,忽略异常,并将路径设为默认的 "MyDocument:",也就是恢复初始的默认路径设置 wechatDir = MY_DOCUMENT; } - // 如果从注册表读取失败,尝试从配置文件读取 try { if (!readSuccess) { - // 获取用户主目录 + // 获取用户主目录,USERPROFILE是系统环境变量,表示当前用户的主目录路径 String userProfile = System.getenv("USERPROFILE"); - // 拼接 WeChat 配置文件的完整路径 + // 拼接WeChat配置文件的完整路径,CONFIG_FILE_PATH应该是相对于用户主目录的配置文件路径字符串,通过字符串拼接形成完整路径 String configPath = userProfile + CONFIG_FILE_PATH; - // 读取配置文件的所有行 + // 读取配置文件的所有行,通过Files.readAllLines方法读取指定路径的文件内容,并以行的形式返回一个List List lines = Files.readAllLines(Paths.get(configPath)); if (!lines.isEmpty()) { - // 如果文件不为空,设置 wechatDir 为读取的第一行内容 + // 如果文件不为空,设置wechatDir为读取的第一行内容,也就是取配置文件中的第一行作为微信文件路径 wechatDir = lines.get(0); } } } catch (IOException e) { - // 如果读取失败,设路径为 "MyDocument:" + // 如果读取配置文件过程中出现IO异常,将路径设为默认的 "MyDocument:" wechatDir = MY_DOCUMENT; } // 如果路径为 "MyDocument:",尝试设置为Documents目录 if (MY_DOCUMENT.equals(wechatDir)) { try { - // 获取文档目录路径 + // 获取文档目录路径,创建一个足够长度的字符数组来存储路径,WinDef.MAX_PATH通常表示系统定义的最大路径长度 char[] documentsPath = new char[WinDef.MAX_PATH]; - // 尝试获取文档目录路径 + // 尝试获取文档目录路径,通过Shell32.INSTANCE.SHGetFolderPath方法获取指定类型的系统文件夹路径,这里获取个人文件夹(通常是Documents文件夹)路径,参数依次表示所有者窗口、文件夹类型、访问令牌等相关信息 int documentsPathSuccess = Shell32.INSTANCE.SHGetFolderPath(null, ShlObj.CSIDL_PERSONAL, null, ShlObj.SHGFP_TYPE_CURRENT, documentsPath).intValue(); // 如果成功获取文档目录路径 if (documentsPathSuccess == WinError.S_OK.intValue()) { - // 成功获取文档目录路径后,设置 wechatDir 为文档目录路径 + // 成功获取文档目录路径后,设置wechatDir为文档目录路径,通过Native.toString方法将字符数组转换为字符串形式的路径 wechatDir = Native.toString(documentsPath); } else { - // 如果获取失败,设置路径为用户主目录下的 "Documents" + // 如果获取失败,设置路径为用户主目录下的 "Documents",通过字符串拼接的方式构建路径 wechatDir = System.getenv("USERPROFILE") + "\\Documents"; } } catch (Exception e) { - // 如果获取失败,设置路径为用户主目录下的 "Documents" + // 如果获取过程中出现异常,同样设置路径为用户主目录下的 "Documents" wechatDir = System.getenv("USERPROFILE") + "\\Documents"; } } - // 拼接 WeChat 文件目录路径 + // 拼接WeChat文件目录路径,WECHAT_FILES_DIR应该是WeChat文件所在目录相对于前面获取的基础路径的相对路径,将其与wechatDir拼接起来返回最终的微信文件目录路径 return wechatDir + WECHAT_FILES_DIR; } @@ -612,145 +845,173 @@ public class WeChatServiceImpl implements WeChatService { */ @Override public String getWxId(int pid) { - // 获取Kernel32实例 + // 获取Kernel32实例,Kernel32通常是用于操作Windows系统底层相关功能的类,这里获取其单例实例,以便后续调用相关系统函数 Kernel32 kernel32 = Kernel32.INSTANCE; - // 打开目标进程句柄 + + // 打开目标进程句柄,通过调用OpenProcess函数,参数0x1F0FFF表示访问权限相关的标志组合(具体权限含义需参照Windows系统相关文档),false表示不继承句柄,pid是传入的目标进程的ID WinNT.HANDLE process = kernel32.OpenProcess(0x1F0FFF, false, pid); - // 设置要匹配的模式字符串 + + // 设置要匹配的模式字符串,这里的模式 "\\Msg\\FTSContact" 可能是用于在进程内存中查找特定内容的特征字符串,具体匹配规则应该由patternScanAll方法定义 String pattern = "\\Msg\\FTSContact"; - // 在目标进程中扫描符合模式的内存位置 + + // 在目标进程中扫描符合模式的内存位置,调用patternScanAll方法进行扫描,返回匹配到的内存地址列表,参数10可能是与扫描范围、深度或者其他相关的控制参数(具体取决于该方法的实现) List addresses = patternScanAll(process, pattern, 10); - // 遍历匹配到的内存位置 + + // 遍历匹配到的内存位置,准备逐个分析这些位置上的数据以提取微信ID for (Pointer address : addresses) { - // 分配内存作为缓冲区读取数据 + // 分配内存作为缓冲区读取数据,设置缓冲区大小为80字节,用于后续从进程内存读取数据暂存到这里 int bufferSize = 80; Memory buffer = new Memory(bufferSize); IntByReference read = new IntByReference(); - // 从指定地址的前一部分开始读取内存,而不是从地址的确切位置开始。 + + // 从指定地址的前一部分开始读取内存,而不是从地址的确切位置开始,通过创建一个新的指针,其值为原地址值减去30,可能是根据内存数据结构特点,认为需要从这个偏移位置开始读取才有意义(具体取决于微信内存布局相关情况) Pointer offsetPointer = new Pointer(Pointer.nativeValue(address) - 30); - // 从指定地址读取内存数据 + + // 从指定地址读取内存数据,调用ReadProcessMemory函数尝试从目标进程内存的指定位置(offsetPointer指向的位置)读取数据到缓冲区buffer中,bufferSize指定读取的最大字节数,read用于返回实际读取的字节数 boolean success = kernel32.ReadProcessMemory(process, offsetPointer, buffer, bufferSize, read); + if (success) { - // 将读取的字节转换为字符串 + // 将读取的字节转换为字符串,先获取缓冲区中实际读取到的字节数组,然后使用Java的String构造函数将字节数组转换为字符串,指定正确的字符编码(这里使用默认编码,可能存在编码问题,可根据实际情况调整)和字节长度(read.getValue()表示实际读取的字节数) byte[] dataBytes = buffer.getByteArray(0, read.getValue()); String data = new String(dataBytes, 0, read.getValue()); - // 对字符串进行处理,提取微信ID信息 + + // 对字符串进行处理,提取微信ID信息,通过以 "\\Msg" 为分隔符进行分割,取分割后的第一部分,可能是认为微信ID相关信息在这之前的部分 data = data.split("\\\\Msg")[0]; - // 通过文件分隔符分割 + + // 通过文件分隔符(在Java中为 "\\")分割字符串,目的可能是进一步细化提取微信ID的操作,将字符串按照文件路径类似的格式进行拆分 String[] newData = data.split("\\\\"); - // 取最后一个为微信Id + + // 取最后一个为微信Id,根据前面的分割逻辑,认为最后一个元素就是所需的微信ID,将其返回 return newData[newData.length - 1]; } } - // 关闭目标进程句柄 + + // 关闭目标进程句柄,操作完成后释放打开的进程句柄资源,调用CloseHandle函数进行关闭 kernel32.CloseHandle(process); - // 返回空 + + // 如果遍历完所有匹配的内存位置都没有成功提取到微信ID,则返回空,表示获取失败 return null; } /** * 在指定进程的虚拟地址空间中扫描所有符合指定模式的内存位置。 * - * @param process 指定进程的句柄 - * @param pattern 要匹配的模式字符串 - * @param findNum 最大匹配数量 - * @return 符合模式的内存位置列表 + * @param process 指定进程的句柄,通过该句柄来访问对应进程的虚拟地址空间等相关资源 + * @param pattern 要匹配的模式字符串,用于在进程内存中查找与之匹配的内容,具体匹配规则应该在内部的扫描逻辑中有相应定义 + * @param findNum 最大匹配数量,即控制最多找到多少个符合模式的内存位置后就停止扫描 + * @return 符合模式的内存位置列表,返回找到的所有符合要求的内存位置信息,以列表形式存储 */ public List patternScanAll(WinNT.HANDLE process, String pattern, int findNum) { - // 存储符合模式的内存位置列表 + // 存储符合模式的内存位置列表,初始化一个空的ArrayList,后续将把扫描到的符合要求的内存位置指针添加到这个列表中 List found = new ArrayList<>(); - // 根据系统架构设置用户空间限制 + + // 根据系统架构设置用户空间限制,通过获取系统属性 "os.arch" 判断系统架构,如果是 "amd64" 架构,设置用户空间限制为 0x7FFFFFFF0000L,否则(比如可能是32位架构等情况)设置为 0x7FFF0000L,这个限制用于界定扫描的虚拟地址空间范围 long userSpaceLimit = "amd64".equals(System.getProperty("os.arch")) ? 0x7FFFFFFF0000L : 0x7FFF0000L; - // 初始下一个扫描位置 + + // 初始下一个扫描位置,从虚拟地址空间的起始位置 0 开始扫描(通常情况下,初始化为 0 便于从头开始遍历地址空间查找匹配内容) long nextRegion = 0; - // 循环扫描直到达到用户空间限制或找到足够的匹配位置 + + // 循环扫描直到达到用户空间限制或找到足够的匹配位置,通过这个循环不断在进程的虚拟地址空间内按页进行扫描查找符合模式的内存位置 while (nextRegion < userSpaceLimit) { - // 执行单页扫描,获取扫描结果 + // 执行单页扫描,获取扫描结果,调用scanPatternPage方法对当前区域(以nextRegion指定起始位置)进行单页扫描,传入进程句柄、起始位置以及模式字符串对应的字节数组(将字符串转换为字节数组以便在底层进行匹配操作),返回一个包含下一个扫描位置和本页扫描匹配到的内存位置列表的Pair对象 Pair> pair = scanPatternPage(process, nextRegion, pattern.getBytes()); - // 更新下一个扫描位置 + + // 更新下一个扫描位置,从pair对象中获取下一个应该扫描的起始位置,以便继续在下一区域进行扫描查找 nextRegion = pair.getLeft(); - // 获取单页扫描的匹配位置列表 + + // 获取单页扫描的匹配位置列表,从pair对象中获取本页扫描得到的符合模式的内存位置列表 List pageFound = pair.getRight(); - // 如果单页扫描找到匹配位置,将其添加到总的匹配位置列表中 + // 如果单页扫描找到匹配位置,将其添加到总的匹配位置列表中,把本页扫描到的所有符合模式的内存位置添加到总的匹配位置列表found中,用于汇总所有扫描到的结果 if (!pageFound.isEmpty()) { found.addAll(pageFound); } - // 如果找到足够的匹配位置,跳出循环 + + // 如果找到足够的匹配位置,跳出循环,当已经找到的匹配位置数量超过了设定的最大匹配数量findNum时,就停止扫描循环,结束整个扫描过程 if (found.size() > findNum) { break; } } - // 返回符合模式的内存位置列表 + + // 返回符合模式的内存位置列表,将汇总后的所有符合模式的内存位置列表返回给调用者,完成整个扫描并返回结果的操作 return found; } /** * 扫描内存页 * - * @param process 指定进程的句柄 - * @param startAddress 开始的内存地址 - * @param pattern 要匹配的模式字符串 - * @return Pair + * @param process 指定进程的句柄,通过该句柄可以对相应进程的内存进行访问操作,比如读取内存数据、查询内存信息等 + * @param startAddress 开始的内存地址,指明从进程虚拟地址空间中的哪个位置开始进行本页的扫描操作 + * @param pattern 要匹配的模式字符串,以字节数组形式传入(通常是要在内存中查找与之匹配的内容所依据的特征模式,比如特定的字节序列等) + * @return Pair,返回一个包含两个元素的Pair对象,第一个元素表示下一个内存区域的起始地址(用于后续继续扫描其他区域),第二个元素是本页扫描找到的符合模式的内存位置列表 */ private Pair> scanPatternPage(WinNT.HANDLE process, long startAddress, byte[] pattern) { - // 获取内存基本信息 + // 获取内存基本信息,创建一个WinNT.MEMORY_BASIC_INFORMATION对象,用于存储查询到的指定虚拟内存地址相关的基本信息,比如内存区域的大小、状态、保护属性等 WinNT.MEMORY_BASIC_INFORMATION mbi = new WinNT.MEMORY_BASIC_INFORMATION(); - // 查询指定虚拟内存地址 + + // 查询指定虚拟内存地址,通过调用Kernel32.INSTANCE.VirtualQueryEx函数,传入进程句柄、要查询的起始内存地址(包装成Pointer类型)以及用于存储信息的mbi对象和其大小信息,来获取该起始地址对应的内存区域的基本情况 Kernel32.INSTANCE.VirtualQueryEx(process, new Pointer(startAddress), mbi, new BaseTSD.SIZE_T(mbi.size())); - // 计算下一个内存区域的起始地址 + + // 计算下一个内存区域的起始地址,根据当前内存区域的基地址(mbi.baseAddress)和区域大小(mbi.regionSize)来计算下一个内存区域应该从哪里开始,一般是当前区域结束后的位置,为后续继续扫描做准备 long nextRegion = Pointer.nativeValue(mbi.baseAddress) + mbi.regionSize.longValue(); - // 定义允许的内存保护标志 + + // 定义允许的内存保护标志,创建一个整数数组,列举出了允许进行扫描匹配操作的内存页面所具有的保护属性,例如可执行、可读可写等,只有符合这些保护属性的内存区域才会进一步进行模式匹配操作 int[] allowedProtections = {WinNT.PAGE_EXECUTE, WinNT.PAGE_EXECUTE_READ, WinNT.PAGE_EXECUTE_READWRITE, WinNT.PAGE_READWRITE, WinNT.PAGE_READONLY}; List foundPointer = new ArrayList<>(); - // 检查内存状态和保护标志是否符合要求 + // 检查内存状态和保护标志是否符合要求,判断当前内存区域的状态是否为已提交(MEM_COMMIT表示内存已分配且可访问)且其保护属性是否在允许的保护标志列表中,如果不满足条件,则直接返回包含下一个区域起始地址和空的匹配位置列表的Pair对象,意味着跳过本页的后续扫描操作 if (!(mbi.state.intValue() == WinNT.MEM_COMMIT && ArrayUtil.contains(allowedProtections, mbi.protect.intValue()))) { return Pair.of(nextRegion, foundPointer); } - // 创建一个 Native Memory 对象,用于存储从进程中读取的内存数据 + // 创建一个 Native Memory 对象,用于存储从进程中读取的内存数据,根据前面查询到的内存区域大小创建一个足够容量的Memory对象,以便后续将从进程内存中读取的数据存储到这里 Memory memory = new Memory(mbi.regionSize.intValue()); - // 创建一个引用对象,用于存储 ReadProcessMemory 函数返回的已读取字节数 + // 创建一个引用对象,用于存储ReadProcessMemory函数返回的已读取字节数,通过IntByReference对象可以在调用ReadProcessMemory后获取实际读取到的字节数量信息 IntByReference bytesRead = new IntByReference(); - // 从指定进程中读取内存数据,并将结果存储到 Native Memory 对象中 + + // 从指定进程中读取内存数据,并将结果存储到Native Memory对象中,调用Kernel32.INSTANCE.ReadProcessMemory函数,从指定进程(process)的指定起始地址(startAddress)读取内存数据,将数据存储到memory对象中,读取的最大字节数依据内存区域大小(mbi.regionSize.intValue())来确定,同时通过bytesRead对象获取实际读取的字节数 Kernel32.INSTANCE.ReadProcessMemory(process, new Pointer(startAddress), memory, mbi.regionSize.intValue(), bytesRead); - // 从 Native Memory 对象中获取已读取的字节数,并创建一个字节数组存储读取的内存数据 + + // 从Native Memory对象中获取已读取的字节数,并创建一个字节数组存储读取的内存数据,从memory对象中获取实际读取到的字节数据,存放到新创建的字节数组buffer中,方便后续在这些数据中查找匹配的模式 byte[] buffer = memory.getByteArray(0, bytesRead.getValue()); - // 查找匹配的模式在内存中的起始位置 + // 查找匹配的模式在内存中的起始位置,调用findMatches函数在读取到的字节数组buffer中查找与传入的模式pattern匹配的起始位置,对于每个找到的起始位置,将其对应的内存地址(相对于本页起始地址startAddress的偏移量加上startAddress得到绝对地址)添加到foundPointer列表中,表示找到了符合模式的内存位置 for (int start : findMatches(buffer, pattern)) { foundPointer.add(new Pointer(startAddress + start)); } + + // 返回Pair对象,包含下一个内存区域的起始地址(用于后续扫描流程继续推进到下一个区域)以及本页扫描找到的符合模式的所有内存位置列表,完成本页扫描并返回结果的操作 return Pair.of(nextRegion, foundPointer); } /** * 在字节数组中查找匹配指定模式的位置索引数组。 * - * @param inputBytes 待查找的字节数组 - * @param patternBytes 要匹配的模式字节数组 - * @return 匹配位置索引数组 + * @param inputBytes 待查找的字节数组,也就是要在这个字节数组中去寻找是否存在与指定模式相匹配的部分 + * @param patternBytes 要匹配的模式字节数组,代表了期望在inputBytes中找到的特定字节序列模式 + * @return 匹配位置索引数组,返回一个整数数组,数组中的每个元素表示在inputBytes中找到的与patternBytes匹配的子数组的起始位置索引 */ private int[] findMatches(byte[] inputBytes, byte[] patternBytes) { - // 初始化匹配位置索引数组 + // 初始化匹配位置索引数组,先创建一个长度为0的空数组,后续会根据实际找到的匹配位置数量动态调整其大小并添加相应的索引值 int[] matches = new int[0]; - // 遍历待查找的字节数组 + // 遍历待查找的字节数组,从索引0开始,直到可以完整匹配模式字节数组的最后一个可能位置(即inputBytes长度减去patternBytes长度的位置,再往后就不够匹配整个模式了),每次循环检查当前位置开始是否能匹配上模式字节数组 for (int i = 0; i <= inputBytes.length - patternBytes.length; i++) { - // 假设当前位置开始存在匹配 + // 假设当前位置开始存在匹配,先默认当前位置开始的子数组与模式字节数组是匹配的,后续通过内层循环来验证这个假设是否成立 boolean match = true; - // 遍历模式字节数组,检查是否与待查找的子数组匹配 + // 遍历模式字节数组,检查是否与待查找的子数组匹配,从模式字节数组的索引0开始,逐个字节对比当前待查找字节数组中从i位置开始的对应字节,看是否完全一致 for (int j = 0; j < patternBytes.length; j++) { if (inputBytes[i + j] != patternBytes[j]) { - // 如果有不匹配的字节,标记为不匹配,并中断内层循环 + // 如果有不匹配的字节,标记为不匹配,并中断内层循环,一旦发现某个字节不一致,就说明从当前i位置开始不匹配模式字节数组,将match标记为false,并跳出内层循环,继续检查下一个i位置 match = false; break; } } - // 如果找到匹配的位置,将其添加到匹配位置索引数组中 + + // 如果找到匹配的位置,将其添加到匹配位置索引数组中,如果经过内层循环验证当前位置确实匹配成功(match为true),则需要把这个匹配位置的索引i添加到matches数组中。 + // 由于Java数组长度不可变,这里先创建一个新的长度比原数组大1的数组newMatches,然后将原数组matches中的元素复制到新数组对应位置,最后把新的匹配位置索引i添加到新数组末尾,再将新数组赋值给matches,完成匹配位置索引的添加操作 if (match) { int[] newMatches = new int[matches.length + 1]; System.arraycopy(matches, 0, newMatches, 0, matches.length); @@ -758,7 +1019,8 @@ public class WeChatServiceImpl implements WeChatService { matches = newMatches; } } - // 返回匹配位置索引数组 + + // 返回匹配位置索引数组,将最终包含所有匹配位置索引的数组返回给调用者,完成整个查找匹配位置索引的功能 return matches; } @@ -771,40 +1033,46 @@ public class WeChatServiceImpl implements WeChatService { @Override public String getVersion(int pid) { // 获取指定进程ID的可执行文件路径。 + // 调用名为getExecutablePath的方法(此处未展示其具体实现,但推测是根据进程ID查找对应可执行文件在磁盘上的存储路径), + // 该路径将作为后续获取文件版本信息的基础,因为所有与文件版本相关的操作都需要基于正确的文件路径来进行。 + String filePath = getExecutablePath(pid); // 用于存储 'GetFileVersionInfoSize' 函数的额外信息(通常未使用)。 + // 被用于传递引用类型的数据(在Java中模拟类似C语言中的指针传递效果),并将其初始值设为0, IntByReference dwDummy = new IntByReference(); dwDummy.setValue(0); // 获取文件版本信息的大小。 + //通过Version.INSTANCE调用GetFileVersionInfoSize函数,这是Windows系统提供的用于获取文件版本信息大小的API函数, + // 通过JNA机制在Java中进行调用。传入文件路径(filePath)以及前面创建的用于接收额外信息的对象(dwDummy), int versionLength = Version.INSTANCE.GetFileVersionInfoSize(filePath, dwDummy); - // 如果长度为0,表示没有版本信息。 + // 如果长度为0,表示没有版本信息。若返回的版本信息长度为0,意味着该文件不存在版本信息或者无法获取到版本信息,此时直接返回null,表示获取失败。 if (versionLength == 0) { return null; } - // 分配足够存储文件版本信息的缓冲区。 + // 分配足够存储文件版本信息的缓冲区。根据前面获取到的版本信息长度versionLength,创建一个字节数组buffer,用于后续存储从文件中读取出来的版本信息内容,确保缓冲区大小足够容纳这些信息。 byte[] buffer = new byte[versionLength]; - // 使用 JNA 的 Memory 类将缓冲区映射到内存。 + // 使用 JNA 的 Memory 类将缓冲区映射到内存。创建一个Pointer对象,通过JNA(Java Native Access)的Memory类将前面创建的字节数组buffer映射到内存中,方便后续与Windows API进行交互,以操作这些内存数据来获取文件版本信息(这是JNA操作内存的一种常见方式)。 Pointer lpData = new Memory(buffer.length); - // 获取文件版本信息。 + // 获取文件版本信息。调用Version.INSTANCE.GetFileVersionInfo函数,尝试从指定文件路径filePath处获取文件版本信息,传入相关参数(如起始位置设为0,信息长度为versionLength以及映射到内存的指针lpData),若获取失败(函数返回false),则返回null,表示无法获取到版本信息。 if (!Version.INSTANCE.GetFileVersionInfo(filePath, 0, versionLength, lpData)) { return null; } - // 用于存储 'VerQueryValue' 函数输出的文件版本信息指针。 + // 用于存储 'VerQueryValue' 函数输出的文件版本信息指针。创建一个PointerByReference对象,用于在后续调用VerQueryValue函数时接收返回的指向文件版本信息具体内容的指针(因为文件版本信息可能是一个较为复杂的数据结构,通过指针来定位具体部分)。 PointerByReference lplpBuffer = new PointerByReference(); - // 用于存储 'VerQueryValue' 函数输出的文件版本信息长度。 + // 用于存储 'VerQueryValue' 函数输出的文件版本信息长度。创建一个IntByReference对象,用于接收VerQueryValue函数返回的文件版本信息的实际长度,便于后续准确处理该部分信息。 IntByReference puLen = new IntByReference(); - // 查询具体的文件版本信息。 + // 查询具体的文件版本信息。调用Version.INSTANCE.VerQueryValue函数,尝试从已经获取到的内存中的文件版本信息(由lpData指向)里查询具体的版本相关内容,传入特定的查询路径(此处是"\\",表示根路径,按相关规范来查询整体版本信息)以及前面创建的用于接收指针和长度的对象,如果查询成功(函数返回true),则进行后续的版本信息提取操作。 if (Version.INSTANCE.VerQueryValue(lpData, "\\", lplpBuffer, puLen)) { - // 将指针映射到 VS_FIXEDFILEINFO 结构。 + // 将指针映射到 VS_FIXEDFILEINFO 结构。通过获取到的指向版本信息的指针lplpBuffer.getValue(),创建一个VerRsrc.VS_FIXEDFILEINFO对象,将其与对应的内存位置关联起来,以便按照该结构定义来读取和解析版本信息(VS_FIXEDFILEINFO是Windows系统中定义的用于存储文件固定版本信息的结构体,这里通过JNA与之对应起来进行操作)。 VerRsrc.VS_FIXEDFILEINFO lplpBufStructure = new VerRsrc.VS_FIXEDFILEINFO(lplpBuffer.getValue()); lplpBufStructure.read(); - // 提取文件版本的主要部分和次要部分。 + // 提取文件版本的主要部分和次要部分。按照VS_FIXEDFILEINFO结构体中定义的成员变量(dwFileVersionMS和dwFileVersionLS分别存储了版本号的高位和低位部分),通过位运算提取出文件版本的四个部分,分别对应主版本号、次版本号等不同的版本分级(具体含义和格式遵循Windows系统对文件版本号的定义)。 long v1 = (lplpBufStructure.dwFileVersionMS.longValue() >> 16) & 0xffff; long v2 = lplpBufStructure.dwFileVersionMS.longValue() & 0xffff; long v3 = (lplpBufStructure.dwFileVersionLS.longValue() >> 16) & 0xffff; @@ -820,52 +1088,84 @@ public class WeChatServiceImpl implements WeChatService { /** * 获取给定进程ID的可执行文件路径。 + * 该方法的主要功能是依据传入的进程ID,借助Windows操作系统提供的相关API,尝试获取对应进程所关联的可执行文件的完整路径信息。 + * 如果在整个获取过程中,由于诸如权限不足、函数调用失败等原因,导致无法成功得到可执行文件的路径,那么将会返回null作为结果。 * - * @param pid 进程ID。 - * @return 可执行文件的完整路径,如果无法获取,则返回 null。 + * @param pid 进程ID。这是一个能够在操作系统层面唯一标识正在运行进程的整数值,通过它可以精准定位到特定的进程,进而去查找与之对应的可执行文件的具体存储位置路径。 + * @return 可执行文件的完整路径,如果无法获取,则返回 null。最终返回值为表示文件完整存储位置的字符串,例如 "C:\\Program Files\\SomeApp\\SomeApp.exe" 这样的格式;要是获取过程出现问题,未能成功获取到路径信息,就返回null给调用者来表明获取失败的情况。 */ private String getExecutablePath(int pid) { // 获取 Kernel32 实例,用于访问 Windows API 函数。 + // Kernel32是在Java中通过Java Native Access(JNA)方式来调用Windows系统底层API时常用的一个类,它相当于一个入口,提供了众多Windows API函数的调用接口。 + // 通过其INSTANCE属性获取单例实例,后续就能利用这个实例去调用各种Windows系统相关的函数,例如打开进程、查询进程信息以及操作进程相关资源等操作都依赖于此来与Windows操作系统底层进行交互。 Kernel32 kernel32 = Kernel32.INSTANCE; // 打开进程以查询信息。PROCESS_QUERY_INFORMATION 和 PROCESS_VM_READ // 是所需的访问权限,以获取进程的可执行文件路径。 + // 调用OpenProcess函数来尝试打开指定的进程,该函数需要传入几个关键参数。其中,WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ这个参数组合表示了对目标进程所需要的访问权限设定。 + // PROCESS_QUERY_INFORMATION权限允许当前代码去查询进程的各类相关信息,像这里要获取的可执行文件路径就依赖于这个权限;而PROCESS_VM_READ权限则使得可以读取进程的虚拟内存内容(虽然在这里主要目的是获取文件路径,但有些获取路径的实现机制可能涉及到读取内存相关操作,所以需要这个权限)。 + // 第二个参数false表示不需要继承句柄,在Windows操作系统多进程环境下,句柄的继承特性有其特定应用场景,这里我们不需要开启这个特性,所以设为false。 + // 最后传入的pid参数明确指定了要打开的是哪个进程,也就是与传入的这个进程ID相对应的进程。函数执行后会返回一个WinNT.HANDLE类型的进程句柄,这个句柄就像是一把“钥匙”,后续会通过它来对打开的这个进程进行各种查询等相关操作。 WinNT.HANDLE process = kernel32.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, pid); try { // 确保成功获取到进程句柄。 + // 对前面通过OpenProcess函数获取到的进程句柄进行判断,只有当进程句柄不为null时,才说明成功打开了对应的目标进程,这样才有条件继续后续获取可执行文件路径的操作; + // 如果进程句柄为null,那就意味着打开进程这个步骤出现了问题,可能是因为当前代码运行的环境没有足够的权限、指定的进程ID对应的进程不存在或者其他一些异常情况导致的,这种情况下自然就没办法继续往下进行获取路径的流程了。 if (process != null) { // 分配字符数组以存储路径。 + // 创建一个字符数组path,其长度设定为WinDef.MAX_PATH,WinDef.MAX_PATH通常是Windows操作系统预先定义好的一个常量,代表了系统中路径字符串所能达到的最大长度。 + // 这么做是为了确保创建的这个数组有足够的空间来容纳可能很长的可执行文件路径字符串,防止出现因为数组长度不够而导致的缓冲区溢出等错误情况,从而能安全地接收即将通过API函数获取到的路径信息。 char[] path = new char[WinDef.MAX_PATH]; // 初始化大小为路径的最大长度。 + // 将size变量初始化为path数组的长度,也就是前面提到的WinDef.MAX_PATH的值,这个size变量后续会在调用QueryFullProcessImageName函数时作为一个参数传入,用于告知函数接收路径信息的缓冲区大小情况。 int size = path.length; // 调用 QueryFullProcessImageName 获取进程的完整路径。 + // 调用Kernel32.INSTANCE.QueryFullProcessImageName函数来尝试获取目标进程的完整可执行文件路径。这个函数的作用就是从已打开的进程(通过前面获取到的process句柄指定)中查询并获取其对应的可执行文件的完整路径信息。 + // 第一个参数process就是前面打开进程得到的句柄,用于明确是针对哪个进程进行操作;第二个参数0通常是一些固定的标志位设置(在这里按照常规用法传入0即可,具体不同取值对应不同的功能扩展,可参考Windows API文档); + // 第三个参数path就是前面创建的用于存储路径字符串的字符数组,函数会将获取到的路径信息填充到这个数组中;第四个参数new IntByReference(size)是通过JNA中的IntByReference类型创建的一个对象,用于传递size变量的引用, + // 这样函数在执行过程中如果发现实际获取到的路径长度超过了传入的初始size值(这种情况可能发生在路径很长,超出了预先估计的WinDef.MAX_PATH长度时),会通过这个引用修改size的值,以便后续代码能知晓实际获取到的路径长度情况(不过这里没有对这种情况做进一步特殊处理,只是按照常规流程来获取路径)。 boolean success = Kernel32.INSTANCE.QueryFullProcessImageName(process, 0, path, new IntByReference(size)); // 如果成功获取路径,返回路径字符串。 + // 根据QueryFullProcessImageName函数的调用结果进行判断,如果返回值success为true,那就意味着成功获取到了目标进程的可执行文件路径信息,此时就可以将存储在path字符数组中的路径信息转换为Java中的字符串类型并返回。 + // 通过new String(path)操作将字符数组转换为字符串,同时调用trim()方法去除字符串两端可能存在的空白字符(比如空格、制表符等),以确保返回的路径字符串格式规范、干净整洁,然后将处理后的路径字符串作为最终结果返回给调用者。 if (success) { return new String(path).trim(); } } } finally { // 确保在操作完成后关闭进程句柄。 + // 在Java的异常处理机制中,finally块中的代码无论try块中的操作是否发生异常,都会被执行。这里的目的就是确保在完成了对进程的相关操作(无论是成功获取到路径还是获取过程中出现了异常)之后, + // 都要关闭之前通过OpenProcess函数打开的进程句柄,释放相关的系统资源。通过判断process句柄是否为null来确定是否成功打开过进程,如果不为null,就调用kernel32.CloseHandle(process)来关闭对应的进程句柄,避免出现资源泄漏等问题。 if (process != null) { kernel32.CloseHandle(process); } } // 如果无法获取路径,返回 null。 + // 如果在前面的整个流程中,无论是打开进程失败,还是获取可执行文件路径的操作失败,最终都没能成功得到有效的路径信息,那么就按照方法的定义,返回null来告知调用者获取可执行文件路径的操作没有成功完成。 return null; } /** * 去掉点比较版本号的方法 + * 该方法的主要作用是将传入的两个版本号字符串形式的数据,通过去除其中的点号(通常版本号格式如 "1.2.3",这里去除点号后变为 "123" 这样的纯数字形式), + * 再将其转换为长整型(Long)数据进行大小比较,以此来判断当前版本是否小于支持的最小版本,最终返回比较结果(true表示当前版本小于支持的最小版本,false表示大于等于)。 * - * @param supportMinVersion 支持的版本 - * @param currentVersion 当前版本 - * @return true or false + * @param supportMinVersion 支持的版本,即期望应用程序所支持的最低版本号,以字符串形式传入,例如 "1.2.3" 这样符合常见版本号格式的字符串。 + * @param currentVersion 当前版本,代表当前实际的版本号,同样以字符串形式传入,格式与支持的版本号格式一致,比如 "1.3.0" 等。 + * @return true or false,返回一个布尔值,若当前版本小于支持的最小版本则返回 true,否则返回 false,表示当前版本是否满足最低版本要求。 */ private boolean compareVersions(String supportMinVersion, String currentVersion) { + // 将支持的版本号字符串中的点号去除,并转换为长整型(Long)数据。 + // 首先调用replaceAll("\\.", "")方法,使用正则表达式 "\\."(在Java字符串中,需要用双反斜杠来表示一个真正的反斜杠,所以写成 "\\." 来匹配点号)来替换掉版本号字符串中的所有点号, long supportMinVersionLong = Long.parseLong(supportMinVersion.replaceAll("\\.", "")); + // 对当前版本号执行与处理支持的版本号相同的操作,即将当前版本号字符串中的点号去除,并转换为长整型(Long)数据。 + // 同样先使用replaceAll("\\.", "")方法去除当前版本号字符串(currentVersion)中的点号,将其转换为纯数字字符串,再通过Long.parseLong()方法将其转换为长整型, + // 得到的结果存储在currentVersionLong变量中,用于和前面转换后的支持的版本号对应的长整型数据进行比较。 long currentVersionLong = Long.parseLong(currentVersion.replaceAll("\\.", "")); + // 通过比较两个长整型版本号数据的大小,返回比较结果。 + // 比较currentVersionLong和supportMinVersionLong的大小关系,如果currentVersionLong小于supportMinVersionLong, + // 则说明当前版本低于支持的最小版本,按照方法的逻辑,返回 true;否则(即当前版本大于等于支持的最小版本)返回 false,以此来告知调用者当前版本是否满足最低版本要求。 return currentVersionLong < supportMinVersionLong; } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/DSNameUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/DSNameUtil.java index 08748b0..df783b8 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/DSNameUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/DSNameUtil.java @@ -4,34 +4,45 @@ import cn.hutool.extra.spring.SpringUtil; import com.xcs.wx.service.UserService; /** - * DSNameUtil + * `DSNameUtil` 工具类,主要用于生成数据源名称(DSName)相关的操作。 + * 在涉及多数据源或者针对不同用户、不同数据库区分数据源的场景下,通过该类提供的方法可以按照一定规则生成对应的数据源名称, + * 方便在项目中对数据源进行准确标识和管理,确保数据的正确访问和操作与相应的数据源对应起来。 * * @author 林雷 * @date 2024年6月27日17:26:43 */ public class DSNameUtil { + // 将构造函数私有化,防止外部实例化该工具类,因为这个类主要提供静态方法来获取数据源名称,不需要创建实例对象,遵循工具类的设计原则。 private DSNameUtil() { } /** - * 获取数据源名称 + * 获取数据源名称的方法,此方法通过获取当前用户(借助 `SpringUtil` 从Spring容器中获取 `UserService` 实例,并调用其 `currentUser` 方法来确定当前用户的标识), + * 然后结合传入的数据库名按照特定规则生成数据源名称,适用于在不清楚具体 `wxId`(用户标识)的情况下,基于当前操作的用户来生成对应的数据源名称,方便业务逻辑中对数据源的统一管理和使用。 * - * @param dbName 数据库名 - * @return dsName + * @param dbName 数据库名,以字符串形式传入,表示要生成数据源名称所对应的数据库名称,这个名称是数据源名称的重要组成部分, + * 用于区分不同的数据库对应的数据源,在多数据库场景下确保每个数据库都有唯一对应的数据源名称标识。 + * @return dsName,返回生成的数据源名称字符串,该字符串是按照一定规则(在这个方法中是结合当前用户和传入的数据库名)组合而成的, + * 可以用于在项目中配置、查找或者操作对应的数据源,调用方根据返回的名称能够准确关联到相应的数据源实体进行后续的数据访问等操作。 */ public static String getDSName(String dbName) { return getDSName(SpringUtil.getBean(UserService.class).currentUser(), dbName); } /** - * 获取数据源名称 + * 获取数据源名称的方法,该方法接收微信用户的唯一标识(`wxId`)和数据库名(`dbName`)作为参数, + * 通过将 `wxId` 和 `dbName` 按照特定格式(中间用 `#` 符号连接)拼接起来生成数据源名称,用于明确地标识特定用户对应的特定数据库的数据源, + * 在多用户、多数据库的复杂场景下,方便准确地定位和区分不同的数据源,使得数据操作能够准确对应到相应的用户和数据库资源上。 * - * @param wxId wxId - * @param dbName 数据库名 - * @return dsName + * @param wxId `wxId`,以字符串形式传入,表示微信用户的唯一标识,用于区分不同用户对应的数据源,确保每个用户访问其专属数据库时使用正确的数据源名称进行关联, + * 在多用户场景下实现数据源的隔离和准确管理,不同用户的数据操作不会相互干扰。 + * @param dbName 数据库名,以字符串形式传入,表示要生成数据源名称所对应的数据库名称,与 `wxId` 一起构成完整的数据源名称,用于区分不同数据库对应的数据源, + * 保证每个数据库都有唯一对应的数据源标识,便于在项目中进行数据源相关的配置和操作。 + * @return dsName,返回生成的数据源名称字符串,这个字符串按照 `wxId + "#" + dbName` 的格式拼接而成, + * 可以作为关键标识在项目的数据源管理、数据访问层等多处地方使用,使得系统能够准确地根据这个名称找到对应的数据源进行数据的读写等操作。 */ public static String getDSName(String wxId, String dbName) { return wxId + "#" + dbName; } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/DateFormatUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/DateFormatUtil.java index 4f93e59..d3bd857 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/DateFormatUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/DateFormatUtil.java @@ -4,7 +4,9 @@ import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; /** - * 时间格式化工具类 + * 时间格式化工具类,该类提供了针对时间戳进行特定格式转换的功能, + * 通过判断时间戳对应的时间与当前时间的关系(如是否是今天、昨天、本周等情况), + * 将时间戳格式化为符合业务需求的易读的时间字符串表示形式,方便在界面展示、数据记录等场景下使用。 * * @author xcs * @date 2023年12月27日 16时06分 @@ -12,37 +14,52 @@ import cn.hutool.core.date.DateUtil; public class DateFormatUtil { /** - * 格式化时间戳 + * 格式化时间戳的方法,用于将给定的以秒为单位的时间戳转换为具有特定格式的时间字符串。 + * 根据时间戳对应的时间与当前时间的相对关系(今天、昨天、本周、本年等不同情况),采用不同的格式化规则进行转换, + * 使得最终返回的字符串能够以简洁且符合日常习惯的方式展示时间信息。 * - * @param timestampInSeconds 时间戳(以秒为单位) - * @return 格式化后的时间字符串 + * @param timestampInSeconds 时间戳(以秒为单位),以长整型传入,表示从某个特定起始时间(如1970年1月1日00:00:00 UTC)到目标时间所经过的秒数, + * 这个时间戳作为要进行格式化转换的原始数据,由外部传入该方法,用于生成对应的格式化后的时间字符串。 + * @return 格式化后的时间字符串,返回的字符串按照上述不同时间关系的判断规则进行格式化,例如如果是今天则返回类似 "10:30"(表示10点30分)这样的格式, + * 如果是昨天则返回 "昨天",如果是本周内其他时间则返回类似 "周二" 这样的格式,依此类推,方便直观展示时间信息,供调用方用于界面显示等用途。 */ public static String formatTimestamp(long timestampInSeconds) { - // 将秒转换为毫秒 + // 将秒转换为毫秒,由于 `DateTime` 构造函数通常需要以毫秒为单位的时间值来创建对应的时间对象, + // 所以将传入的以秒为单位的时间戳(`timestampInSeconds`)乘以1000,将其转换为毫秒,然后通过 `DateTime` 的构造函数创建一个表示对应时间的 `DateTime` 对象, + // 方便后续基于这个对象进行时间相关的判断和格式化操作,存储在 `dateTime` 变量中。 DateTime dateTime = new DateTime(timestampInSeconds * 1000); DateTime now = DateUtil.date(); - // 如果是今天 + // 如果是今天,通过调用 `DateUtil.isSameDay` 方法,传入创建的时间对象(`dateTime`)和表示当前时间的 `now` 对象作为参数, + // 判断 `dateTime` 所代表的时间是否与当前时间是同一天,如果是,则使用 `DateUtil.format` 方法按照 "H:mm"(表示小时:分钟)的格式对 `dateTime` 进行格式化, + // 例如将对应今天的某个时间戳格式化为类似 "14:20" 的字符串形式并返回,这种格式适合在当天内展示具体时间点的场景,符合日常使用习惯。 if (DateUtil.isSameDay(dateTime, now)) { return DateUtil.format(dateTime, "H:mm"); } - // 如果是昨天 + // 如果是昨天,同样通过调用 `DateUtil.isSameDay` 方法,不过这次传入 `dateTime` 和通过 `DateUtil.yesterday` 方法获取的表示昨天时间的对象作为参数, + // 判断 `dateTime` 是否代表昨天的时间,如果是,则直接返回字符串 "昨天",这种简洁的表示方式在很多业务场景下(如消息列表中显示消息发送时间等)方便用户快速知晓时间的大致范围。 if (DateUtil.isSameDay(dateTime, DateUtil.yesterday())) { return "昨天"; } - // 如果是本周(而不是昨天) + // 如果是本周(而不是昨天),首先通过 `dateTime.isAfterOrEquals` 方法判断 `dateTime` 所代表的时间是否在本周的起始时间之后(或等于本周起始时间), + // 本周起始时间通过 `DateUtil.beginOfWeek(now)` 方法获取(这里的 `now` 是当前时间对象),然后再通过 `DateUtil.isSameWeek` 方法判断 `dateTime` 是否与当前时间在同一周内(第三个参数 `true` 可能表示按照某种特定的周计算规则,比如包含周一开始到周日结束等情况), + // 如果满足这两个条件,说明 `dateTime` 代表的时间是本周内除昨天之外的其他时间,此时通过 `DateUtil.dayOfWeekEnum(dateTime).toChinese("周")` 方法, + // 先获取 `dateTime` 对应的是星期几(以枚举形式),再将其转换为中文表示并加上 "周" 字,例如格式化为 "周三" 这样的字符串返回,便于直观展示本周内时间信息。 if (dateTime.isAfterOrEquals(DateUtil.beginOfWeek(now)) && DateUtil.isSameWeek(dateTime, now, true)) { return DateUtil.dayOfWeekEnum(dateTime).toChinese("周"); } - // 如果不是本年 - if (dateTime.year() != now.year()) { + // 如果不是本年,通过比较 `dateTime` 对象的年份(通过 `year` 方法获取)与当前时间 `now` 的年份是否不同, + // 如果不同,说明 `dateTime` 代表的时间不是本年的时间,此时使用 `DateUtil.format` 方法按照 "yy年M月d日"(表示两位年份、月份、日)的格式对 `dateTime` 进行格式化, + if (dateTime.year()!= now.year()) { return DateUtil.format(dateTime, "yy年M月d日"); } - // 如果都不是,则显示月日 + // 如果都不是,则显示月日,若前面所有的时间关系判断都不满足(即既不是今天、昨天、本周内,也不是非本年的情况), + // 说明 `dateTime` 代表的时间是本年但不是本周内的其他时间,此时使用 `DateUtil.format` 方法按照 "M月d日"(表示月份、日)的格式对 `dateTime` 进行格式化, + // 例如格式化为 "7月15日" 这样的字符串返回,以简洁的方式展示本年其他时间的日期信息。 return DateUtil.format(dateTime, "M月d日"); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/DirUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/DirUtil.java index 8f8365e..6e6c58d 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/DirUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/DirUtil.java @@ -3,7 +3,9 @@ package com.xcs.wx.util; import java.nio.file.FileSystems; /** - * DirUtil + * `DirUtil` 工具类,主要用于处理与目录相关的操作,例如获取各种特定用途的目录路径、动态组装目录路径等功能。 + * 通过定义一些常量表示常见的目录名称以及利用系统相关属性和文件分隔符,方便地生成符合业务需求的文件目录路径字符串, + * 为项目中涉及文件存储、配置管理等操作提供了统一的目录路径获取方式,有助于提高代码的可维护性和可读性。 * * @author 林雷 * @date 2024年6月27日17:26:43 @@ -11,59 +13,72 @@ import java.nio.file.FileSystems; public class DirUtil { /** - * 获得工作目录 + * 获得工作目录,通过调用 `System.getProperty("user.dir")` 获取当前用户的工作目录, + * 这个目录通常是项目运行时所在的基础目录,后续很多目录路径的构建会基于这个目录展开,将其存储为一个不可变的静态常量字符串,方便在类中多次使用。 */ private static final String USER_DIR = System.getProperty("user.dir"); /** - * 文件分隔符 + * 文件分隔符,通过调用 `FileSystems.getDefault().getSeparator()` 获取当前操作系统默认的文件分隔符(在Windows系统下通常是 `\`,在Linux等系统下通常是 `/`), + * 将其存储为静态常量字符串,用于在拼接目录路径时正确分隔不同层级的文件夹名称,确保生成的目录路径在不同操作系统下都能正确使用。 */ private static final String SEPARATOR = FileSystems.getDefault().getSeparator(); /** - * 数据目录 + * 数据目录,定义一个表示数据目录名称的静态常量字符串 `DATA`,用于在构建与数据相关的更具体目录路径时作为其中的一部分, + * 例如数据库目录、图片目录等可能都位于这个数据目录之下,具体含义和使用场景取决于项目中对文件目录结构的整体规划。 */ private static final String DATA = "data"; /** - * 文件导出目录 + * 文件导出目录,定义一个表示文件导出目录名称的静态常量字符串 `EXPORT`,用于获取专门用于导出文件的目录路径, + * 在项目中有文件导出功能时,通过这个常量可以方便地定位到对应的导出目录,以便正确保存导出的文件。 */ private static final String EXPORT = "export"; /** - * 数据库目录 + * 数据库目录,定义一个表示数据库目录名称的静态常量字符串 `DB`,用于构建与数据库相关的目录路径, + * 比如存储不同用户对应的数据库文件所在的目录等情况,方便对数据库文件进行统一的管理和访问。 */ private static final String DB = "db"; /** - * 图片目录 + * 图片目录,定义一个表示图片目录名称的静态常量字符串 `IMG`,用于获取存放图片文件的目录路径, + * 在项目中有图片存储需求(如保存用户头像、聊天记录中的图片等)时,通过这个常量可以准确找到对应的图片存储目录。 */ private static final String IMG = "img"; /** - * user.config + * `user.config`,定义一个表示用户配置文件名称的静态常量字符串 `USER_CONFIG`,用于构建用户配置文件所在的目录路径, + * 方便在需要读取或写入用户配置信息时能够准确定位到对应的配置文件。 */ - private static final String USER_CONFIG = "User.config"; + private static final String USER_CONFIG = "user.config"; /** - * SwitchUser.config + * `SwitchUser.config`,定义一个表示用户切换配置文件名称的静态常量字符串 `SWITCH_USER_CONFIG`,用于获取与用户切换相关配置文件所在的目录路径, + * 在涉及多用户切换功能且有对应配置保存需求的场景下,可以通过这个常量准确找到相关配置文件的存放位置。 */ private static final String SWITCH_USER_CONFIG = "SwitchUser.config"; - + // 将构造函数私有化,防止外部实例化该工具类,因为这个类主要提供静态方法来获取目录路径,不需要创建实例对象,遵循工具类的设计原则。 private DirUtil() { } /** - * 动态组装目录 + * 动态组装目录的方法,该方法可以根据传入的多个文件夹名称参数,按照操作系统的文件分隔符规则,将它们拼接成一个完整的目录路径字符串。 + * 适用于根据不同业务场景灵活组装各种自定义的目录路径,提高目录路径构建的灵活性和通用性。 * - * @param dirs 文件夹名称 - * @return 路径 + * @param dirs 文件夹名称,以可变参数形式传入,表示要拼接成目录路径的各个文件夹名称字符串, + * 可以传入任意数量的文件夹名称,方法会按照顺序依次将它们拼接起来,并在中间插入正确的文件分隔符。 + * @return 路径,返回拼接好的目录路径字符串,这个字符串可以直接用于文件操作(如文件读取、写入等)中作为文件的路径参数, + * 确保生成的路径符合当前操作系统的格式要求,方便定位到对应的目录位置。 */ public static String getDir(String... dirs) { - // 拼接路径 + // 拼接路径,创建一个 `StringBuilder` 对象 `sb`,用于高效地拼接目录路径字符串,避免频繁创建新的字符串对象带来的性能开销, + // 后续通过不断追加文件夹名称和文件分隔符来构建完整的目录路径。 StringBuilder sb = new StringBuilder(); - // 遍历 + // 遍历,通过循环遍历传入的文件夹名称数组 `dirs`,将每个文件夹名称依次追加到 `sb` 对象中, + // 并且在除了最后一个文件夹名称之外的每个名称后面添加文件分隔符,以正确构建符合格式要求的目录路径。 for (int i = 0; i < dirs.length; i++) { sb.append(dirs[i]); if ((i + 1) < dirs.length) { @@ -74,67 +89,97 @@ public class DirUtil { } /** - * 获取图片目录 + * 获取图片目录的方法,用于获取特定微信用户(通过 `wxId` 标识)对应的图片目录路径。 + * 该路径是基于项目的工作目录、数据目录、数据库目录以及用户 `wxId` 和图片目录名称等信息拼接而成, + * 方便在需要访问该用户的图片文件时能够准确定位到对应的目录位置。 * - * @param wxId wxId - * @return dir + * @param wxId wxId,以字符串形式传入,表示微信用户的唯一标识,通过这个标识来区分不同用户的图片目录, + * 确保为每个用户都能准确构建出其专属的图片存储目录路径,在多用户场景下正确管理图片文件。 + * @return dir,返回构建好的图片目录路径字符串,这个字符串可以作为参数用于图片文件的相关操作(如图片保存、读取等), + * 调用方可以根据返回的路径准确找到对应用户的图片存储目录。 */ public static String getImgDir(String wxId) { return USER_DIR + SEPARATOR + DATA + SEPARATOR + DB + SEPARATOR + wxId + SEPARATOR + IMG; } /** - * 获取图片目录 + * 获取图片目录(带文件名)的方法,在获取特定微信用户(通过 `wxId` 标识)对应图片目录路径的基础上, + * 进一步添加了具体的文件名,用于获取某个用户图片目录下特定文件的完整路径,方便对单个图片文件进行精确的操作(如删除、获取文件信息等)。 * - * @param wxId wxId - * @return dir + * @param wxId wxId,以字符串形式传入,表示微信用户的唯一标识,用于定位到对应的用户图片目录,与前面的 `getImgDir` 方法作用类似,先确定用户对应的图片目录位置。 + * @param fileName 文件名,以字符串形式传入,表示要操作的具体图片文件的名称,通过将其与用户图片目录路径拼接,得到该文件的完整路径, + * 确保能够准确指向特定用户图片目录下的具体文件,满足不同业务场景下对单个图片文件操作的需求。 + * @return dir,返回构建好的包含文件名的图片目录路径字符串,这个字符串可以直接用于针对特定图片文件的操作, + * 调用方根据返回的路径能够精准定位到对应的图片文件,实现诸如读取图片内容、修改图片属性等操作。 */ public static String getImgDirWithName(String wxId, String fileName) { return USER_DIR + SEPARATOR + DATA + SEPARATOR + DB + SEPARATOR + wxId + SEPARATOR + IMG + SEPARATOR + fileName; } /** - * 获取用户切换配置目录 + * 获取用户切换配置目录的方法,用于获取存放用户切换相关配置文件的目录路径, + * 该路径是基于项目的工作目录、数据目录以及用户切换配置文件名称等信息拼接而成, + * 方便在需要读取或写入用户切换配置信息时能够准确找到对应的目录位置,确保对用户切换配置文件进行正确的管理和操作。 * - * @return 目录 + * @return 目录,返回构建好的用户切换配置目录路径字符串,这个字符串可以作为参数用于用户切换配置文件的相关操作(如配置文件读取、更新等), + * 调用方可以根据返回的路径准确找到对应的配置文件所在目录,进而进行相应的配置管理操作。 */ public static String getSwitchUserDir() { return USER_DIR + SEPARATOR + DATA + SEPARATOR + SWITCH_USER_CONFIG; } /** - * 获取用户配置目录 + * 获取用户配置目录的方法,用于获取特定微信用户(通过 `wxId` 标识)对应的用户配置文件所在的目录路径, + * 该路径是基于项目的工作目录、数据目录、数据库目录、用户 `wxId` 以及用户配置文件名称等信息拼接而成, + * 方便在需要读取或更新该用户的配置信息时能够准确找到对应的配置文件位置,实现对不同用户配置文件的有效管理。 * - * @return 目录 + * @param wxId wxId,以字符串形式传入,表示微信用户的唯一标识,用于定位到对应的用户配置文件所在目录,确保为每个用户都能准确构建出其专属的配置文件存储目录路径, + * 满足多用户场景下不同用户配置管理的需求。 + * @return 目录,返回构建好的用户配置目录路径字符串,这个字符串可以作为参数用于用户配置文件的相关操作(如配置文件读取、修改等), + * 调用方可以根据返回的路径准确找到对应用户的配置文件所在目录,进而进行相应的配置信息管理操作。 */ public static String getUserDir(String wxId) { return USER_DIR + SEPARATOR + DATA + SEPARATOR + DB + SEPARATOR + wxId + SEPARATOR + USER_CONFIG; } /** - * 获取数据库目录 + * 获取数据库目录的方法,用于获取项目中数据库文件所在的基础目录路径, + * 该路径是基于项目的工作目录和数据目录以及数据库目录名称等信息拼接而成, + * 方便在需要操作数据库文件(如数据库备份、恢复等)时能够准确找到对应的目录位置,对数据库相关文件进行统一的管理和操作。 * - * @return 目录 + * @return 目录,返回构建好的数据库目录路径字符串,这个字符串可以作为参数用于数据库文件的相关操作(如数据库文件查找、复制等), + * 调用方可以根据返回的路径准确找到数据库文件所在的目录,进而进行相应的数据库管理操作。 */ public static String getDbDir() { return USER_DIR + SEPARATOR + DATA + SEPARATOR + DB; } /** - * 获取数据库目录 + * 获取数据库目录(针对特定用户)的方法,用于获取特定微信用户(通过 `wxId` 标识)对应的数据库文件所在的目录路径, + * 该路径是基于项目的工作目录、数据目录、数据库目录以及用户 `wxId` 等信息拼接而成, + * 方便在需要针对特定用户的数据库文件进行操作(如查询该用户相关的数据记录、备份该用户的数据库等)时能够准确找到对应的目录位置, + * 实现对不同用户数据库文件的分别管理和操作。 * - * @return 目录 + * @param wxId wxId,以字符串形式传入,表示微信用户的唯一标识,用于定位到对应的用户数据库文件所在目录,确保为每个用户都能准确构建出其专属的数据库文件存储目录路径, + * 满足多用户场景下不同用户数据库管理的需求。 + * @return 目录,返回构建好的针对特定用户的数据库目录路径字符串,这个字符串可以作为参数用于该用户数据库文件的相关操作(如数据库文件读取、写入等), + * 调用方可以根据返回的路径准确找到对应用户的数据库文件所在目录,进而进行相应的数据库操作。 */ public static String getDbDir(String wxId) { return USER_DIR + SEPARATOR + DATA + SEPARATOR + DB + SEPARATOR + wxId + SEPARATOR; } /** - * 获取导出目录 + * 获取导出目录(带文件名)的方法,用于获取项目中文件导出的目标目录路径,并添加了具体的文件名, + * 该路径是基于项目的工作目录、数据目录以及文件导出目录名称等信息拼接而成,再结合传入的文件名, + * 可以准确得到导出文件的完整路径,方便在文件导出功能中正确保存导出的文件,确保文件被保存到指定的导出目录位置。 * - * @return 目录 + * @param fileName 文件名,以字符串形式传入,表示要导出的文件的具体名称,通过将其与文件导出目录路径拼接,得到该文件的完整导出路径, + * 确保导出的文件能够准确存放在指定的导出目录下,满足不同业务场景下文件导出的需求。 + * @return 目录,返回构建好的包含文件名的文件导出目录路径字符串,这个字符串可以作为参数用于文件导出操作(如创建文件、写入文件内容等), + * 调用方根据返回的路径能够精准定位到导出文件的保存位置,实现文件的正确导出和存储。 */ public static String getExportDir(String fileName) { return USER_DIR + SEPARATOR + DATA + SEPARATOR + EXPORT + SEPARATOR + fileName; } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/ImgDecoderUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/ImgDecoderUtil.java index 5507842..8331c0b 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/ImgDecoderUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/ImgDecoderUtil.java @@ -10,7 +10,9 @@ import java.nio.file.Files; import java.nio.file.Paths; /** - * 图片解密工具类 + * `ImgDecoderUtil` 图片解密工具类,主要用于对特定格式(`.dat` 文件,推测其加密存储了图片数据)的文件进行解密,并转换为常见的图片格式(如 `JPEG`、`PNG`、`GIF` 等)进行保存。 + * 通过分析文件头部信息来确定文件类型和解密密钥,然后对文件内容进行逐字节异或操作实现解密,最后将解密后的数据保存为对应的图片文件,方便后续查看和使用图片内容, + * 在涉及图片数据加密存储且需要解密还原的业务场景中发挥作用。 * * @author xcs * @date 2023年12月28日 15时44分 @@ -19,36 +21,47 @@ import java.nio.file.Paths; public class ImgDecoderUtil { /** - * 图片文件头信息定义:JPEG, PNG, GIF + * 图片文件头信息定义:JPEG, PNG, GIF,定义一个静态的整型数组 `PIC_HEAD`,存储了常见图片格式(`JPEG`、`PNG`、`GIF`)文件头部的部分特征字节信息, + * 用于后续通过比对 `.dat` 文件头部字节来确定其对应的图片文件类型以及解密密钥,这些特征字节是识别不同图片格式的关键依据之一,按照顺序依次排列在数组中,方便遍历比对操作。 */ private static final int[] PIC_HEAD = {0xff, 0xd8, 0x89, 0x50, 0x47, 0x49}; /** - * 解密.dat文件并将其转换为图片。 - * 根据文件类型将输出文件保存为相应的图片格式。 + * 解密 `.dat` 文件并将其转换为图片的方法。根据文件类型将输出文件保存为相应的图片格式。 + * 该方法实现了从读取输入文件、确定文件类型和解密密钥、构造输出文件路径,到对文件内容进行解密并保存为图片文件的完整流程, + * 是整个工具类的核心方法,用于对外提供 `.dat` 文件到图片文件的解密转换功能。 * - * @param filePath 输入文件的路径。 - * @param outPath 输出文件的目标路径。 + * @param filePath 输入文件的路径,以字符串形式传入,表示需要解密的 `.dat` 文件在磁盘上的存储位置, + * 这个路径作为方法操作的数据源,用于读取文件内容进行解密处理,必须是合法有效的文件路径,否则可能导致文件读取失败等问题。 + * @param outPath 输出文件的目标路径,以字符串形式传入,表示解密后生成的图片文件要保存的目标目录路径, + * 方法会根据这个路径以及确定的图片文件名构造完整的输出文件路径,确保解密后的图片能够正确保存到指定位置,方便后续查找和使用。 + * @return 返回解密后生成的图片文件的完整路径,如果在解密过程中出现文件不存在、文件类型不支持、读取或写入文件失败等异常情况,则返回 `null`, + * 调用方可以根据返回值判断是否成功解密并获取到对应的图片文件路径,进而进行后续操作(如展示图片、进一步处理图片等)。 */ public static String decodeDat(String filePath, String outPath) { try { - // 创建文件对象并检查文件是否存在 + // 创建文件对象并检查文件是否存在,通过传入的文件路径(`filePath`)创建一个 `File` 类型的对象 `file`, + // 用于后续对文件的存在性判断以及其他相关操作,接着调用 `exists` 方法检查文件是否实际存在于磁盘上,如果不存在则直接返回 `null`,表示无法进行解密操作,方法结束。 File file = new File(filePath); if (!file.exists()) { return null; } - // 获取文件的解密代码 + // 获取文件的解密代码,调用 `getCode` 方法,传入文件路径(`filePath`)作为参数,该方法会读取文件头部信息来分析并确定文件类型以及对应的解密密钥, + // 返回一个包含两个元素的整型数组,第一个元素表示文件类型(对应 `PIC_HEAD` 数组中的索引位置,不同索引代表不同图片格式),第二个元素就是解密密钥, + // 将返回的数组存储在 `codeResult` 变量中,并从中提取文件类型(`fileType`)和解密密钥(`decodeCode`)分别赋值给对应的变量,方便后续操作使用。 int[] codeResult = getCode(filePath); int fileType = codeResult[0]; int decodeCode = codeResult[1]; - // 如果解密代码为-1,说明文件类型不支持或读取有误,退出方法 + // 如果解密代码为 -1,说明文件类型不支持或读取有误,退出方法,判断 `decodeCode` 是否等于 -1,如果是则表示在获取文件解密代码的过程中出现问题, + // 例如文件头部信息不符合已知的图片格式特征,无法确定有效的解密密钥,此时直接返回 `null`,表示无法完成解密操作,方法结束。 if (decodeCode == -1) { return null; } - // 文件后缀 + // 文件后缀,根据前面获取到的文件类型(`fileType`),通过 `switch` 语句来确定对应的文件后缀名,用于后续构造解密后生成的图片文件的完整文件名, + // 不同的 `fileType` 值对应不同的常见图片格式后缀(如 `1` 对应 `.jpg`、`3` 对应 `.png`、`5` 对应 `.gif` 等),如果 `fileType` 不匹配已知的情况,则默认使用 `.jpg` 作为后缀。 String extension; switch (fileType) { @@ -65,62 +78,85 @@ public class ImgDecoderUtil { extension = ".jpg"; break; } - // 获取文件名(不包含扩展名) + + // 获取文件名(不包含扩展名),调用 `file` 对象(代表输入的 `.dat` 文件)的 `getName` 方法获取文件的原始名称(包含扩展名), + // 然后通过字符串替换操作(将 `.dat` 后缀去除)得到不包含扩展名的文件名,存储在 `fileName` 变量中,用于后续构造新的图片文件名。 String fileName = file.getName(); - // 文件名+后缀 + // 文件名 + 后缀,将前面获取到的不包含扩展名的文件名(`fileName`)与确定的文件后缀(`extension`)进行拼接,得到包含正确后缀的完整图片文件名,存储在 `picName` 变量中, + // 这个文件名就是解密后要保存的图片文件的最终文件名,符合对应的图片格式规范,方便后续保存操作和识别使用。 String picName = fileName.replace(".dat", "") + extension; - // 构造输出文件的完整路径 + // 构造输出文件的完整路径,使用 `Paths.get` 方法,传入输出文件的目标路径(`outPath`)和前面构造好的图片文件名(`picName`)作为参数, + // 按照当前操作系统的文件路径格式规则构造出完整的输出文件路径字符串,存储在 `fileOutPath` 变量中,这个路径就是解密后生成的图片文件要保存的具体位置,后续会基于此路径进行文件写入操作。 String fileOutPath = Paths.get(outPath, picName).toString(); - // 检查输出文件是否已存在 + // 检查输出文件是否已存在,通过创建一个新的 `File` 类型对象(传入构造好的输出文件路径 `fileOutPath`),并调用其 `exists` 方法检查该文件是否已经存在于磁盘上, + // 如果已经存在,则直接返回这个输出文件路径,表示无需重复解密操作,直接可以使用已存在的图片文件,方法结束。 if (new File(fileOutPath).exists()) { return fileOutPath; } - // 读取文件数据 + // 读取文件数据,调用 `Files.readAllBytes` 方法,传入 `Paths.get` 方法构造的输入文件路径(`filePath`)对应的 `Path` 对象作为参数, + // 一次性读取文件的所有字节内容到一个字节数组 `data` 中,方便后续对整个文件内容进行解密操作,这个字节数组包含了 `.dat` 文件中加密存储的图片数据。 byte[] data = Files.readAllBytes(Paths.get(filePath)); - // 创建输出流并写入解密后的数据 + // 创建输出流并写入解密后的数据,通过 `try-with-resources` 语句创建一个 `FileOutputStream` 类型的对象 `fileOut`, + // 传入构造好的输出文件路径(`fileOutPath`)作为参数,用于将解密后的数据写入到对应的文件中,在语句块内通过循环遍历读取到的文件字节数组(`data`), + // 对每个字节进行异或操作(使用前面获取到的解密密钥 `decodeCode`)实现解密,并通过 `fileOut` 的 `write` 方法将解密后的字节逐个写入到输出文件中,完成图片文件的生成操作, + // 如果在写入过程中出现异常(如磁盘空间不足、权限问题等)会自动关闭输出流并抛出异常,由外层的 `try-catch` 块进行捕获处理。 try (FileOutputStream fileOut = new FileOutputStream(fileOutPath)) { for (byte b : data) { - // 对每个字节进行异或操作以解密 fileOut.write(b ^ decodeCode); } } return fileOutPath; } catch (IOException e) { + // 如果在文件读取、解密、写入等操作过程中出现 `IOException` 异常(如文件不存在、无法读取文件、无法写入文件等情况), + // 通过 `log.error` 方法记录错误信息,"decode dat failed" 作为错误描述标识,同时传入捕获到的异常对象 `e`,方便后续查看日志排查问题,然后返回 `null`,表示解密操作失败,方法结束。 log.error("decode dat failed", e); } return null; } /** - * 获取.dat文件的解密代码。 - * 读取文件的前两个字节并与已知的图片头信息比对,以确定文件类型和解密密钥。 + * 获取 `.dat` 文件的解密代码的方法。读取文件的前两个字节并与已知的图片头信息比对,以确定文件类型和解密密钥。 + * 该方法是 `decodeDat` 方法中确定文件如何解密的关键支撑方法,通过分析 `.dat` 文件头部少量字节信息来推断其对应的图片格式以及解密所需的密钥, + * 为后续的解密操作提供必要的数据依据,内部实现了字节读取、比对验证等逻辑来准确获取解密相关信息。 * - * @param filePath 输入文件的路径。 - * @return 返回一个包含文件类型和解密密钥的数组。 + * @param filePath 输入文件的路径,以字符串形式传入,表示需要获取解密代码的 `.dat` 文件在磁盘上的存储位置, + * 方法会基于这个路径尝试读取文件头部字节进行分析,必须是合法有效的文件路径,否则可能导致文件读取失败等问题。 + * @return 返回一个包含文件类型和解密密钥的数组,数组的第一个元素表示文件类型(对应 `PIC_HEAD` 数组中的索引位置,不同索引代表不同图片格式), + * 第二个元素就是确定的解密密钥,如果在读取文件头部信息、比对验证等过程中出现问题(如文件不符合已知图片格式特征、读取字节数不足等情况),则返回 `{-1, -1}` 表示获取失败, + * 供调用它的 `decodeDat` 方法根据返回值判断是否能正常进行解密操作以及后续如何处理。 */ private static int[] getCode(String filePath) { - // 创建一个文件对象 + // 创建一个文件对象,通过传入的文件路径(`filePath`)创建一个 `File` 类型的对象 `file`,用于后续对文件的相关判断(如是否为目录等)以及文件读取操作, + // 这是操作文件的基础步骤,基于这个对象可以进一步与文件系统交互获取文件内容等信息。 File file = new File(filePath); - // 检查文件是否是一个目录 + // 检查文件是否是一个目录,调用 `file` 对象的 `isDirectory` 方法判断传入的文件路径对应的是否是一个目录,如果是目录则不符合要求(期望是一个文件), + // 此时直接返回 `{-1, -1}`,表示无法获取解密代码,方法结束,因为对目录无法进行后续读取字节比对等操作来确定解密信息。 if (file.isDirectory()) { return new int[]{-1, -1}; } try (FileInputStream datFile = new FileInputStream(filePath)) { - // 准备一个字节数组来读取文件的前两个字节 + // 准备一个字节数组来读取文件的前两个字节,创建一个长度为2的字节数组 `datRead`,用于存储从 `.dat` 文件中读取的前两个字节内容, + // 这两个字节是后续分析文件类型和解密密钥的关键数据来源,通过读取它们并与已知的图片头信息进行比对来推断相关解密信息。 byte[] datRead = new byte[2]; - // 读取前两个字节 - if (datFile.read(datRead, 0, 2) != 2) { + // 读取前两个字节,调用 `FileInputStream` 对象(`datFile`)的 `read` 方法,尝试从文件中读取前两个字节到 `datRead` 字节数组中, + // 并传入起始索引0以及要读取的字节数2作为参数,规定读取的范围和数量,如果实际读取的字节数不等于2(可能是文件已损坏、权限不足等原因导致无法完整读取), + // 则直接返回 `{-1, -1}`,表示无法获取有效的解密代码,方法结束。 + if (datFile.read(datRead, 0, 2)!= 2) { return new int[]{-1, -1}; } - // 遍历图片头信息,检查文件类型 + // 遍历图片头信息,检查文件类型,通过循环遍历 `PIC_HEAD` 数组(存储了常见图片格式文件头部特征字节信息),步长为2(因为每次要比对两个字节特征), + // 对于每个循环位置,进行以下操作来确定文件类型和解密密钥:首先计算解密代码(通过将读取到的文件第一个字节与 `PIC_HEAD` 数组中对应位置的字节进行异或操作得到), + // 然后使用这个解密代码对第二个字节进行验证(即将读取到的文件第二个字节与解密代码进行异或操作),最后检查验证后的字节是否匹配 `PIC_HEAD` 数组中对应位置的下一个字节(即比对第二个字节特征是否一致), + // 如果匹配成功,则说明找到了对应的文件类型,此时返回一个包含文件类型(当前循环的索引 `i`)和解密代码(`code`)的整型数组,表示获取到了有效的解密信息,方法结束; + // 如果遍历完整个 `PIC_HEAD` 数组都没有找到匹配的情况,则表示该文件不符合已知的图片格式特征,无法确定解密代码,最后会返回 `{-1, -1}`,表示获取失败。 for (int i = 0; i < PIC_HEAD.length; i += 2) { // 计算解密代码 int code = (datRead[0] & 0xff) ^ PIC_HEAD[i]; @@ -133,14 +169,20 @@ public class ImgDecoderUtil { } } } catch (IOException e) { + // 如果在文件读取等操作过程中出现 `IOException` 异常(如文件不存在、无法读取文件等情况),通过 `log.error` 方法记录错误信息, + // "decode dat getCode failed" 作为错误描述标识,同时传入捕获到的异常对象 `e`,方便后续查看日志排查问题,然后返回 `{-1, -1}`,表示获取解密代码失败,方法结束。 log.error("decode dat getCode failed", e); } - // 如果没有匹配的文件类型,返回错误代码 + // 如果没有匹配的文件类型,返回错误代码,若在前面遍历 `PIC_HEAD` 数组并进行比对验证的过程中都没有找到匹配的文件类型, + // 则执行到此处,返回 `{-1, -1}`,表示无法确定有效的文件类型和解密代码,供调用该方法的地方根据返回值进行相应处理(如判断无法解密文件等情况)。 return new int[]{-1, -1}; } public static void main(String[] args) { + // 这是一个简单的测试入口方法,调用 `decodeDat` 方法,传入示例的输入文件路径(`D:\\xuchengsheng\\output_file1`)和输出文件的目标路径(`D:\\`)作为参数, + // 尝试对指定的 `.dat` 文件进行解密并转换为图片文件保存到指定输出路径下,在实际应用中可以根据具体需求传入不同的真实文件路径来进行测试或者实际的解密操作, + // 不过这里直接使用硬编码的路径只是为了简单演示方法的调用方式,实际使用时可能需要从外部获取更灵活准确的文件路径参数。 decodeDat("D:\\xuchengsheng\\output_file1", "D:\\"); } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/LZ4Util.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/LZ4Util.java index 23a2fde..2096b31 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/LZ4Util.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/LZ4Util.java @@ -2,13 +2,13 @@ package com.xcs.wx.util; import lombok.extern.slf4j.Slf4j; import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream; - import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; /** - * LZ4 解压工具类 + * `LZ4Util` 是一个 `LZ4` 解压工具类,主要用于对经过 `LZ4` 压缩算法压缩后的字节数据进行解压操作,并将解压后的数据转换为字符串形式返回。 + * 在项目中如果存在使用 `LZ4` 算法压缩存储的数据(例如为了节省存储空间或者网络传输带宽等情况),通过这个工具类可以方便地将其还原为原始的文本内容,便于后续的处理和使用。 * * @author xcs * @date 2023年12月31日14:58:01 @@ -16,36 +16,53 @@ import java.nio.charset.StandardCharsets; @Slf4j public class LZ4Util { + // 将构造函数私有化,防止外部实例化该工具类,因为这个类主要提供静态方法来实现解压功能,不需要创建实例对象,遵循工具类的设计原则。 private LZ4Util() { } /** - * 解压 + * 解压的方法,用于对输入的 `LZ4` 压缩后的数据进行解压处理,并将解压得到的数据转换为字符串返回。 + * 该方法内部通过一系列输入流的包装和读取操作,逐步将压缩数据还原为原始的字节数据,再将字节数据按照指定的字符编码转换为字符串,最后进行一些必要的清理操作后返回结果。 * - * @param compressedData 压缩后的数据 - * @return 解压后的字符串 + * @param compressedData 压缩后的数据,以字节数组形式传入,表示经过 `LZ4` 压缩算法压缩后的原始数据, + * 这个字节数组作为解压操作的数据源,由外部获取并传入该方法,例如可能是从文件读取、网络接收等途径获取到的压缩数据,方法会对其进行解压处理。 + * @return 解压后的字符串,返回的字符串是将解压后的字节数据按照 `UTF-8` 字符编码转换而成的文本内容,如果在解压过程中出现任何异常情况(如数据格式错误、流读取异常等),则返回 `null`, + * 调用方可以根据返回值判断是否成功解压,并对解压后的字符串进行后续的业务处理(如展示文本、解析数据结构等操作)。 */ public static String decompress(byte[] compressedData) { try (ByteArrayInputStream byteIn = new ByteArrayInputStream(compressedData); BufferedInputStream bufferedIn = new BufferedInputStream(byteIn); BlockLZ4CompressorInputStream lz4In = new BlockLZ4CompressorInputStream(bufferedIn)) { + // 创建一个可变的字符串构建器,用于逐步拼接解压后转换为字符串的各个部分数据,初始为空,随着解压过程的推进,将不断追加解压后的数据块对应的字符串内容, + // 最终形成完整的解压后字符串,相比直接使用普通字符串拼接,`StringBuilder` 可以提高性能,避免频繁创建新的字符串对象带来的开销。 StringBuilder sb = new StringBuilder(); + // 创建一个长度为4096字节的字节数组作为缓冲区,用于从解压输入流中按块读取数据,每次读取的数据会临时存储在这个缓冲区中, + // 然后再将缓冲区中的有效数据转换为字符串进行拼接,选择4096字节大小是一种常见的合理缓冲区设置,既能兼顾读取效率,又不至于占用过多内存。 byte[] buffer = new byte[4096]; int n; - while ((n = lz4In.read(buffer)) != -1) { + // 通过循环不断从 `BlockLZ4CompressorInputStream`(用于读取 `LZ4` 压缩数据并解压的输入流)中读取数据块到缓冲区 `buffer` 中, + // 只要读取到的字节数 `n` 不等于 -1(表示还有数据可读),就持续进行循环操作,对每次读取到的数据块进行处理,直到读取完所有的压缩数据并解压完成。 + while ((n = lz4In.read(buffer))!= -1) { + // 将从缓冲区 `buffer` 中读取到的有效字节数据(长度为 `n`)按照 `UTF-8` 字符编码转换为字符串, + // `UTF-8` 是一种广泛使用的通用字符编码方式,适用于处理各种文本内容,通过 `new String` 构造函数传入字节数组、起始索引(0,表示从数组开头开始转换)、要转换的字节长度(`n`)以及字符编码 `StandardCharsets.UTF_8` 参数来完成转换操作, + // 得到的字符串 `chunk` 就是本次读取解压后的数据块对应的文本内容,将其追加到 `sb`(`StringBuilder` 对象)中,逐步构建完整的解压后字符串。 String chunk = new String(buffer, 0, n, StandardCharsets.UTF_8); sb.append(chunk); } - // 删除最后一个字符 + // 删除最后一个字符,判断 `sb`(`StringBuilder` 对象)的长度是否大于0,如果大于0说明有数据存在, + // 由于某些原因(可能是压缩数据格式或者解压逻辑相关导致最后多了一个多余的字符等情况),在这里进行一个清理操作,调用 `deleteCharAt` 方法删除 `sb` 中最后一个字符, + // 确保返回的字符串符合预期的内容格式要求,然后将处理后的 `sb` 转换为字符串并返回,得到最终的解压后字符串内容。 if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } catch (Exception e) { + // 如果在创建输入流、读取解压数据、转换字符串或者其他相关操作过程中出现了任何异常(如 `IOException` 等各种异常情况), + // 通过 `log.error` 方法记录错误信息,"LZ4解压数据失败" 作为错误描述标识,同时传入捕获到的异常对象 `e`,方便后续查看日志排查问题,然后返回 `null`,表示解压操作失败,方法结束。 log.error("LZ4解压数据失败", e); } return null; } -} +} \ No newline at end of file diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/Pbkdf2HmacUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/Pbkdf2HmacUtil.java index c09f801..dc14b71 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/Pbkdf2HmacUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/Pbkdf2HmacUtil.java @@ -13,12 +13,13 @@ import java.util.Arrays; * @date 2023年12月25日 09时35分 **/ public class Pbkdf2HmacUtil { - + // 将构造函数私有化,防止外部实例化该工具类,因为这个类主要提供静态方法来实现算法相关功能,不需要创建实例对象,遵循工具类的设计原则。 private Pbkdf2HmacUtil() { } /** - * 算法 + * 算法,定义一个静态的字符串常量 `ALGORITHM`,指定了所使用的加密算法为 `HmacSHA1`, + * 这是一种基于哈希的消息认证码(HMAC)算法结合安全哈希算法 `SHA1` 的加密方式,在整个工具类的密钥派生和验证等操作中作为核心的算法依据,确保加密过程的一致性和安全性。 */ private static final String ALGORITHM = "HmacSHA1"; @@ -33,45 +34,60 @@ public class Pbkdf2HmacUtil { * @throws NoSuchAlgorithmException InvalidKeyException 抛出异常 */ public static byte[] pbkdf2Hmac(byte[] password, byte[] salt, int iterations, int dkLen) throws NoSuchAlgorithmException, InvalidKeyException { - // 初始化Mac实例 + // 初始化 `Mac` 实例,通过调用 `Mac.getInstance` 方法,传入指定的算法名称(`ALGORITHM`,即 `HmacSHA1`)作为参数, + // 获取一个用于执行 `HMAC` 运算的 `Mac` 实例对象 `mac`,这个对象是后续进行 `HMAC` 相关计算(如生成密钥派生过程中的中间结果等)的核心操作对象,需要确保能够正确初始化。 Mac mac = Mac.getInstance(ALGORITHM); - // 使用密码和算法初始化Mac + // 使用密码和算法初始化 `Mac`,调用 `mac` 对象的 `init` 方法,传入一个通过 `SecretKeySpec` 构造函数创建的密钥规范对象作为参数, + // 这个密钥规范对象使用传入的密码字节数组(`password`)和指定的算法名称(`ALGORITHM`)进行初始化,表示将以这个密码作为密钥,按照指定的算法进行后续的 `HMAC` 运算, + // 如果密码格式不符合算法要求等情况可能会在此处抛出 `InvalidKeyException` 异常,需要确保密码的合法性和与算法的兼容性。 mac.init(new SecretKeySpec(password, ALGORITHM)); - // 用于存储最终结果的数组 + // 用于存储最终结果的数组,创建一个长度为 `dkLen`(期望生成的密钥长度)的字节数组 `result`,用于逐步存储在密钥派生过程中计算得到的最终密钥数据, + // 初始时数组中的元素为默认值(全0字节等情况),随着计算的推进,会将各个派生块计算后符合要求的字节填充到这个数组中,最终形成完整的派生密钥。 byte[] result = new byte[dkLen]; - // 用于存储盐值和计数器的数组 + // 用于存储盐值和计数器的数组,创建一个长度为 `salt.length + 4` 的字节数组 `block`,用于在每次派生块计算过程中临时存储盐值以及一个4字节的计数器信息, + // 这个数组在后续的计算中起到关键作用,会根据不同的迭代和派生块情况进行数据更新和参与运算,通过组合盐值和计数器来生成不同的中间输入数据用于 `HMAC` 运算。 byte[] block = new byte[salt.length + 4]; - // 将盐值复制到block数组 + // 将盐值复制到 `block` 数组,通过调用 `System.arraycopy` 方法,将传入的盐值字节数组(`salt`)中的数据从起始索引0开始,复制到 `block` 数组的起始索引0位置, + // 复制的长度为盐值字节数组的长度(`salt.length`),确保 `block` 数组中先存储了正确的盐值信息,为后续添加计数器并进行 `HMAC` 运算做准备。 System.arraycopy(salt, 0, block, 0, salt.length); - // 主循环,对每个派生块进行处理 + // 主循环,对每个派生块进行处理,通过 `for` 循环,从1开始,循环次数根据期望生成的密钥长度(`dkLen`)和每次 `HMAC` 运算结果的长度(`mac.getMacLength`)来确定, + // 目的是按照 `PBKDF2` 算法要求,对每个需要生成的派生块依次进行计算处理,每次循环代表处理一个派生块,在循环内部完成添加计数器、多次 `HMAC` 运算以及结果累加等操作,逐步构建最终的派生密钥。 for (int i = 1; i <= (dkLen + mac.getMacLength() - 1) / mac.getMacLength(); i++) { - // 在block数组的盐值后面添加计数器 + // 在 `block` 数组的盐值后面添加计数器,通过对循环变量 `i` 进行位运算,将其分解为4个字节,并依次存储到 `block` 数组中盐值之后的位置(从 `salt.length` 索引开始), + // 这样每个派生块都有一个唯一的计数器值与之关联,用于区分不同的派生块,在后续的 `HMAC` 运算中作为变化的输入数据,增加了派生密钥的随机性和安全性。 block[salt.length] = (byte) (i >>> 24); block[salt.length + 1] = (byte) (i >>> 16); block[salt.length + 2] = (byte) (i >>> 8); block[salt.length + 3] = (byte) i; - // 计算第一次迭代的结果U + // 计算第一次迭代的结果 `U`,调用 `mac` 对象的 `doFinal` 方法,传入 `block` 数组作为参数,对添加了计数器的 `block` 数组数据进行一次 `HMAC` 运算, + // 得到的结果字节数组 `u` 就是本次派生块第一次迭代的中间结果,这个结果后续会在多次迭代中不断更新和参与运算,是构建派生密钥的重要中间数据。 byte[] u = mac.doFinal(block); - // T数组,用于存储异或结果 + // `T` 数组,用于存储异或结果,通过调用 `u.clone` 方法创建一个与 `u` 数组内容完全相同的新字节数组 `t`, + // 这个 `t` 数组用于在后续的多次迭代中存储每次 `HMAC` 运算结果与之前结果进行异或操作后的累计值,最终会将 `t` 数组中的部分内容复制到最终结果数组 `result` 中,形成派生密钥的一部分。 byte[] t = u.clone(); - // 内循环,进行额外的迭代以增加安全性 + // 内循环,进行额外的迭代以增加安全性,通过 `for` 循环,从1开始(因为第一次迭代已经在前面完成了),循环次数为指定的迭代次数减1(`iterations - 1`), + // 在每次循环中对中间结果 `U` 再次进行 `HMAC` 运算,并将新的运算结果与之前的累计结果 `T` 进行异或操作,不断累加迭代结果,以此增加派生密钥的安全性,使得最终生成的密钥更难被破解。 for (int j = 1; j < iterations; j++) { - // 对U再次进行HMAC运算 + // 对 `U` 再次进行 `HMAC` 运算,再次调用 `mac` 对象的 `doFinal` 方法,传入上一次迭代得到的 `U` 数组(`u`)作为参数, + // 对其进行又一次 `HMAC` 运算,得到新的中间结果字节数组,覆盖原来的 `u` 数组内容,用于后续与累计结果 `T` 的异或操作以及下一次迭代。 u = mac.doFinal(u); - // 将结果U与T进行异或,累加迭代结果 + // 将结果 `U` 与 `T` 进行异或,累加迭代结果,通过循环遍历 `t` 数组(累计结果数组)的每个元素,将其与对应的 `u` 数组(本次 `HMAC` 运算结果数组)中的元素进行异或操作, + // 使用 `^=` 运算符实现异或并赋值,将异或后的结果更新到 `t` 数组中,完成一次迭代结果的累加操作,使得 `t` 数组不断积累每次迭代的异或结果,增强了派生密钥的安全性。 for (int k = 0; k < t.length; k++) { t[k] ^= u[k]; } } - // 将T的内容复制到最终结果数组中 + // 将 `T` 的内容复制到最终结果数组中,通过调用 `System.arraycopy` 方法,将 `t` 数组中的数据从起始索引0开始,复制到最终结果数组 `result` 中, + // 复制的起始位置根据当前处理的派生块索引(`i - 1`)以及每次 `HMAC` 运算结果的长度(`mac.getMacLength`)来确定,复制的长度取 `t` 数组长度和剩余需要填充到 `result` 数组的长度(根据 `dkLen` 和当前派生块索引计算)中的较小值, + // 以此将每个派生块计算得到的有效结果逐步填充到最终结果数组 `result` 中,最终形成完整的派生密钥。 System.arraycopy(t, 0, result, (i - 1) * mac.getMacLength(), Math.min(t.length, dkLen - (i - 1) * mac.getMacLength())); } - // 返回最终的密钥 + // 返回最终的密钥,在完成所有派生块的计算和结果填充后,将包含完整派生密钥的 `result` 字节数组返回,供调用方用于后续的加密、解密或者其他安全相关操作。 return result; } @@ -86,7 +102,8 @@ public class Pbkdf2HmacUtil { * @throws Exception 抛出异常 */ public static boolean checkKey(byte[] byteKey, byte[] macSalt, byte[] hashMac, byte[] message) throws Exception { - // 使用PBKDF2算法生成MAC密钥 + // 使用 `PBKDF2` 算法生成 `MAC` 密钥,调用 `pbkdf2Hmac` 方法,传入待验证的密钥字节数组(`byteKey`)、`MAC` 盐值(`macSalt`)、迭代次数2以及生成的密钥长度32作为参数, + // 按照 `PBKDF2` 密钥派生算法重新生成一个用于 `MAC` 计算的密钥字节数组 `macKey`,这个密钥将用于后续初始化 `Mac` 实例进行 `MAC` 运算,确保在验证过程中使用与预期一致的密钥生成方式。 byte[] macKey = pbkdf2Hmac(byteKey, macSalt, 2, 32); Mac mac = Mac.getInstance(ALGORITHM); SecretKeySpec keySpec = new SecretKeySpec(macKey, ALGORITHM); diff --git a/wx-dump-admin/src/main/java/com/xcs/wx/util/XmlUtil.java b/wx-dump-admin/src/main/java/com/xcs/wx/util/XmlUtil.java index bac49d8..48363de 100644 --- a/wx-dump-admin/src/main/java/com/xcs/wx/util/XmlUtil.java +++ b/wx-dump-admin/src/main/java/com/xcs/wx/util/XmlUtil.java @@ -5,7 +5,8 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import lombok.extern.slf4j.Slf4j; /** - * xml解析工具类 + * `XmlUtil` 是一个 `xml` 解析工具类,主要借助 `Jackson` 库中的 `XmlMapper` 来实现将 `XML` 格式的字符串解析为指定的 `Java` 类对象的功能。 + * 在项目中如果需要处理接收到的 `XML` 数据(例如与外部系统进行数据交互,对方以 `XML` 格式传递信息等场景),通过这个工具类可以方便地将其转换为便于在 `Java` 程序中操作的对象形式,利于后续的业务逻辑处理。 * * @author xcs * @date 2024年01月24日 14时14分 @@ -13,29 +14,45 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class XmlUtil { + // 创建一个静态的 `XmlMapper` 实例对象 `MAPPER`,用于后续进行 `XML` 到 `Java` 对象的转换操作。 + // `XmlMapper` 是 `Jackson` 库中专门用于处理 `XML` 格式数据的类,它提供了各种方法来解析 `XML` 以及将 `Java` 对象序列化为 `XML` 格式等功能,在这里作为核心工具进行 `XML` 解析工作。 private static final XmlMapper MAPPER = new XmlMapper(); + // 将构造函数私有化,防止外部实例化该工具类,因为这个类主要提供静态方法来实现 `XML` 解析功能,不需要创建实例对象,遵循工具类的设计原则。 private XmlUtil() { } /** - * 解析 XML + * 解析 `XML` 的方法,用于将输入的 `XML` 格式的字符串内容解析为指定类型的 `Java` 类对象。 + * 该方法首先会对输入的字符串进行一些预处理(判断并截取包含 ` 泛型参数 - * @return T + * @param content 被解析的内容,以字符串形式传入,表示需要解析的 `XML` 格式的数据内容,这个字符串应该是符合 `XML` 语法规范的文本, + * 可以是从文件读取、网络接收或者其他途径获取到的 `XML` 数据,由外部传入该方法进行解析处理,若不符合 `XML` 格式要求可能导致解析失败。 + * @param valueType 被解析的类对象,以 `Java` 的 `Class` 类型传入,明确指定了要将 `XML` 数据解析成的目标 `Java` 类, + * 这个类应该具有与 `XML` 数据结构相对应的属性(例如通过 `Jackson` 库支持的注解进行了正确的映射配置等情况),以便 `XmlMapper` 能够准确地将 `XML` 中的元素和属性值填充到对应的类属性中,完成解析转换操作。 + * @param 泛型参数,用于表示返回对象的类型与传入的 `valueType` 参数指定的类型一致,使得方法在返回解析后的对象时能够保证类型的准确性,方便调用方直接使用解析得到的对象进行后续业务操作,无需进行额外的类型转换。 + * @return T,返回解析后的对象,其类型为传入的 `valueType` 参数所指定的 `Java` 类类型,如果在解析过程中出现 `JsonProcessingException`(因为 `Jackson` 在处理 `XML` 解析时底层可能会涉及到类似 `JSON` 处理的相关逻辑,所以会抛出此类异常)等异常情况, + * 则通过日志记录错误信息("parse xml failed" 作为错误描述标识,同时记录具体的异常对象方便排查问题),并返回 `null`,调用方可以根据返回值判断是否成功解析,并对解析后的对象进行后续的业务处理(如调用对象的方法、获取对象属性值等操作)。 */ public static T parseXml(String content, Class valueType) { try { + // 查找输入字符串中 ` 0) { + // 截取包含 `