newreq #5

Merged
pxfml5kje merged 11 commits from 潘妍 into main 1 year ago

@ -1,195 +1,199 @@
<p align="center">
<a href="https://wx.xxccss.com/"><img src="image/logo.png" width="45%"></a>
</p>
<p align="center">
<strong>🍬Java版微信聊天记录备份与管理工具</strong>
</p>
<p align="center">
👉 <a href="https://wx.xxccss.com/">https://wx.xxccss.com/</a> 👈
</p>
<p align="center">
<a href="https://hellogithub.com/repository/5055dcceee434dc5851ac9897cb27396" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=5055dcceee434dc5851ac9897cb27396&claim_uid=AVv4KeNnZs2Ig3a" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
<p align="center">
<a href="https://github.com/xuchengsheng/spring-reading/stargazers"><img src="https://img.shields.io/github/stars/xuchengsheng/wx-dump-4j?logo=github&logoColor=%23EF2D5E&label=Stars&labelColor=%23000000&color=%23EF2D5E&cacheSeconds=3600" alt="Stars Badge"/></a>
<a href="https://github.com/xuchengsheng"><img src="https://img.shields.io/github/followers/xuchengsheng?label=Followers&logo=github&logoColor=%23FC521F&labelColor=%231A2477&color=%23FC521F&cacheSeconds=3600" alt="Follow Badge"></a>
<a href="https://github.com/xuchengsheng/wx-dump-4j/fork"><img src="https://img.shields.io/github/forks/xuchengsheng/wx-dump-4j?label=Forks&logo=github&logoColor=%23F2BB13&labelColor=%23BE2323&color=%23F2BB13" alt="Fork Badge"></a>
<a href="https://github.com/xuchengsheng/wx-dump-4j/watchers"><img src="https://img.shields.io/github/watchers/xuchengsheng/wx-dump-4j?label=Watchers&logo=github&logoColor=%23FF4655&labelColor=%234169E1&color=%23FF4655&cacheSeconds=3600" alt="Watchers Badge"></a>
</p>
<p align="center">
<img src="https://img.shields.io/badge/Java-11%2B-%23437291?logo=openjdk&logoColor=%23437291"/>
<img src="https://img.shields.io/badge/Spring-5.3.10-%23437291?logo=Spring&logoColor=%236DB33F&color=%236DB33F"/>
<img src="https://img.shields.io/badge/SpringBoot-2.5.5-%23437291?logo=SpringBoot&logoColor=%236DB33F&color=%236DB33F"/>
<img src="https://img.shields.io/badge/JNA-5.8.0-%23437291?logo=JNA&logoColor=%23228B22&color=%23228B22"/>
<img src="https://img.shields.io/badge/Hutool-5.8.16-%23437291?logo=JNA&logoColor=%23F08080&color=%23F08080"/>
<img src="https://img.shields.io/badge/easyexcel-5.8.16-%23437291?logo=JNA&logoColor=%23D2691E&color=%23D2691E"/>
<img src="https://img.shields.io/badge/protobuf-3.25.1-%23437291?logo=JNA&logoColor=%23800080&color=%23800080"/>
<img src="https://img.shields.io/badge/mapstruct-1.4.2-%23437291?logo=JNA&logoColor=%23DC143C&color=%23DC143C"/>
<img src="https://img.shields.io/badge/druid-1.2.20-%23437291?logo=JNA&logoColor=%23C71585&color=%23C71585"/>
<img src="https://img.shields.io/badge/mybatisPlus-3.5.4.1-%23437291?logo=JNA&logoColor=%234B0082&color=%234B0082"/>
<img src="https://img.shields.io/badge/sqlite-3.34.0-%23437291?logo=JNA&logoColor=%230000CD&color=%230000CD"/>
<img src="https://img.shields.io/badge/lombok-1.18.20-%23437291?logo=JNA&logoColor=%23008B8B&color=%23008B8B"/>
</p>
-------------------------------------------------------------------------------
## 📚 简介
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)
## 🍱请我吃盒饭?
作者晚上还要写博客✍️,平时还需要工作💼,如果帮到了你可以请作者吃个盒饭🥡
<div>
<img alt="logo" src="image/WeChatPay.png" style="width: 240px;height: 260px">
<img alt="logo" src="image/Alipay.png" style="width: 240px;height: 260px">
</div>
## ⭐️扫码关注微信公众号
关注后,回复关键字📲 **加群**📲,即可加入我们的技术交流群,与更多开发者一起交流学习。
在此我们真诚地邀请您访问我们的GitHub项目页面如果您觉得***wx-dump-4j***对您有帮助,请顺手点个⭐️**Star**⭐️!每一颗星星都是我们前进的动力,是对我们努力的最大肯定。非常感谢您的支持!
<div>
<img alt="logo" src="image/wechat-mp.png" height="180px">>
</div>
## 👀 演示图
<table>
<tr>
<td><img src="image/screenshot/dashboard.png"/></td>
<td><img src="image/screenshot/session.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/contact.png"/></td>
<td><img src="image/screenshot/recover-contact.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/feeds.png"/></td>
<td><img src="image/screenshot/chat.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/chatroom.png"/></td>
<td><img src="image/screenshot/chatroom-detail.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/database.png"/></td>
<td><img src="image/screenshot/database-list.png"/></td>
</tr>
</table>
<p align="center">
<a href="https://wx.xxccss.com/"><img src="image/logo.png" width="45%"></a>
</p>
<p align="center">
<strong>🍬Java版微信聊天记录备份与管理工具</strong>
</p>
<p align="center">
👉 <a href="https://wx.xxccss.com/">https://wx.xxccss.com/</a> 👈
</p>
<p align="center">
<a href="https://hellogithub.com/repository/5055dcceee434dc5851ac9897cb27396" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=5055dcceee434dc5851ac9897cb27396&claim_uid=AVv4KeNnZs2Ig3a" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
<p align="center">
<a href="https://github.com/xuchengsheng/spring-reading/stargazers"><img src="https://img.shields.io/github/stars/xuchengsheng/wx-dump-4j?logo=github&logoColor=%23EF2D5E&label=Stars&labelColor=%23000000&color=%23EF2D5E&cacheSeconds=3600" alt="Stars Badge"/></a>
<a href="https://github.com/xuchengsheng"><img src="https://img.shields.io/github/followers/xuchengsheng?label=Followers&logo=github&logoColor=%23FC521F&labelColor=%231A2477&color=%23FC521F&cacheSeconds=3600" alt="Follow Badge"></a>
<a href="https://github.com/xuchengsheng/wx-dump-4j/fork"><img src="https://img.shields.io/github/forks/xuchengsheng/wx-dump-4j?label=Forks&logo=github&logoColor=%23F2BB13&labelColor=%23BE2323&color=%23F2BB13" alt="Fork Badge"></a>
<a href="https://github.com/xuchengsheng/wx-dump-4j/watchers"><img src="https://img.shields.io/github/watchers/xuchengsheng/wx-dump-4j?label=Watchers&logo=github&logoColor=%23FF4655&labelColor=%234169E1&color=%23FF4655&cacheSeconds=3600" alt="Watchers Badge"></a>
</p>
<p align="center">
<img src="https://img.shields.io/badge/Java-11%2B-%23437291?logo=openjdk&logoColor=%23437291"/>
<img src="https://img.shields.io/badge/Spring-5.3.10-%23437291?logo=Spring&logoColor=%236DB33F&color=%236DB33F"/>
<img src="https://img.shields.io/badge/SpringBoot-2.5.5-%23437291?logo=SpringBoot&logoColor=%236DB33F&color=%236DB33F"/>
<img src="https://img.shields.io/badge/JNA-5.8.0-%23437291?logo=JNA&logoColor=%23228B22&color=%23228B22"/>
<img src="https://img.shields.io/badge/Hutool-5.8.16-%23437291?logo=JNA&logoColor=%23F08080&color=%23F08080"/>
<img src="https://img.shields.io/badge/easyexcel-5.8.16-%23437291?logo=JNA&logoColor=%23D2691E&color=%23D2691E"/>
<img src="https://img.shields.io/badge/protobuf-3.25.1-%23437291?logo=JNA&logoColor=%23800080&color=%23800080"/>
<img src="https://img.shields.io/badge/mapstruct-1.4.2-%23437291?logo=JNA&logoColor=%23DC143C&color=%23DC143C"/>
<img src="https://img.shields.io/badge/druid-1.2.20-%23437291?logo=JNA&logoColor=%23C71585&color=%23C71585"/>
<img src="https://img.shields.io/badge/mybatisPlus-3.5.4.1-%23437291?logo=JNA&logoColor=%234B0082&color=%234B0082"/>
<img src="https://img.shields.io/badge/sqlite-3.34.0-%23437291?logo=JNA&logoColor=%230000CD&color=%230000CD"/>
<img src="https://img.shields.io/badge/lombok-1.18.20-%23437291?logo=JNA&logoColor=%23008B8B&color=%23008B8B"/>
</p>
## 📚 简介
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)
## 🍱请我吃盒饭?
作者晚上还要写博客✍️,平时还需要工作💼,如果帮到了你可以请作者吃个盒饭🥡
<div>
<img alt="logo" src="image/WeChatPay.png" style="width: 240px;height: 260px">
<img alt="logo" src="image/Alipay.png" style="width: 240px;height: 260px">
</div>
## ⭐️扫码关注微信公众号
关注后,回复关键字📲 **加群**📲,即可加入我们的技术交流群,与更多开发者一起交流学习。
在此我们真诚地邀请您访问我们的GitHub项目页面如果您觉得***wx-dump-4j***对您有帮助,请顺手点个⭐️**Star**⭐️!每一颗星星都是我们前进的动力,是对我们努力的最大肯定。非常感谢您的支持!
<div>
<img alt="logo" src="image/wechat-mp.png" height="180px">>
</div>
## 👀 演示图
<table>
<tr>
<td><img src="image/screenshot/dashboard.png"/></td>
<td><img src="image/screenshot/session.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/contact.png"/></td>
<td><img src="image/screenshot/recover-contact.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/feeds.png"/></td>
<td><img src="image/screenshot/chat.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/chatroom.png"/></td>
<td><img src="image/screenshot/chatroom-detail.png"/></td>
</tr>
<tr>
<td><img src="image/screenshot/database.png"/></td>
<td><img src="image/screenshot/database-list.png"/></td>
</tr>
</table>
=======
# wx-dump-4j
>>>>>>> c4b6449ae2686772c4ab318ba75389b6a4df21ea

@ -12,33 +12,68 @@ import java.net.UnknownHostException;
import java.util.Date;
/**
* `WxDumpApplication` Spring Boot
* `@SpringBootApplication` Spring Boot AOP 便使
* `main` 访便
*
* @author xcs
* @date 20231221 1702
**/
@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:<port><contextPath>` 的格式拼接字符串,其中 `<port>` 是前面获取到的端口号,`<contextPath>` 是获取到的应用上下文路径,
// 这个地址方便开发者在本地通过浏览器等方式访问正在运行的应用,用于本地的调试和测试等操作。
String localUrl = "http://localhost:" + port + contextPath;
// 构建应用在网络环境下访问的 URL 地址,按照 `http://<localHostAddress>:<port><contextPath>` 的格式拼接字符串,其中 `<localHostAddress>` 是前面获取到的本地主机 IP 地址,`<port>` 是端口号,`<contextPath>` 是应用上下文路径,
// 这个地址使得同一网络中的其他设备可以通过该地址访问到正在运行的应用,实现应用的网络访问功能。
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("╚════════════════════════════════════════════════════╝");
}
}
}

@ -2,19 +2,12 @@ package com.xcs.wx.repository;
import com.xcs.wx.domain.ChatRoomInfo;
/**
* Repository
*
* @author xcs
* @date 2023122118:38:19
*/
//@author xcs
// 定义ChatRoomInfoRepository接口该接口通常用于处理与聊天群信息相关的数据访问操作
// 遵循常见的Repository设计模式将数据访问逻辑抽象出来方便后续进行具体的实现与替换比如切换不同的数据库实现等
public interface ChatRoomInfoRepository {
/**
*
*
* @param chatRoomName
* @return ChatRoomInfo
*/
//查询群聊信息
ChatRoomInfo queryChatRoomInfo(String chatRoomName);
}

@ -9,39 +9,35 @@ import com.xcs.wx.domain.vo.ExportChatRoomVO;
import java.util.List;
/**
* Repository
*
* @author xcs
* @date 2023122118:38:19
* Repository访
* Repository访
*
*/
public interface ChatRoomRepository {
/**
*
*
* 使MyBatis PlusPage便
* @param chatRoomDTO
* @return ChatRoom
*/
Page<ChatRoomVO> queryChatRoom(ChatRoomDTO chatRoomDTO);
/**
*
*
*
* @param chatRoomName
* @return ChatRoom
*/
ChatRoom queryChatRoomDetail(String chatRoomName);
/**
*
*
*
* @return
*/
int countChatRoom();
/**
*
*
* ExcelExportChatRoomVO
* @return ExportChatRoomVO
*/
List<ExportChatRoomVO> exportChatRoom();

@ -1,18 +1,17 @@
package com.xcs.wx.repository;
/**
* Repository
*
* @author xcs
* @date 202461815:31:54
* Repository访
* Repository访
*/
public interface ContactHeadImgRepository {
/**
*
*
*
*
* @param usrName
* @return
* @return byte[]
*
*/
byte[] getContactHeadImg(String usrName);
}

@ -4,15 +4,16 @@ import java.util.List;
import java.util.Map;
/**
* Repository
*
* @author xcs
* @date 2023122118:38:19
* Repository
* 便
*
*/
public interface ContactHeadImgUrlRepository {
/**
*
* URL
* URL
* 便
*
* @param usrNames
* @return
@ -20,8 +21,8 @@ public interface ContactHeadImgUrlRepository {
Map<String, String> queryHeadImgUrl(List<String> usrNames);
/**
*
*
* URL
* 使
* @param userName
* @return
*/

@ -6,23 +6,26 @@ import java.util.List;
import java.util.Map;
/**
* Repository
*
* @author xcs
* @date 2023122217:27:24
* Repository访
* 访
* 便使
*/
public interface ContactLabelRepository {
/**
*
*
* Map
* MapKeyID
* Value便
* ID
* @return Map
*/
Map<String, String> queryContactLabelAsMap();
/**
*
*
* List
* ContactLabelContactLabel
* ContactLabel
* 便
* @return ContactLabel
*/
List<ContactLabel> queryContactLabelAsList();

@ -11,69 +11,75 @@ import java.util.Map;
import java.util.Set;
/**
* Repository
*
* @author xcs
* @date 20231222 1420
* Repository访
* Repository访
* 便
**/
public interface ContactRepository {
/**
*
*
* ContactDTO
* ContactDTO
* ContactVO
* 便
* @param contactDTO
* @return ContactVO
*/
Page<ContactVO> queryContact(ContactDTO contactDTO);
/**
*
*
*
* AllContactVOAllContactVO
* 便使
* @return AllContactVO
*/
List<AllContactVO> queryAllContact();
/**
*
*
*
*
*
* @param userName
* @return
*/
String getContactNickname(String userName);
/**
*
*
* getContactNickname(String userName)
* 便使
* @param userName
* @return
*/
String getNickName(String userName);
/**
* Id
*
* IdId
* Set<String>Id
*
* @return Contact
*/
Set<String> getContactWithMp();
/**
*
*
*
* Map
* 便使
* @param userNames
* @return
*/
Map<String, String> getContactNickname(List<String> userNames);
/**
*
*
*
*
* @return
*/
int countContact();
/**
*
*
* ExcelExportContactVO
* List<ExportContactVO>
* 便
* @return ExportContactVO
*/
List<ExportContactVO> exportContact();

@ -6,16 +6,18 @@ import com.xcs.wx.domain.dto.RecoverContactDTO;
import java.util.List;
/**
* Repository
*
* @author xcs
* @date 202461415:18:11
* Repository访
* 访
*
**/
public interface FTSContactContentRepository {
/**
*
*
* RecoverContactDTO
* FTSContactContentRecoverContactDTO
*
* FTSContactContentFTSContactContent
* 便
* @return FTSContactContent
*/
List<FTSContactContent> queryContactContent(RecoverContactDTO recoverContactDTO);

@ -3,16 +3,17 @@ package com.xcs.wx.repository;
import java.util.List;
/**
* 使 Repository
*
* @author xcs
* @date 202412311:20:56
* 使 Repository使访
* 访使
* 便使
*/
public interface FTSRecentUsedRepository {
/**
* 使
*
* 使使
*
* 使使使
*
* @return
*/
List<String> queryRecentUsedKeyWord();

@ -5,18 +5,20 @@ import com.xcs.wx.domain.Feeds;
import com.xcs.wx.domain.dto.FeedsDTO;
/**
* Repository
*
* @author xcs
* @date 20240103 1656
**/
* Repository访
* 访Repository
* 便
*/
public interface FeedsRepository {
/**
*
*
* @param feedsDTO
* @return Feeds
* FeedsDTO
* FeedsDTO
* Page<Feeds>Feeds
* FeedsFeeds
* 便
* @param feedsDTO
* @return Feeds Feeds便
*/
Page<Feeds> queryFeeds(FeedsDTO feedsDTO);
}

@ -1,18 +1,18 @@
package com.xcs.wx.repository;
/**
* Repository
*
* @author xcs
* @date 20240103 1656
* Repository访
* 访
*
**/
public interface HardLinkImageAttributeRepository {
/**
*
*
* @param md5 md5
* @return
* MD5
* MD5
* HTML <img> src
* @param md5 md5
* @return
*/
String queryHardLinkImage(byte[] md5);
}

@ -1,18 +1,21 @@
package com.xcs.wx.repository;
/**
* Repository
*
* Repository访
* 访
* 便
* @author xcs
* @date 20240103 1656
**/
public interface HardLinkVideoAttributeRepository {
/**
*
*
* MD5MD5
* MD5
*
*
* @param md5 md5
* @return
* @return
*/
String queryHardLinkVideo(byte[] md5);
}

@ -8,61 +8,62 @@ import com.xcs.wx.domain.vo.TopContactsVO;
import java.util.List;
/**
* Repository
*
* Repository访
* 访
* 便
* @author xcs
* @date 2023122515:31:37
*/
public interface MsgRepository {
/**
* talker
*
* talkertalkernextSequence
* MsgtalkerID
* nextSequence
* MsgMsg
* 便
* @param talker
* @param nextSequence
* @return Msg
*/
List<Msg> queryMsgByTalker(String talker, Long nextSequence);
/**
*
*
* talkerMsg
* CSVExcel
* 便Msg
* @param talker
* @return Msg
*/
List<Msg> exportMsg(String talker);
/**
*
*
*
* MsgTypeDistributionVOMsgTypeDistributionVO
*
* @return MsgTypeDistributionVO
*/
List<MsgTypeDistributionVO> msgTypeDistribution();
/**
* 15
*
* 15CountRecentMsgsVO
* CountRecentMsgsVO
* 便
* @return MsgTrendVO
*/
List<CountRecentMsgsVO> countRecentMsgs();
/**
* 10
*
* 1010
* TopContactsVOTopContactsVO
*
* @return MsgRankVO
*/
List<TopContactsVO> topContacts();
/**
*
*
*
*
* @return
*/
int countSent();
/**
*
*
*
* 使
* @return
*/
int countReceived();

@ -5,15 +5,19 @@ import com.xcs.wx.domain.vo.SessionVO;
import java.util.List;
/**
* Repository
*
* Repository访
* 访
* 便
* @author xcs
* @date 2023122117:33:19
*/
public interface SessionRepository {
/**
*
*
* SessionVOSessionVO
* SessionVO
* 便便
*
* @return Session
*/

@ -1,16 +1,16 @@
package com.xcs.wx.repository;
/**
* SQLite Repository
*
* SQLite RepositorySQLite
* 访SQLite
* 便使JDBCSQLite
* @author xcs
* @date 202461309:19:24
*/
public interface SqliteMasterRepository {
/**
*
*
* tableNameSQLite
*
*
* @param tableName
* @return
*/

@ -7,6 +7,8 @@ import com.xcs.wx.domain.vo.PageVO;
/**
*
*
*
*
* @author xcs
* @date 2023123118:18:58
@ -15,24 +17,34 @@ public interface ChatRoomService {
/**
*
* ChatRoomDTO
* PageVO<ChatRoomVO>ChatRoomVO
* PageVO
*
* @param chatRoomDTO
* @return ChatRoomVO
* @param chatRoomDTO
*
* @return ChatRoomVOPageVO<ChatRoomVO>
*/
PageVO<ChatRoomVO> queryChatRoom(ChatRoomDTO chatRoomDTO);
/**
*
*
*
* ChatRoomDetailVO便
*
* @param chatRoomName
* @return ChatRoomDetailVO
* @param chatRoomName
* @return ChatRoomDetailVOChatRoomDetailVO使
*/
ChatRoomDetailVO queryChatRoomDetail(String chatRoomName);
/**
*
* excel
* excel
*
*
* @return excel
* @return excelexcel便
*/
String exportChatRoom();
}
}

@ -2,6 +2,9 @@ package com.xcs.wx.service;
/**
*
*
*
*
*
* @author xcs
* @date 2023123118:18:58
@ -10,9 +13,13 @@ public interface ContactHeadImgService {
/**
*
*
* 便
*
*
* @param userName
* @return
* @param userName
*
* @return
*/
byte[] avatar(String userName);
}
}

@ -1,11 +1,13 @@
package com.xcs.wx.service;
import com.xcs.wx.domain.vo.ContactLabelVO;
import java.util.List;
/**
*
*
*
*
*
* @author xcs
* @date 2023123118:18:58
@ -14,8 +16,12 @@ public interface ContactLabelService {
/**
*
*
* ContactLabelVOList便
*
*
* @return ContactLabel
* @return ContactLabelList<ContactLabelVO>ContactLabelVO
* ContactLabelVO
*/
List<ContactLabelVO> queryContactLabel();
}
}

@ -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 2023122214:49:52
@ -18,30 +19,43 @@ public interface ContactService {
/**
*
* ContactDTO
* PageVO<ContactVO>ContactVO
* PageVO便
*
* @param contactDTO
* @return ContactVO
* @param contactDTO
*
* @return ContactVOPageVO<ContactVO>
*/
PageVO<ContactVO> queryContact(ContactDTO contactDTO);
/**
*
* AllContactVO
* List便
*
* @return AllContactVO
* @return AllContactVOList<AllContactVO>AllContactVO
* AllContactVO
*/
List<AllContactVO> queryAllContact();
/**
*
* ContactLabelVO
* List便
*
* @return ContactLabel
* @return ContactLabelList<ContactLabelVO>ContactLabelVO
* ContactLabelVO
*/
List<ContactLabelVO> queryContactLabel();
/**
*
* excel
* excel
*
*
* @return excel
* @return excelexcel便
*/
String exportContact();
}
}

@ -6,6 +6,8 @@ import java.util.List;
/**
*
*
* 便
*
* @author xcs
* @date 202412317:24:36
@ -14,36 +16,56 @@ public interface DashboardService {
/**
*
*
* StatsPanelVO
* 便
*
* @return StatsPanelVO
* @return StatsPanelVOStatsPanelVOStatsPanelVO
*
*/
StatsPanelVO statsPanel();
/**
*
*
* MsgTypeDistributionVOList
* 便
*
* @return MsgTypeDistributionVO
* @return MsgTypeDistributionVOList<MsgTypeDistributionVO>MsgTypeDistributionVO
* MsgTypeDistributionVO
*/
List<MsgTypeDistributionVO> msgTypeDistribution();
/**
*
*
* CountRecentMsgsVOList
* 便
*
* @return MsgTrendVO
* @return MsgTrendVOList<CountRecentMsgsVO>CountRecentMsgsVO
*
*/
List<CountRecentMsgsVO> countRecentMsgs();
/**
*
*
* TopContactsVOList
* 便
*
* @return MsgTrendVO
* @return MsgTrendVOList<TopContactsVO>TopContactsVO
* TopContactsVO
*/
List<TopContactsVO> topContacts();
/**
* 使
* 使使
* RecentUsedKeyWordVOList
* 便
*
* @return
* @return List<RecentUsedKeyWordVO>RecentUsedKeyWordVO
* 使使RecentUsedKeyWordVO使
*/
List<RecentUsedKeyWordVO> queryRecentUsedKeyWord();
}
}

@ -8,6 +8,9 @@ import java.util.List;
/**
*
*
*
* wxId
*
* @author xcs
* @date 2023122519:28:37
@ -16,17 +19,27 @@ public interface DatabaseService {
/**
*
* SseEmitterDecryptDTO
* SseEmitterSpringServer-Sent EventsSSE
* 使
* DecryptDTODecryptDTO
*
*
* @param emitter sse
* @param decryptDTO
* @param emitter sse便
* @param decryptDTO
*/
void decrypt(SseEmitter emitter, DecryptDTO decryptDTO);
/**
*
* wxId
* DatabaseVOList
* DatabaseVO便
* 使
*
* @param wxId wxId
* @return DatabaseVO
* @param wxId wxId
* @return DatabaseVOList<DatabaseVO>DatabaseVO
*
*/
List<DatabaseVO> getDatabase(String wxId);
}
}

@ -4,6 +4,8 @@ import com.xcs.wx.domain.bo.DecryptBO;
/**
*
*
* 便
*
* @author xcs
* @date 2023121019: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);
}
}

@ -6,6 +6,8 @@ import com.xcs.wx.domain.vo.PageVO;
/**
*
*
*
*
* @author xcs
* @date 20241317:25:26
@ -14,9 +16,13 @@ public interface FeedsService {
/**
*
* FeedsDTO
* FeedsDTO
* PageVO<FeedsVO>FeedsVO
* PageVO便
*
* @param feedsDTO
* @return FeedsVO
* @param feedsDTO
* @return FeedsVOPageVO<FeedsVO>
*/
PageVO<FeedsVO> queryFeeds(FeedsDTO feedsDTO);
}
}

@ -5,6 +5,8 @@ import org.springframework.http.ResponseEntity;
/**
*
* 便
* MD5
*
* @author xcs
* @date 202411822:06:46
@ -13,25 +15,36 @@ public interface ImageService {
/**
* Md5
* MD5md5MD5
* ResponseEntity<Resource>ResponseEntityHTTP
*
* Resource便
*
* @param md5 md5
* @return ResponseEntity
* @param md5 md5MD5
* @return ResponseEntityResponseEntity<Resource>HTTP
* Resource
*/
ResponseEntity<Resource> downloadImgMd5(String md5);
/**
*
* pathURL
* ResponseEntity<Resource>
* 便
*
* @param path
* @return ResponseEntity
* @param path
* @return ResponseEntityResponseEntity<Resource>
*/
ResponseEntity<Resource> downloadImg(String path);
/**
*
* localPath "C:/images/myPic.jpg"
* ResponseEntity<Resource>
*
*
* @param localPath
* @return ResponseEntity
* @param localPath 便
* @return ResponseEntityResponseEntity<Resource>便使
*/
ResponseEntity<Resource> downloadImgFormLocal(String localPath);
}
}

@ -6,6 +6,8 @@ import java.util.List;
/**
*
* 便
*
*
* @author xcs
* @date 2023122515: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 MsgVOList<MsgVO> `MsgVO` `MsgVO`
*
*/
List<MsgVO> queryMsg(String talker, Long nextSequence);
/**
*
* Id`talker` Excel
*
*
* @param talker Id
* @return
* @param talker Id
* @return 便
*/
String exportMsg(String talker);
}
}

@ -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 202461415:28:11
@ -15,15 +16,21 @@ public interface RecoverContactService {
/**
*
*
* @return RecoverContactVO
* RecoverContactDTO
* RecoverContactDTO
* RecoverContactVOList便
* @return RecoverContactVOList<RecoverContactVO>RecoverContactVO
* RecoverContactVO
*/
List<RecoverContactVO> queryRecoverContact(RecoverContactDTO recoverContactDTO);
/**
*
*
*
* 便
*
* @return
* @return
*/
String exportRecoverContact();
}

@ -5,7 +5,8 @@ import com.xcs.wx.domain.vo.SessionVO;
import java.util.List;
/**
*
*
* 便
*
* @author xcs
* @date 20231221 1716
@ -13,9 +14,12 @@ import java.util.List;
public interface SessionService {
/**
*
* `SessionVO`
* `SessionVO` `SessionVO`
*
*
* @return SessionVO
* @return List<SessionVO> `SessionVO`
*
*/
List<SessionVO> querySession();
}
}

@ -7,7 +7,9 @@ import com.xcs.wx.domain.vo.UserVO;
import java.util.List;
/**
*
*
*
* 使便
*
* @author xcs
* @date 20231221 1716
@ -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> `UserVO`
*
*/
List<UserVO> 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);
}
}

@ -5,7 +5,9 @@ import com.xcs.wx.domain.vo.WeChatConfigVO;
import java.util.List;
/**
*
*
*
* 便
*
* @author xcs
* @date 2023122509:37:30
@ -13,58 +15,87 @@ import java.util.List;
public interface WeChatService {
/**
*
*
* `WeChatConfigVO`
* `WeChatConfigVO` `WeChatConfigVO`
*
*
* @return WeChatDTO
* @return List<WeChatConfigVO> `WeChatConfigVO`
*
*/
List<WeChatConfigVO> readWeChatConfig();
/**
* ID
*
* @return ID
* ID
* ID
* @return ID `List<Integer>` ID
* ID
* 使 ID
*/
List<Integer> 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);
/**
* IDID
* IDID
* IDID
* ID
* ID
*
* @param pid ID
* @return ID
* @param pid IDID ID
* `pid`
* @return IDID
* 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);
}
}

@ -30,7 +30,9 @@ import java.util.stream.Collectors;
/**
*
* ChatRoomService
* 访RepositoryMapping
* 访Controller
*
* @author xcs
* @date 2023123118: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;
/**
* ChatRoomDTOPageVO<ChatRoomVO>
*
*
* @param chatRoomDTO
* @return PageVO<ChatRoomVO> ChatRoomVOChatRoomVO
* PageVO
*/
@Override
public PageVO<ChatRoomVO> 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;
})
// 处理头像为空问题
// 处理头像为空问题,再次遍历分页群聊数据中的每一条群聊记录,
// 如果群聊记录的头像URLHeadImgUrl为空字符串即头像为空则设置默认的联系人头像路径。
.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<ExportChatRoomVO> 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<ChatRoomProto.Member> membersList = chatRoom.getMembersList();
// 群成员的微信Id
// 群成员的微信Id通过流操作提取群成员列表中每个成员的微信Id使用map方法将ChatRoomProto.Member对象转换为对应的微信Id字符串
// 最后通过collect方法将所有微信Id字符串收集到一个列表中方便后续批量查询头像和昵称等操作。
List<String> memberWxIds = membersList.stream().map(ChatRoomProto.Member::getWxId).collect(Collectors.toList());
// 群成员头像
// 群成员头像调用contactHeadImgUrlRepository的queryHeadImgUrl方法批量查询群成员微信Id对应的头像URL地址
// 返回一个头像URL地址的映射关系键为微信Id值为头像URL用于后续填充群成员的头像信息。
Map<String, String> headImgUrlMap = contactHeadImgUrlRepository.queryHeadImgUrl(memberWxIds);
// 群成员昵称
// 群成员昵称调用contactRepository的getContactNickname方法批量查询群成员微信Id对应的联系人昵称
// 返回一个联系人昵称的映射关系键为微信Id值为昵称用于后续填充群成员的昵称信息。
Map<String, String> 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 Integer0
*/
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();
}
}

@ -7,7 +7,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
*
* ContactHeadImgService
* 访ContactHeadImgRepository
*
* @author xcs
* @date 2023123118: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
* ContactHeadImgRepositorygetContactHeadImg
*
*
* @param userName
* @return byte[]
*/
@Override
public byte[] avatar(String userName) {
// 调用contactHeadImgService即ContactHeadImgRepository的getContactHeadImg方法传入用户名作为参数
// 以获取对应的联系人头像数据,并将获取到的数据直接返回给该方法的调用者。
return contactHeadImgService.getContactHeadImg(userName);
}
}

@ -12,7 +12,9 @@ import java.util.List;
import java.util.Optional;
/**
*
* ContactLabelService
* 访ContactLabelRepository
* ContactLabelMapping
*
* @author xcs
* @date 2023123118: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> ContactLabelVOContactLabelVO
* Collections.emptyList()
*/
@Override
public List<ContactLabelVO> 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<ContactLabelVO>类型的对象,避免出现空指针异常等情况。
.orElse(Collections.emptyList());
}
}
}

@ -18,7 +18,9 @@ import java.util.*;
import java.util.stream.Collectors;
/**
*
* ContactService
* 访RepositoryMapping
* 访Controller
*
* @author xcs
* @date 20231222 1442
@ -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;
/**
* ContactDTOPageVO<ContactVO>
*
*
* @param contactDTO
* @return PageVO<ContactVO> ContactVOContactVO
* PageVO
*/
@Override
public PageVO<ContactVO> 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<String, String> contactLabelMap = contactLabelRepository.queryContactLabelAsMap();
for (ContactVO contactVO : page.getRecords()) {
// 分割当前联系人标签
// 分割当前联系人标签将联系人视图对象ContactVO中存储的标签ID列表以逗号分隔的字符串形式进行分割
// 转换为标签ID字符串的流Stream然后通过map操作根据contactLabelMap查找每个标签ID对应的标签名称
// 再使用filter过滤掉为空字符串的标签名称可能存在无效的标签ID情况最后收集为一个包含有效标签名称的列表。
List<String> 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));
}
/**
* contactRepositoryqueryAllContact
* AllContactVOAllContactVO
*
* @return List<AllContactVO> AllContactVO
*
*/
@Override
public List<AllContactVO> queryAllContact() {
return contactRepository.queryAllContact();
}
/**
* ContactLabelVO
* 访
*
* @return List<ContactLabelVO> ContactLabelVOContactLabelVO
* Collections.emptyList()
*/
@Override
public List<ContactLabelVO> 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<ContactLabelVO>类型的对象,避免出现空指针异常等情况。
.orElse(Collections.emptyList());
}
/**
* ExcelcontactRepositoryexportContact
*
*
* @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;
}
}
}

@ -14,7 +14,9 @@ import java.util.Map;
import java.util.stream.Collectors;
/**
*
* DashboardService
* 访Repository
* 访使
*
* @author xcs
* @date 20240123 1724
@ -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> MsgTypeDistributionVO
* Collections.emptyList()
*/
@Override
public List<MsgTypeDistributionVO> 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<MsgTypeDistributionVO>类型的对象,避免出现空指针异常等情况。
.orElse(Collections.emptyList());
}
/**
* msgRepositorycountRecentMsgs15
* CountRecentMsgsVO
*
* @return List<CountRecentMsgsVO> CountRecentMsgsVO
*
*/
@Override
public List<CountRecentMsgsVO> countRecentMsgs() {
return msgRepository.countRecentMsgs();
}
/**
* 10访
* URL
*
* @return List<TopContactsVO> TopContactsVO10
* Collections.emptyList()
*/
@Override
public List<TopContactsVO> 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<String> userNames = topContacts.stream().map(TopContactsVO::getUserName).collect(Collectors.toList());
// 联系人昵称
// 联系人昵称调用contactRepository的getContactNickname方法批量查询用户列表对应的联系人昵称
// 返回一个以用户名为键String类型、对应联系人昵称为值String类型的Map方便后续根据用户名查找昵称来设置联系人的昵称信息。
Map<String, String> nicknameMap = contactRepository.getContactNickname(userNames);
// 联系人头像
// 联系人头像调用contactHeadImgUrlRepository的queryHeadImgUrl方法批量查询用户列表对应的联系人头像URL地址
// 返回一个以用户名为键String类型、对应头像URL为值String类型的Map用于后续根据用户名查找头像URL来设置联系人的头像信息。
Map<String, String> 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<TopContactsVO>类型的对象,避免出现空指针异常等情况。
.orElse(Collections.emptyList());
}
/**
* 使使访使
* RecentUsedKeyWordVORecentUsedKeyWordVO
*
* @return List<RecentUsedKeyWordVO> RecentUsedKeyWordVO使
* 使
*/
@Override
public List<RecentUsedKeyWordVO> queryRecentUsedKeyWord() {
return recentUsedRepository.queryRecentUsedKeyWord()
// 将获取到的关键字字符串列表转换为流,方便后续进行操作。
.stream()
// 对每个关键字字符串调用RecentUsedKeyWordVO的构造方法创建一个RecentUsedKeyWordVO对象
// 这样就将原始的关键字字符串转换为包含更多相关属性(如果类中有定义的话)的对象形式。
.map(RecentUsedKeyWordVO::new)
// 将转换后的RecentUsedKeyWordVO对象收集为一个列表最终返回该列表其包含了所有最近使用过的关键字相关信息。
.collect(Collectors.toList());
}
}
}

@ -41,7 +41,10 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* DatabaseServiceApplicationRunner
* DatabaseService
* ApplicationRunnerSpring Boot
*
*
* @author xcs
* @date 2023122519: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和微信用户IDwxId拼接出微信数据库文件所在的完整目录路径。
String dbPath = decryptDTO.getBasePath() + separator + decryptDTO.getWxId();
// 秘钥
// 秘钥调用weChatService的getKey方法传入进程IDpid和微信数据库文件目录路径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方法传入微信用户IDwxId作为参数获取解密后数据库文件的输出目录路径用于存放解密后的文件。
String outputPath = DirUtil.getDbDir(decryptDTO.getWxId());
// 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录
// 使用Files.walk创建一个Stream来遍历给定路径下的所有文件和目录通过Paths.get方法将字符串形式的扫描路径转换为Path对象
// 然后利用Files.walk方法获取一个包含该路径下所有文件和子目录的Stream方便后续对其进行过滤和处理操作。
try (Stream<Path> stream = Files.walk(Paths.get(scanPath))) {
// 过滤出非目录的文件
// 过滤出非目录的文件调用getWeChatDb方法传入文件流stream和输出目录路径outputPath作为参数
// 对遍历到的文件和目录进行筛选只保留文件非目录信息并转换为DecryptBO对象列表该列表包含了要解密的文件相关信息。
List<DecryptBO> 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();
}
}
/**
* IDwxId.db
* DatabaseVO
*
*
* @param wxId IDwxId
* @return List<DatabaseVO> DatabaseVODatabaseVO
* Collections.emptyList()
*/
@Override
public List<DatabaseVO> 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<Path> 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<DatabaseVO>类型。
return Collections.emptyList();
}
/**
* db
* db
* DecryptBO".db"DecryptBO
*
* @param stream
* @param outputPath
* @return DecryptBO
* @param stream Stream<Path>Files.walk
* @param outputPath DecryptBO
* @return List<DecryptBO> DecryptBODecryptBO
*
*/
private List<DecryptBO> getWeChatDb(Stream<Path> 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 BootApplicationRunnerrun
* db.db
* registerDataSourcedb
*
* @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<Path> 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方法传入微信用户IDwxId和数据库名称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);
}
}

@ -20,7 +20,9 @@ import java.security.GeneralSecurityException;
import java.util.Arrays;
/**
*
* DecryptService
* JavaAPI
* 访使
*
* @author xcs
* @date 2023122511:09:07
@ -31,71 +33,101 @@ import java.util.Arrays;
public class DecryptServiceImpl implements DecryptService {
/**
* SQLite
* SQLiteSQLite
*
*/
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
* Key32使
* 使HMAC
*/
private static final int HASH_KEY_LENGTH = 32;
/**
* passwordDecryptBOdecryptBO
*
*
* @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 truefalse
* @param page 0
* @return truefalse
*/
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/NoPaddingkeyivinput
* JavaAPIjavax.cryptoCipher
*
* @param key
* @param iv
* @param input
* @return
* @throws GeneralSecurityException
* @param key
* @param iv AESCBC
* @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);
}
}
}

@ -24,7 +24,9 @@ import java.util.Optional;
import java.util.stream.Collectors;
/**
*
* FeedsService
* 访RepositoryFeedsMapping
* 便使
*
* @author xcs
* @date 20241317: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;
/**
* FeedsDTOPageVO<FeedsVO>
* XML
*
* @param feedsDTO
* @return PageVO<FeedsVO> FeedsVOFeedsVO
* PageVO
*/
@Override
public PageVO<FeedsVO> 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<FeedsVO> 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 FeedsMediaVOFeedsMediaVOURLURL
*
*/
private List<FeedsMediaVO> getMedia(TimelineObjectBO timelineObjectBO) {
List<FeedsMediaVO> feedsMediaVos = new ArrayList<>();
// 获取媒体
// 获取媒体从TimelineObjectBO对象中获取媒体列表信息通过调用getContentObject方法获取其中的内容对象再获取该内容对象中的媒体列表mediaList
// 该媒体列表包含了朋友圈中发布的所有媒体如图片、视频等的详细信息结构每个元素包含URL、缩略图等属性
List<TimelineObjectBO.ContentObject.Media> mediaList = timelineObjectBO.getContentObject().getMediaList();
// 判断媒体列表是否为空如果为空即不存在媒体信息则直接返回空的FeedsMediaVO对象列表避免后续不必要的操作。
if (CollUtil.isEmpty(mediaList)) {
return feedsMediaVos;
}
@ -122,14 +157,18 @@ public class FeedsServiceImpl implements FeedsService {
}
/**
*
* TimelineObjectBO
* FeedsLocationVOnull
*
* @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
* xmlXMLxmlTimelineObjectBO
* XML使XmlUtil
* null
*
* @param xml xml
* @return TimelineObjectBO
* @param xml xmlXML便
* @return TimelineObjectBO TimelineObjectBOXML
* null
*/
private TimelineObjectBO parseXmlToObj(String xml) {
try {
@ -154,6 +196,8 @@ public class FeedsServiceImpl implements FeedsService {
xml = xml.replace("&#x02;", "");
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;

@ -22,7 +22,9 @@ import java.nio.file.Files;
import java.nio.file.Paths;
/**
*
* ImageService
* HardLinkImageAttributeRepositoryUserService
* MD5使
*
* @author xcs
* @date 202411822: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;
/**
* MD5MD5URLURL
* IMAGE_JPEG
* 404
*
* @param md5 MD5MD5URL
* @return ResponseEntity<Resource> ResourceResponseEntity
* IMAGE_JPEG404
*/
@Override
public ResponseEntity<Resource> 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方法获取当前用户的微信IDwxId用于后续确定图片文件所在的目录路径等信息
// 基于用户来定位图片资源的存储位置,确保不同用户的图片数据不会混淆。
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方法传入微信IDwxId作为参数获取用于存放处理后可能经过解密等操作图片的文件夹路径
// 方便后续将解密或处理后的图片放置到该文件夹中进行统一管理。
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
* HTTPIMAGE_JPEG
* 404
*
* @param path URL
*
* @return ResponseEntity<Resource> ResourceResponseEntity
* IMAGE_JPEG404
*/
@Override
public ResponseEntity<Resource> downloadImg(String path) {
// 获取用户信息
// 获取用户信息调用userService的currentUser方法获取当前用户的微信IDwxId用于后续确定图片保存的目录路径等信息
// 确保图片按照用户相关的规则进行存储和管理。
String wxId = userService.currentUser();
// 返回默认图片
// 返回默认图片调用DirUtil的getImgDirWithName方法传入微信IDwxId和一个随机生成的文件名通过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> ResourceResponseEntity
* IMAGE_JPEG404
*/
@Override
public ResponseEntity<Resource> downloadImgFormLocal(String localPath) {
try {
// 获取用户信息
// 获取用户信息调用userService的currentUser方法获取当前用户的微信IDwxId用于后续结合其他信息确定图片文件的实际路径以及相关操作
// 确保针对的是当前用户对应的本地图片资源。
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方法传入微信IDwxId作为参数获取用于存放处理后可能经过解密等操作图片的文件夹路径
// 方便后续将解密或处理后的图片放置到该文件夹中进行统一管理。
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();
}
}

@ -29,7 +29,9 @@ import java.util.List;
import java.util.stream.Collectors;
/**
*
* MsgService
* MsgRepositoryMsgMapping
* Excel使
*
* @author xcs
* @date 20231225 1704
@ -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;
/**
* talkernextSequenceMsgVO
* ID
*
* @param talker
* @param nextSequence Long
*
* @return List<MsgVO> MsgVOMsgVO使
*
*/
@Override
public List<MsgVO> queryMsg(String talker, Long nextSequence) {
// 根据聊天对象和下一条消息序列号查询消息调用msgRepository的queryMsgByTalker方法传入聊天对象talker和下一条消息序列号nextSequence作为参数
// 从数据库中获取与该聊天对象相关的所有消息记录返回一个包含Msg对象的列表Msg对象可能是数据库中存储消息的原始数据结构形式。
List<Msg> 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方法传入消息对应的聊天对象IDmsgVO.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());
}
/**
* talkerExcel
* 使EasyExcelExcel
*
* @param talker
* @return String Excel
*/
@Override
public String exportMsg(String talker) {
// 查询要导出的消息列表调用msgRepository的exportMsg方法传入聊天对象talker作为参数从数据库中获取该聊天对象的所有消息记录
// 返回一个包含Msg对象的列表这些消息记录将作为后续导出操作的数据来源。
List<Msg> msgList = msgRepository.exportMsg(talker);
// 根据时间排序
// 根据时间排序对转换后的MsgVO对象列表进行排序操作通过stream将列表转换为流然后使用sorted方法结合Comparator.comparing(MsgVO::getCreateTime)按照消息的创建时间属性进行排序,
// 使得导出的消息数据在Excel文件中按照时间先后顺序展示方便查看消息的时间线顺序。
List<MsgVO> 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
* IdIDwxId
* IDID
*
* @param talker
* @param msgVO VO
* @return wxId
* @param talker
* @param msgVO VOMsgVOID
* @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<MsgProto.SubMessage2> 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;
}
/**
*
* IDwxIdURL
* contactHeadImgUrlRepositoryURL
*
* @param wxId
* @param wxId IDIDURL
*/
private String getChatAvatar(String wxId) {
return contactHeadImgUrlRepository.queryHeadImgUrlByUserName(wxId);

@ -20,7 +20,9 @@ import java.util.Set;
import java.util.stream.Collectors;
/**
*
* RecoverContactService
* FTSContactContentRepositoryContactRepositoryRecoverContactMapping
* Excel
*
* @author xcs
* @date 202461415: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;
/**
* RecoverContactDTORecoverContactVO
* RecoverContactVO
*
* @param recoverContactDTO RecoverContactDTO
* @return List<RecoverContactVO> RecoverContactVORecoverContactVO
* 使
*/
@Override
public List<RecoverContactVO> queryRecoverContact(RecoverContactDTO recoverContactDTO) {
// 根据查询条件查询联系人内容信息调用ftsContactContentRepository的queryContactContent方法传入RecoverContactDTO对象作为参数
// 从数据库中获取符合查询条件的联系人相关内容信息返回一个包含FTSContactContent对象的列表这些对象包含了联系人的部分关键数据如别名等用于后续处理。
List<FTSContactContent> ftsContactContents = ftsContactContentRepository.queryContactContent(recoverContactDTO);
// 获取具有公众号属性的联系人集合调用contactRepository的getContactWithMp方法获取一个包含具有公众号属性的联系人别名的集合Set<String>
// 用于后续在查询到的联系人内容信息中进行筛选,排除掉已经是公众号的联系人,重点关注可能真正需要找回的联系人。
Set<String> 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
* 使EasyExcelExcel
*
* @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;
}
}

@ -13,7 +13,9 @@ import java.util.Collections;
import java.util.List;
/**
*
* SessionService
* SessionRepository
* 便使
*
* @author xcs
* @date 20231221 1717
@ -22,27 +24,43 @@ import java.util.List;
@RequiredArgsConstructor
public class SessionServiceImpl implements SessionService {
// 通过依赖注入获取会话数据访问仓库,用于执行与会话相关的数据库查询等操作,比如查询所有的会话信息记录等,为后续处理提供原始数据来源。
private final SessionRepository sessionRepository;
/**
* 访
*
*
*
* @return List<SessionVO> SessionVOSessionVO
* 使
*/
@Override
public List<SessionVO> 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<SessionVO>)的结果。
.orElse(Collections.emptyList());
}
}

@ -28,7 +28,9 @@ import java.util.List;
import java.util.Optional;
/**
* UserService
* UserServiceUserService
*
* 使
*
* @author xcs
* @date 202461516: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;
/**
* UserInfoVOID
* UserBOUserBOUserInfoVOnull
*
* @return UserInfoVO UserInfoVOID便使
* null
*/
@Override
public UserInfoVO userInfo() {
// 当前选中账号
// 当前选中账号调用currentUser方法获取当前选中的用户账号微信ID用于后续定位该用户相关的文件、数据等信息
// 这是获取当前用户详细信息的基础,后续操作都围绕该账号对应的资源展开。
String wxId = currentUser();
// 空校验
// 空校验判断获取到的微信ID是否为null如果为null说明没有获取到当前有效的用户账号直接返回null避免后续出现空指针等异常情况。
if (wxId == null) {
return null;
}
// 当前账号目录
// 当前账号目录调用DirUtil的getUserDir方法传入微信IDwxId作为参数获取该用户在文件系统中对应的目录路径
// 该目录下可能存放着与该用户相关的配置文件等数据,是后续读取用户详细信息的来源位置。
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方法根据微信IDuserBO.getWxId()从数据源获取昵称并设置到UserBO对象中进行补全。
if (StrUtil.NULL.equals(userBO.getNickname())) {
userBO.setNickname(getNickName(userBO.getWxId()));
}
return userMapping.convert(userBO);
}
/**
* URLIDgetAvatarIDURL
* IDnull
*
* @return String URLIDnull
*/
@Override
public String avatar() {
String wxId = currentUser();
// 空校验
// 空校验判断获取到的微信ID是否为null如果为null说明没有获取到当前有效的用户账号直接返回null避免后续调用获取头像方法时出现空指针等异常情况。
if (wxId == null) {
return null;
}
return getAvatar(wxId);
}
/**
* IDgetNickNameID
* IDnull
*
* @return String IDnull
*/
@Override
public String nickname() {
String wxId = currentUser();
// 空校验
// 空校验判断获取到的微信ID是否为null如果为null说明没有获取到当前有效的用户账号直接返回null避免后续调用获取昵称方法时出现空指针等异常情况。
if (wxId == null) {
return null;
}
return getNickName(wxId);
}
/**
* UserVOUserVO
* IDIDUserVO
*
* @return List<UserVO> UserVOUserVOIDURL
* 便
*/
@Override
public List<UserVO> users() {
// 用户信息
// 用户信息创建一个空的UserVO对象列表用于后续存储所有用户的信息通过逐个添加UserVO对象来构建完整的用户列表数据。
List<UserVO> users = new ArrayList<>();
// 获取微信Id
// 获取微信Id调用getWxIds方法获取所有的微信ID列表该列表包含了系统中所有用户对应的微信账号标识是后续遍历构建每个用户信息的基础数据来源。
List<String> wxIds = getWxIds();
// 遍历
// 遍历通过循环遍历获取到的微信ID列表对每个微信ID进行相关信息的获取和封装操作构建对应的UserVO对象并添加到用户列表中。
for (String wxId : wxIds) {
// 当前选中账号
// 当前选中账号通过比较当前微信IDwxId和当前用户的微信ID通过currentUser方法获取是否相等判断该用户是否为当前选中的用户
// 返回一个布尔值用于后续在UserVO对象中标识该用户的当前选中状态。
boolean current = wxId.equals(currentUser());
// 头像
// 头像调用getAvatar方法传入当前微信IDwxId作为参数从数据源获取该用户对应的头像URL地址用于在构建UserVO对象时设置头像信息。
String avatar = getAvatar(wxId);
// 昵称
// 昵称调用getNickName方法传入当前微信IDwxId作为参数从数据源获取该用户对应的昵称用于在构建UserVO对象时设置昵称信息。
String nickName = getNickName(wxId);
// 用户信息
// 用户信息创建一个新的UserVO对象将当前微信IDwxId、获取到的昵称nickName、头像URLavatar以及是否为当前用户current等信息作为参数传入构造函数
// 构建一个完整的UserVO对象并添加到用户列表users完成一个用户信息的封装和添加操作。
users.add(new UserVO(wxId, nickName, avatar, current));
}
return users;
}
/**
* IDwxId
* 便使
*
* @param wxId IDID
*/
@Override
public void switchUser(String wxId) {
FileUtil.writeString(wxId, DirUtil.getSwitchUserDir(), "UTF-8");
}
/**
* ID
* IDIDIDnull
* 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);
}
/**
* UserBOJSON
* 便使
*
* @param userBO UserBOID
* JSON
*/
@Override
public void saveUser(UserBO userBO) {
FileUtil.writeString(JSONUtil.toJsonStr(userBO), DirUtil.getUserDir(userBO.getWxId()), "UTF-8");
}
/**
* IDwxId
* UserBOnull
*
* @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();
}
/**
* IDID
* ID
* IOException
*
* @return List<String> ID
*
*/
/**
* Id
*
* @return wxIds
*/
private List<String> getWxIds() {
// 用户信息
// 用户信息创建一个空的字符串列表用于后续存储所有用户对应的微信ID通过逐个添加微信ID来构建完整的列表数据。
List<String> 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<Path> stream = Files.newDirectoryStream(path)) {
// 遍历
// 遍历通过循环遍历Directory
for (Path entry : stream) {
// 判断是否为文件夹
if (FileUtil.isDirectory(entry)) {
@ -171,27 +254,51 @@ public class UserServiceImpl implements UserService {
}
/**
* wxId
* wxId
* IDwxIdURL
* ID DataSourceType.MICRO_MSG_DB
* 访URLURL
*
* @param wxId wxId
* @return
* @param wxId IDIDURL
* @return IDURLURL使
* IDnull访
*/
private String getAvatar(String wxId) {
// 使用DynamicDataSourceContextHolder的push方法来设置当前使用的数据源。
// 它会根据传入的微信IDwxId以及指定的数据源类型DataSourceType.MICRO_MSG_DB通过DSNameUtil的getDSName方法来获取具体的数据源名称
// 并将其“推”入到当前的数据源上下文环境中使得后续的数据访问操作如下面的查询头像URL操作会在这个指定的数据源上进行以此确保能从正确的数据源获取数据。
DynamicDataSourceContextHolder.push(DSNameUtil.getDSName(wxId, DataSourceType.MICRO_MSG_DB));
// 调用contactHeadImgUrlRepository这应该是一个数据访问层的接口实现类用于与存储头像URL相关的数据存储进行交互的queryHeadImgUrlByUserName方法
// 传入微信IDwxId作为参数从当前设定好的数据源中查询该用户名也就是微信ID对应的头像URL地址。
// 这个查询操作可能涉及到数据库的查询语句执行等具体操作由contactHeadImgUrlRepository的具体实现类来负责完成与底层数据存储的交互逻辑最终获取到头像URL并赋值给avatar变量。
String avatar = contactHeadImgUrlRepository.queryHeadImgUrlByUserName(wxId);
// 在获取完头像URL地址后使用DynamicDataSourceContextHolder的clear方法来清除之前设置的数据源上下文。
// 这样做是为了避免对后续其他可能涉及数据源操作的代码产生影响,将数据源的状态恢复到默认或者之前的状态,保持整个应用的数据源使用环境的正确和有序。
DynamicDataSourceContextHolder.clear();
return avatar;
}
/**
* wxId
* wxId
* IDwxId
* DataSourceType.MICRO_MSG_DB
* 访
*
* @param wxId wxId
* @return
* @param wxId IDID
* @return ID便
* IDnull访
*/
private String getNickName(String wxId) {
// 借助DynamicDataSourceContextHolder的push方法来设定当前要使用的数据源。
// 依据传入的微信IDwxId以及指定的数据源类型DataSourceType.MICRO_MSG_DB利用DSNameUtil的getDSName方法确定具体的数据源名称
// 然后将这个数据源名称“推”入当前的数据源上下文,使得后续的数据访问操作会在该指定数据源上执行,确保能从正确的数据源获取用户昵称数据。
DynamicDataSourceContextHolder.push(DSNameUtil.getDSName(wxId, DataSourceType.MICRO_MSG_DB));
// 调用contactRepository这通常是一个用于与存储联系人相关数据的存储进行交互的数据访问层接口实现类的getNickName方法
// 把微信IDwxId作为参数传入从当前设定好的数据源中查询获取对应的用户昵称。
// 具体的查询实现是由contactRepository的具体实现类负责的可能会涉及到数据库表的查询、数据提取等操作最终获取到的用户昵称赋值给nickName变量。
String nickName = contactRepository.getNickName(wxId);
// 在获取完用户昵称后使用DynamicDataSourceContextHolder的clear方法清除之前设置的数据源上下文
// 目的是恢复数据源状态,防止对后续其他数据源相关操作造成不必要的干扰,确保整个应用在数据源使用方面的正常和规范。
DynamicDataSourceContextHolder.clear();
return nickName;
}

@ -4,34 +4,45 @@ import cn.hutool.extra.spring.SpringUtil;
import com.xcs.wx.service.UserService;
/**
* DSNameUtil
* `DSNameUtil` DSName
*
* 便访
*
* @author
* @date 202462717: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;
}
}
}

@ -4,7 +4,9 @@ import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
/**
*
*
*
* 便使
*
* @author xcs
* @date 20231227 1606
@ -12,37 +14,52 @@ import cn.hutool.core.date.DateUtil;
public class DateFormatUtil {
/**
*
*
*
* 使
*
* @param timestampInSeconds
* @return
* @param timestampInSeconds 19701100:00:00 UTC
*
* @return "10:30"1030
* "昨天" "周二" 便
*/
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日");
}
}
}

@ -3,7 +3,9 @@ package com.xcs.wx.util;
import java.nio.file.FileSystems;
/**
* DirUtil
* `DirUtil`
* 便
*
*
* @author
* @date 202462717: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;
}
}
}

@ -10,7 +10,9 @@ import java.nio.file.Files;
import java.nio.file.Paths;
/**
*
* `ImgDecoderUtil` `.dat` `JPEG``PNG``GIF`
* 便使
*
*
* @author xcs
* @date 20231228 1544
@ -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:\\");
}
}
}

@ -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 2023123114: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;
}
}
}

@ -13,12 +13,13 @@ import java.util.Arrays;
* @date 20231225 0935
**/
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);

@ -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 20240124 1414
@ -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`
* `<?xml` `XML` `XmlMapper` `Java` `null`
*
* @param content
* @param valueType
* @param <T>
* @return T
* @param content `XML` `XML`
* `XML` `XML`
* @param valueType `Java` `Class` `XML` `Java`
* `XML` `Jackson` 便 `XmlMapper` `XML`
* @param <T> `valueType` 使便使
* @return T `valueType` `Java` `JsonProcessingException` `Jackson` `XML` `JSON`
* "parse xml failed" 便 `null`
*/
public static <T> T parseXml(String content, Class<T> valueType) {
try {
// 查找输入字符串中 `<?xml` 标签的起始位置,通过调用 `indexOf` 方法,查找字符串 `content` 中第一次出现 `<?xml` 的索引位置,
// 目的是判断输入的字符串是否包含完整的 `XML` 头部标识(因为有时候获取到的内容可能前面会有一些无关的前缀信息等情况),如果找到了 `<?xml` 的起始位置且大于0表示不是从字符串开头开始的前面有其他内容需要去除
// 则进行下一步的截取操作,确保后续解析的是完整有效的 `XML` 内容部分。
int xmlStart = content.indexOf("<?xml");
if (xmlStart > 0) {
// 截取包含 `<?xml` 开头的有效 `XML` 内容部分,通过调用 `substring` 方法,从找到的 `<?xml` 起始位置(`xmlStart`)开始截取字符串,
// 得到只包含有效 `XML` 内容的新字符串,并重新赋值给 `content` 变量,这样后续使用 `content` 进行解析时就是基于正确的 `XML` 数据部分了,避免因多余前缀信息导致解析错误。
content = content.substring(xmlStart);
}
// 使用预先创建的 `XmlMapper` 实例(`MAPPER`)进行 `XML` 到 `Java` 类对象的解析转换操作,调用 `readValue` 方法,传入处理后的 `XML` 内容字符串(`content`)和目标 `Java` 类的 `Class` 类型(`valueType`)作为参数,
// `XmlMapper` 会根据 `XML` 的结构以及目标类的定义(如属性、注解等信息)将 `XML` 中的元素和属性值对应地填充到创建的 `Java` 类对象中,最后返回解析得到的目标 `Java` 类对象,完成整个解析过程。
return MAPPER.readValue(content, valueType);
} catch (JsonProcessingException e) {
// 如果在使用 `XmlMapper` 进行 `XML` 解析过程中出现 `JsonProcessingException` 异常(例如 `XML` 格式不符合要求、目标类与 `XML` 结构映射配置错误等原因导致无法正确解析),
// 通过 `log.error` 方法记录错误信息,"parse xml failed" 作为错误描述标识,同时传入捕获到的异常对象 `e`,方便后续查看日志排查问题,然后返回 `null`,表示解析操作失败,方法结束。
log.error("parse xml failed", e);
}
return null;
}
}
}
Loading…
Cancel
Save