初始化提交

Signed-off-by: 赵新乐 <zxl78585@126.com>
This commit is contained in:
zxl78585 committed 2026-05-08 11:41:00 +08:00
commit 4c18e13733
46 files changed
+1531

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
.idea
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
</parent>
<groupId>com.f8q8</groupId>
<artifactId>qyWechatArchive</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.f8q8.qywechatarchive.QyWechatArchiveApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,12 @@
package com.f8q8.qywechatarchive;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QyWechatArchiveApplication {
public static void main(String[] args) {
SpringApplication.run(QyWechatArchiveApplication.class, args);
}
}
@@ -0,0 +1,9 @@
package com.f8q8.qywechatarchive.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.f8q8.qywechatarchive.common.entity.SysApiToken;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysApiTokenDao extends BaseMapper<SysApiToken> {
}
@@ -0,0 +1,9 @@
package com.f8q8.qywechatarchive.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.f8q8.qywechatarchive.common.entity.SysApiUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysApiUserDao extends BaseMapper<SysApiUser> {
}
@@ -0,0 +1,9 @@
package com.f8q8.qywechatarchive.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.f8q8.qywechatarchive.common.entity.WxChatRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface WxChatRecordDao extends BaseMapper<WxChatRecord> {
}
@@ -0,0 +1,9 @@
package com.f8q8.qywechatarchive.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.f8q8.qywechatarchive.common.entity.WxSyncState;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface WxSyncStateDao extends BaseMapper<WxSyncState> {
}
@@ -0,0 +1,20 @@
package com.f8q8.qywechatarchive.common.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.f8q8.qywechatarchive.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName(value = "sys_api_token")
public class SysApiToken extends BaseEntity {
private String userId;
private String accessToken;
private String refreshToken;
private Date expireTime;
private Integer status;
}
@@ -0,0 +1,18 @@
package com.f8q8.qywechatarchive.common.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.f8q8.qywechatarchive.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName(value = "sys_api_user")
public class SysApiUser extends BaseEntity {
private String username;
private String password;
private String salt;
private String realName;
private Integer status;
}
@@ -0,0 +1,27 @@
package com.f8q8.qywechatarchive.common.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.f8q8.qywechatarchive.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName(value = "wx_chat_record")
public class WxChatRecord extends BaseEntity {
private String msgId;
private Long seq;
private String action;
private String fromType;
private String fromId;
private String roomId;
private String toList;
private String msgType;
private String content;
private LocalDateTime msgTime;
private LocalDateTime syncTime;
private String corpId;
}
@@ -0,0 +1,22 @@
package com.f8q8.qywechatarchive.common.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.f8q8.qywechatarchive.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName(value = "wx_sync_state")
public class WxSyncState extends BaseEntity {
private String corpId;
private String syncType;
private String nextCursor;
private LocalDateTime lastMsgTime;
private Long totalCount;
private Integer syncStatus;
private String errorMsg;
}
@@ -0,0 +1,4 @@
package com.f8q8.qywechatarchive.common.exception;
public class GlobalException extends RuntimeException {
}
@@ -0,0 +1,18 @@
package com.f8q8.qywechatarchive.common.exception;
import com.f8q8.qywechatarchive.core.utils.Result;
import com.f8q8.qywechatarchive.core.utils.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = GlobalException.class)
public Result<?> handleGlobalException(GlobalException e) {
log.error(e.getMessage(), e);
return ResultUtils.error(e.getMessage());
}
}
@@ -0,0 +1,7 @@
package com.f8q8.qywechatarchive.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.f8q8.qywechatarchive.common.entity.SysApiToken;
public interface SysApiTokenService extends IService<SysApiToken> {
}
@@ -0,0 +1,7 @@
package com.f8q8.qywechatarchive.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.f8q8.qywechatarchive.common.entity.SysApiUser;
public interface SysApiUserService extends IService<SysApiUser> {
}
@@ -0,0 +1,7 @@
package com.f8q8.qywechatarchive.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.f8q8.qywechatarchive.common.entity.WxChatRecord;
public interface WxChatRecordService extends IService<WxChatRecord> {
}
@@ -0,0 +1,7 @@
package com.f8q8.qywechatarchive.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.f8q8.qywechatarchive.common.entity.WxSyncState;
public interface WxSyncStateService extends IService<WxSyncState> {
}
@@ -0,0 +1,13 @@
package com.f8q8.qywechatarchive.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.f8q8.qywechatarchive.common.dao.SysApiTokenDao;
import com.f8q8.qywechatarchive.common.entity.SysApiToken;
import com.f8q8.qywechatarchive.common.service.SysApiTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysApiTokenServiceImpl extends ServiceImpl<SysApiTokenDao, SysApiToken> implements SysApiTokenService {
}
@@ -0,0 +1,13 @@
package com.f8q8.qywechatarchive.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.f8q8.qywechatarchive.common.dao.SysApiUserDao;
import com.f8q8.qywechatarchive.common.entity.SysApiUser;
import com.f8q8.qywechatarchive.common.service.SysApiUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysApiUserServiceImpl extends ServiceImpl<SysApiUserDao, SysApiUser> implements SysApiUserService {
}
@@ -0,0 +1,13 @@
package com.f8q8.qywechatarchive.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.f8q8.qywechatarchive.common.dao.WxChatRecordDao;
import com.f8q8.qywechatarchive.common.entity.WxChatRecord;
import com.f8q8.qywechatarchive.common.service.WxChatRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class WxChatRecordServiceImpl extends ServiceImpl<WxChatRecordDao, WxChatRecord> implements WxChatRecordService {
}
@@ -0,0 +1,13 @@
package com.f8q8.qywechatarchive.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.f8q8.qywechatarchive.common.dao.WxSyncStateDao;
import com.f8q8.qywechatarchive.common.entity.WxSyncState;
import com.f8q8.qywechatarchive.common.service.WxSyncStateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class WxSyncStateServiceImpl extends ServiceImpl<WxSyncStateDao, WxSyncState> implements WxSyncStateService {
}
@@ -0,0 +1,31 @@
package com.f8q8.qywechatarchive.core.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public abstract class BaseEntity implements Serializable {
@TableId
private String id;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private String createByName;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateByName;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
private String remark;
@Version
private Long version;
@TableLogic
private Integer delFlag;
}
@@ -0,0 +1,15 @@
package com.f8q8.qywechatarchive.core.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
}
@@ -0,0 +1,41 @@
package com.f8q8.qywechatarchive.core.utils;
public class ResultUtils {
private static final Integer SUCCESS_CODE = 200;
private static final String SUCCESS_MESSAGE = "成功";
private static final Integer ERROR_CODE = 500;
public static <T> Result<T> success(Integer code, String message, T data) {
return new Result<>(code, message, data);
}
public static <T> Result<T> success(String message, T data) {
return success(SUCCESS_CODE, message, data);
}
public static <T> Result<T> success(T data) {
return success(SUCCESS_CODE, SUCCESS_MESSAGE, data);
}
public static <T> Result<T> success() {
return success(SUCCESS_CODE, SUCCESS_MESSAGE, null);
}
public static <T> Result<T> error(Integer code, String message, T data) {
return new Result<>(code, message, data);
}
public static <T> Result<T> error(String message, T data) {
return error(ERROR_CODE, message, data);
}
public static <T> Result<T> error(String message) {
return error(ERROR_CODE, message, null);
}
public static <T> Result<T> error(Integer code, String message) {
return error(code, message, null);
}
}
@@ -0,0 +1,73 @@
package com.f8q8.qywechatarchive.modules.api.action;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.f8q8.qywechatarchive.common.entity.SysApiUser;
import com.f8q8.qywechatarchive.common.service.SysApiUserService;
import com.f8q8.qywechatarchive.core.utils.Result;
import com.f8q8.qywechatarchive.core.utils.ResultUtils;
import com.f8q8.qywechatarchive.modules.api.security.JwtTokenProvider;
import com.f8q8.qywechatarchive.modules.api.vo.LoginRequest;
import com.f8q8.qywechatarchive.modules.api.vo.LoginResponse;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(value = "api/v1/auth")
public class AuthController {
@Resource
SysApiUserService sysApiUserService;
@Resource
PasswordEncoder passwordEncoder;
@Resource
JwtTokenProvider jwtTokenProvider;
/**
* 登录接口 — 用户名密码换取JWT Token
*/
@PostMapping("login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// 1. 查询用户
SysApiUser user = sysApiUserService.getOne(
new LambdaQueryWrapper<SysApiUser>()
.eq(SysApiUser::getUsername, request.getUsername())
);
// 2. 校验用户状态和密码
if (user == null || user.getStatus() != 1) {
log.warn("登录失败 - 用户不存在或已禁用: {}", request.getUsername());
return ResultUtils.error(400, "用户名或密码错误");
}
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
log.warn("登录失败 - 密码错误: {}", request.getUsername());
return ResultUtils.error(400, "用户名或密码错误");
}
// 3. 生成Token
String accessToken = jwtTokenProvider.generateAccessToken(user.getUsername());
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getUsername());
log.info("用户登录成功: {}", user.getUsername());
// 4. 返回Token信息
LoginResponse resp = LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(jwtTokenProvider.getAccessExpiration())
.username(user.getUsername())
.build();
return ResultUtils.success(resp);
}
}
@@ -0,0 +1,21 @@
package com.f8q8.qywechatarchive.modules.api.action;
import com.f8q8.qywechatarchive.core.utils.Result;
import com.f8q8.qywechatarchive.core.utils.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(value = "api/v1/callback")
public class CallbackController {
@PostMapping(value = "notify")
public Result<Void> notify(@RequestBody String data) {
log.info("企业微信回调数据:{}", data);
return ResultUtils.success();
}
}
@@ -0,0 +1,69 @@
package com.f8q8.qywechatarchive.modules.api.action;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.f8q8.qywechatarchive.common.entity.WxChatRecord;
import com.f8q8.qywechatarchive.common.service.WxChatRecordService;
import com.f8q8.qywechatarchive.core.utils.Result;
import com.f8q8.qywechatarchive.core.utils.ResultUtils;
import com.f8q8.qywechatarchive.modules.api.vo.ChatRecordQuery;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(value = "api/v1/chat-records")
public class ChatRecordController {
@Resource
WxChatRecordService wxChatRecordService;
/**
* 分页查询会话记录
*/
@GetMapping
public Result<Page<WxChatRecord>> queryRecords(@Valid ChatRecordQuery query) {
Page<WxChatRecord> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<WxChatRecord> wrapper = new LambdaQueryWrapper<>();
// 按发送方查询
wrapper.eq(StringUtils.hasText(query.getFromId()),
WxChatRecord::getFromId, query.getFromId());
// 按群聊ID查询
wrapper.eq(StringUtils.hasText(query.getRoomId()),
WxChatRecord::getRoomId, query.getRoomId());
// 按消息类型查询
wrapper.eq(StringUtils.hasText(query.getMsgType()),
WxChatRecord::getMsgType, query.getMsgType());
// 按时间范围查询
wrapper.ge(query.getStartTime() != null,
WxChatRecord::getMsgTime, query.getStartTime());
wrapper.le(query.getEndTime() != null,
WxChatRecord::getMsgTime, query.getEndTime());
// 核心索引按时间倒序
wrapper.orderByDesc(WxChatRecord::getMsgTime);
Page<WxChatRecord> result = wxChatRecordService.page(page, wrapper);
return ResultUtils.success(result);
}
/**
* 根据消息ID查询单条记录
*/
@GetMapping("{msgId}")
public Result<WxChatRecord> getByMsgId(@PathVariable String msgId) {
WxChatRecord record = wxChatRecordService.getOne(
new LambdaQueryWrapper<WxChatRecord>()
.eq(WxChatRecord::getMsgId, msgId));
if (record == null) {
return ResultUtils.error(404, "消息不存在");
}
return ResultUtils.success(record);
}
}
@@ -0,0 +1,77 @@
package com.f8q8.qywechatarchive.modules.api.client;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
@Slf4j
public class NativeLibraryLoader {
private static final String TMP_DIR_NAME = "qyWechatArchive";
private static boolean loaded = false;
public static Path loadLibraryMulti(String classpathResource) {
// 1. 确定临时目录(/tmp/ww-audit-libs)
String tmpDir = System.getProperty("java.io.tmpdir");
Path extractDir = Paths.get(tmpDir, TMP_DIR_NAME);
try {
Files.createDirectories(extractDir);
} catch (IOException e) {
throw new RuntimeException("无法创建临时库目录: " + extractDir, e);
}
// 2. 提取 .so 文件(只提取资源文件名部分)
String libName = classpathResource.substring(classpathResource.lastIndexOf('/') + 1);
Path targetFile = extractDir.resolve(libName);
// 如果目标文件已存在,检查是否相同;同一 JVM 进程内文件被锁定,无需重复提取
if (!Files.exists(targetFile)) {
try (InputStream in = NativeLibraryLoader.class.getClassLoader()
.getResourceAsStream(classpathResource)) {
if (in == null) {
throw new RuntimeException(
"classpath 中找不到资源: " + classpathResource);
}
Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
log.info("已提取 native 库到: {}", targetFile.toAbsolutePath());
} catch (IOException e) {
throw new RuntimeException("提取 native 库失败: " + classpathResource, e);
}
}
// 3. 设置可执行权限(Linux 环境)
if (!System.getProperty("os.name").toLowerCase().contains("win")) {
try {
targetFile.toFile().setExecutable(true);
targetFile.toFile().setReadable(true);
} catch (SecurityException e) {
log.warn("无法设置可执行权限,将尝试直接加载: {}", targetFile.toAbsolutePath());
}
}
// 4. 使用 System.load 加载
System.load(targetFile.toAbsolutePath().toString());
return targetFile.toAbsolutePath();
}
/**
* 从 classpath 加载指定的 .so 文件
*
* @param classpathResource .so 文件在 classpath 中的路径,
* 例如 "native/linux-x86-64/libWeWorkFinanceSdk_Java.so"
*/
public static synchronized void loadLibrary(String classpathResource) {
if (loaded) {
log.info("Native 库已加载,跳过重复加载");
return;
}
Path path = loadLibraryMulti(classpathResource);
loaded = true;
log.info("Native 库加载成功: {}", path);
}
}
@@ -0,0 +1,92 @@
package com.f8q8.qywechatarchive.modules.api.client;
import com.tencent.wework.Finance;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Getter
@Component
public class WeComSdkClient {
private long sdk;
@Value("${wecom.corp-id}")
String corpId;
@Value("${wecom.secret}")
String secret;
static {
final String os = System.getProperty("os.name").toLowerCase();
if (os.contains("linux")) {
NativeLibraryLoader.loadLibrary("native/linux/libWeWorkFinanceSdk_Java.so");
} else if (os.contains("win")) {
NativeLibraryLoader.loadLibraryMulti("native/win/libcrypto-3-x64.dll");
NativeLibraryLoader.loadLibraryMulti("native/win/libcurl-x64.dll");
NativeLibraryLoader.loadLibraryMulti("native/win/WeWorkFinanceSdk.dll");
// 加载指定路径下文件
} else {
throw new UnsupportedOperationException("不支持的操作系统:" + os);
}
}
@PostConstruct
public void init() {
sdk = Finance.NewSdk();
int ret = Finance.Init(sdk, corpId, secret);
if (ret != 0) {
throw new RuntimeException("WeWork SDK 初始化失败,错误码:" + ret);
}
log.info("企业微信会话存档SDK初始化完成, sdk={}", sdk);
}
@PreDestroy
public void destroy() {
if (sdk != 0) {
Finance.DestroySdk(sdk);
log.info("企业微信会话存档SDK已销毁");
}
}
/**
* 获取聊天记录(每次调用返回0或1的Slice,需循环拉取)
*
* @param seq 起始序号
* @param limit 最大条数
* @param proxy 代理(可为空)
* @param paswd 代理密码(可为空)
* @param timeout 超时秒数
* @return 切片数据(需调用 Finance.GetContentFromSlice 解析)
*/
public long getChatData(long seq, int limit, String proxy, String paswd, int timeout) {
return Finance.GetChatData(sdk, seq, limit, proxy, paswd, timeout, null);
}
public String getContentFromSlice(long slice) {
return Finance.GetContentFromSlice(slice);
}
public void freeSlice(long slice) {
Finance.FreeSlice(slice);
}
/**
* 解密消息内容(RSA解密后的AES密钥 + 密文)
*/
public String decryptChatData(String encryptRandomKey, String encryptChatMsg) {
long msgPtr = Finance.NewSlice();
int ret = Finance.DecryptData(sdk, encryptRandomKey, encryptChatMsg, msgPtr);
if (ret != 0) {
log.error("消息解密失败, ret={}", ret);
return null;
}
String decrypted = Finance.GetContentFromSlice(msgPtr);
Finance.FreeSlice(msgPtr);
return decrypted;
}
}
@@ -0,0 +1,63 @@
package com.f8q8.qywechatarchive.modules.api.config;
import com.f8q8.qywechatarchive.modules.api.security.JwtAuthenticationEntryPoint;
import com.f8q8.qywechatarchive.modules.api.security.JwtAuthenticationFilter;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Resource
JwtAuthenticationFilter jwtAuthenticationFilter;
@Resource
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 安全过滤链 — Spring Boot 4 推荐链式配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(API接口场景)
.csrf(AbstractHttpConfigurer::disable)
// 无状态会话
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 异常处理 — Token过期/无效时返回401
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint))
// 路由权限配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/actuator/health").permitAll() // 健康检查放行
.requestMatchers("/api/v1/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated())
// JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@@ -0,0 +1,36 @@
package com.f8q8.qywechatarchive.modules.api.security;
import com.f8q8.qywechatarchive.core.utils.Result;
import com.f8q8.qywechatarchive.core.utils.ResultUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Resource
ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Result<?> result = ResultUtils.error(401, "Token已过期或无效,请重新登录");
objectMapper.writeValue(response.getWriter(), result);
log.warn("认证失败 - URI: {}, 原因: {}", request.getRequestURI(), authException.getMessage());
}
}
@@ -0,0 +1,61 @@
package com.f8q8.qywechatarchive.modules.api.security;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromHeader(request);
if (StringUtils.hasText(token)) {
try {
if (jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.extractUsername(token);
// 设置认证信息到Spring Security上下文
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 可选:将剩余有效时间放入响应头
long remaining = jwtTokenProvider.getRemainingTime(token);
response.setHeader("X-Token-Remaining", String.valueOf(remaining));
}
} catch (ExpiredJwtException e) {
// 交由 AuthenticationEntryPoint 统一返回401
SecurityContextHolder.clearContext();
log.debug("Token已过期: {}", e.getMessage());
}
}
filterChain.doFilter(request, response);
}
private String extractTokenFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@@ -0,0 +1,100 @@
package com.f8q8.qywechatarchive.modules.api.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Getter
@Component
public class JwtTokenProvider {
@Value("${jwt.access-expiration}")
long accessExpiration;
@Value("${jwt.secret}")
String secret;
@Value("${jwt.refresh-expiration}")
long refreshExpiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成token
*
* @param username 用户名
* @return
*/
public String generateAccessToken(String username) {
return buildToken(username, accessExpiration * 1000);
}
public String generateRefreshToken(String username) {
return buildToken(username, refreshExpiration * 1000);
}
private String buildToken(String subject, long expirationMs) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", expirationMs == accessExpiration * 1000 ? "ACCESS" : "REFRESH");
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(getSigningKey())
.compact();
}
/**
* 从Token中提取用户名
*/
public String extractUsername(String token) {
return extractAllClaims(token).getSubject();
}
/**
* 校验Token是否有效
*/
public boolean validateToken(String token) {
try {
extractAllClaims(token);
return true;
} catch (ExpiredJwtException e) {
// Token 过期 — 外层捕获后返回401
throw e;
} catch (JwtException | IllegalArgumentException e) {
// Token 格式错误或签名无效
return false;
}
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取Token剩余有效时间(秒)
*/
public long getRemainingTime(String token) {
Claims claims = extractAllClaims(token);
return (claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000;
}
}
@@ -0,0 +1,11 @@
package com.f8q8.qywechatarchive.modules.api.service;
public interface WeComSyncService {
/**
* 执行一次会话消息同步
*
* @param corpId 企业ID(支持多企业扩展)
* @return 同步条数
*/
int syncMessages(String corpId);
}
@@ -0,0 +1,237 @@
package com.f8q8.qywechatarchive.modules.api.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.f8q8.qywechatarchive.common.entity.WxChatRecord;
import com.f8q8.qywechatarchive.common.entity.WxSyncState;
import com.f8q8.qywechatarchive.common.service.WxChatRecordService;
import com.f8q8.qywechatarchive.common.service.WxSyncStateService;
import com.f8q8.qywechatarchive.modules.api.client.WeComSdkClient;
import com.f8q8.qywechatarchive.modules.api.service.WeComSyncService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@Service
public class WeComSyncServiceImpl implements WeComSyncService {
@Resource
WxSyncStateService wxSyncStateService;
@Resource
WeComSdkClient weComSdkClient;
@Resource
WxChatRecordService wxChatRecordService;
@Resource
ObjectMapper objectMapper;
@Value("${wecom.sync.batch-size}")
int batchSize;
@Value("${wecom.sync.interval}")
int interval;
// 防止同企业并发拉取(单机)
private final ReentrantLock lock = new ReentrantLock();
@Override
public int syncMessages(String corpId) {
if (!lock.tryLock()) {
log.warn("同步任务正在进行中,跳过本次执行 corpId={}", corpId);
return 0;
}
try {
return doSync(corpId);
} finally {
lock.unlock();
}
}
@Transactional(rollbackFor = Exception.class)
public int doSync(String corpId) {
// 1. 获取上次同步游标
WxSyncState state = wxSyncStateService.getOne(
new LambdaQueryWrapper<WxSyncState>()
.eq(WxSyncState::getCorpId, corpId)
.eq(WxSyncState::getSyncType, "chat"));
long lastSeq = 0;
if (state != null && state.getNextCursor() != null) {
try {
lastSeq = Long.parseLong(state.getNextCursor());
} catch (NumberFormatException e) {
log.warn("游标格式异常,从0开始拉取 corpId={}", corpId);
}
}
// 2. 获取SDK连接(实际生产中需要先调用 GetChatData 获取slice,此处模拟)
int limit = batchSize;
int timeout = 5000;
log.info("开始拉取会话消息 corpId={}, 起始seq={}, 每批{}条", corpId, lastSeq, limit);
// 3. 循环拉取消息切片
long currentSeq = lastSeq;
int totalSynced = 0;
long maxSeq = 0; // 本批次拉取到的最大seq
List<WxChatRecord> recordsToInsert = new ArrayList<>();
while (true) {
long slice = weComSdkClient.getChatData(currentSeq, limit, null, null, timeout);
if (slice == 0) {
log.warn("获取slice为空,seq={}", currentSeq);
break;
}
String rawJson = weComSdkClient.getContentFromSlice(slice);
weComSdkClient.freeSlice(slice);
if (rawJson == null || rawJson.isEmpty()) {
log.info("slice内容为空,可能已拉取完毕 seq={}", currentSeq);
break;
}
// 解析企业微信返回的JSON结构
ObjectNode objectNode = objectMapper.readTree(rawJson).asObject();
int errcode = objectNode.get("errcode").asInt();
if (errcode != 0) {
log.error("拉取消息失败, errcode={}, errmsg={}", errcode, objectNode.get("errmsg").asString());
// 保存异常状态
updateSyncState(corpId, String.valueOf(currentSeq), null, totalSynced, false, objectNode.get("errmsg").asString());
return totalSynced;
}
ArrayNode chatDataList = objectNode.get("chatdata").asArray();
if (chatDataList == null || chatDataList.isEmpty()) {
log.info("本次拉取chatdata为空,同步结束");
break;
}
// 4. 遍历每条消息
for (int i = 0; i < chatDataList.size(); i++) {
ObjectNode chatData = chatDataList.get(i).asObject();
String msgId = chatData.get("msgid").asString();
long seq = chatData.get("seq").asLong();
String encryptRandomKey = chatData.get("encrypt_random_key").asString();
String encryptChatMsg = chatData.get("encrypt_chat_msg").asString();
// 去重检查
if (wxChatRecordService.count(
new LambdaQueryWrapper<WxChatRecord>().eq(WxChatRecord::getMsgId, msgId)) > 0) {
log.debug("消息已存在,跳过 msgId={}", msgId);
currentSeq = Math.max(currentSeq, seq + 1);
maxSeq = Math.max(maxSeq, seq);
continue;
}
// 5. 解密消息
String decryptedMsg = weComSdkClient.decryptChatData(encryptRandomKey, encryptChatMsg);
if (decryptedMsg == null) {
log.error("消息解密失败,跳过 msgId={}", msgId);
currentSeq = Math.max(currentSeq, seq + 1);
continue;
}
// 6. 解析消息内容并转换为实体
WxChatRecord record = parseRecord(msgId, seq, decryptedMsg, corpId);
if (record != null) {
recordsToInsert.add(record);
totalSynced++;
}
// 更新当前seq(取当前seq+1,因为SDK要求从上次最大seq+1继续拉)
currentSeq = Math.max(currentSeq, seq + 1);
maxSeq = Math.max(maxSeq, seq);
}
// 如果返回条数小于limit,通常表示已拉完
if (chatDataList.size() < limit) {
log.info("返回条数({})小于拉取限制({}),结束拉取", chatDataList.size(), limit);
break;
}
}
// 7. 批量入库
if (!recordsToInsert.isEmpty()) {
wxChatRecordService.saveBatch(recordsToInsert); // 需自定义批量插入方法,或使用MyBatis-Plus的saveBatch
log.info("批量插入{}条会话消息", recordsToInsert.size());
}
// 8. 更新同步状态(游标记录最后已拉取的最大seq,下次从此处开始)
String nextCursor = String.valueOf(Math.max(currentSeq, maxSeq + 1));
updateSyncState(corpId, nextCursor,
recordsToInsert.isEmpty() ? null : recordsToInsert.get(recordsToInsert.size() - 1).getMsgTime(),
totalSynced, true, null);
log.info("同步完成 corpId={}, 本批插入{}条, 新游标={}", corpId, totalSynced, nextCursor);
return totalSynced;
}
/**
* 解析解密后的消息JSON并构建实体
*/
private WxChatRecord parseRecord(String msgId, long seq, String decryptedJson, String corpId) {
try {
ObjectNode msgObj = objectMapper.readTree(decryptedJson).asObject();
WxChatRecord record = new WxChatRecord();
record.setMsgId(msgId);
record.setSeq(seq);
record.setAction(msgObj.get("action").asString());
record.setFromType(msgObj.get("from_type").asString());
record.setFromId(msgObj.get("from").asString());
record.setRoomId(msgObj.get("roomid").asString());
// toList 可能是字符串数组
JsonNode toList = msgObj.get("tolist");
record.setToList(toList.asString());
record.setMsgType(msgObj.get("msgtype").asString());
// 整个消息内容以JSON存储
record.setContent(decryptedJson);
// 时间处理(企微消息时间通常为毫秒时间戳)
long msgTimeMillis = msgObj.get("msgtime").asLong();
record.setMsgTime(LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(msgTimeMillis),
ZoneId.systemDefault()));
record.setCorpId(corpId);
record.setSyncTime(LocalDateTime.now());
return record;
} catch (Exception e) {
log.error("解析消息失败 msgId={}", msgId, e);
return null;
}
}
/**
* 更新或插入同步状态记录
*/
private void updateSyncState(String corpId, String nextCursor, LocalDateTime lastMsgTime,
int addCount, boolean success, String errorMsg) {
WxSyncState state = wxSyncStateService.getOne(
new LambdaQueryWrapper<WxSyncState>()
.eq(WxSyncState::getCorpId, corpId)
.eq(WxSyncState::getSyncType, "chat"));
if (state == null) {
state = new WxSyncState();
state.setCorpId(corpId);
state.setSyncType("chat");
state.setTotalCount(0L);
}
state.setNextCursor(nextCursor);
if (lastMsgTime != null) {
state.setLastMsgTime(lastMsgTime);
}
state.setTotalCount(state.getTotalCount() + addCount);
state.setSyncStatus(success ? 1 : 0);
state.setErrorMsg(errorMsg);
state.setUpdateTime(LocalDateTime.now());
wxSyncStateService.saveOrUpdate(state); // 需自行实现或使用MyBatis-Plus的saveOrUpdate
}
}
@@ -0,0 +1,20 @@
package com.f8q8.qywechatarchive.modules.api.vo;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class ChatRecordQuery {
private Integer pageNum;
private Integer pageSize;
private String fromId;
private String roomId;
private String msgType;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
}
@@ -0,0 +1,10 @@
package com.f8q8.qywechatarchive.modules.api.vo;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}
@@ -0,0 +1,15 @@
package com.f8q8.qywechatarchive.modules.api.vo;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class LoginResponse {
private String accessToken;
private String refreshToken;
private String tokenType;
private long expiresIn;
private String username;
}
@@ -0,0 +1,128 @@
package com.tencent.wework;
/* sdk返回数据
typedef struct Slice_t {
char* buf;
int len;
} Slice_t;
typedef struct MediaData {
char* outindexbuf;
int out_len;
char* data;
int data_len;
int is_finish;
} MediaData_t;
*/
public class Finance {
public native static long NewSdk();
/**
* 初始化函数
* Return值=0表示该API调用成功
*
* @param [in] sdk NewSdk返回的sdk指针
* @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
* @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
* @return 返回是否初始化成功
* 0 - 成功
* !=0 - 失败
*/
public native static int Init(long sdk, String corpid, String secret);
/**
* 拉取聊天记录函数
* Return值=0表示该API调用成功
*
* @param [in] sdk NewSdk返回的sdk指针
* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误
* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
* @param [out] chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, Long chatData);
/**
* 拉取媒体消息函数
* Return值=0表示该API调用成功
*
* @param [in] sdk NewSdk返回的sdk指针
* @param [in] sdkFileid 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid
* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
* @param [in] indexbuf 媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。
* @param [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记)
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
/**
* @param [in] encrypt_key, getchatdata返回的encrypt_key
* @param [in] encrypt_msg, getchatdata返回的content
* @param [out] msg, 解密的消息明文
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
* @brief 解析密文
*/
public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
public native static void DestroySdk(long sdk);
public native static long NewSlice();
/**
* @return
* @brief 释放slice,和NewSlice成对使用
*/
public native static void FreeSlice(long slice);
/**
* @return 内容
* @brief 获取slice内容
*/
public native static String GetContentFromSlice(long slice);
/**
* @return 内容
* @brief 获取slice内容长度
*/
public native static int GetSliceLen(long slice);
public native static long NewMediaData();
public native static void FreeMediaData(long mediaData);
/**
* @return outindex
* @brief 获取mediadata outindex
*/
public native static String GetOutIndexBuf(long mediaData);
/**
* @return data
* @brief 获取mediadata data数据
*/
public native static byte[] GetData(long mediaData);
public native static int GetIndexLen(long mediaData);
public native static int GetDataLen(long mediaData);
/**
* @return 1完成、0未完成
* @brief 判断mediadata是否结束
*/
public native static int IsMediaDataFinish(long mediaData);
static {
System.loadLibrary("WeWorkFinanceSdk");
}
}
+25
View File
@@ -0,0 +1,25 @@
spring:
datasource:
url: jdbc:mysql://frps.zxl78585.com:3306/qy_wechat_archive?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: qyWechat
password: qyWechat@2026Test
hikari:
maximum-pool-size: 20
minimum-idle: 2
jwt:
secret: cd95c74d1b2c4897aeb47bb2f3c84def|3d11ad80ed54444aa9df6b93cd52dca6||3dc5eaa6d86c464aacbf583bc1bd88f5|73853cf87bab4d77b284b42322408582|994a243c19f14e56a03c2761a2f29557|fc8b51a9940d4fe1bde8031bf7cc10b5|d3ed7dfa7c2e4247918ac3502d317435|563387d19ca94287bcfcb5034cdb2eb4
# 2小时
access-expiration: 7200
# 7天
refresh-expiration: 604800
wecom:
corp-id: 111
secret: 2222
# 消息同步配置
sync:
# 是否启用自动同步
enabled: true
# 同步间隔(秒)
interval: 300
# 每次拉取条数
batch-size: 500
+25
View File
@@ -0,0 +1,25 @@
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/wecom?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: wecom
password: wecom@2026Prod
hikari:
maximum-pool-size: 20
minimum-idle: 2
jwt:
secret: cd95c74d1b2c4897aeb47bb2f3c84def|3d11ad80ed54444aa9df6b93cd52dca6||3dc5eaa6d86c464aacbf583bc1bd88f5|73853cf87bab4d77b284b42322408582|994a243c19f14e56a03c2761a2f29557|fc8b51a9940d4fe1bde8031bf7cc10b5|d3ed7dfa7c2e4247918ac3502d317435|563387d19ca94287bcfcb5034cdb2eb4
# 2小时
access-expiration: 7200
# 7天
refresh-expiration: 604800
wecom:
corp-id: wwb42d275eae6910cc
secret: KyFHB6q9g-8X5pH9fUhojSk8xifkGzWmvn96UnwMMgo
# 消息同步配置
sync:
# 是否启用自动同步
enabled: true
# 同步间隔(秒)
interval: 300
# 每次拉取条数
batch-size: 500
+18
View File
@@ -0,0 +1,18 @@
server:
port: 8088
spring:
profiles:
active: dev
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
type: com.zaxxer.hikari.HikariDataSource
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
db-config:
id-type: assign_uuid
logic-delete-field: del_flag
logic-delete-value: 1
logic-not-delete-value: 0
Binary file not shown.
Binary file not shown.
Binary file not shown.