├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── dockerhub.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── README.md ├── docker-bake.hcl ├── mvnw ├── pom.xml ├── sdk └── libWeWorkFinanceSdk_Java.so └── src └── main ├── assembly └── assembly.xml ├── java └── com │ ├── chinayin │ └── wework │ │ └── chatdata │ │ ├── WeworkApplication.java │ │ ├── config │ │ ├── ExecutorServiceConfig.java │ │ ├── MnsClientConfig.java │ │ ├── OssClientConfig.java │ │ ├── WeWorkConfig.java │ │ └── WeWorkEncryptConfig.java │ │ ├── model │ │ ├── ChatDataDTO.java │ │ ├── ChatDataDetailDTO.java │ │ ├── MediaFileDTO.java │ │ ├── MessageDTO.java │ │ └── messagetype │ │ │ ├── AbstractTypeFile.java │ │ │ ├── TypeAgree.java │ │ │ ├── TypeCalendar.java │ │ │ ├── TypeCard.java │ │ │ ├── TypeChatRecord.java │ │ │ ├── TypeCollect.java │ │ │ ├── TypeDisagree.java │ │ │ ├── TypeDoc.java │ │ │ ├── TypeEmotion.java │ │ │ ├── TypeFile.java │ │ │ ├── TypeImage.java │ │ │ ├── TypeLink.java │ │ │ ├── TypeLocation.java │ │ │ ├── TypeMarkdown.java │ │ │ ├── TypeMeeting.java │ │ │ ├── TypeMixed.java │ │ │ ├── TypeNews.java │ │ │ ├── TypeQyDiskFile.java │ │ │ ├── TypeRedPacket.java │ │ │ ├── TypeRevoke.java │ │ │ ├── TypeSphFeed.java │ │ │ ├── TypeText.java │ │ │ ├── TypeTodo.java │ │ │ ├── TypeVideo.java │ │ │ ├── TypeVoice.java │ │ │ ├── TypeVoiptext.java │ │ │ ├── TypeVote.java │ │ │ ├── TypeWeapp.java │ │ │ └── item │ │ │ ├── TypeChatRecordItem.java │ │ │ └── TypeMixedItem.java │ │ ├── schedule │ │ └── ChatDataSchedule.java │ │ └── service │ │ ├── ChatDataService.java │ │ ├── ClientService.java │ │ ├── EncryptService.java │ │ ├── FileOutputService.java │ │ ├── MnsClientService.java │ │ ├── OssClientService.java │ │ └── impl │ │ ├── ChatDataServiceImpl.java │ │ ├── ClientServiceImpl.java │ │ ├── EncryptServiceImpl.java │ │ ├── FileOutputServiceImpl.java │ │ ├── MnsClientServiceImpl.java │ │ └── OssClientServiceImpl.java │ └── tencent │ └── wework │ └── Finance.java └── resources ├── application.properties ├── logback-spring.xml └── supervisord.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | .env 5 | .git 6 | .gitattributes 7 | .gitignore 8 | .github 9 | target/ 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [**.{yml, sh}] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [Dockerfile] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | target-branch: "master" 6 | schedule: 7 | interval: "monthly" 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | target-branch: "master" 11 | schedule: 12 | interval: "weekly" 13 | ignore: 14 | - dependency-name: com.alibaba:fastjson 15 | versions: [">=2.0"] 16 | - dependency-name: org.springframework.boot:spring-boot-starter-parent 17 | versions: [">=3.0"] 18 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Set variables 15 | id: vars 16 | run: | 17 | echo "repository=${GITHUB_ACTOR}/$(basename ${GITHUB_REPOSITORY})" >> $GITHUB_OUTPUT 18 | echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 27 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 28 | - name: Build and push 29 | uses: docker/bake-action@v5 30 | env: 31 | version: ${{ steps.vars.outputs.tag }} 32 | with: 33 | pull: true 34 | push: true 35 | no-cache: true 36 | files: docker-bake.hcl 37 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Docker Hub Description 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Set variables 15 | id: vars 16 | run: | 17 | echo "repository=${GITHUB_ACTOR}/weworkchat-sdk" >> $GITHUB_OUTPUT 18 | - name: Docker Hub Description 19 | uses: peter-evans/dockerhub-description@v4 20 | with: 21 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 22 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 23 | repository: ${{ steps.vars.outputs.repository }} 24 | short-description: ${{ github.event.repository.description }} 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Ci 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | java: [8] 14 | name: Release JDK ${{ matrix.java }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup java 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'adopt' 21 | java-version: ${{ matrix.java }} 22 | cache: 'maven' 23 | - name: Build with Maven 24 | run: mvn clean package -B --file pom.xml 25 | - name: Release 26 | uses: softprops/action-gh-release@v2 27 | if: startsWith(github.ref, 'refs/tags/') 28 | with: 29 | generate_release_notes: "true" 30 | files: | 31 | target/*.jar 32 | target/*.tar.gz 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | java: [8, 17] 11 | name: Test JDK ${{ matrix.java }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup java 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'adopt' 18 | java-version: ${{ matrix.java }} 19 | cache: 'maven' 20 | - name: Build with Maven 21 | run: mvn clean package -B --file pom.xml 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | logs/ 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | # IntelliJ IDEA 27 | .idea 28 | *.iws 29 | *.iml 30 | *.ipr 31 | 32 | # NetBeans 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | build/ 39 | 40 | # VS Code 41 | .vscode/ 42 | 43 | # other 44 | !.mvn/wrapper/maven-wrapper.properties 45 | !**/src/main/** 46 | !**/src/test/** 47 | **/.DS_Store 48 | target/ 49 | **/application-dev.properties 50 | **/application-prod.properties -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://maven.aliyun.com/repository/public/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://maven.aliyun.com/repository/public/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | #-------------------------------------------------------------------------- 4 | # WeworkChatSDK 5 | #-------------------------------------------------------------------------- 6 | # 7 | 8 | FROM --platform=linux/amd64 chinayin/maven:3-jdk-8 AS builder 9 | 10 | RUN set -eux \ 11 | #&& install_packages tree vim \ 12 | && mkdir -p ~/.m2 \ 13 | #&& echo 'aliyun*aliyunhttps://maven.aliyun.com/repository/public' >> ~/.m2/settings.xml \ 14 | && mkdir /app 15 | 16 | WORKDIR /app 17 | ADD . /app 18 | 19 | RUN set -eux \ 20 | && mvn -B package --file pom.xml \ 21 | && ls -l target \ 22 | && SDK_VERSION=$(grep '' pom.xml |head -n1 |tr -cd "[0-9.]") \ 23 | && echo $SDK_VERSION \ 24 | && cp -f target/WeworkChatSDK-${SDK_VERSION}-bundle.tar.gz target/bundle.tar.gz 25 | 26 | FROM --platform=linux/amd64 chinayin/openjdk:8-jre 27 | ENV TZ=PRC 28 | ENV PARAMS="" 29 | 30 | COPY --from=builder /app/target/bundle.tar.gz /app/bundle.tar.gz 31 | WORKDIR /app 32 | 33 | RUN set -eux \ 34 | && tar -xzf bundle.tar.gz -C /app --strip-components=1 \ 35 | && mkdir -p /usr/lib64 \ 36 | && mv *.so /usr/lib64 \ 37 | && rm -f bundle.tar.gz 38 | 39 | ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS WeworkChatSDK-*.jar $PARAMS"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### WeworkChatSDK 2 | 3 | [![Build Status](https://github.com/chinayin/WeworkChatSDK/workflows/JavaCI/badge.svg)](https://github.com/chinayin/WeworkChatSDK/actions) 4 | [![Author](https://img.shields.io/badge/author-@chinayin-blue.svg)](https://github.com/chinayin) 5 | ![license](https://img.shields.io/github/license/chinayin/WeworkChatSDK.svg) 6 | 7 | 企业微信会话存档服务(WeworkChatSDK),提供一键接入java sdk,投递数据到阿里云MNS、OSS。 8 | 9 | ### 功能介绍: 10 | 11 | - 实时获取企业微信会话存档聊天数据,处理速度快。 12 | - 多线程处理聊天资源,支持上G聊天附件文件上传`对象存储 OSS`。 13 | - 聊天、资源数据投递`队列服务 MNS`,非java技术栈也可通过队列消费。 14 | - 支持docker部署。 15 | - 生产环境已稳定使用。 16 | 17 | ### 准备工作: 18 | 19 | - 申请并配置阿里云`消息服务 MNS`、`对象存储 OSS`。 20 | - 获取阿里云AccessKey,配置 `application.properties` 中相应配置项。 21 | - 打包,更新配置文件。 22 | - 安装`supervisord`,配置并执行程序。 23 | 24 | 25 | ### 配置文件`application.properties`说明 26 | 27 | #### 阿里云 消息服务 MNS 28 | ```properties 29 | # 服务可用区endpoint (区分内外网地址) 30 | mns.endpoint=http:// 31 | mns.accessKeyId= 32 | mns.accessKeySecret= 33 | # 消息队列名称 (可自定义) 34 | mns.queue=WeworkSyncMessageQueue 35 | ``` 36 | 37 | #### 阿里云 对象存储 OSS 38 | ```properties 39 | # 服务可用区endpoint (区分内外网地址) 40 | oss.endpoint=oss-cn-beijing.aliyuncs.com 41 | oss.accessKeyId= 42 | oss.accessKeySecret= 43 | # Bucket名称 (可自定义) 44 | oss.bucket=WeworkResources 45 | ``` 46 | 47 | #### 企业微信会话存档 - 应用配置 48 | ```properties 49 | # 企业ID(在我的企业-企业信息里找到) 50 | app.corpId= 51 | # 应用Secret(会话存档应用中找到) 52 | app.corpSecret= 53 | ``` 54 | 55 | #### 企业微信会话存档 - 私钥配置 56 | 说明:privateKey.后面的值为版本号(int值) 57 | ```properties 58 | encrypt.privateKey.1= 59 | encrypt.privateKey.2= 60 | ``` 61 | 62 | ### 部署 63 | 64 | - 复制`sdk`目录下`.so`文件至Java Library 65 | 66 | 官方`.so`下载链接:https://developer.work.weixin.qq.com/document/path/91774 67 | 68 | 查看目录方法: 69 | ```java 70 | public class Demo { 71 | public static void main(String[] args) { 72 | System.out.println(System.getProperty("java.library.path")); 73 | } 74 | } 75 | ``` 76 | 77 | - 生成打包文件 78 | ```bash 79 | ./mvnw clean package 80 | ``` 81 | 82 | - 命令行方式运行 83 | ```bash 84 | java -jar WeworkChatSDK-[version].jar 85 | # 生产环境 (目录存在 application-prod.properties ) 86 | java -jar WeworkChatSDK-[version].jar --spring.profiles.active=prod 87 | ``` 88 | 89 | - `supervisord`方式运行,参照配置文件: `src\main\resources\supervisord.conf` 90 | 91 | 92 | ### 附录: 93 | 94 | #### 使用Alibaba开源的Java诊断工具 `Arthas` 95 | 96 | https://arthas.aliyun.com/ 97 | 98 | 下载arthas-boot.jar,然后用java -jar的方式启动: 99 | ```bash 100 | curl -O https://arthas.aliyun.com/arthas-boot.jar 101 | java -jar arthas-boot.jar 102 | ``` 103 | 104 | 打印帮助信息: 105 | ```bash 106 | java -jar arthas-boot.jar -h 107 | ``` 108 | 109 | 选择应用java进程: 110 | ```bash 111 | $ $ java -jar arthas-boot.jar 112 | * [1]: 12345 113 | ``` 114 | 115 | 输入dashboard,按回车/enter,会展示当前进程的信息,按ctrl+c可以中断执行。 116 | 117 | #### 企业微信会话存档官方文档 118 | 119 | https://developer.work.weixin.qq.com/document/path/91361 120 | 121 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | variable "version" { 2 | default = "" 3 | } 4 | 5 | variable "repo" { 6 | default = "chinayin/weworkchat-sdk" 7 | } 8 | 9 | variable "registry" { 10 | default = "docker.io" 11 | } 12 | 13 | variable "repository" { 14 | default = "${registry}/${repo}" 15 | } 16 | 17 | function "platforms" { 18 | params = [] 19 | result = ["linux/amd64"] 20 | } 21 | 22 | target "_all_platforms" { 23 | platforms = platforms() 24 | } 25 | 26 | group "default" { 27 | targets = ["jre8"] 28 | } 29 | 30 | target "jre8" { 31 | inherits = ["_all_platforms"] 32 | context = "." 33 | tags = [ 34 | "${repository}:latest", 35 | "${repository}:${version}", 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.chinayin 6 | WeworkChatSDK 7 | WeworkChatSDK 8 | 企业微信会话存档服务(WeworkChatSDK),提供一键接入java sdk,导出数据到阿里云msn、oss 9 | 1.4.1 10 | 11 | 12 | 13 | Apache License, Version 2.0 14 | https://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-parent 21 | 2.7.18 22 | 23 | 24 | 25 | 26 | 1.8 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-configuration-processor 37 | true 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | true 43 | 44 | 45 | com.alibaba 46 | fastjson 47 | 1.2.83 48 | 49 | 50 | com.aliyun.mns 51 | aliyun-sdk-mns 52 | 1.3.0 53 | 54 | 55 | com.aliyun.oss 56 | aliyun-sdk-oss 57 | 3.18.1 58 | 59 | 60 | io.sentry 61 | sentry-spring-boot-starter 62 | 6.34.0 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.junit.vintage 71 | junit-vintage-engine 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-maven-plugin 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-assembly-plugin 86 | 3.7.1 87 | 88 | 89 | bundle 90 | package 91 | 92 | single 93 | 94 | 95 | 96 | src/main/assembly/assembly.xml 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /sdk/libWeWorkFinanceSdk_Java.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinayin/WeworkChatSDK/1b039f2403f6ca6c9b0f8d81805962d50340e29e/sdk/libWeWorkFinanceSdk_Java.so -------------------------------------------------------------------------------- /src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bundle 3 | 4 | tar.gz 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | src/main/resources 15 | / 16 | 17 | application.properties 18 | 19 | 20 | 21 | 0644 22 | 23 | 24 | sdk 25 | / 26 | 27 | *.so 28 | 29 | 30 | 31 | ${project.build.directory} 32 | / 33 | 34 | ${project.build.finalName}.jar 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/WeworkApplication.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata; 2 | 3 | import com.tencent.wework.Finance; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.WebApplicationType; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.builder.SpringApplicationBuilder; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | /** 11 | * @author chianyin 12 | */ 13 | @SpringBootApplication 14 | @EnableScheduling 15 | @Slf4j 16 | public class WeworkApplication { 17 | 18 | public static void main(String[] args) { 19 | // 检查sdk 20 | try { 21 | long sdk = Finance.NewSdk(); 22 | Finance.DestroySdk(sdk); 23 | } catch (Exception ex) { 24 | log.error("WeWorkSdk.NewSdk error, ", ex.getMessage()); 25 | System.exit(1); 26 | } 27 | new SpringApplicationBuilder(WeworkApplication.class) 28 | .web(WebApplicationType.NONE) 29 | .run(args); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/config/ExecutorServiceConfig.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.config; 2 | 3 | import lombok.Data; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.util.concurrent.*; 10 | 11 | /** 12 | * @author chianyin 13 | */ 14 | @Configuration 15 | @Slf4j 16 | @Data 17 | @ConfigurationProperties(prefix = "pool") 18 | public class ExecutorServiceConfig { 19 | 20 | // 核心线程池数 21 | private Integer corePoolSize = 10; 22 | // 最大线程池数 23 | private Integer maxPoolSize = 20; 24 | // 任务队列的容量 25 | private Integer queueCapacity = 10; 26 | // 非核心线程的存活时间 27 | private Integer keepAliveSeconds = 10; 28 | 29 | @Bean("threadPool") 30 | public ExecutorService newFixedThreadPool() { 31 | return new ThreadPoolExecutor( 32 | corePoolSize, 33 | maxPoolSize, 34 | keepAliveSeconds, 35 | TimeUnit.SECONDS, 36 | new ArrayBlockingQueue<>(queueCapacity), 37 | Executors.defaultThreadFactory(), 38 | new ThreadPoolExecutor.CallerRunsPolicy() 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/config/MnsClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.config; 2 | 3 | import com.aliyun.mns.client.CloudAccount; 4 | import com.aliyun.mns.client.MNSClient; 5 | import lombok.Data; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * @author chianyin 13 | * 阿里云MNS队列配置 14 | */ 15 | @Configuration 16 | @Slf4j 17 | @Data 18 | @ConfigurationProperties(prefix = "mns") 19 | public class MnsClientConfig { 20 | 21 | private String accessKeyId; 22 | 23 | private String accessKeySecret; 24 | 25 | private String endpoint; 26 | 27 | private String queue; 28 | 29 | @Bean 30 | public MNSClient mnsClient() { 31 | log.info(endpoint); 32 | return (new CloudAccount(accessKeyId, accessKeySecret, endpoint)).getMNSClient(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/config/OssClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.config; 2 | 3 | import com.aliyun.oss.OSS; 4 | import com.aliyun.oss.OSSClientBuilder; 5 | import lombok.Data; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * @author chianyin 13 | * 阿里云OSS上传配置 14 | */ 15 | @Configuration 16 | @Slf4j 17 | @Data 18 | @ConfigurationProperties(prefix = "oss") 19 | public class OssClientConfig { 20 | 21 | private String accessKeyId; 22 | 23 | private String accessKeySecret; 24 | 25 | private String endpoint; 26 | 27 | private String bucket; 28 | 29 | @Bean 30 | public OSS ossClient() { 31 | return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/config/WeWorkConfig.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | 8 | /** 9 | * @author chianyin 10 | * 企业微信配置文件 11 | */ 12 | @Configuration 13 | @Data 14 | @ConfigurationProperties(prefix = "app") 15 | public class WeWorkConfig { 16 | 17 | private Integer limit = 100; 18 | 19 | private Integer timeout = 10; 20 | 21 | private String corpId; 22 | 23 | private String corpSecret; 24 | 25 | private String env; 26 | 27 | private String proxy; 28 | 29 | private String passwd; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/config/WeWorkEncryptConfig.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import java.util.Map; 8 | import java.util.TreeMap; 9 | 10 | 11 | /** 12 | * @author chianyin 13 | * 企业微信私钥 14 | * key格式 : publicKeyVer,privateKey 15 | * 这个是由 pkcs1 转来的 pkcs8.key 16 | */ 17 | @Configuration 18 | @Data 19 | @ConfigurationProperties(prefix = "encrypt") 20 | public class WeWorkEncryptConfig { 21 | 22 | /** 23 | * 加密私钥 <版本号,私钥内容> 24 | */ 25 | private Map privateKey = new TreeMap<>(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/ChatDataDTO.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author chianyin 10 | * 企业微信接口获取的传输对象 11 | */ 12 | @Data 13 | public class ChatDataDTO { 14 | 15 | /** 16 | * 0表示成功,错误返回非0错误码,需要参看errmsg。Uint32类型 17 | */ 18 | @JSONField(name = "errcode") 19 | private Integer errCode; 20 | 21 | /** 22 | * 返回信息,如非空为错误原因。String类型 23 | */ 24 | @JSONField(name = "errmsg") 25 | private String errMsg; 26 | 27 | /** 28 | * 聊天记录数据内容。数组类型。包括seq、msgid等内容 29 | */ 30 | @JSONField(name = "chatdata") 31 | protected List chatData; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/ChatDataDetailDTO.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 企业微信接口获取的传输内层对象 9 | */ 10 | @Data 11 | public class ChatDataDetailDTO { 12 | 13 | /** 14 | * 消息的seq值,标识消息的序号。再次拉取需要带上上次回包中最大的seq。Uint64类型,范围0-pow(2,64)-1 15 | */ 16 | @JSONField(name = "seq") 17 | private long seq; 18 | 19 | /** 20 | * 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。String类型。msgid以_external结尾的消息,表明该消息是一条外部消息。 21 | */ 22 | @JSONField(name = "msgid") 23 | private String msgId; 24 | 25 | /** 26 | * 加密此条消息使用的公钥版本号。Uint32类型 27 | */ 28 | @JSONField(name = "publickey_ver") 29 | private Integer publicKeyVer; 30 | 31 | /** 32 | * 使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。String类型 33 | */ 34 | @JSONField(name = "encrypt_random_key") 35 | private String encryptRandomKey; 36 | 37 | /** 38 | * 消息密文。需要业务方使用将encrypt_random_key解密得到的内容,与encrypt_chat_msg,传入sdk接口DecryptData,得到消息明文。String类型 39 | */ 40 | @JSONField(name = "encrypt_chat_msg") 41 | private String encryptChatMsg; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/MediaFileDTO.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | import com.chinayin.wework.chatdata.model.messagetype.TypeEmotion; 6 | import com.chinayin.wework.chatdata.model.messagetype.TypeFile; 7 | import com.chinayin.wework.chatdata.model.messagetype.TypeImage; 8 | import com.chinayin.wework.chatdata.model.messagetype.TypeVideo; 9 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeChatRecordItem; 10 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeMixedItem; 11 | import lombok.Data; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | /** 19 | * @author chianyin 20 | */ 21 | @Data 22 | @Slf4j 23 | public class MediaFileDTO { 24 | 25 | /** 26 | * 固定值,队列服务时使用 27 | */ 28 | @JSONField(ordinal = 1, name = "job") 29 | private String job = "ChatMessageMedia"; 30 | 31 | @JSONField(ordinal = 2, name = "msgtype") 32 | private String msgType; 33 | 34 | @JSONField(ordinal = 3, name = "msgid") 35 | private String msgId; 36 | 37 | @JSONField(ordinal = 4, name = "sdkfileid") 38 | private String sdkFileId; 39 | 40 | @JSONField(ordinal = 5, name = "md5sum") 41 | private String md5sum; 42 | 43 | @JSONField(ordinal = 6, name = "file_ext") 44 | private String fileExt; 45 | 46 | @JSONField(ordinal = 7, name = "file_size") 47 | private long fileSize; 48 | 49 | /** 50 | * oss保存文件名 51 | */ 52 | @JSONField(ordinal = 8, name = "oss_url") 53 | private String ossUrl; 54 | 55 | public static final String MSG_TYPE_IMAGE = "image"; 56 | public static final String MSG_TYPE_VOICE = "voice"; 57 | public static final String MSG_TYPE_VIDEO = "video"; 58 | public static final String MSG_TYPE_EMOTION = "emotion"; 59 | public static final String MSG_TYPE_FILE = "file"; 60 | // 61 | public static final String MSG_TYPE_MIXED = "mixed"; 62 | public static final String MSG_TYPE_CHAT_RECORD = "chatrecord"; 63 | 64 | public static final List NEED_MEDIA_FILE_TYPE = new ArrayList() {{ 65 | add(MSG_TYPE_IMAGE); 66 | add(MSG_TYPE_VOICE); 67 | add(MSG_TYPE_VIDEO); 68 | add(MSG_TYPE_EMOTION); 69 | add(MSG_TYPE_FILE); 70 | }}; 71 | 72 | public MediaFileDTO() { 73 | 74 | } 75 | 76 | /** 77 | * 普通消息 78 | * 79 | * @param msgDTO 80 | */ 81 | public MediaFileDTO(MessageDTO msgDTO) { 82 | this.msgType = msgDTO.getMsgType(); 83 | this.msgId = msgDTO.getMsgId(); 84 | switch (msgDTO.getMsgType()) { 85 | case MSG_TYPE_IMAGE: 86 | this.sdkFileId = msgDTO.getImage().getSdkFileId(); 87 | this.md5sum = msgDTO.getImage().getMd5sum(); 88 | this.fileSize = msgDTO.getImage().getFileSize(); 89 | this.fileExt = "jpg"; 90 | break; 91 | case MSG_TYPE_EMOTION: 92 | this.sdkFileId = msgDTO.getEmotion().getSdkFileId(); 93 | this.md5sum = msgDTO.getEmotion().getMd5sum(); 94 | this.fileSize = msgDTO.getEmotion().getImageSize(); 95 | if (msgDTO.getEmotion().getType() == 1) { 96 | this.fileExt = "gif"; 97 | } else { 98 | this.fileExt = "png"; 99 | } 100 | break; 101 | case MSG_TYPE_VOICE: 102 | this.sdkFileId = msgDTO.getVoice().getSdkFileId(); 103 | this.md5sum = msgDTO.getVoice().getMd5sum(); 104 | this.fileSize = msgDTO.getVoice().getVoiceSize(); 105 | this.fileExt = "amr"; 106 | break; 107 | case MSG_TYPE_VIDEO: 108 | this.sdkFileId = msgDTO.getVideo().getSdkFileId(); 109 | this.md5sum = msgDTO.getVideo().getMd5sum(); 110 | this.fileSize = msgDTO.getVideo().getFileSize(); 111 | this.fileExt = "mp4"; 112 | break; 113 | case MSG_TYPE_FILE: 114 | this.sdkFileId = msgDTO.getFile().getSdkFileId(); 115 | this.md5sum = msgDTO.getFile().getMd5sum(); 116 | this.fileSize = msgDTO.getFile().getFileSize(); 117 | this.fileExt = msgDTO.getFile().getFileExt(); 118 | break; 119 | } 120 | // 发现oss上传时有文件md5是空的情况,特殊处理一下 121 | this.fixMsgMd5sum(); 122 | } 123 | 124 | /** 125 | * 混合消息 126 | * 127 | * @param item 128 | * @param message 129 | */ 130 | public MediaFileDTO(TypeMixedItem item, MessageDTO message) { 131 | this.msgType = item.getType(); 132 | this.msgId = message.getMsgId(); 133 | switch (this.msgType) { 134 | case MSG_TYPE_IMAGE: 135 | TypeImage image = JSON.parseObject(item.getContent(), TypeImage.class); 136 | this.sdkFileId = image.getSdkFileId(); 137 | this.md5sum = image.getMd5sum(); 138 | this.fileSize = image.getFileSize(); 139 | this.fileExt = "jpg"; 140 | break; 141 | case MSG_TYPE_EMOTION: 142 | TypeEmotion emo = JSON.parseObject(item.getContent(), TypeEmotion.class); 143 | this.sdkFileId = emo.getSdkFileId(); 144 | this.md5sum = emo.getMd5sum(); 145 | this.fileSize = emo.getImageSize(); 146 | if (emo.getType() == 1) { 147 | this.fileExt = "gif"; 148 | } else { 149 | this.fileExt = "png"; 150 | } 151 | break; 152 | case MSG_TYPE_VOICE: 153 | // mixed消息无法同时有语音 154 | break; 155 | case MSG_TYPE_VIDEO: 156 | TypeVideo video = JSON.parseObject(item.getContent(), TypeVideo.class); 157 | this.sdkFileId = video.getSdkFileId(); 158 | this.md5sum = video.getMd5sum(); 159 | this.fileSize = video.getFileSize(); 160 | this.fileExt = "mp4"; 161 | break; 162 | case MSG_TYPE_FILE: 163 | TypeFile file = JSON.parseObject(item.getContent(), TypeFile.class); 164 | this.sdkFileId = file.getSdkFileId(); 165 | this.md5sum = file.getMd5sum(); 166 | this.fileSize = file.getFileSize(); 167 | this.fileExt = file.getFileExt(); 168 | break; 169 | } 170 | // 发现oss上传时有文件md5是空的情况,特殊处理一下 171 | this.fixMsgMd5sum(); 172 | } 173 | 174 | /** 175 | * 转发消息 176 | * 177 | * @param item 178 | * @param message 179 | */ 180 | public MediaFileDTO(TypeChatRecordItem item, MessageDTO message) { 181 | this.msgType = this.parseChatRecordType(item.getType()); 182 | this.msgId = message.getMsgId(); 183 | switch (this.msgType) { 184 | case MSG_TYPE_IMAGE: 185 | TypeImage image = JSON.parseObject(item.getContent(), TypeImage.class); 186 | this.sdkFileId = image.getSdkFileId(); 187 | this.md5sum = image.getMd5sum(); 188 | this.fileSize = image.getFileSize(); 189 | this.fileExt = "jpg"; 190 | break; 191 | case MSG_TYPE_EMOTION: 192 | TypeEmotion emo = JSON.parseObject(item.getContent(), TypeEmotion.class); 193 | this.sdkFileId = emo.getSdkFileId(); 194 | this.md5sum = emo.getMd5sum(); 195 | this.fileSize = emo.getImageSize(); 196 | if (emo.getType() == 1) { 197 | this.fileExt = "gif"; 198 | } else { 199 | this.fileExt = "png"; 200 | } 201 | break; 202 | case MSG_TYPE_VOICE: 203 | // mixed消息无法同时有语音 204 | break; 205 | case MSG_TYPE_VIDEO: 206 | TypeVideo video = JSON.parseObject(item.getContent(), TypeVideo.class); 207 | this.sdkFileId = video.getSdkFileId(); 208 | this.md5sum = video.getMd5sum(); 209 | this.fileSize = video.getFileSize(); 210 | this.fileExt = "mp4"; 211 | break; 212 | case MSG_TYPE_FILE: 213 | TypeFile file = JSON.parseObject(item.getContent(), TypeFile.class); 214 | this.sdkFileId = file.getSdkFileId(); 215 | this.md5sum = file.getMd5sum(); 216 | this.fileSize = file.getFileSize(); 217 | this.fileExt = file.getFileExt(); 218 | break; 219 | } 220 | // 发现oss上传时有文件md5是空的情况,特殊处理一下 221 | this.fixMsgMd5sum(); 222 | } 223 | 224 | /** 225 | * 格式化为普通type值 226 | * 227 | * @param type 228 | * @return 229 | */ 230 | private String parseChatRecordType(String type) { 231 | return type.replace("ChatRecord", "").toLowerCase(); 232 | } 233 | 234 | /** 235 | * 修复md5sum为空情况 236 | */ 237 | private void fixMsgMd5sum() { 238 | if (Objects.nonNull(this.sdkFileId) && ( 239 | Objects.isNull(this.md5sum) || this.md5sum.length() == 0 240 | )) { 241 | log.error("md5sum empty, msgId: %s", this.msgId); 242 | // 随机生成一个 243 | String md5sum = "md5sum-" + System.currentTimeMillis(); 244 | this.md5sum = md5sum; 245 | } 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/MessageDTO.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.chinayin.wework.chatdata.model.messagetype.*; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author chianyin 11 | * 企业微信接口获取的实际解密后消息 12 | */ 13 | @Data 14 | public class MessageDTO { 15 | 16 | /** 17 | * 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。String类型 18 | */ 19 | @JSONField(ordinal = 1, name = "action") 20 | private String action; 21 | 22 | /** 23 | * 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。String类型 24 | */ 25 | @JSONField(ordinal = 2, name = "msgid") 26 | private String msgId; 27 | 28 | /** 29 | * 消息类型。String类型 30 | */ 31 | @JSONField(ordinal = 3, name = "msgtype") 32 | private String msgType; 33 | 34 | /** 35 | * 消息发送时间戳,utc时间,ms单位。 36 | */ 37 | @JSONField(ordinal = 4, name = "msgtime") 38 | private Long msgTime; 39 | 40 | /** 41 | * 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。String类型 42 | */ 43 | @JSONField(ordinal = 20, name = "from") 44 | private String from; 45 | 46 | /** 47 | * 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。数组,内容为string类型 48 | */ 49 | @JSONField(ordinal = 20, name = "tolist") 50 | private List toList; 51 | 52 | /** 53 | * 群聊消息的群id。如果是单聊则为空。String类型 54 | */ 55 | @JSONField(ordinal = 20, name = "roomid") 56 | private String roomId; 57 | 58 | 59 | /** 60 | * ============ 消息类型 ============ 61 | * @link https://developer.work.weixin.qq.com/document/path/91774 62 | */ 63 | 64 | /** 65 | * 文本 66 | */ 67 | @JSONField(ordinal = 10, name = "text") 68 | private TypeText text; 69 | 70 | /** 71 | * 图片 72 | */ 73 | @JSONField(ordinal = 10, name = "image") 74 | private TypeImage image; 75 | 76 | /** 77 | * 撤回消息 78 | */ 79 | @JSONField(ordinal = 10, name = "revoke") 80 | private TypeRevoke revoke; 81 | 82 | /** 83 | * 同意会话聊天内容 84 | */ 85 | @JSONField(ordinal = 10, name = "agree") 86 | private TypeAgree agree; 87 | 88 | @JSONField(ordinal = 10, name = "disagree") 89 | private TypeDisagree disagree; 90 | 91 | /** 92 | * 语音 93 | */ 94 | @JSONField(ordinal = 10, name = "voice") 95 | private TypeVoice voice; 96 | 97 | /** 98 | * 视频 99 | */ 100 | @JSONField(ordinal = 10, name = "video") 101 | private TypeVideo video; 102 | 103 | /** 104 | * 名片 105 | */ 106 | @JSONField(ordinal = 10, name = "card") 107 | private TypeCard card; 108 | 109 | /** 110 | * 位置 111 | */ 112 | @JSONField(ordinal = 10, name = "location") 113 | private TypeLocation location; 114 | 115 | /** 116 | * 表情 117 | */ 118 | @JSONField(ordinal = 10, name = "emotion") 119 | private TypeEmotion emotion; 120 | 121 | /** 122 | * 文件 123 | */ 124 | @JSONField(ordinal = 10, name = "file") 125 | private TypeFile file; 126 | 127 | /** 128 | * 链接 129 | */ 130 | @JSONField(ordinal = 10, name = "link") 131 | private TypeLink link; 132 | 133 | /** 134 | * 小程序 135 | */ 136 | @JSONField(ordinal = 10, name = "weapp") 137 | private TypeWeapp weapp; 138 | 139 | /** 140 | * 会话记录 141 | */ 142 | @JSONField(ordinal = 10, name = "chatrecord") 143 | private TypeChatRecord chatRecord; 144 | 145 | /** 146 | * 待办 147 | */ 148 | @JSONField(ordinal = 10, name = "todo") 149 | private TypeTodo todo; 150 | 151 | /** 152 | * 投票 153 | */ 154 | @JSONField(ordinal = 10, name = "vote") 155 | private TypeVote vote; 156 | 157 | /** 158 | * 填表 159 | */ 160 | @JSONField(ordinal = 10, name = "collect") 161 | private TypeCollect collect; 162 | 163 | /** 164 | * 红包 165 | * and 166 | * 互通红包 167 | */ 168 | @JSONField(ordinal = 10, name = "redpacket", alternateNames = {"redpacket"}) 169 | private TypeRedPacket redpacket; 170 | 171 | /** 172 | * 会议邀请 173 | */ 174 | @JSONField(ordinal = 10, name = "meeting") 175 | private TypeMeeting meeting; 176 | 177 | /** 178 | * 切换企业日志 179 | * todo 180 | */ 181 | 182 | /** 183 | * 在线文档 184 | */ 185 | @JSONField(ordinal = 10, name = "doc") 186 | private TypeDoc doc; 187 | 188 | /** 189 | * markdown 190 | */ 191 | @JSONField(ordinal = 10, name = "info") 192 | private TypeMarkdown markdown; 193 | 194 | /** 195 | * 图文 196 | */ 197 | @JSONField(ordinal = 10, name = "info") 198 | private TypeNews news; 199 | 200 | /** 201 | * 日程 202 | */ 203 | @JSONField(ordinal = 10, name = "calendar") 204 | private TypeCalendar calendar; 205 | 206 | /** 207 | * 混合消息 208 | */ 209 | @JSONField(ordinal = 10, name = "mixed") 210 | private TypeMixed mixed; 211 | 212 | 213 | /** 214 | * 音视频通话 215 | */ 216 | @JSONField(ordinal = 10, name = "info") 217 | private TypeVoiptext voiptext; 218 | 219 | /** 220 | * 音频存档 221 | * todo 222 | */ 223 | 224 | /** 225 | * 音频共享文档 226 | * todo 227 | */ 228 | 229 | /** 230 | * 视频号消息 231 | */ 232 | @JSONField(ordinal = 10, name = "sphfeed") 233 | private TypeSphFeed sphFeed; 234 | 235 | /** 236 | * 微盘文件 237 | */ 238 | @JSONField(ordinal = 10, name = "info") 239 | private TypeQyDiskFile qydiskfile; 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/AbstractTypeFile.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 基础文件 9 | */ 10 | @Data 11 | abstract public class AbstractTypeFile { 12 | 13 | /** 14 | * 媒体资源的id信息。String类型 15 | */ 16 | @JSONField(name = "sdkfileid") 17 | private String sdkFileId; 18 | 19 | /** 20 | * 资源的md5值,供进行校验。String类型 21 | */ 22 | private String md5sum; 23 | 24 | /** 25 | * 资源的文件大小。Uint32类型 26 | */ 27 | @JSONField(name = "filesize") 28 | private Integer fileSize; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeAgree.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 同意会话聊天内容消息(同意和不同意共用) 9 | */ 10 | @Data 11 | public class TypeAgree { 12 | 13 | /** 14 | * 同意/不同意协议者的userid,外部企业默认为external_userid 15 | */ 16 | @JSONField(name = "userid") 17 | private String userId; 18 | 19 | /** 20 | * 同意/不同意协议的时间,utc时间,ms单位。 21 | */ 22 | @JSONField(name = "agree_time") 23 | private Long agreeTime; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeCalendar.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 日程消息 9 | */ 10 | @Data 11 | public class TypeCalendar { 12 | 13 | /** 14 | * 日程主题 15 | */ 16 | private String title; 17 | 18 | /** 19 | * 日程组织者 20 | */ 21 | @JSONField(name = "creatorname") 22 | private String creatorName; 23 | 24 | /** 25 | * 日程参与人 26 | */ 27 | @JSONField(name = "attendeename") 28 | private String[] attendeeName; 29 | 30 | /** 31 | * 日程开始时间。Utc时间,单位秒 32 | */ 33 | @JSONField(name = "starttime") 34 | private Long startTime; 35 | 36 | /** 37 | * 日程结束时间。Utc时间,单位秒 38 | */ 39 | @JSONField(name = "endtime") 40 | private Long endTime; 41 | 42 | /** 43 | * 日程地点 44 | */ 45 | private String place; 46 | 47 | /** 48 | * 日程备注 49 | */ 50 | private String remarks; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeCard.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 名片消息 9 | */ 10 | @Data 11 | public class TypeCard { 12 | 13 | /** 14 | * 名片所有者所在的公司名称 15 | */ 16 | @JSONField(name = "corpname") 17 | private String corpName; 18 | 19 | /** 20 | * 名片所有者的id,同一公司是userid,不同公司是external_userid 21 | */ 22 | @JSONField(name = "userid") 23 | private String userId; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeChatRecord.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeChatRecordItem; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author chianyin 11 | * 会话记录消息 12 | */ 13 | @Data 14 | public class TypeChatRecord { 15 | 16 | /** 17 | * 聊天记录标题。String类型 18 | */ 19 | @JSONField(ordinal = 1) 20 | private String title; 21 | 22 | /** 23 | * 消息记录内的消息内容,批量数据 24 | */ 25 | @JSONField(ordinal = 2) 26 | private List item; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeCollect.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author chianyin 10 | * 填表消息 11 | */ 12 | @Data 13 | public class TypeCollect { 14 | 15 | /** 16 | * 填表消息所在的群名称 17 | */ 18 | @JSONField(name = "room_name") 19 | private String roomName; 20 | 21 | /** 22 | * 创建者在群中的名字 23 | */ 24 | private String creator; 25 | 26 | /** 27 | * 创建的时间 28 | */ 29 | @JSONField(name = "create_time") 30 | private String createTime; 31 | 32 | /** 33 | * 表名 34 | */ 35 | private String title; 36 | 37 | /** 38 | * 表内容 39 | */ 40 | private List details; 41 | 42 | } 43 | 44 | @Data 45 | class TypeCollectItem { 46 | 47 | /** 48 | * 表项id 49 | */ 50 | private long id; 51 | /** 52 | * 表项名称 53 | */ 54 | private String ques; 55 | 56 | /** 57 | * 表项类型 有Text(文本),Number(数字),Date(日期),Time(时间) 58 | */ 59 | private String type; 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeDisagree.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 同意会话聊天内容消息(同意和不同意共用) 9 | */ 10 | @Data 11 | public class TypeDisagree { 12 | 13 | /** 14 | * 同意/不同意协议者的userid,外部企业默认为external_userid 15 | */ 16 | @JSONField(name = "userid") 17 | private String userId; 18 | 19 | /** 20 | * 同意/不同意协议的时间,utc时间,ms单位。 21 | */ 22 | @JSONField(name = "disagree_time") 23 | private Long agreeTime; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeDoc.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 在线文档消息 9 | */ 10 | @Data 11 | public class TypeDoc { 12 | 13 | /** 14 | * 在线文档名称 15 | */ 16 | private String title; 17 | 18 | /** 19 | * 在线文档链接 20 | */ 21 | @JSONField(name = "link_url") 22 | private String linkUrl; 23 | 24 | /** 25 | * 在线文档创建者。本企业成员创建为userid;外部企业成员创建为external_userid 26 | */ 27 | @JSONField(name = "doc_creator") 28 | private String docCreator; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeEmotion.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 表情消息 9 | */ 10 | @Data 11 | public class TypeEmotion extends AbstractTypeFile { 12 | 13 | /** 14 | * 表情类型,png或者gif.1表示gif 2表示png。Uint32类型 15 | */ 16 | private Integer type; 17 | 18 | /** 19 | * 表情图片宽度。Uint32类型 20 | */ 21 | private Integer width; 22 | 23 | /** 24 | * 表情图片高度。Uint32类型 25 | */ 26 | private Integer height; 27 | 28 | /** 29 | * 资源的文件大小。Uint32类型 30 | */ 31 | @JSONField(name = "imagesize") 32 | private Integer imageSize; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeFile.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 文件消息 9 | */ 10 | @Data 11 | public class TypeFile extends AbstractTypeFile { 12 | 13 | /** 14 | * 文件名称。String类型 15 | */ 16 | private String filename; 17 | 18 | /** 19 | * 文件类型后缀。String类型 20 | */ 21 | @JSONField(name = "fileext") 22 | private String fileExt; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeImage.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author chianyin 7 | * 图片消息 8 | */ 9 | @Data 10 | public class TypeImage extends AbstractTypeFile { 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeLink.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 链接消息 9 | */ 10 | @Data 11 | public class TypeLink { 12 | 13 | /** 14 | * 消息标题 15 | */ 16 | private String title; 17 | 18 | /** 19 | * 消息描述 20 | */ 21 | private String description; 22 | 23 | /** 24 | * 链接url地址 25 | */ 26 | @JSONField(name = "link_url") 27 | private String linkUrl; 28 | 29 | /** 30 | * 链接图片url 31 | */ 32 | @JSONField(name = "image_url") 33 | private String imageUrl; 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeLocation.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author chianyin 7 | * 位置消息 8 | */ 9 | @Data 10 | public class TypeLocation { 11 | 12 | /** 13 | * 经度,单位double 14 | */ 15 | private Double longitude; 16 | 17 | /** 18 | * 经纬度度,单位double 19 | */ 20 | private Double latitude; 21 | 22 | /** 23 | * 地址信息 24 | */ 25 | private String address; 26 | 27 | /** 28 | * 位置信息的title 29 | */ 30 | private String title; 31 | 32 | /** 33 | * 缩放比例 34 | */ 35 | private Integer zoom; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeMarkdown.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author chianyin 7 | * MarkDown格式消息 8 | */ 9 | @Data 10 | public class TypeMarkdown { 11 | 12 | /** 13 | * markdown消息内容,目前为机器人发出的消息 14 | */ 15 | private String content; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeMeeting.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 会议邀请消息 9 | */ 10 | @Data 11 | public class TypeMeeting { 12 | 13 | /** 14 | * 会议主题 15 | */ 16 | private String topic; 17 | 18 | /** 19 | * 会议开始时间 Utc时间 20 | */ 21 | @JSONField(name = "starttime") 22 | private Long startTime; 23 | 24 | /** 25 | * 会议结束时间 Utc时间 26 | */ 27 | @JSONField(name = "endtime") 28 | private Long endTime; 29 | 30 | /** 31 | * 会议地址 32 | */ 33 | private String address; 34 | 35 | /** 36 | * 会议备注 37 | */ 38 | private String remarks; 39 | 40 | /** 41 | * 会议消息类型。101发起会议邀请消息、102处理会议邀请消息 42 | */ 43 | @JSONField(name = "meetingtype") 44 | private Integer meetingType; 45 | 46 | /** 47 | * 会议id。方便将发起、处理消息进行对照 48 | */ 49 | @JSONField(name = "meetingid") 50 | private Long meetingId; 51 | 52 | /** 53 | * 会议邀请处理状态。1参加会议、2拒绝会议、3待定、4未被邀请、5会议已取消、6会议已过期、7不在房间内。Uint32类型。只有meetingtype为102的时候此字段才有内容 54 | */ 55 | @JSONField(name = "status") 56 | private Integer status; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeMixed.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeMixedItem; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author chianyin 10 | * 混合消息 11 | */ 12 | @Data 13 | public class TypeMixed { 14 | 15 | /** 16 | * 混合消息列表 17 | */ 18 | private List item; 19 | } -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeNews.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author chianyin 10 | * 图文消息 11 | */ 12 | @Data 13 | public class TypeNews { 14 | 15 | /** 16 | * 图文消息数组,每个item结构包含title、description、url、picurl等结构 17 | */ 18 | private List item; 19 | 20 | } 21 | 22 | @Data 23 | class TypeNewsItem { 24 | 25 | /** 26 | * 图文消息标题 27 | */ 28 | private String title; 29 | 30 | /** 31 | * 图文消息描述 32 | */ 33 | private String description; 34 | 35 | /** 36 | * 图文消息点击跳转地址 37 | */ 38 | private String url; 39 | 40 | /** 41 | * 图文消息配图的url 42 | */ 43 | @JSONField(name = "picurl") 44 | private String picUrl; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeQyDiskFile.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 微盘文件 9 | */ 10 | @Data 11 | public class TypeQyDiskFile { 12 | 13 | /** 14 | * 文件名称 15 | */ 16 | @JSONField(name = "filename") 17 | private String filename; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeRedPacket.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 红包消息 9 | */ 10 | @Data 11 | public class TypeRedPacket { 12 | 13 | /** 14 | * 红包消息类型。1 普通红包、2 拼手气群红包、3 激励群红包 15 | */ 16 | private Integer type; 17 | 18 | /** 19 | * 红包祝福语 20 | */ 21 | private String wish; 22 | 23 | /** 24 | * 红包总个数 25 | */ 26 | @JSONField(name = "totalcnt") 27 | private Integer totalCnt; 28 | 29 | /** 30 | * 红包总金额,单位为分 31 | */ 32 | @JSONField(name = "totalamount") 33 | private Integer totalAmount; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeRevoke.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 撤回消息 9 | */ 10 | @Data 11 | public class TypeRevoke { 12 | 13 | /** 14 | * 标识撤回的原消息的msgid 15 | */ 16 | @JSONField(name = "pre_msgid") 17 | private String preMsgId; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeSphFeed.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 视频号消息 9 | */ 10 | @Data 11 | public class TypeSphFeed { 12 | 13 | /** 14 | * 视频号消息类型。2 图片、4 视频、9 直播 15 | */ 16 | @JSONField(name = "feed_type") 17 | private Integer feedType; 18 | 19 | /** 20 | * 视频号账号名称 21 | */ 22 | @JSONField(name = "sph_name") 23 | private String sphName; 24 | 25 | /** 26 | * 视频号消息描述 27 | */ 28 | @JSONField(name = "feed_desc") 29 | private String feedDesc; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeText.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author chianyin 7 | * 文本消息 8 | */ 9 | @Data 10 | public class TypeText { 11 | 12 | /** 13 | * type 为 text 的时候文本内容 14 | */ 15 | private String content; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeTodo.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author chianyin 7 | * 小程序消息 8 | */ 9 | @Data 10 | public class TypeTodo { 11 | 12 | /** 13 | * 待办的来源文本 14 | */ 15 | private String title; 16 | 17 | /** 18 | * 待办的具体内容 19 | */ 20 | private String content; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeVideo.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 视频消息 9 | */ 10 | @Data 11 | public class TypeVideo extends AbstractTypeFile { 12 | 13 | /** 14 | * 视频播放长度。Uint32类型 15 | */ 16 | @JSONField(name = "play_length") 17 | private Integer playLength; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeVoice.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 语音消息 9 | */ 10 | @Data 11 | public class TypeVoice extends AbstractTypeFile { 12 | 13 | /** 14 | * 语音消息大小。Uint32类型 15 | */ 16 | @JSONField(name = "voice_size") 17 | private Integer voiceSize; 18 | 19 | /** 20 | * 播放长度。Uint32类型 21 | */ 22 | @JSONField(name = "play_length") 23 | private Integer playLength; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeVoiptext.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 音视频通话 9 | */ 10 | @Data 11 | public class TypeVoiptext extends AbstractTypeFile { 12 | 13 | @JSONField(name = "callduration") 14 | private Integer callduration; 15 | 16 | @JSONField(name = "invitetype") 17 | private Integer invitetype; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeVote.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 投票消息 9 | */ 10 | @Data 11 | public class TypeVote { 12 | 13 | /** 14 | * 投票主题 15 | */ 16 | @JSONField(name = "votetitle") 17 | private String title; 18 | 19 | /** 20 | * 投票选项,可能多个内容 21 | */ 22 | @JSONField(name = "voteitem") 23 | private String[] item; 24 | 25 | /** 26 | * 投票类型.101发起投票、102参与投票 27 | */ 28 | @JSONField(name = "votetype") 29 | private Integer type; 30 | 31 | /** 32 | * 投票id,方便将参与投票消息与发起投票消息进行前后对照 33 | */ 34 | @JSONField(name = "voteid") 35 | private String id; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/TypeWeapp.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 小程序消息 9 | */ 10 | @Data 11 | public class TypeWeapp { 12 | 13 | /** 14 | * 消息标题 15 | */ 16 | private String title; 17 | 18 | /** 19 | * 消息描述 20 | */ 21 | private String description; 22 | 23 | /** 24 | * 用户名称 25 | */ 26 | @JSONField(name = "username") 27 | private String userName; 28 | 29 | /** 30 | * 小程序名称 31 | */ 32 | @JSONField(name = "displayname") 33 | private String displayName; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/item/TypeChatRecordItem.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype.item; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 会话记录消息 item 内容 9 | */ 10 | @Data 11 | public class TypeChatRecordItem { 12 | 13 | /** 14 | * 每条聊天记录的具体消息类型:ChatRecordText/ ChatRecordFile/ ChatRecordImage/ ChatRecordVideo/ ChatRecordLink/ ChatRecordLocation/ ChatRecordMixed …. 15 | */ 16 | @JSONField(ordinal = 1) 17 | private String type; 18 | 19 | /** 20 | * 消息时间,utc时间,单位秒。 21 | */ 22 | @JSONField(ordinal = 2, name = "msgtime") 23 | private long msgTime; 24 | 25 | /** 26 | * 消息内容。Json串,内容为对应类型的json。String类型 27 | */ 28 | @JSONField(ordinal = 3) 29 | private String content; 30 | 31 | /** 32 | * 是否来自群会话。Bool类型 33 | */ 34 | @JSONField(ordinal = 4, name = "from_chatroom") 35 | private boolean fromChatRoom; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/model/messagetype/item/TypeMixedItem.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.model.messagetype.item; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author chianyin 8 | * 混合消息Item 9 | */ 10 | 11 | @Data 12 | public class TypeMixedItem { 13 | 14 | /** 15 | * 混合消息单条类型 16 | */ 17 | @JSONField(ordinal = 1) 18 | private String type; 19 | 20 | /** 21 | * 混合消息内容 22 | */ 23 | @JSONField(ordinal = 2) 24 | private String content; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/schedule/ChatDataSchedule.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.schedule; 2 | 3 | import com.chinayin.wework.chatdata.service.ChatDataService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Resource; 9 | import java.io.*; 10 | import java.util.Objects; 11 | 12 | /** 13 | * @author chianyin 14 | * 获取微信会话消息 15 | */ 16 | @Slf4j 17 | @Component 18 | public class ChatDataSchedule { 19 | 20 | /** 21 | * seq 文件保存位置 22 | */ 23 | private static final String seqFileName = "/tmp/wework/seq.txt"; 24 | 25 | @Resource 26 | private ChatDataService chatDataService; 27 | 28 | @Scheduled(fixedDelay = 1000) 29 | public void syncChatData() { 30 | long seqNum = this.getSeqNum(); 31 | log.info("seqNum : {}", seqNum); 32 | try { 33 | long latestSeqNum = chatDataService.getChatData(seqNum); 34 | this.setSeqNum(latestSeqNum); 35 | } catch (Exception e) { 36 | log.error("[异常]主流程失败: {}", e.getMessage()); 37 | } 38 | } 39 | 40 | private int getSeqNum() { 41 | try { 42 | BufferedReader in = new BufferedReader(new FileReader(seqFileName)); 43 | String seqNum = in.readLine(); 44 | log.info("[流程]读取 seqNum: {}", seqNum); 45 | if (Objects.nonNull(seqNum)) { 46 | return Integer.valueOf(seqNum); 47 | } 48 | } catch (Exception e) { 49 | log.error("[异常]读取 seqNum 失败: {}", e.getMessage()); 50 | } 51 | return 0; 52 | } 53 | 54 | private void setSeqNum(long seqNum) { 55 | try { 56 | BufferedWriter out = new BufferedWriter(new FileWriter(seqFileName)); 57 | out.write(String.valueOf(seqNum)); 58 | out.close(); 59 | log.info("[流程]记录 seqNum: {}", seqNum); 60 | } catch (IOException e) { 61 | log.error("[异常]记录 seqNum 失败: {}", e.getMessage()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/ChatDataService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | /** 4 | * @author chianyin 5 | */ 6 | public interface ChatDataService { 7 | 8 | /** 9 | * 拉微信消息 10 | * 11 | * @param seq 起始位置 12 | * @return 拉取位置 13 | */ 14 | long getChatData(long seq); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/ClientService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | /** 4 | * @author chianyin 5 | */ 6 | public interface ClientService { 7 | long getSdk(); 8 | 9 | void downloadMediaFile(String sdkFileId, String file, long fileSize); 10 | 11 | String decryptData(Integer publicKeyVer, String encryptKey, String encryptChatMsg); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/EncryptService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | /** 4 | * @author chianyin 5 | */ 6 | public interface EncryptService { 7 | 8 | /** 9 | * 解密内容 10 | * 11 | * @param encryptRandomKey 12 | */ 13 | String decodeEncryptRandomKey(Integer publicKeyVer, String encryptRandomKey); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/FileOutputService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | /** 4 | * @author chianyin 5 | */ 6 | public interface FileOutputService { 7 | 8 | /** 9 | * 把消息保存到文件 10 | * 11 | * @param msg 消息 12 | */ 13 | void saveMessage(String msg); 14 | 15 | /** 16 | * 记录未处理的消息 17 | * 18 | * @param msg 消息 19 | */ 20 | void saveOriginMessage(String msg); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/MnsClientService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | import com.aliyun.mns.model.Message; 4 | 5 | /** 6 | * @author chianyin 7 | */ 8 | public interface MnsClientService { 9 | /** 10 | * 发送消息到 mns 11 | * 12 | * @param msg 13 | * @return 14 | */ 15 | String sendMessageToQueue(String msg); 16 | 17 | String sendMessageToQueue(Message msg); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/OssClientService.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service; 2 | 3 | /** 4 | * @author chianyin 5 | */ 6 | public interface OssClientService { 7 | 8 | /** 9 | * 获取媒体地址 10 | * 11 | * @param objectName 12 | * @return 13 | */ 14 | String getMediaOssUrl(String objectName); 15 | 16 | /** 17 | * 判断文件是否存在 18 | * 19 | * @param objectName 20 | * @return 21 | */ 22 | Boolean doesObjectExist(String objectName); 23 | 24 | /** 25 | * 上传资源到 oss 26 | * 27 | * @param objectName 28 | * @param file 29 | */ 30 | void upload(String objectName, String file); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/ChatDataServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.chinayin.wework.chatdata.config.WeWorkConfig; 5 | import com.chinayin.wework.chatdata.model.ChatDataDTO; 6 | import com.chinayin.wework.chatdata.model.ChatDataDetailDTO; 7 | import com.chinayin.wework.chatdata.model.MediaFileDTO; 8 | import com.chinayin.wework.chatdata.model.MessageDTO; 9 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeChatRecordItem; 10 | import com.chinayin.wework.chatdata.model.messagetype.item.TypeMixedItem; 11 | import com.chinayin.wework.chatdata.service.*; 12 | import com.tencent.wework.Finance; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.annotation.Qualifier; 15 | import org.springframework.stereotype.Service; 16 | 17 | import javax.annotation.Resource; 18 | import java.text.SimpleDateFormat; 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.concurrent.ExecutorService; 24 | 25 | /** 26 | * @author chianyin 27 | */ 28 | @Service("FinanceService") 29 | @Slf4j 30 | public class ChatDataServiceImpl implements ChatDataService { 31 | 32 | @Resource 33 | private WeWorkConfig weworkConfig; 34 | 35 | @Resource 36 | private MnsClientService queueService; 37 | 38 | @Resource 39 | private OssClientService ossService; 40 | 41 | @Resource 42 | private FileOutputService fileService; 43 | 44 | @Qualifier("threadPool") 45 | @Resource 46 | private ExecutorService executorService; 47 | 48 | @Resource 49 | private ClientService clientService; 50 | 51 | @Override 52 | public long getChatData(long seq) { 53 | // 拉取消息 54 | long slice = Finance.NewSlice(); 55 | int ret = Finance.GetChatData(clientService.getSdk(), seq, weworkConfig.getLimit(), weworkConfig.getProxy(), weworkConfig.getPasswd(), weworkConfig.getTimeout(), slice); 56 | if (ret != 0) { 57 | log.error("[异常]微信会话获取数据失败, {}", ret); 58 | Finance.FreeSlice(slice); 59 | return seq; 60 | } 61 | // 获取slice内容 62 | ChatDataDTO chatDataDTO = JSON.parseObject(Finance.GetContentFromSlice(slice), ChatDataDTO.class); 63 | if (chatDataDTO.getErrCode() != 0) { 64 | log.error("[异常]拉消息失败, {}", chatDataDTO); 65 | Finance.FreeSlice(slice); 66 | return seq; 67 | } 68 | if (Objects.isNull(chatDataDTO.getChatData()) || chatDataDTO.getChatData().size() == 0) { 69 | log.info("[流程]暂无消息 seqNum {}, {}", seq, chatDataDTO); 70 | Finance.FreeSlice(slice); 71 | return seq; 72 | } 73 | // 逐条处理 74 | for (ChatDataDetailDTO chatDataDetailDTO : chatDataDTO.getChatData()) { 75 | this.handleMessage(chatDataDetailDTO); 76 | seq = chatDataDetailDTO.getSeq(); 77 | } 78 | // 79 | Finance.FreeSlice(slice); 80 | return seq; 81 | } 82 | 83 | 84 | private void handleMessage(ChatDataDetailDTO chatDataDetailDTO) { 85 | String originMsg = clientService.decryptData(chatDataDetailDTO.getPublicKeyVer(), chatDataDetailDTO.getEncryptRandomKey(), chatDataDetailDTO.getEncryptChatMsg()); 86 | if (Objects.isNull(originMsg)) { 87 | return; 88 | } 89 | 90 | // 保存原始数据 91 | fileService.saveOriginMessage(originMsg); 92 | 93 | // 解析数据 94 | MessageDTO messageDTO = null; 95 | try { 96 | messageDTO = JSON.parseObject(originMsg, MessageDTO.class); 97 | } catch (Exception e) { 98 | log.error("[异常]数据解析失败, {}", originMsg); 99 | } 100 | if (Objects.isNull(messageDTO)) { 101 | return; 102 | } 103 | log.info("msgId: {}, seq: {}", chatDataDetailDTO.getMsgId(), chatDataDetailDTO.getSeq()); 104 | 105 | // 发送去消息队列处理,队列中高优先级 106 | String msg = JSON.toJSONString(messageDTO); 107 | log.debug("msg: {}", msg); 108 | com.aliyun.mns.model.Message mns = new com.aliyun.mns.model.Message(msg); 109 | mns.setPriority(3); 110 | queueService.sendMessageToQueue(mns); 111 | 112 | // 非发送类是没有type的,跳过处理 113 | if (Objects.isNull(messageDTO.getMsgType())) { 114 | log.debug("msg_type is null, {}, {}", chatDataDetailDTO.getMsgId(), msg); 115 | return; 116 | } 117 | 118 | // 上传媒体文件 119 | try { 120 | List mediaList = new ArrayList<>(); 121 | // mixed消息,需要判断后拆开处理 122 | if (Objects.nonNull(messageDTO.getMixed()) && Objects.nonNull(messageDTO.getMixed().getItem())) { 123 | for (TypeMixedItem item : messageDTO.getMixed().getItem()) { 124 | if (Objects.isNull(item)) 125 | continue; 126 | MediaFileDTO media = new MediaFileDTO(item, messageDTO); 127 | log.debug("mixed.media, {}", JSON.toJSONString(media)); 128 | mediaList.add(media); 129 | } 130 | } else if (Objects.nonNull(messageDTO.getChatRecord()) && Objects.nonNull(messageDTO.getChatRecord().getItem())) { 131 | for (TypeChatRecordItem item : messageDTO.getChatRecord().getItem()) { 132 | if (Objects.isNull(item)) 133 | continue; 134 | MediaFileDTO media = new MediaFileDTO(item, messageDTO); 135 | log.debug("chatrecord.media, {}", JSON.toJSONString(media)); 136 | mediaList.add(media); 137 | } 138 | } else { 139 | mediaList.add(new MediaFileDTO(messageDTO)); 140 | } 141 | for (MediaFileDTO media : mediaList) { 142 | if (Objects.nonNull(media.getSdkFileId())) { 143 | executorService.submit(() -> handleMedia(media)); 144 | } 145 | } 146 | } catch (Exception ex) { 147 | log.error("[异常]媒体文件解析或处理失败, {}, {}", chatDataDetailDTO.getMsgId(), ex.getMessage()); 148 | } 149 | 150 | } 151 | 152 | private void handleMedia(MediaFileDTO media) { 153 | String sdkFileId = media.getSdkFileId(); 154 | String md5 = media.getMd5sum(); 155 | String day = (new SimpleDateFormat("yyyy/MM")).format(new Date()); 156 | String file = "wework/" + day + "/" + md5 + "." + media.getFileExt(); 157 | String objectName = "wework/" + day + "/" + md5 + "." + media.getFileExt(); 158 | try { 159 | // 当有超级多文件待下载时,且文件较大,带宽会拉满,先判断oss是否存在同名文件 160 | Boolean found = ossService.doesObjectExist(objectName); 161 | if (found) { 162 | log.info("媒体文件跳过: {}, {}, {}", media.getFileSize(), objectName, sdkFileId); 163 | return; 164 | } 165 | // 下载 166 | clientService.downloadMediaFile(sdkFileId, file, media.getFileSize()); 167 | // 上传 168 | ossService.upload(objectName, file); 169 | // 170 | String ossUrl = ossService.getMediaOssUrl(objectName); 171 | media.setOssUrl(ossUrl); 172 | // 发送去消息队列处理,低优先级+延迟 173 | String msg = JSON.toJSONString(media); 174 | log.debug("media: {}", msg); 175 | com.aliyun.mns.model.Message mns = new com.aliyun.mns.model.Message(msg); 176 | mns.setPriority(10); 177 | mns.setDelaySeconds(10); 178 | queueService.sendMessageToQueue(mns); 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | log.error("[异常]处理媒体文件, {}, {}", e.getMessage(), sdkFileId); 182 | } 183 | } 184 | 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/ClientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.chinayin.wework.chatdata.config.WeWorkConfig; 4 | import com.chinayin.wework.chatdata.service.ClientService; 5 | import com.chinayin.wework.chatdata.service.EncryptService; 6 | import com.tencent.wework.Finance; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | 10 | import javax.annotation.Resource; 11 | import java.io.File; 12 | import java.io.FileOutputStream; 13 | import java.util.Objects; 14 | 15 | /** 16 | * @author chianyin 17 | */ 18 | @Service("ClientService") 19 | @Slf4j 20 | public class ClientServiceImpl implements ClientService { 21 | 22 | @Resource 23 | private WeWorkConfig weworkConfig; 24 | 25 | @Resource 26 | private EncryptService encryptService; 27 | 28 | private long sdk = 0; 29 | 30 | public long getSdk() { 31 | if (sdk == 0) 32 | init(); 33 | return sdk; 34 | } 35 | 36 | private void init() { 37 | // 初始化sdk 38 | sdk = Finance.NewSdk(); 39 | int status = Finance.Init(sdk, weworkConfig.getCorpId(), weworkConfig.getCorpSecret()); 40 | if (status != 0) { 41 | log.error("[异常]MediaServiceImpl.sdk.init失败, {}", status); 42 | Finance.DestroySdk(sdk); 43 | throw new RuntimeException(String.format("[异常]MediaServiceImpl.sdk.init失败, %s", status)); 44 | } 45 | } 46 | 47 | public void downloadMediaFile(String sdkFileId, String file, long fileSize) { 48 | log.info("下载媒体文件 size: {}, {}", fileSize, sdkFileId); 49 | // 判断文件夹是否存在 50 | File fs = new File(file); 51 | if (!fs.getParentFile().exists()) { 52 | fs.getParentFile().mkdirs(); 53 | } 54 | // 判断本地是否存在文件并且文件大小与远程一致,防止多次下载 55 | if (fs.exists() && fs.length() == fileSize) { 56 | log.debug("媒体文件本地已存在 {},{}", file, sdkFileId); 57 | return; 58 | } 59 | // 下载媒体文件 60 | String buf = ""; 61 | long index = 0; 62 | int retry = 0; 63 | while (true) { 64 | if (retry >= 3) { 65 | log.debug("媒体文件下载重试次数超过3次, {}", sdkFileId); 66 | break; 67 | } 68 | long mediaData = Finance.NewMediaData(); 69 | int ret = Finance.GetMediaData(sdk, buf, sdkFileId, weworkConfig.getProxy(), weworkConfig.getPasswd(), weworkConfig.getTimeout(), mediaData); 70 | if (ret != 0) { 71 | log.error("获取媒体文件异常, {}", ret); 72 | Finance.FreeMediaData(mediaData); 73 | retry++; 74 | continue; 75 | } 76 | log.debug("media_data_long: {}, len: {}, data_len: {}, is_finish: {}", index, Finance.GetIndexLen(mediaData), Finance.GetDataLen(mediaData), Finance.IsMediaDataFinish(mediaData)); 77 | try { 78 | //大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 79 | FileOutputStream outputStream = new FileOutputStream(fs, true); 80 | outputStream.write(Finance.GetData(mediaData)); 81 | outputStream.close(); 82 | } catch (Exception e) { 83 | log.error("[异常]写本地文件失败, {}", e.getMessage()); 84 | Finance.FreeMediaData(mediaData); 85 | retry++; 86 | continue; 87 | } 88 | if (Finance.IsMediaDataFinish(mediaData) == 1) { 89 | Finance.FreeMediaData(mediaData); 90 | break; 91 | } 92 | index++; 93 | buf = Finance.GetOutIndexBuf(mediaData); 94 | Finance.FreeMediaData(mediaData); 95 | } 96 | log.info("[流程]媒体文件 size: {}, {}, {}", fs.length(), file, sdkFileId); 97 | } 98 | 99 | public String decryptData(Integer publicKeyVer, String encryptKey, String encryptChatMsg) { 100 | // 解密key 101 | String decodeKey = encryptService.decodeEncryptRandomKey(publicKeyVer, encryptKey); 102 | if (Objects.isNull(decodeKey)) { 103 | return null; 104 | } 105 | long slice = Finance.NewSlice(); 106 | int decryptStatus = Finance.DecryptData(sdk, decodeKey, encryptChatMsg, slice); 107 | if (decryptStatus != 0) { 108 | log.error("DecryptData, {}", decryptStatus); 109 | Finance.FreeSlice(slice); 110 | return null; 111 | } 112 | String msg = Finance.GetContentFromSlice(slice); 113 | Finance.FreeSlice(slice); 114 | return msg; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/EncryptServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.chinayin.wework.chatdata.config.WeWorkEncryptConfig; 4 | import com.chinayin.wework.chatdata.service.EncryptService; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.annotation.Resource; 9 | import javax.crypto.Cipher; 10 | import java.security.KeyFactory; 11 | import java.security.PrivateKey; 12 | import java.security.spec.PKCS8EncodedKeySpec; 13 | import java.util.Base64; 14 | import java.util.Objects; 15 | 16 | /** 17 | * @author chianyin 18 | * 加解密处理 19 | */ 20 | @Slf4j 21 | @Service("EncryptService") 22 | public class EncryptServiceImpl implements EncryptService { 23 | 24 | @Resource 25 | private WeWorkEncryptConfig encryptConfig; 26 | 27 | private String getPriKey(Integer publicKeyVer) { 28 | return encryptConfig.getPrivateKey().get(String.valueOf(publicKeyVer)); 29 | } 30 | 31 | public String decodeEncryptRandomKey(Integer publicKeyVer, String encryptRandomKey) { 32 | String key = getPriKey(publicKeyVer); 33 | if (Objects.isNull(key)) { 34 | log.error("decodeEncryptRandomKey failed, privateKey(keyVer={}) is null", publicKeyVer); 35 | return null; 36 | } 37 | Base64.Decoder decoder = Base64.getMimeDecoder(); 38 | byte[] keyBytes; 39 | try { 40 | keyBytes = decoder.decode(key); 41 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); 42 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 43 | PrivateKey privateKey = keyFactory.generatePrivate(keySpec); 44 | Cipher c1 = Cipher.getInstance("RSA"); 45 | c1.init(Cipher.DECRYPT_MODE, privateKey); 46 | byte[] temp = c1.doFinal(Base64.getMimeDecoder().decode(encryptRandomKey)); 47 | return new String(temp); 48 | } catch (Exception e) { 49 | log.error("decodeEncryptRandomKey failed, {}", e.getMessage()); 50 | } 51 | return null; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/FileOutputServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.chinayin.wework.chatdata.service.FileOutputService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | /** 13 | * @author chianyin 14 | * 保存消息到文件 15 | */ 16 | @Slf4j 17 | @Service("FileOutputService") 18 | public class FileOutputServiceImpl implements FileOutputService { 19 | 20 | private static String path = "./logs"; 21 | 22 | @Override 23 | public void saveMessage(String msg) { 24 | String file = path + "/msg-" + this.getDateStr(); 25 | this.record(file, msg); 26 | } 27 | 28 | @Override 29 | public void saveOriginMessage(String msg) { 30 | String file = path + "/origin-msg-" + this.getDateStr(); 31 | this.record(file, msg); 32 | } 33 | 34 | private boolean record(String file, String msg) { 35 | try { 36 | // 判断文件夹是否存在 37 | // if (!fs.getParentFile().exists()) { 38 | // fs.getParentFile().mkdirs(); 39 | // } 40 | FileOutputStream outputStream = new FileOutputStream(new File(file), true); 41 | outputStream.write(msg.getBytes()); 42 | outputStream.write("\n".getBytes()); 43 | outputStream.close(); 44 | return true; 45 | } catch (Exception e) { 46 | log.error(e.getMessage()); 47 | } 48 | return false; 49 | } 50 | 51 | private String getDateStr() { 52 | return (new SimpleDateFormat("yyyy-MM-dd")).format((new Date())) + ".log"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/MnsClientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.aliyun.mns.client.CloudQueue; 4 | import com.aliyun.mns.client.MNSClient; 5 | import com.aliyun.mns.common.ClientException; 6 | import com.aliyun.mns.common.ServiceException; 7 | import com.aliyun.mns.model.Message; 8 | import com.chinayin.wework.chatdata.config.MnsClientConfig; 9 | import com.chinayin.wework.chatdata.service.MnsClientService; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.Resource; 14 | 15 | /** 16 | * @author chianyin 17 | * 阿里云mns 18 | */ 19 | @Service("MnsClientService") 20 | @Slf4j 21 | public class MnsClientServiceImpl implements MnsClientService { 22 | 23 | @Resource 24 | private MNSClient client; 25 | 26 | @Resource 27 | private MnsClientConfig config; 28 | 29 | @Override 30 | public String sendMessageToQueue(String msg) { 31 | return this.sendMessageToQueue(new Message(msg)); 32 | } 33 | 34 | /** 35 | * priority 发送优先级(取值范围1~16,其中1为最高优先级,队列默认优先级为8) 36 | * 37 | * @param msg 38 | * @return 39 | */ 40 | @Override 41 | public String sendMessageToQueue(Message msg) { 42 | try { 43 | CloudQueue queue = client.getQueueRef(config.getQueue()); 44 | Message message = queue.putMessage(msg); 45 | String msgId = message.getMessageId(); 46 | log.info("[流程]投递 mns 成功, {}, {}", msgId, message.getRequestId()); 47 | return msgId; 48 | } catch (ClientException e) { 49 | log.error("[异常]投递 mns 失败, Caught an ClientException: {}", e.toString()); 50 | } catch (ServiceException e) { 51 | log.error("[异常]投递 mns 失败, Caught an ServiceException: {}", e.toString()); 52 | } catch (Exception e) { 53 | log.error("[异常]投递 mns 失败, {}", e.getMessage()); 54 | } 55 | return null; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/chinayin/wework/chatdata/service/impl/OssClientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.chinayin.wework.chatdata.service.impl; 2 | 3 | import com.aliyun.oss.ClientException; 4 | import com.aliyun.oss.OSS; 5 | import com.aliyun.oss.OSSClientBuilder; 6 | import com.aliyun.oss.OSSException; 7 | import com.aliyun.oss.model.ObjectMetadata; 8 | import com.aliyun.oss.model.PutObjectRequest; 9 | import com.aliyun.oss.model.PutObjectResult; 10 | import com.chinayin.wework.chatdata.config.OssClientConfig; 11 | import com.chinayin.wework.chatdata.service.OssClientService; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.annotation.Resource; 16 | import java.io.File; 17 | 18 | /** 19 | * @author chianyin 20 | * 阿里云oss 21 | */ 22 | @Service("OssClientService") 23 | @Slf4j 24 | public class OssClientServiceImpl implements OssClientService { 25 | 26 | @Resource 27 | private OssClientConfig config; 28 | 29 | /** 30 | * 获取媒体地址 31 | * 32 | * @param objectName 33 | * @return 34 | */ 35 | public String getMediaOssUrl(String objectName) { 36 | return "https://" + config.getBucket() + "." + config.getEndpoint() + "/" + objectName; 37 | } 38 | 39 | public Boolean doesObjectExist(String objectName) { 40 | OSS ossClient = new OSSClientBuilder().build( 41 | config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret()); 42 | try { 43 | return ossClient.doesObjectExist(config.getBucket(), objectName); 44 | } catch (OSSException ex) { 45 | if (ex.getErrorCode().equals("FileAlreadyExists")) { 46 | return true; 47 | } else { 48 | log.error("[异常]Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.\n" + 49 | "Error Message: {}\n" + 50 | "Error Code: {}\n" + 51 | "Request ID: {}\n" + 52 | "Host ID: {}", 53 | ex.getErrorMessage(), ex.getErrorCode(), ex.getRequestId(), ex.getHostId()); 54 | } 55 | } catch (ClientException ex) { 56 | log.error("[异常]Caught an ClientException, which means the client encountered " 57 | + "a serious internal problem while trying to communicate with OSS, " 58 | + "such as not being able to access the network.\nError Message: {}", ex.getMessage()); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } finally { 62 | ossClient.shutdown(); 63 | } 64 | return false; 65 | } 66 | 67 | /** 68 | * 上传资源到 oss 69 | * 70 | * @param objectName 71 | * @param file 72 | */ 73 | public void upload(String objectName, String file) { 74 | File fs = new File(file); 75 | log.info("{} .size = {}", objectName, fs.length()); 76 | if (!fs.exists() || fs.length() < 1) { 77 | log.error("本地 oss 文件不存在, {} ", objectName); 78 | return; 79 | } 80 | OSS ossClient = new OSSClientBuilder().build( 81 | config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret()); 82 | try { 83 | // 优先判断文件是否存在 84 | boolean found = ossClient.doesObjectExist(config.getBucket(), objectName); 85 | if (found) { 86 | log.info("[流程]上传 oss 同名跳过, {}", objectName); 87 | } else { 88 | // 禁止覆盖同名文件 https://help.aliyun.com/document_detail/146172.html 89 | ObjectMetadata metadata = new ObjectMetadata(); 90 | metadata.setHeader("x-oss-forbid-overwrite", "true"); 91 | // 上传 92 | PutObjectRequest putObjectRequest = new PutObjectRequest(config.getBucket(), objectName, fs, metadata); 93 | PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest); 94 | //log.info("[流程]上传 oss 成功, {}", putObjectResult.getResponse().getRequestId()); 95 | } 96 | // 上传完删除文件 97 | fs.delete(); 98 | } catch (OSSException ex) { 99 | // 同名文件不需要上传 100 | if (ex.getErrorCode().equals("FileAlreadyExists")) { 101 | log.info("[流程]上传 oss 同名跳过, {}", objectName); 102 | } else { 103 | log.error("[异常]Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.\n" + 104 | "Error Message: {}\n" + 105 | "Error Code: {}\n" + 106 | "Request ID: {}\n" + 107 | "Host ID: {}", 108 | ex.getErrorMessage(), ex.getErrorCode(), ex.getRequestId(), ex.getHostId()); 109 | } 110 | // 存在同名文件避免后续产生垃圾文件,删除文件 111 | fs.delete(); 112 | } catch (ClientException ex) { 113 | log.error("[异常]Caught an ClientException, which means the client encountered " 114 | + "a serious internal problem while trying to communicate with OSS, " 115 | + "such as not being able to access the network.\nError Message: {}", ex.getMessage()); 116 | } catch (Exception e) { 117 | e.printStackTrace(); 118 | log.error("[异常]上传 oss 失败, {}", e.getMessage()); 119 | } finally { 120 | ossClient.shutdown(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/tencent/wework/Finance.java: -------------------------------------------------------------------------------- 1 | package com.tencent.wework; 2 | 3 | /* sdk返回数据 4 | typedef struct Slice_t { 5 | char* buf; 6 | int len; 7 | } Slice_t; 8 | 9 | typedef struct MediaData { 10 | char* outindexbuf; 11 | int out_len; 12 | char* data; 13 | int data_len; 14 | int is_finish; 15 | } MediaData_t; 16 | */ 17 | 18 | import lombok.extern.slf4j.Slf4j; 19 | 20 | @Slf4j 21 | public class Finance { 22 | public native static long NewSdk(); 23 | 24 | /** 25 | * 初始化函数 26 | * Return值=0表示该API调用成功 27 | * 28 | * @param [in] sdk NewSdk返回的sdk指针 29 | * @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 30 | * @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 31 | * @return 返回是否初始化成功 32 | * 0 - 成功 33 | * !=0 - 失败 34 | */ 35 | public native static int Init(long sdk, String corpid, String secret); 36 | 37 | /** 38 | * 拉取聊天记录函数 39 | * Return值=0表示该API调用成功 40 | * 41 | * @param [in] sdk NewSdk返回的sdk指针 42 | * @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 43 | * @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 44 | * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 45 | * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 46 | * @param [out] chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。 47 | * @return 返回是否调用成功 48 | * 0 - 成功 49 | * !=0 - 失败 50 | */ 51 | public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData); 52 | 53 | /** 54 | * 拉取媒体消息函数 55 | * Return值=0表示该API调用成功 56 | * 57 | * @param [in] sdk NewSdk返回的sdk指针 58 | * @param [in] sdkFileid 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid 59 | * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 60 | * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 61 | * @param [in] indexbuf 媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。 62 | * @param [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记) 63 | * @return 返回是否调用成功 64 | * 0 - 成功 65 | * !=0 - 失败 66 | */ 67 | public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData); 68 | 69 | /** 70 | * @param [in] encrypt_key, getchatdata返回的encrypt_key 71 | * @param [in] encrypt_msg, getchatdata返回的content 72 | * @param [out] msg, 解密的消息明文 73 | * @return 返回是否调用成功 74 | * 0 - 成功 75 | * !=0 - 失败 76 | * @brief 解析密文 77 | */ 78 | public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg); 79 | 80 | public native static void DestroySdk(long sdk); 81 | 82 | public native static long NewSlice(); 83 | 84 | /** 85 | * @return 86 | * @brief 释放slice,和NewSlice成对使用 87 | */ 88 | public native static void FreeSlice(long slice); 89 | 90 | /** 91 | * @return 内容 92 | * @brief 获取slice内容 93 | */ 94 | public native static String GetContentFromSlice(long slice); 95 | 96 | /** 97 | * @return 内容 98 | * @brief 获取slice内容长度 99 | */ 100 | public native static int GetSliceLen(long slice); 101 | 102 | public native static long NewMediaData(); 103 | 104 | public native static void FreeMediaData(long mediaData); 105 | 106 | /** 107 | * @return outindex 108 | * @brief 获取mediadata outindex 109 | */ 110 | public native static String GetOutIndexBuf(long mediaData); 111 | 112 | /** 113 | * @return data 114 | * @brief 获取mediadata data数据 115 | */ 116 | public native static byte[] GetData(long mediaData); 117 | 118 | public native static int GetIndexLen(long mediaData); 119 | 120 | public native static int GetDataLen(long mediaData); 121 | 122 | /** 123 | * @return 1完成、0未完成 124 | * @brief 判断mediadata是否结束 125 | */ 126 | public native static int IsMediaDataFinish(long mediaData); 127 | 128 | static { 129 | log.info("java.library.path = {}", System.getProperty("java.library.path")); 130 | System.loadLibrary("WeWorkFinanceSdk_Java"); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # app 2 | app.corpId= 3 | app.corpSecret= 4 | app.env=prod 5 | # log 6 | log.path=./logs 7 | # pool 8 | pool.corePoolSize=10 9 | pool.maximumPoolSize=20 10 | pool.queueCapacity=20 11 | pool.keepAliveSeconds=10 12 | # mns 13 | mns.endpoint=http:// 14 | mns.accessKeyId= 15 | mns.accessKeySecret= 16 | mns.queue= 17 | # oss 18 | oss.endpoint=oss-cn-beijing.aliyuncs.com 19 | oss.accessKeyId= 20 | oss.accessKeySecret= 21 | oss.bucket= 22 | # sentry 23 | sentry.dsn= 24 | # privateKey 25 | encrypt.privateKey.1= 26 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ${CONSOLE_LOG_PATTERN} 11 | 12 | 13 | 14 | 15 | ${log.path}/wework.log 16 | 17 | ${log.path}/%d{yyyy-MM}/wework-%d{yyyyMMdd}-%i.gz 18 | 100MB 19 | 90 20 | 21 | 22 | ${FILE_LOG_PATTERN} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/resources/supervisord.conf: -------------------------------------------------------------------------------- 1 | [program:WeworkChatSDK] 2 | command=java -jar WeworkChatSDK-1.0.0.jar --spring.profiles.active=prod 3 | directory=/app/target/WeworkChatSDK-1.0.0 4 | autorestart=true 5 | startsecs=10 6 | stdout_logfile=NONE 7 | stderr_logfile=NONE --------------------------------------------------------------------------------