commit
4c18e13733
46 files changed
+1531
No files matched your search
+40
@@ -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
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
+13
@@ -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 {
|
||||||
|
}
|
||||||
+13
@@ -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 {
|
||||||
|
}
|
||||||
+13
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+36
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -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);
|
||||||
|
}
|
||||||
+237
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
Binary file not shown.
Reference in new issue
Block a user