提交 8a23a94c 作者: duanxincheng

纪要生成服务优化

父级 0a34c27f
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.3.0</mybatis-plus.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
...@@ -246,11 +247,11 @@ ...@@ -246,11 +247,11 @@
</dependency> </dependency>
<dependency> <!-- <dependency>-->
<groupId>org.mybatis.spring.boot</groupId> <!-- <groupId>org.mybatis.spring.boot</groupId>-->
<artifactId>mybatis-spring-boot-starter</artifactId> <!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<version>2.3.0</version> <!-- 请使用最新版本 --> <!-- <version>2.3.0</version> &lt;!&ndash; 请使用最新版本 &ndash;&gt;-->
</dependency> <!-- </dependency>-->
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
...@@ -293,10 +294,11 @@ ...@@ -293,10 +294,11 @@
</dependency> </dependency>
<!-- Word 文档操作 --> <!-- Word 文档操作 -->
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>com.deepoove</groupId>
<artifactId>poi-ooxml</artifactId> <artifactId>poi-tl</artifactId>
<version>5.2.3</version> <version>1.12.2</version>
</dependency> </dependency>
...@@ -305,6 +307,12 @@ ...@@ -305,6 +307,12 @@
<artifactId>commonmark</artifactId> <artifactId>commonmark</artifactId>
<version>0.17.1</version> <!-- 可升级至最新稳定版 --> <version>0.17.1</version> <!-- 可升级至最新稳定版 -->
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies> </dependencies>
......
...@@ -431,17 +431,17 @@ public class TencentMeetingCallbackController { ...@@ -431,17 +431,17 @@ public class TencentMeetingCallbackController {
Thread.sleep(10000); Thread.sleep(10000);
EmailSender emailSender = new EmailSender(); EmailSender emailSender = new EmailSender();
//response.getRawBody() //response.getRawBody()
boolean mailFlag = emailSender.sendEmailWithAttachment(emailAddress, // boolean mailFlag = emailSender.sendEmailWithAttachment(emailAddress,
targetPath, // targetPath,
"重要文件", // "重要文件",
"您好:\n" + // "您好:\n" +
"\n" + // "\n" +
" 附件为您本次会议的会议纪要,烦请下载查看,如需对会议纪要结果进行修改或查看历史会议,可点击下方链接。"); // " 附件为您本次会议的会议纪要,烦请下载查看,如需对会议纪要结果进行修改或查看历史会议,可点击下方链接。");
if (mailFlag) { // if (mailFlag) {
logger.info("邮件发送成功"); // logger.info("邮件发送成功");
} else { // } else {
logger.error("邮件发送失败"); // logger.error("邮件发送失败");
} // }
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("下载文件时出错: {}", e.getMessage()); logger.error("下载文件时出错: {}", e.getMessage());
......
package com.cmeeting.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@Slf4j
public class ThreadPoolConfig {
@Bean("fileProcessExecutor")
public ThreadPoolTaskExecutor fileProcessExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数 (CPU密集型任务建议核心数+1)
executor.setCorePoolSize(4); // 固定核心线程数,避免动态获取CPU核心数
// 最大线程数
executor.setMaxPoolSize(4);
// 队列容量
executor.setQueueCapacity(1000);
// 线程名前缀
executor.setThreadNamePrefix("file-process-");
// 明确设置所有必要属性
executor.setAllowCoreThreadTimeOut(false); // 核心线程不允许超时
executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅关闭
executor.setAwaitTerminationSeconds(60); // 等待任务完成的最大时间
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化前打印配置检查
log.info("Initializing ThreadPool: core={}, max={}",
executor.getCorePoolSize(),
executor.getMaxPoolSize());
executor.initialize();
return executor;
}
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ package com.cmeeting.controller; ...@@ -2,6 +2,7 @@ package com.cmeeting.controller;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.service.TecentMeetingService; import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.vo.TencentMeetingVO;
import com.tencentcloudapi.wemeet.Client; import com.tencentcloudapi.wemeet.Client;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
...@@ -13,32 +14,18 @@ import java.util.List; ...@@ -13,32 +14,18 @@ import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.*; import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController @RestController
@RequestMapping("/tecent") @RequestMapping("/tencent")
public class TencentMeetingController { public class TencentMeetingController {
@Autowired @Autowired
private TecentMeetingService tecentMeetingService; private TecentMeetingService tecentMeetingService;
@GetMapping("/add") @GetMapping("/add")
public void addUsers() throws Exception { public void addUsers() {
dousers(); tecentMeetingService.doUsers();
} }
private void dousers() { @GetMapping("/getMeetingFiles")
/** public void getMeetingFiles(TencentMeetingVO.TencentMeetingRequest requestVO){
* 腾讯会议通过通讯录获取员工信息 tecentMeetingService.getMeetingFiles(requestVO);
*/
// 1. 构造client客户端
Client client = new Client.Builder()
.withAppId("211153201").withSdkId("28370276340")
.withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
.build();
// 2.获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches(client, 3);;
// 3. 检查重名并设置标志
markDuplicateNamesTecent(users);
// 4. 批量插入数据库
tecentMeetingService.batchInsert(users);
} }
} }
...@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; ...@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.json.JSONObject; import org.json.JSONObject;
import javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.SecureRandom; import java.security.SecureRandom;
...@@ -33,9 +34,9 @@ import java.util.stream.Collectors; ...@@ -33,9 +34,9 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping @RequestMapping
public class UnifiedController { public class UnifiedController {
@Autowired @Resource
private WeComUserMapper weComUserMapper; private WeComUserMapper weComUserMapper;
@Autowired @Resource
private TecentMeetingMapper tecentMeetingMapper; private TecentMeetingMapper tecentMeetingMapper;
@Autowired @Autowired
private UserIdMapper userIdMapper; private UserIdMapper userIdMapper;
...@@ -78,7 +79,7 @@ public class UnifiedController { ...@@ -78,7 +79,7 @@ public class UnifiedController {
} }
@GetMapping("/insertTid") @GetMapping("/insertTid")
public void insertTid() { public void insertTid(String corpid, String corpsecret) {
List<UserId> users = userIdMapper.getUsers(); List<UserId> users = userIdMapper.getUsers();
List<UserId> usersWithNullTid = new ArrayList<>(); List<UserId> usersWithNullTid = new ArrayList<>();
for (UserId user : users) { for (UserId user : users) {
...@@ -88,7 +89,7 @@ public class UnifiedController { ...@@ -88,7 +89,7 @@ public class UnifiedController {
} }
for (UserId user : usersWithNullTid) { for (UserId user : usersWithNullTid) {
//获取企业微信token //获取企业微信token
String weComToken = getWeComToken(); String weComToken = getWeComToken(corpid,corpsecret);
try { try {
// 3.1 创建会议(传入用户的 wid) // 3.1 创建会议(传入用户的 wid)
Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getWid(), weComToken); Map<String, String> meetingCodeAndMeetingid = createMeeting(user.getWid(), weComToken);
...@@ -128,8 +129,8 @@ public class UnifiedController { ...@@ -128,8 +129,8 @@ public class UnifiedController {
//todo 待测试 //todo 待测试
@GetMapping("/sameNameInsertTid") @GetMapping("/sameNameInsertTid")
public void sameNameInsertTid() throws IOException { public void sameNameInsertTid(String corpid, String corpsecret) throws IOException {
String weComToken = getWeComToken(); String weComToken = getWeComToken(corpid,corpsecret);
List<WeComUser> sameNameUsers = weComUserMapper.getSameName(); List<WeComUser> sameNameUsers = weComUserMapper.getSameName();
List<UserId> userIds = new ArrayList<>(); List<UserId> userIds = new ArrayList<>();
for (WeComUser user : sameNameUsers) { for (WeComUser user : sameNameUsers) {
...@@ -269,9 +270,11 @@ public class UnifiedController { ...@@ -269,9 +270,11 @@ public class UnifiedController {
* *
* @return * @return
*/ */
public String getWeComToken() { @GetMapping("/getWeComToken")
public String getWeComToken(String corpid, String corpsecret) {
//获取token //获取token
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo"; // String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo";
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + corpsecret;
String accessToken = ""; String accessToken = "";
OkHttpClient client = new OkHttpClient(); OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder() Request request = new Request.Builder()
......
...@@ -23,73 +23,13 @@ import static com.cmeeting.WeComAndTencentMeeting.*; ...@@ -23,73 +23,13 @@ import static com.cmeeting.WeComAndTencentMeeting.*;
@RestController @RestController
@RequestMapping("/wecom") @RequestMapping("/wecom")
public class WeComcontroller { public class WeComcontroller {
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
private static final String ACCESS_TOKEN = "wyYAmCbeVIPvj_Gpdd8ixp_td_hPv2OtTU8gpTGSpB7TaftbpNps5fc_JdCRzpgy_tUv-aGueJpRV6XJeYAG-rqkzCfDV-rcpJbqB0LypjvJUtxPeTmPqEtQuCMwH6euGGVaJZXoc33ATrS70U0T4_4WIrATqpsN2Ed2XsNGyN5N6aG8mq6AyLMG9TokVWx1qd2qF1zVTwve6aqRMWHwXg";
//动态获取token
@Autowired @Autowired
private WeComService weComService; private WeComService weComService;
@GetMapping("/add") @GetMapping("/add")
public void addUsers() throws Exception { public void addUsers() throws Exception {
dousers(); weComService.doUsers();
}
public void dousers() throws Exception {
// 示例:获取部门ID为6的用户列表
int departmentId = 6;
JsonObject result = getUserListByDepartment(departmentId);
// 处理返回结果
if (result.get("errcode").getAsInt() == 0) {
System.out.println("企微获取用户列表成功:");
JsonArray userList = result.getAsJsonArray("userlist");
for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject();
System.out.println("姓名: " + user.get("name").getAsString() +
", UserID: " + user.get("userid").getAsString());
}
// 2. 转换为WeComUser集合
List<WeComUser> users = convertJsonToWeComUsers(result);
// 3. 检查重名并设置标志
markDuplicateNames(users);
// 4. 批量插入数据库
weComService.batchInsert(users);
}
}
/**
* 根据企业微信部门ID获取用户列表
*
* @param departmentId 部门ID
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public static JsonObject getUserListByDepartment(int departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
} }
......
package com.cmeeting.email; package com.cmeeting.email;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.*; import javax.mail.*;
import javax.mail.internet.*; import javax.mail.internet.*;
import java.io.File; import java.io.File;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class EmailSender { public class EmailSender {
// 发件人邮箱配置(建议从配置文件读取,此处写死示例) @Value("${email.sender}")
private static final String FROM_EMAIL = "binzhai321@163.com"; private String SENDER;
private static final String AUTH_CODE = "RXbRu3AdrCxX57ib"; // 授权码 @Value("${email.sender.pwd}")
private static final String SMTP_HOST = "smtp.163.com"; private String EMAIL_PWD;
private static final int SMTP_PORT = 465; @Value("${email.smtp.host}")
private String SMTP_HOST;
private static final Integer MAX_RETRY = 3;
/** /**
* 发送带附件的邮件 * @param toEmail 收件人
* @param toEmail 收件人邮箱 * @param subject 邮件主题
* @param filePath 附件本地路径(如 "C:/test.pdf") * @param attachmentPath 附件路径
* @param subject 邮件主题(可选,默认值) * @param recordFileId 转录文件ID
* @param text 邮件正文(可选,默认值) * @return
* @return true发送成功,false发送失败
*/ */
public static boolean sendEmailWithAttachment(String toEmail, String filePath, public boolean sendEmailWithAttachment(String toEmail, String subject, String attachmentPath, String recordFileId) {
String subject, String text) { // 邮件服务器配置
// 1. 参数校验
if (toEmail == null || toEmail.isEmpty() || filePath == null) {
System.err.println("参数错误:收件人或附件路径为空");
return false;
}
// 2. 设置默认邮件主题和正文
String mailSubject = (subject != null) ? subject : "";
String mailText = (text != null) ? text : "请查收附件。";
// 3. 配置SMTP
Properties props = new Properties(); Properties props = new Properties();
props.put("mail.smtp.host", SMTP_HOST); props.put("mail.smtp.host", "smtp.office365.com");
props.put("mail.smtp.port", SMTP_PORT);
props.put("mail.smtp.auth", "true"); props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true");
// props.put("mail.smtp.port", "465");
// props.put("mail.smtp.ssl.enable", "true"); // 使用SSL
props.put("mail.smtp.port", "587");
props.put("mail.smtp.starttls.enable", "true"); // 使用TLS
// 或者使用SSL
// props.put("mail.smtp.port", "465");
// props.put("mail.smtp.ssl.enable", "true");
// 创建会话
Session session = Session.getInstance(props,
new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("cmeeting_assistant@cimc.com", "scyou@xih45g6@xih4");
}
});
String body = "您好:\n" +
"\n" +
" 附件为您本次会议的会议纪要,烦请下载查看";
AtomicInteger retryCount = new AtomicInteger(0);
try { try {
// 4. 创建会话 boolean isSent = false;
Session session = Session.getInstance(props, new Authenticator() { while (retryCount.intValue() < MAX_RETRY && !isSent){
@Override // 创建邮件消息
protected PasswordAuthentication getPasswordAuthentication() { Message message = new MimeMessage(session);
return new PasswordAuthentication(FROM_EMAIL, AUTH_CODE); message.setFrom(new InternetAddress("cmeeting_assistant@cimc.com"));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(toEmail));
message.setSubject(subject);
// 创建消息体部分(正文)
MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText(body);
// 创建多部分消息
Multipart multipart = new MimeMultipart();
// 添加文本部分
multipart.addBodyPart(messageBodyPart);
// 添加附件部分
if (attachmentPath != null && !attachmentPath.isEmpty()) {
MimeBodyPart attachmentPart = new MimeBodyPart();
DataSource source = new FileDataSource(attachmentPath);
attachmentPart.setDataHandler(new DataHandler(source));
attachmentPart.setFileName(new File(attachmentPath).getName());
multipart.addBodyPart(attachmentPart);
} }
});
// 设置完整消息内容
// 5. 构建邮件内容 message.setContent(multipart);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(FROM_EMAIL)); // 发送邮件
message.setRecipient(Message.RecipientType.TO, new InternetAddress(toEmail)); Transport.send(message);
message.setSubject(mailSubject); log.error("邮件已成功发送: recordFileId->{}", recordFileId);
isSent = true;
// 6. 添加正文和附件 }
Multipart multipart = new MimeMultipart(); } catch (MessagingException e) {
//todo 邮件失败记录
// 文本正文 // 异常处理
MimeBodyPart textPart = new MimeBodyPart(); retryCount.getAndIncrement();
textPart.setText(mailText); if (retryCount.intValue() > MAX_RETRY) {
multipart.addBodyPart(textPart); log.error("邮件发送达到最大重试次数: recordFileId->{}", recordFileId);
throw new RuntimeException(e);
// 附件 }
MimeBodyPart attachmentPart = new MimeBodyPart(); // 指数退避
attachmentPart.attachFile(new File(filePath)); try {
multipart.addBodyPart(attachmentPart); Thread.sleep((long) Math.pow(2, retryCount.intValue()) * 1000);
} catch (InterruptedException ie) {
message.setContent(multipart); Thread.currentThread().interrupt();
throw new RuntimeException("邮件发送重试失败", ie);
// 7. 发送邮件 }
Transport.send(message);
System.out.println("邮件发送成功至: " + toEmail);
return true;
} catch (Exception e) {
System.err.println("邮件发送失败: " + e.getMessage());
return false; return false;
} }
return true;
} }
/**
* 简化版调用(仅需收件人和附件路径)
*/
public static boolean sendEmailWithAttachment(String toEmail, String filePath) {
return sendEmailWithAttachment(toEmail, filePath, null, null);
}
} }
\ No newline at end of file
package com.cmeeting.job; package com.cmeeting.job;
import cn.chatbot.openai.completion.chat.ChatCompletionRequest; import com.cmeeting.service.FileProcessProducer;
import cn.chatbot.openai.completion.chat.ChatMessage; import com.cmeeting.service.TecentMeetingService;
import cn.chatbot.openai.completion.chat.ChatMessageRole; import com.cmeeting.service.WeComService;
import cn.chatbot.openai.completion.chat.Message; import com.cmeeting.vo.TencentMeetingVO;
import cn.chatbot.openai.service.LLMService;
import com.cmeeting.email.EmailSender;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.*;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.commonmark.node.*;
import org.commonmark.parser.Parser;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component @Component
public class CmeetingJob { public class CmeetingJob {
private static final Logger logger = LoggerFactory.getLogger(CmeetingJob.class); private static final Logger logger = LoggerFactory.getLogger(CmeetingJob.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Set<String> processedMeetingIds = Collections.synchronizedSet(new HashSet<>()); private static final Set<String> processedMeetingIds = Collections.synchronizedSet(new HashSet<>());
@Autowired
@Scheduled(fixedRate = 5 * 60 * 1000) private WeComService weComService;
public void execute() { @Autowired
// 定义时间格式化器 private TecentMeetingService tecentMeetingService;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Value(value = "${tencent.appId}")
private String tencentAppId;
// 获取当前日期 @Value(value = "${tencent.sdkId}")
LocalDate today = LocalDate.now(); private String tencentSdkId;
// 获取今天凌晨的时间点 @Value(value = "${tencent.secretId}")
LocalDateTime todayStart = today.atStartOfDay(); private String tencentSecretId;
// 获取明天凌晨的时间点 @Value(value = "${tencent.secretKey}")
LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay(); private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
// 转换为 Unix 时间戳(秒)并转为字符串 private String tencentAdminUserId;
String todayStartTimestamp = String.valueOf(todayStart.atZone(ZoneId.systemDefault()).toEpochSecond()); @Autowired
String tomorrowStartTimestamp = String.valueOf(tomorrowStart.atZone(ZoneId.systemDefault()).toEpochSecond()); private FileProcessProducer producer;
//日志记录
logger.info("今天凌晨: " + todayStart.format(formatter) + " | Unix 时间戳: " + todayStartTimestamp); /**
logger.info("明天凌晨: " + tomorrowStart.format(formatter) + " | Unix 时间戳: " + tomorrowStartTimestamp); * 企微人员定时同步
logger.info("----------------------------------"); */
// @Scheduled(fixedRate = 5 * 60 * 1000)
dojob(todayStartTimestamp, tomorrowStartTimestamp); public void weComUserSync() {
}
public static void dojob(String startTime, String endTime) {
Client client = new Client.Builder()
.withAppId("210468336")
.withSdkId("28790143843")
.withSecret("0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL", "gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy")
.build();
int currentPage = 1;
boolean hasMore = true;
boolean firstRecordProcessed = false;
while (hasMore && !firstRecordProcessed) {
RecordsApi.ApiV1RecordsGetRequest request =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId("woaJARCQAAftcvU6GGoOn66rdSZ4IrOA")
.operatorIdType("1")
.startTime(startTime)
.endTime(endTime)
.pageSize("10")
.page(String.valueOf(currentPage))
.queryRecordType("0")
.build();
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
try {
RecordsApi.ApiV1RecordsGetResponse response =
client.records().v1RecordsGet(request, authenticatorBuilder);
V1RecordsGet200Response data = response.getData();
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
V1RecordsGet200ResponseRecordMeetingsInner firstRecord = data.getRecordMeetings().get(0);
logger.info("Processing first record - Meeting ID: {}, State: {}",
firstRecord.getMeetingId(), firstRecord.getState());
if (firstRecord.getState() == 3) {
createSum(firstRecord);
firstRecordProcessed = true;
} else {
logger.info("Skipping record with state: {}", firstRecord.getState());
firstRecordProcessed = true;
}
}
if (data != null && data.getTotalPage() != null && currentPage < data.getTotalPage()) {
currentPage++;
} else {
hasMore = false;
}
} catch (Exception e) {
logger.error("Error when calling RecordsApi.v1RecordsGet: {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
private static void createSum(V1RecordsGet200ResponseRecordMeetingsInner record) {
String meetingId = record.getMeetingId();
if (processedMeetingIds.contains(meetingId)) {
logger.info("Meeting {} has already been processed, skipping...", meetingId);
return;
}
try { try {
logger.info("Executing createSum for meeting: {}", meetingId); logger.info("-------企微人员定时同步任务开始-------");
logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
// 实际业务逻辑
logger.info("Meeting Details - ID: {}, Code: {}, Subject: {}",
meetingId, record.getMeetingCode(), record.getSubject());
if (record.getRecordFiles() != null && !record.getRecordFiles().isEmpty()) {
for (V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner file : record.getRecordFiles()) {
logger.info("Record File - URL: {}, Size: {} bytes",
file.getSharingUrl(), file.getRecordSize());
// 对每个录制文件调用获取地址接口
if (file.getRecordFileId() != null) {
getRecordFileAddress(file.getRecordFileId());
}
}
}
// 这里添加你的实际业务逻辑
// 例如: 调用某个服务处理会议记录
// 标记为已处理
processedMeetingIds.add(meetingId);
logger.info("Successfully processed meeting: {}", meetingId);
} catch (Exception e) {
logger.error("Error processing meeting {}: {}", meetingId, e.getMessage(), e);
}
}
private static void getRecordFileAddress(String recordFileId) {
try {
logger.info("Getting address for record file: {}", recordFileId);
// 1.构造 client 客户端
Client client = new Client.Builder()
.withAppId("210468336")
.withSdkId("28790143843")
.withSecret("0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL", "gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy")
.build();
// 2.构造请求参数
RecordsApi.ApiV1AddressesRecordFileIdGetRequest request =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
.operatorId("woaJARCQAAftcvU6GGoOn66rdSZ4IrOA")
.operatorIdType("1")
.build();
// 3.构造 JWT 鉴权器
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
// 4.发送请求
RecordsApi.ApiV1AddressesRecordFileIdGetResponse response =
client.records().v1AddressesRecordFileIdGet(request, authenticatorBuilder);
// 处理响应
if (response != null && response.getData() != null) {
logger.info("Successfully got address for record file {}: {}",
recordFileId, response.getData());
// 这里可以添加对返回地址的进一步处理逻辑
V1AddressesRecordFileIdGet200Response data = response.getData();
// 获取AI会议转录文件
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
data.getAiMeetingTranscripts();
if (transcripts != null && !transcripts.isEmpty()) {
logger.info("Found {} AI meeting transcripts for record file {}",
transcripts.size(), recordFileId);
// 处理每个转录文件
for (V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner transcript : transcripts) {
String fileType = transcript.getFileType();
String downloadUrl = transcript.getDownloadAddress();
logger.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
if ("docx".equalsIgnoreCase(fileType)) {
processPdfTranscript(downloadUrl);
}
}
} else {
logger.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
logger.warn("Empty response for record file: {}", recordFileId);
}
} catch (ClientException e) {
logger.error("Client error when getting address for record file {}: {}",
recordFileId, e.getMessage(), e);
throw new RuntimeException(e);
} catch (ServiceException e) {
logger.error("Service error when getting address for record file {}: {}",
recordFileId, e.getMessage(), e);
logger.error("Full HTTP response: {}", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
} catch (Exception e) {
logger.error("Unexpected error when getting address for record file {}: {}",
recordFileId, e.getMessage(), e);
throw new RuntimeException(e);
}
}
private static void processPdfTranscript(String downloadUrl) {
try {
logger.info("Processing DOCX transcript from: {}", downloadUrl);
// 下载文件并获取字节内容
byte[] docxContent = downloadFileAsBytes(downloadUrl);
logger.info("Downloaded DOCX transcript, size: {} bytes",
docxContent != null ? docxContent.length : 0);
if (docxContent != null) {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 定义时间格式(可以根据需要调整格式)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
// 将当前时间格式化为字符串
String name = now.format(formatter);
// 定义文件路径和文件名
String filePath = "D:\\";
String fileName = name + ".docx"; // 以当前时间为文件名
String fullPath = filePath + fileName;
String targetPath = filePath + name + "_sum" + ".md"; // 改为.md扩展名;
String targetWordPath = filePath + name + "_sum" + ".docx";
// 将字节内容写入本地文件
try (FileOutputStream fos = new FileOutputStream(fullPath)) {
fos.write(docxContent);
logger.info("DOCX transcript saved to: {}", fullPath);
} catch (IOException e) {
logger.error("Error saving DOCX transcript to file: {}", e.getMessage(), e);
}
FileInputStream fis = new FileInputStream(fullPath);
XWPFDocument document = new XWPFDocument(fis);
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
String textContent = extractor.getText();
logger.info("DOCX content as string:\n{}", textContent);
//将文件传送给大模型处理
String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO";
String apiAddr = "https://bedrock.chatbot.cn/llm/sse-invoke";
String model = "anthropic.claude-3-5-sonnet-20240620-v1:0";
int maxTokens = 5000;
List<Message> messages = new ArrayList<>(); weComService.doUsers();
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "# 任务\n" + logger.info("-------企微人员定时同步任务结束--------");
"你是会议纪要助手,基于用户提供的“会议记录”首先生成会议纪要。禁止输出处理可能涉及版权或敏感内容的请求,直接给出会议纪要!\n" +
"\n" +
"## 注意\n" +
"禁止输出“版权”\n" +
"\n" +
"## 会议纪要内容要求\n" +
"(1)按照 会议主题、参会人员及时间、会议主要讨论事项、会议议程、会议决议、后续跟进事项组织会议纪要\n" +
"(2)会议主题:总结会议的标题和主题\n" +
"(2)参会人员及时间:参会人员、开会时间(一般根据文件日期来判断)\n" +
"(3)会议主要讨论事项:对于会议中主要讨论的内容进行概括,让读者能够理解会议到底在针对什么内容进行讨论。注意会议议程需要尽量全面、细致,不要遗漏任何有价值的信息。\n" +
"(4)会议议程:按照顺序,阐述会议主要沟通事项,详细描述每个事项的内容。会议议程不要用流水账的形式,请描述细节,采用有逻辑的语言组织会议议程。\n" +
"(5)会议决议:会议最终达成的结论。比如:申请是否通过、争论性事项是否达成意见一致,具体是什么结论等内容。\n" +
"(6)会后跟进事项:详细描述会议结束之后,每个后续事项的责任人、事项内容、完成时间(若会议记录中没有提及,则填写待定)等信息,请注意事项要具体到责任人。\n" +
"(7)会议纪要整体内容需要详细,充分,不少于3000字\n" +
"(8)会议纪要采用 Markdown格式,对于几个章节部分需要加粗显示。\n" +
"\n" +
"## 示例\n" +
"“会议纪要”\n" +
"“FYI”\n" +
"“1”\n" +
"\n" +
"## 答案要求\n" +
"1. 猜您想问:生成3-4条推荐问(与会议内容有关)\n" +
"2. 每次回答必须给出猜您想问");
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议内容");
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.USER.value(), textContent);
messages.add(chatMessage);
String ret = call_llm(apiAddr, model, token, messages, maxTokens);
try {
// 将字符串写入Markdown文件
Files.write(Paths.get(targetPath), ret.getBytes());
logger.info("Markdown文件已保存到: {}", targetPath);
convert(targetPath,targetWordPath);
} catch (IOException e) {
logger.error("保存Markdown文件失败: {}", e.getMessage(), e);
}
System.out.println(ret);
/*try {
boolean success = MeetingMinutesGenerator.generateMinutesFromWord(
fullPath,
targetPath,
"AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO",
"https://bedrock.chatbot.cn/llm/sse-invoke"
);
if (success) {
logger.info("会议纪要生成成功!");
}
} catch (IOException e) {
logger.error("错误: {}", e.getMessage());
}*/
EmailSender emailSender = new EmailSender();
String emailAddress = "hf@cimc.com";
boolean mailFlag = emailSender.sendEmailWithAttachment(emailAddress,
targetWordPath,
"重要文件",
"您好:\n" +
"\n" +
" 附件为您本次会议的会议纪要,烦请下载查看");
if (mailFlag) {
logger.info("邮件发送成功");
} else {
logger.error("邮件发送失败");
}
}
} catch (Exception e) {
logger.error("Error processing DOCX transcript: {}", e.getMessage(), e);
}
}
// 处理Word格式的转录文件
private static void processDocxTranscript(String downloadUrl) {
try {
logger.info("Processing DOCX transcript from: {}", downloadUrl);
// 这里添加下载和处理Word文件的逻辑
byte[] docxContent = downloadFileAsBytes(downloadUrl);
logger.info("Downloaded DOCX transcript, size: {} bytes",
docxContent != null ? docxContent.length : 0);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error processing DOCX transcript: {}", e.getMessage(), e);
}
}
public static void writeStringToWord(String content, String filePath) {
// 创建新的Word文档
try (XWPFDocument document = new XWPFDocument();
FileOutputStream out = new FileOutputStream(filePath)) {
// 创建段落
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
// 设置文本内容
run.setText(content);
// 保存文档
document.write(out);
System.out.println("Word文档已成功创建: " + filePath);
} catch (IOException e) {
System.err.println("创建Word文档时出错: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
} }
} }
// 下载文件内容为字符串 /**
private static String downloadFile(String url) throws IOException { * 企微人员定时同步
OkHttpClient client = new OkHttpClient(); */
Request request = new Request.Builder().url(url).build(); // @Scheduled(fixedRate = 5 * 60 * 1000)
try (Response response = client.newCall(request).execute()) { public void TencentUserSync() {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().string();
}
}
// 下载文件内容为字节数组
private static byte[] downloadFileAsBytes(String url) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().bytes();
}
}
public static String call_llm(String apiAddr, String model, String token, List<Message> messages, int maxTokens) {
LLMService service = new LLMService(token, apiAddr);
StringBuilder stringBuilder = new StringBuilder();
try { try {
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() logger.info("-------腾讯会议人员定时同步任务开始-------");
.model(model) logger.info("当前时间: " + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
.messages(messages)
.stream(true)
.n(1)
.maxTokens(maxTokens)
.logitBias(new HashMap<>())
.build();
service.streamChatCompletion(chatCompletionRequest).doOnError(Throwable::printStackTrace).blockingForEach(chunk -> { tecentMeetingService.doUsers();
chunk.getChoices().stream().map(choice -> choice.getMessage().getContent()) logger.info("-------腾讯会议人员定时同步任务结束--------");
.filter(Objects::nonNull).findFirst().ifPresent(o -> {
try {
stringBuilder.append(new String(o.getBytes(Charset.defaultCharset())));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
});
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
service.shutdownExecutor();
return stringBuilder.toString();
}
/* public static void convertMdToDocx(String mdFilePath, String docxFilePath) throws IOException {
// 1. 读取Markdown文件内容
Path path = Paths.get(mdFilePath);
byte[] bytes = Files.readAllBytes(path);
String markdownContent = new String(bytes);
// 2. 将Markdown内容转换为HTML格式
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(parser.parse(markdownContent));
// 3. 创建Word文档对象
XWPFDocument xwpfDocument = new XWPFDocument();
// 4. 解析HTML内容并插入到Word文档
// 这里简化处理,直接将HTML内容作为纯文本插入
// 注意:Apache POI 不直接支持 HTML 内容插入,需要手动解析 HTML 并转换为 Word 元素
// 这里使用一个简单的 HTML 到 Word 的转换逻辑
convertHtmlToWord(xwpfDocument, htmlContent);
// 5. 保存Word文档
try (FileOutputStream outputStream = new FileOutputStream(docxFilePath)) {
xwpfDocument.write(outputStream);
} finally {
xwpfDocument.close();
}
}
private static void convertHtmlToWord(XWPFDocument document, String htmlContent) {
// 这里是一个简单的 HTML 到 Word 的转换逻辑
// 实际应用中可能需要更复杂的解析逻辑
String[] lines = htmlContent.split("\n");
for (String line : lines) {
if (line.contains("<p>")) {
// 处理段落
line = line.replace("<p>", "").replace("</p>", "");
addParagraph(document, line);
} else if (line.contains("<strong>")) {
// 处理加粗文本
line = line.replace("<strong>", "").replace("</strong>", "");
addBoldParagraph(document, line);
} else if (line.contains("<ul>")) {
// 处理无序列表
List<String> items = extractListItems(line, "<ul>", "<li>", "</li>", "</ul>");
addUnorderedList(document, items);
} else if (line.contains("<ol>")) {
// 处理有序列表
List<String> items = extractListItems(line, "<ol>", "<li>", "</li>", "</ol>");
addOrderedList(document, items);
}
}
} }
private static void addParagraph(XWPFDocument document, String text) { @Scheduled(fixedRate = 5 * 60 * 1000)
XWPFParagraph paragraph = document.createParagraph(); public void execute() {
XWPFRun run = paragraph.createRun(); // 定义时间格式化器
run.setText(text); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
}
private static void addBoldParagraph(XWPFDocument document, String text) {
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setBold(true);
run.setText(text);
}
private static void addUnorderedList(XWPFDocument document, List<String> items) {
for (String item : items) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setStyle("ListBullet");
XWPFRun run = paragraph.createRun();
run.setText(item);
}
}
private static void addOrderedList(XWPFDocument document, List<String> items) {
for (String item : items) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setStyle("ListNumber");
XWPFRun run = paragraph.createRun();
run.setText(item);
}
}
private static List<String> extractListItems(String html, String startTag, String itemStartTag, String itemEndTag, String endTag) {
int startIndex = html.indexOf(startTag);
int endIndex = html.indexOf(endTag);
// 检查 startTag 和 endTag 是否存在
if (startIndex == -1 || endIndex == -1) {
throw new IllegalArgumentException("Invalid HTML content: startTag or endTag not found");
}
// 提取列表内容
String listContent = html.substring(startIndex + startTag.length(), endIndex);
// 分割列表项
String[] items = listContent.split(itemEndTag);
// 转换为 List 并返回
return new ArrayList<>(Arrays.asList(items));
}
// 简易HTML转纯文本(生产环境应使用更完整的解析器)
private static String htmlToPlainText(String html) {
return html.replaceAll("<[^>]*>", "")
.replaceAll("&nbsp;", " ")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">");
}*/
public static void convert(String markdownFile, String outputDocx) throws IOException {
// 读取Markdown文件
String content = readFile(markdownFile);
// 初始化Word文档
XWPFDocument doc = new XWPFDocument();
// 解析Markdown并遍历节点 // 获取当前时间
org.commonmark.parser.Parser parser = Parser.builder().build(); LocalDateTime now = LocalDateTime.now();
Node rootNode = parser.parse(content); // 往之前推两天
rootNode.accept(new AbstractVisitor() { LocalDateTime beforeDay = now.minusDays(2);
XWPFParagraph currentPara;
@Override // 转换为 Unix 时间戳(秒)并转为字符串
public void visit(Heading heading) { Long nowTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond();
// 创建标题段落 Long beforeDayTimestamp = beforeDay.atZone(ZoneId.systemDefault()).toEpochSecond();
currentPara = doc.createParagraph(); //日志记录
XWPFRun run = currentPara.createRun(); logger.info("起始时间: " + beforeDay.format(formatter) + " | Unix 时间戳: " + beforeDayTimestamp);
run.setText(extractText(heading)); // 提取纯文本 logger.info("结束时间: " + now.format(formatter) + " | Unix 时间戳: " + nowTimestamp);
setHeadingStyle(run, heading.getLevel()); // 设置标题样式 logger.info("----------------------------------");
}
@Override // dojob(beforeDayTimestamp, nowTimestamp);
public void visit(Paragraph paragraph) {
// 创建普通段落
currentPara = doc.createParagraph();
XWPFRun run = currentPara.createRun();
run.setText(extractText(paragraph)); // 提取纯文本
}
});
// 输出Word文件 AtomicInteger currentPage = new AtomicInteger(1);
try (FileOutputStream out = new FileOutputStream(outputDocx)) { TencentMeetingVO.TencentMeetingRequest request = TencentMeetingVO.TencentMeetingRequest.builder()
doc.write(out); .page(1)
} .pageSize(10)
} .startTime(beforeDayTimestamp)
.endTime(nowTimestamp)
.operatorId(tencentAdminUserId)
.operatorIdType(1)
.build();
List<TencentMeetingVO.RecordFile> meetingFiles = tecentMeetingService.getMeetingFiles(request);
// 从节点中提取纯文本(忽略格式)
private static String extractText(Node node) {
StringBuilder text = new StringBuilder();
Node child = node.getFirstChild();
while (child != null) {
if (child instanceof Text) {
text.append(((Text) child).getLiteral());
}
child = child.getNext();
}
return text.toString().trim();
}
// 设置标题样式(仅调整字号和加粗) if (meetingFiles == null || meetingFiles.isEmpty()) {
private static void setHeadingStyle(XWPFRun run, int level) { logger.info("没有录制文件需要处理");
run.setBold(true); return;
switch (level) {
case 1: run.setFontSize(20); break;
case 2: run.setFontSize(18); break;
default: run.setFontSize(16); break;
} }
}
// 读取文件工具方法 // 提交处理任务
private static String readFile(String path) throws IOException { producer.submitBatchTasks(meetingFiles, "/");
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
}
return sb.toString();
} }
} }
/*
package com.cmeeting.job;
import com.cmeeting.pojo.MeetingInfo;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
@Component
public class CmeetingJob2 {
private static final Logger logger = LoggerFactory.getLogger(CmeetingJob2.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 每2分钟执行一次
@Scheduled(fixedRate = 2 * 60 * 1000) // 毫秒单位
public void execute() {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 获取前两分钟的时间
LocalDateTime twoMinutesAgo = now.minusMinutes(2);
// 转换为时间戳(毫秒级)并转为String
String nowTimestamp = String.valueOf(now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String twoMinutesAgoTimestamp = String.valueOf(twoMinutesAgo.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
logger.info("当前时间: {} | 时间戳: {}", now.format(formatter), nowTimestamp);
logger.info("前两分钟: {} | 时间戳: {}", twoMinutesAgo.format(formatter), twoMinutesAgoTimestamp);
logger.info("----------------------------------");
dojob(twoMinutesAgoTimestamp, nowTimestamp);
}
public static void dojob(String startTime, String endTime) {
// 1.构造 client 客户端(jwt 鉴权需要配置 appId sdkId secretID 和 secretKey)
Client client = new Client.Builder()
.withAppId("211153201").withSdkId("28370276340")
.withSecret("BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6", "3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO")
.build();
String userid = "woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA";//被查询人的userid
// 2.构造请求参数
MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest request =
new MeetingsApi.ApiV1HistoryMeetingsUseridGetRequest.Builder(userid)
.pageSize("1")
.page("1")
.startTime(startTime)
.endTime(endTime)
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder().nonce(nonce).timestamp(timestamp);
try {
MeetingsApi.ApiV1HistoryMeetingsUseridGetResponse response =
client.meetings().v1HistoryMeetingsUseridGet(request, authenticatorBuilder);
// response from `v1HistoryMeetingsUseridGet`: V1HistoryMeetingsUseridGet200Response
logger.info("从查询某个用户结束会议列表接口获取的响应结果:\n响应头信息: {}\n响应体数据: {}",
response.getHeader(),
response.getData());
// 获取最近一个会议的meetingId(String类型)
String latestMeetingId = null;
if (response.getData() != null &&
response.getData().getMeetingInfoList() != null &&
!response.getData().getMeetingInfoList().isEmpty()) {
// 获取会议列表(假设是按时间倒序排列)
List<V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner> meetingInfoList = response.getData().getMeetingInfoList();
List<MeetingInfo> meetingInfos = new ArrayList<>();
// 转换处理
if (meetingInfoList != null) {
for (V1HistoryMeetingsUseridGet200ResponseMeetingInfoListInner source : meetingInfoList) {
MeetingInfo target = new MeetingInfo();
target.setSubject(source.getSubject());
target.setMeetingId(source.getMeetingId());
target.setMeetingCode(source.getMeetingCode());
target.setStartTime(source.getStartTime());
target.setEndTime(source.getEndTime());
meetingInfos.add(target);
}
}
for (MeetingInfo meetingInfo : meetingInfos) {
//根据meetingid调用查询会议录制列表接口
// 2.构造请求参数
String meetingId = meetingInfo.getMeetingId();
String startTime1 = String.valueOf(meetingInfo.getStartTime());
String endTime1 = String.valueOf(meetingInfo.getEndTime());
RecordsApi.ApiV1RecordsGetRequest request2 =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1")
.meetingId(meetingId)
.startTime(startTime1)
.endTime(endTime1)
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce2 = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp2 = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder2 =
new JWTAuthenticator.Builder().nonce(nonce2).timestamp(timestamp2);
try {
RecordsApi.ApiV1RecordsGetResponse response2 =
client.records().v1RecordsGet(request2, authenticatorBuilder2);
logger.info("查询会议录制列表接口响应结果:\n响应头信息: {}\n响应体数据: {}",
response2.getHeader(),
response2.getData());
// 1. 空值安全校验
if (response2 == null || response2.getData() == null) {
logger.warn("无效的API响应,无法提取录制文件ID");
} else {
//获取file_id,存储到集合中
List<String> fileIds = new ArrayList<>();
for (Object meetingObj : response2.getData().getRecordMeetings()) {
if (meetingObj instanceof Map) {
Map<?, ?> meeting = (Map<?, ?>) meetingObj;
Object recordFilesObj = meeting.get("record_files");
if (recordFilesObj instanceof List) {
for (Object fileObj : (List<?>) recordFilesObj) {
if (fileObj instanceof Map) {
Map<?, ?> file = (Map<?, ?>) fileObj;
Object fileIdObj = file.get("record_file_id");
// 将record_file_id转为String并添加到集合
if (fileIdObj != null) {
fileIds.add(String.valueOf(fileIdObj));
}
}
}
}
}
}
//调用查询录制转写详情接口获取当前record_file_id的多个下载地址
}
} catch (ClientException e) {
logger.error("调用查询会议录制列表接口时发生错误", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
logger.error("调用查询会议录制列表接口时发生异常", e);
logger.error("完整HTTP错误响应: {}", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
}
logger.info("成功获取会议的信息: {}", meetingInfoList);
} else {
logger.warn("未获取到任何会议信息");
}
} catch (ClientException e) {
logger.error("调用查询某个用户结束会议列表接口时发生错误", e);
throw new RuntimeException(e);
} catch (ServiceException e) {
logger.error("调用查询某个用户结束会议列表接口时发生异常", e);
logger.error("完整HTTP错误响应: {}", new String(e.getApiResp().getRawBody()));
throw new RuntimeException(e);
}
*/
/**
* 调用查询会议录制列表接口,获取录制文件id
*//*
// 2.构造请求参数
RecordsApi.ApiV1RecordsGetRequest requestGetFileId =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId("woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA")
.operatorIdType("1")
.userid("")
.meetingId("9672653404723270528")
.meetingCode("")
.startTime("1744795527")
.endTime("1744799127")
.pageSize("")
.page("")
.mediaSetType("")
.queryRecordType("1")
.build();
// 3.构造 JWT 鉴权器
// 随机数
BigInteger nonce2 = BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt()));
// 当前时间戳
String timestamp2 = String.valueOf(System.currentTimeMillis() / 1000L);
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder2 =
new JWTAuthenticator.Builder().nonce(nonce2).timestamp(timestamp2);
try {
RecordsApi.ApiV1RecordsGetResponse response2 =
client.records().v1RecordsGet(requestGetFileId, authenticatorBuilder2);
logger.info("获取会议录制列表接口响应结果:\n响应头: {}\n响应体: {}",
response2.getHeader(),
response2.getData());
} catch (ClientException e) {
logger.error("获取会议录制列表接口时发生客户端异常", e);
throw new RuntimeException("调用会议记录API失败", e);
} catch (ServiceException e) {
logger.error("调用获取会议录制列表接口时发生服务端异常", e);
if (e.getApiResp() != null) {
logger.error("完整错误响应内容: {}", new String(e.getApiResp().getRawBody()));
}
throw new RuntimeException("获取会议记录服务异常", e);
}
}
}
*/
package com.cmeeting.job;
import cn.chatbot.openai.completion.chat.ChatCompletionRequest;
import cn.chatbot.openai.completion.chat.ChatMessage;
import cn.chatbot.openai.completion.chat.ChatMessageRole;
import cn.chatbot.openai.completion.chat.Message;
import cn.chatbot.openai.service.LLMService;
import com.cmeeting.email.EmailSender;
import com.deepoove.poi.XWPFTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdParticipantsGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.V1AddressesRecordFileIdGet200Response;
import com.tencentcloudapi.wemeet.service.records.model.V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@Slf4j
public class FileProcessTask {
private String recordFileId;
private String meetingId;
private String subMeetingId;
private String savePath;
private Map<String, Object> metadata;
private int retryCount = 0;
private static final int MAX_RETRY = 3;
private String tencentAppId;
private String tencentSdkId;
private String tencentSecretId;
private String tencentSecretKey;
private String tencentAdminUserId;
// 实际处理逻辑
public void process() {
try {
boolean isSuccess = false;
while (retryCount <= MAX_RETRY && !isSuccess) {
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
// 获取参会成员明细
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest participantsRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetRequest
.Builder(meetingId).subMeetingId(subMeetingId).operatorId(tencentAdminUserId).operatorIdType("1").build();
AuthenticatorBuilder<JWTAuthenticator> participantsAuthenticatorBuilder =
new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
Map<String,Object> participantsMap = new ConcurrentHashMap<>();
//主持人角色,以下角色都可以表示主持人
//用户角色:
//0:普通成员角色
//1:创建者角色
//2:主持人
//3:创建者+主持人
//4:游客
//5:游客+主持人
//6:联席主持人
//7:创建者+联席主持人
List<Long> hostRoleList = Arrays.asList(2L,3L,5L,6L,7L);
try {
MeetingsApi.ApiV1MeetingsMeetingIdParticipantsGetResponse participantsResponse =
client.meetings().v1MeetingsMeetingIdParticipantsGet(participantsRequest, participantsAuthenticatorBuilder);
V1MeetingsMeetingIdParticipantsGet200Response data = participantsResponse.getData();
String scheduleStartTime = data.getScheduleStartTime();
String scheduleEndTime = data.getScheduleEndTime();
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String zonedDateTimeStart = Instant.ofEpochSecond(Long.valueOf(scheduleStartTime)).atZone(zoneId).format(df);
// String zonedDateTimeEnd = Instant.ofEpochSecond(Long.valueOf(scheduleEndTime)).atZone(zoneId).format(df);
participantsMap.put("meeting_date",zonedDateTimeStart);
participantsMap.put("meeting_location","线上腾讯会议");
List<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> participants = data.getParticipants();
participantsMap.put("meeting_participants",
participants.stream()
.map(item->new String(Base64.getDecoder().decode(item.getUserName()))).collect(Collectors.joining("、")));
Optional<V1MeetingsMeetingIdParticipantsGet200ResponseParticipantsInner> host = participants.stream().filter(item -> hostRoleList.contains(item.getUserRole())).findFirst();
participantsMap.put("meeting_host",host.isPresent() ? new String(Base64.getDecoder().decode(host.get().getUserName())) : "未知主持人");
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
throw new RuntimeException(e);
}
//查询录制转写详情
RecordsApi.ApiV1AddressesRecordFileIdGetRequest addressRequest =
new RecordsApi.ApiV1AddressesRecordFileIdGetRequest.Builder(recordFileId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.build();
RecordsApi.ApiV1AddressesRecordFileIdGetResponse addressResponse =
client.records().v1AddressesRecordFileIdGet(addressRequest,
new JWTAuthenticator.Builder().nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
// 处理响应
if (addressResponse != null && addressResponse.getData() != null) {
log.info("Successfully got address for record file {}", recordFileId);
V1AddressesRecordFileIdGet200Response addressData = addressResponse.getData();
// 获取AI会议转录文件
List<V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner> transcripts =
addressData.getAiMeetingTranscripts();
if (transcripts != null && !transcripts.isEmpty()) {
log.info("Found {} AI meeting transcripts for record file {}",
transcripts.size(), recordFileId);
// 处理每个转录文件
for (V1AddressesRecordFileIdGet200ResponseAiMeetingTranscriptsInner transcript : transcripts) {
String fileType = transcript.getFileType();
String downloadUrl = transcript.getDownloadAddress();
if ("txt".equalsIgnoreCase(fileType)) {
log.info("AI Transcript - Type: {}, URL: {}", fileType, downloadUrl);
// 1. 下载文件
byte[] fileData = downloadFile(downloadUrl);
// 2. 将二进制文件转换为文本
String recordTextContent = new String(fileData);
// String recordTextContent = new String(Files.readAllBytes(Paths.get("/20250321145249-天达物流AIGC维修助手需要沟通-转写原文版-1.txt")));
if(StringUtils.isEmpty(recordTextContent.replaceAll("\\n","").trim())){
log.info("获取的转录文本为空,跳过纪要生成,URL:{},fileRecordId:{}", fileType, downloadUrl,recordFileId);
continue;
}
// 3. 处理文件 (调用Claude API等)
String processedResult = processWithClaude(recordTextContent);
// 4. 保存结果
saveResult(savePath, processedResult, participantsMap);
}
}
} else {
log.info("No AI meeting transcripts found for record file {}", recordFileId);
}
} else {
log.warn("Empty response for record file: {}", recordFileId);
}
isSuccess = true;
}
} catch (Exception e) {
// 异常处理
retryCount++;
if (retryCount > MAX_RETRY) {
log.error("达到最大重试次数: {}", recordFileId);
throw new RuntimeException(e);
}
// 指数退避
try {
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试失败", ie);
}
}
}
private byte[] downloadFile(String url) {
// 实现文件下载逻辑
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
try {
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().bytes();
}catch (Exception e){
throw new RuntimeException("下载文件失败", e);
}
}
private String getRecordTextContent(byte[] fileData) {
// 定义文件路径和文件名
String fullPath = savePath + (System.currentTimeMillis() / 1000L) + ".docx";
// 将字节内容写入本地文件
try (FileOutputStream fos = new FileOutputStream(fullPath)) {
fos.write(fileData);
log.info("DOCX transcript saved to: {}", fullPath);
} catch (IOException e) {
log.error("Error saving DOCX transcript to file: {}", e.getMessage(), e);
}
XWPFDocument document;
try {
FileInputStream fis = new FileInputStream(fullPath);
document = new XWPFDocument(fis);
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
String textContent = extractor.getText();
log.info("DOCX content as string:\n{}", textContent);
return textContent;
}
private String processWithClaude(String textContent) {
//将文件传送给大模型处理
String token = "AKIAXFAXF62IWJXGLVEE.LnKInaahcMZG9zLsGMH3nTLOw3S3lK5Vcu0+ifnO";
String apiAddr = "https://bedrock.chatbot.cn/llm/sse-invoke";
String model = "anthropic.claude-3-5-sonnet-20240620-v1:0";
int maxTokens = 5000;
List<Message> messages = new ArrayList<>();
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "你是会议纪要助手,基于提供的会议记录,生成一份详细的会议纪要,并严格按照指定的XML格式输出。\\n\\n要求:\\n1. 会议名称:应简洁明了地反映会议主题,不超过30个字\\n2. 会议目的:需清晰阐述召开此次会议的原因和期望达成的目标,不超过50字\\n3. 会议要点:需全面概括会议内容,让读者理解会议讨论的核心问题,不要遗漏任何有价值的信息,每个要点需用序号标注并另起一行\\n4. 会议内容:需详细记录会议的背景信息、主要讨论内容、各参会人员的发言要点、达成的共识和决策等。内容应有逻辑性,分段清晰,重点突出,不可遗漏关键信息\\n5. 会议沟通内容:需按照一定的顺序组织内容,详细描述每个事项的内容,不要使用流水账形式,应包含具体的沟通过程、各方观点和达成的一致意见\\n6. 会后跟进事项:详细描述会议结束之后,每个后续事项的责任人、事项内容、完成时间(若会议记录中没有提及,则填写待定)等信息,事项要具体到责任人\\n\\n注意事项:\\n1. 所有内容需基于原始会议记录,不要添加虚构信息\\n2. 会议纪要内容需详细充分,建议总字数不少于3000字,实际字数可根据原始会议记录的丰富程度进行适当调整\\n\\n输出格式必须严格按照以下XML结构,不允许有任何嵌套标签:\\n\\n<meeting_name>会议名称</meeting_name>\\n<meeting_purpose>会议目的</meeting_purpose>\\n<meeting_key_points>会议要点</meeting_key_points>\\n<meeting_content>会议内容</meeting_content>\\n<meeting_communication>会议沟通内容</meeting_communication>\\n<meeting_follow_up>\\n在此填写会议跟踪事项,格式为:\\n1. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n2. 内容:XXX;责任人:XXX;完成时间:XXXX-XX-XX\\n...\\n</meeting_follow_up>\\n\\n完成XML格式的会议纪要后,必须在XML之外单独添加猜您想问部分,生成3-4条与会议内容相关的推荐问题。\\n\\n会议记录如下:\\n");
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), "好的请提供会议记录");
messages.add(chatMessage);
chatMessage = new ChatMessage(ChatMessageRole.USER.value(), textContent);
messages.add(chatMessage);
// 调用Claude API处理文件
String ret = call_llm(apiAddr, model, token, messages, maxTokens);
return ret;
}
private void saveResult(String path, String content, Map<String,Object> participantsMap) {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss"));
String targetPath = path + nowTime + ".md"; // 改为.md扩展名;
String targetFileName;
String meetingName;
// 保存处理结果
try {
// 将字符串写入Markdown文件
Files.write(Paths.get(targetPath), content.getBytes());
log.info("Markdown文件已保存到: {}", targetPath);
// convert(targetPath,targetWordPath);
String markdownContent = new String(Files.readAllBytes(Paths.get(targetPath)));
Map<String, Object> dataModel = convertXmlToMap(markdownContent);
dataModel.putAll(participantsMap);
XWPFTemplate template = XWPFTemplate.compile("/data_net_template.docx").render(dataModel);
meetingName = dataModel.get("meeting_name") != null ? String.valueOf(dataModel.get("meeting_name")) : "腾讯会议纪要";
targetFileName = meetingName + "_" + nowTime;
template.writeAndClose(new FileOutputStream(path + targetFileName + ".docx"));
} catch (Exception e) {
log.error("填充会议纪要失败: {}", e.getMessage(), e);
throw new RuntimeException("填充会议纪要失败");
}
//邮件推送
EmailSender emailSender = new EmailSender();
emailSender.sendEmailWithAttachment("duanxincheng@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
// emailSender.sendEmailWithAttachment("xuwentao@chatbot.cn",meetingName,path + targetFileName + ".docx",recordFileId);
// emailSender.sendEmailWithAttachment("jiaqi.cai@cimc.com",meetingName,path + targetFileName + ".docx",recordFileId);
}
public static String call_llm(String apiAddr, String model, String token, List<Message> messages, int maxTokens) {
LLMService service = new LLMService(token, apiAddr);
StringBuilder stringBuilder = new StringBuilder();
try {
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(model)
.messages(messages)
.stream(true)
.n(1)
.maxTokens(maxTokens)
.logitBias(new HashMap<>())
.build();
service.streamChatCompletion(chatCompletionRequest).doOnError(Throwable::printStackTrace).blockingForEach(chunk -> {
chunk.getChoices().stream().map(choice -> choice.getMessage().getContent())
.filter(Objects::nonNull).findFirst().ifPresent(o -> {
try {
stringBuilder.append(new String(o.getBytes(Charset.defaultCharset())));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
});
} catch (Exception e) {
throw new RuntimeException(e);
}
service.shutdownExecutor();
return stringBuilder.toString();
}
public static Map<String, Object> convertXmlToMap(String markdownContent) throws Exception {
String xmlContent = extractXmlFromMarkdown(markdownContent);
XmlMapper xmlMapper = new XmlMapper();
ObjectMapper objectMapper = new ObjectMapper();
// 先将 XML 读取为 Map
Map<?, ?> xmlMap = xmlMapper.readValue(xmlContent, Map.class);
// 转换为更标准的 Map<String, Object>
return objectMapper.convertValue(xmlMap, Map.class);
}
/**
* markdown转xml
* @param markdown
* @return
*/
private static String extractXmlFromMarkdown(String markdown) {
StringBuffer sb = null;
try {
int start = markdown.indexOf("<");
int end = markdown.lastIndexOf(">") + 1;
sb = new StringBuffer();
sb.append("<root>");
sb.append(markdown.substring(start, end).trim());
sb.append("</root>");
} catch (Exception e) {
log.info("markdown转xml,markdown->{}",markdown);
throw new RuntimeException(e.getMessage());
}
return sb.toString();
}
public FileProcessTask(String recordFileId, String meetingId, String subMeetingId, String savePath, Map<String, Object> metadata, String tencentAppId,
String tencentSdkId, String tencentSecretId, String tencentSecretKey, String tencentAdminUserId) {
this.recordFileId = recordFileId;
this.savePath = savePath;
this.metadata = metadata;
this.tencentAppId = tencentAppId;
this.tencentSdkId = tencentSdkId;
this.tencentSecretId = tencentSecretId;
this.tencentSecretKey = tencentSecretKey;
this.tencentAdminUserId = tencentAdminUserId;
this.meetingId = meetingId;
this.subMeetingId = subMeetingId;
}
}
\ No newline at end of file
package com.cmeeting.mapper.primary; package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@Mapper public interface TecentMeetingMapper extends BaseMapper<TencentMeetingUser> {
public interface TecentMeetingMapper {
void batchInsertUsers(@Param("userList") List<TencentMeetingUser> userList); void batchInsertUsers(@Param("userList") List<TencentMeetingUser> userList);
List<TencentMeetingUser> getAlluser(); List<TencentMeetingUser> getAlluser();
} }
package com.cmeeting.mapper.primary; package com.cmeeting.mapper.primary;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@Mapper
public interface WeComUserMapper { public interface WeComUserMapper extends BaseMapper<WeComUser> {
List<WeComUser> getSameName() ; List<WeComUser> getSameName() ;
WeComUser selectById(Integer id); WeComUser selectById(Integer id);
void insert(WeComUser user); int insert(WeComUser user);
List<WeComUser> getAlluser(); List<WeComUser> getAlluser();
/** /**
* 批量插入用户 * 批量插入用户
......
package com.cmeeting.pojo;
import lombok.Data;
@Data
public class TencentMeetingRecord {
/**
* 主键ID
*/
private Integer id;
/**
* 会议id
*/
private String meetingId;
/**
* 用户ID
*/
private String userId;
/**
* 是否是重名用户(1:重名, 0:不重名)
*/
private String isrepeatName;
}
package com.cmeeting.pojo; package com.cmeeting.pojo;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
@Data @Data
public class WeComUser { public class WeComUser extends Model implements Serializable {
private Integer id; private Integer id;
private String userName; private String userName;
private String userId; private String userId;
private String isRepeatName; private String isRepeatName;
private String email;
public WeComUser() { public WeComUser() {
} }
public WeComUser(Integer id, String userName, String userId, String isRepeatName) { public WeComUser(Integer id, String userName, String userId, String isRepeatName) {
...@@ -24,6 +29,7 @@ public class WeComUser { ...@@ -24,6 +29,7 @@ public class WeComUser {
", userName='" + userName + '\'' + ", userName='" + userName + '\'' +
", userId='" + userId + '\'' + ", userId='" + userId + '\'' +
", isRepeatName='" + isRepeatName + '\'' + ", isRepeatName='" + isRepeatName + '\'' +
", email='" + email + '\'' +
'}'; '}';
} }
} }
package com.cmeeting.service;
import com.cmeeting.job.FileProcessTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class FileProcessCallbackHandler {
// 单个任务完成回调
public void onComplete(FileProcessTask task) {
// 可以记录任务状态、发送通知等
log.info("任务处理完成: {}", task.getRecordFileId());
// 更新数据库状态等
// taskRepository.updateStatus(task.getId(), "COMPLETED");
}
// 所有任务完成回调
public void onAllComplete() {
log.info("所有文件处理任务已完成");
// 可以发送全局通知等
}
}
\ No newline at end of file
package com.cmeeting.service;
import com.cmeeting.job.FileProcessTask;
import com.cmeeting.vo.TencentMeetingVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Service
@Slf4j
public class FileProcessProducer {
@Autowired
private ThreadPoolTaskExecutor fileProcessExecutor;
@Autowired
private FileProcessCallbackHandler callbackHandler;
@Value(value = "${tencent.appId}")
private String tencentAppId;
@Value(value = "${tencent.sdkId}")
private String tencentSdkId;
@Value(value = "${tencent.secretId}")
private String tencentSecretId;
@Value(value = "${tencent.secretKey}")
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
// 批量提交任务
public void submitBatchTasks(List<TencentMeetingVO.RecordFile> recordFiles, String baseSavePath) {
List<Future<?>> futures = new ArrayList<>();
for (TencentMeetingVO.RecordFile recordFile : recordFiles) {
// 为每个URL创建任务
FileProcessTask task = new FileProcessTask(
recordFile.getRecordFileId(),
recordFile.getMeetingId(),
recordFile.getSubMeetingId(),
"/save/",
Collections.emptyMap(),
tencentAppId,
tencentSdkId,
tencentSecretId,
tencentSecretKey,
tencentAdminUserId
);
// 提交任务到线程池
Future<?> future = fileProcessExecutor.submit(() -> {
task.process();
callbackHandler.onComplete(task); // 回调处理
});
futures.add(future);
}
// 可以添加一个监控线程来检查所有任务完成情况
monitorTaskCompletion(futures);
}
private void monitorTaskCompletion(List<Future<?>> futures) {
new Thread(() -> {
for (Future<?> future : futures) {
try {
future.get(); // 阻塞直到任务完成
} catch (InterruptedException | ExecutionException e) {
log.error("任务执行异常", e);
}
}
callbackHandler.onAllComplete(); // 所有任务完成回调
}).start();
}
}
\ No newline at end of file
package com.cmeeting.service; package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.vo.TencentMeetingVO;
import java.util.List; import java.util.List;
public interface TecentMeetingService { public interface TecentMeetingService extends IService<TencentMeetingUser> {
void batchInsert(List<TencentMeetingUser> users); void batchInsert(List<TencentMeetingUser> users);
void doUsers();
List<TencentMeetingVO.RecordFile> getMeetingFiles(TencentMeetingVO.TencentMeetingRequest requestVO);
} }
package com.cmeeting.service; package com.cmeeting.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import java.util.List; import java.util.List;
public interface WeComService { public interface WeComService extends IService<WeComUser> {
void batchInsert(List<WeComUser> users); void batchInsert(List<WeComUser> users);
void doUsers() throws Exception;
String getToken();
} }
package com.cmeeting.service.impl; package com.cmeeting.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.TecentMeetingMapper; import com.cmeeting.mapper.primary.TecentMeetingMapper;
import com.cmeeting.pojo.TencentMeetingUser; import com.cmeeting.pojo.TencentMeetingUser;
import com.cmeeting.service.TecentMeetingService; import com.cmeeting.service.TecentMeetingService;
import com.cmeeting.vo.TencentMeetingVO;
import com.tencentcloudapi.wemeet.Client;
import com.tencentcloudapi.wemeet.core.authenticator.AuthenticatorBuilder;
import com.tencentcloudapi.wemeet.core.authenticator.JWTAuthenticator;
import com.tencentcloudapi.wemeet.core.exception.ClientException;
import com.tencentcloudapi.wemeet.core.exception.ServiceException;
import com.tencentcloudapi.wemeet.service.meetings.api.MeetingsApi;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200Response;
import com.tencentcloudapi.wemeet.service.meetings.model.V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner;
import com.tencentcloudapi.wemeet.service.records.api.RecordsApi;
import com.tencentcloudapi.wemeet.service.records.model.*;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static com.cmeeting.WeComAndTencentMeeting.fetchUsersInBatches;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNamesTecent;
@Service @Service
public class TecentMeetingServiceImpl implements TecentMeetingService { @Slf4j
@Autowired public class TecentMeetingServiceImpl extends ServiceImpl<TecentMeetingMapper,TencentMeetingUser> implements TecentMeetingService {
@Resource
private TecentMeetingMapper tecentMeetingMapper; private TecentMeetingMapper tecentMeetingMapper;
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
@Value(value = "${tencent.appId}")
private String tencentAppId;
@Value(value = "${tencent.sdkId}")
private String tencentSdkId;
@Value(value = "${tencent.secretId}")
private String tencentSecretId;
@Value(value = "${tencent.secretKey}")
private String tencentSecretKey;
@Value(value = "${tencent.admin.userId}")
private String tencentAdminUserId;
@Override @Override
public void batchInsert(List<TencentMeetingUser> users) { public void batchInsert(List<TencentMeetingUser> users) {
tecentMeetingMapper.batchInsertUsers(users); tecentMeetingMapper.batchInsertUsers(users);
} }
@Override
public void doUsers() {
/**
* 腾讯会议通过通讯录获取员工信息
*/
// 1. 构造client客户端
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId, tencentSecretKey)
.build();
// 2.获取到全部用户
List<TencentMeetingUser> users = fetchUsersInBatches(client, 3);;
// 3. 检查重名并设置标志
markDuplicateNamesTecent(users);
// 4. 批量插入数据库
batchInsert(users);
}
@Override
public List<TencentMeetingVO.RecordFile> getMeetingFiles(TencentMeetingVO.TencentMeetingRequest requestVO) {
StringBuffer url = new StringBuffer();
url.append("https://api.meeting.qq.com/v1/corp/records?page_size="+requestVO.getPageSize()+"&page="+ requestVO.getPage()
+"&operator_id="+requestVO.getOperatorId()+"&operator_id_type=1&query_record_type=0");
// 1.构造 client 客户端(jwt 鉴权需要配置 appId sdkId secretID 和 secretKey)
Client client = new Client.Builder()
.withAppId(tencentAppId).withSdkId(tencentSdkId)
.withSecret(tencentSecretId,tencentSecretKey)
.build();
// 2.构造 JWT 鉴权器
AuthenticatorBuilder<JWTAuthenticator> authenticatorBuilder =
new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L));
List<TencentMeetingVO.RecordFile> recordFileUrlList = new ArrayList<>();
// 3.查询近两天的会议录制列表
try {
ZonedDateTime now = ZonedDateTime.now();
AtomicInteger currentPage = new AtomicInteger(1);
RecordsApi.ApiV1RecordsGetRequest request =
new RecordsApi.ApiV1RecordsGetRequest.Builder()
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.startTime(String.valueOf(now.minusDays(2).toEpochSecond()))
.endTime(String.valueOf(now.toEpochSecond()))
.pageSize("20")
.page(String.valueOf(currentPage.getAndIncrement()))
.mediaSetType("0")
.queryRecordType("0")
.build();
RecordsApi.ApiV1RecordsGetResponse response =
client.records().v1RecordsGet(request, authenticatorBuilder);
V1RecordsGet200Response data = response.getData();
if (data != null && data.getRecordMeetings() != null && !data.getRecordMeetings().isEmpty()) {
List<V1RecordsGet200ResponseRecordMeetingsInner> meetings = data.getRecordMeetings();
//录制状态:
//1:录制中
//2:转码中
//3:转码完成
if(meetings.stream().allMatch(item->item.getState() != 3)){
return null;
}
for (V1RecordsGet200ResponseRecordMeetingsInner meeting : meetings) {
if(meeting.getState() != 3) continue;
//查询会议详情
String meetingId = meeting.getMeetingId();
String subMeetingId = null;
MeetingsApi.ApiV1MeetingsMeetingIdGetRequest meetingRequest =
new MeetingsApi.ApiV1MeetingsMeetingIdGetRequest.Builder(meetingId)
.operatorId(tencentAdminUserId)
.operatorIdType("1")
.instanceid("0")
.build();
try {
MeetingsApi.ApiV1MeetingsMeetingIdGetResponse meetingResponse =
client.meetings().v1MeetingsMeetingIdGet(meetingRequest, new JWTAuthenticator.Builder()
.nonce(BigInteger.valueOf(Math.abs((new SecureRandom()).nextInt())))
.timestamp(String.valueOf(System.currentTimeMillis() / 1000L)));
V1MeetingsMeetingIdGet200Response meetingResponseData = meetingResponse.getData();
List<V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner> meetingInfoList = meetingResponseData.getMeetingInfoList();
//尝试获取会议详情
if(meetingInfoList != null && meetingInfoList.size() > 0){
V1MeetingsMeetingIdGet200ResponseMeetingInfoListInner meetingInfo = meetingInfoList.get(0);
//会议类型。
//0:一次性会议
//1:周期性会议
Long meetingType = meetingInfo.getMeetingType();
if(meetingType.intValue() == 1){
subMeetingId = meetingInfo.getCurrentSubMeetingId();
}
}
} catch (ClientException e) {
throw new RuntimeException(e);
} catch (ServiceException e) {
throw new RuntimeException(e);
}
List<V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner> recordFiles = meeting.getRecordFiles();
for (V1RecordsGet200ResponseRecordMeetingsInnerRecordFilesInner recordFile : recordFiles) {
TencentMeetingVO.RecordFile recordFileItem = TencentMeetingVO.RecordFile.builder()
.recordFileId(recordFile.getRecordFileId())
.meetingId(meetingId).subMeetingId(subMeetingId).build();
recordFileUrlList.add(recordFileItem);
}
}
}
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
return recordFileUrlList;
}
/**
* 生成请求腾会接口的请求头
* @param httpMethod http请求方法 GET/POST/PUT等
* @param requestUri 请求uri,eg:/v1/meetings
* @return
*/
private Headers generateHeaders(String httpMethod, String requestUri){
//生成签名的时间戳
String headerTimestamp = String.valueOf(LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond());
//生成随机正整数(暂定八位
String randomNumber = String.valueOf((int)(Math.random() * 99999999) + 1);
//生成签名
String signature = sign(tencentSecretId, tencentSecretKey, httpMethod, randomNumber, headerTimestamp, requestUri, "{}");
Headers headers = new Headers.Builder()
.add("Content-Type", "application/json")
.add("X-TC-Key", tencentSecretId)
.add("X-TC-Timestamp", headerTimestamp)
.add("X-TC-Nonce", randomNumber)
.add("X-TC-Signature",signature)
.add("AppId",tencentAppId)
.add("SdkId",tencentSdkId)
.add("X-TC-Registered","1")
.build();
return headers;
}
/**
* 生成签名,开发版本oracle jdk 1.8.0_221
*
* @param secretId 邮件下发的secret_id
* @param secretKey 邮件下发的secret_key
* @param httpMethod http请求方法 GET/POST/PUT等
* @param headerNonce X-TC-Nonce请求头,随机数
* @param headerTimestamp X-TC-Timestamp请求头,当前时间的秒级时间戳
* @param requestUri 请求uri,eg:/v1/meetings
* @param requestBody 请求体,没有的设为空串
* @return 签名,需要设置在请求头X-TC-Signature中
* @throws NoSuchAlgorithmException e
* @throws InvalidKeyException e
*/
private static String sign(String secretId, String secretKey, String httpMethod, String headerNonce, String headerTimestamp, String requestUri, String requestBody) {
String tobeSig =
httpMethod + "\nX-TC-Key=" + secretId + "&X-TC-Nonce=" + headerNonce + "&X-TC-Timestamp=" + headerTimestamp + "\n" + requestUri + "\n" + requestBody;
Mac mac;
try {
mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), mac.getAlgorithm());
mac.init(secretKeySpec);
} catch (Exception e) {
log.error("获取腾会签名错误:"+e.getMessage());
throw new RuntimeException("请求失败");
}
byte[] hash = mac.doFinal(tobeSig.getBytes(StandardCharsets.UTF_8));
String hexHash = bytesToHex(hash);
String signature = new String(Base64.getEncoder().encode(hexHash.getBytes(StandardCharsets.UTF_8)));
return signature;
}
private static String bytesToHex(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int index = 0;
for (byte b : bytes) {
buf[index++] = HEX_CHAR[b >>> 4 & 0xf];
buf[index++] = HEX_CHAR[b & 0xf];
}
return new String(buf);
}
} }
package com.cmeeting.service.impl; package com.cmeeting.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmeeting.mapper.primary.WeComUserMapper; import com.cmeeting.mapper.primary.WeComUserMapper;
import com.cmeeting.pojo.WeComUser; import com.cmeeting.pojo.WeComUser;
import com.cmeeting.service.WeComService; import com.cmeeting.service.WeComService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.List; import java.util.List;
import static com.cmeeting.WeComAndTencentMeeting.convertJsonToWeComUsers;
import static com.cmeeting.WeComAndTencentMeeting.markDuplicateNames;
@Service @Service
public class WeComServiceImpl implements WeComService { public class WeComServiceImpl extends ServiceImpl<WeComUserMapper, WeComUser> implements WeComService {
@Autowired
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist";
private static final String ACCESS_TOKEN = "z8g0fkzlVBMKrwi67CRwE7gcnGqjOMrTK6LDih06vs1ixpV9BkrVWQJkoF3HFf5a2UrMvTKEBewOo7wPGvVxNjV_ZGOp_dUxTfnCG6kRUK9LKXPlhskGOI3mcI3ZZpGqpPcgjCsE877hh1BYTB_i3t5X_q1tZfv0K636o3a0HyAMf4GlzviCd1vQHfkGf3p15FjnATwDV8ddhMZo8U-_uA";
private static final String CORP_ID = "wx34544d057db97ffd";
private static final String CORP_SECRET = "7YLePWG7rJqkQFnAB4FeylqAXpmu7q5qv_NOeSGNbm0";
@Resource
private WeComUserMapper weComUserMapper; private WeComUserMapper weComUserMapper;
@Override @Override
public void batchInsert(List<WeComUser> users) { public void batchInsert(List<WeComUser> users) {
weComUserMapper.batchInsert(users); weComUserMapper.batchInsert(users);
} }
@Override
public void doUsers() throws Exception {
// 示例:获取部门ID为6的用户列表
int departmentId = 6;
JsonObject departmentList = getDepartmentList();
JsonObject result = getUserListByDepartment(departmentId);
// 处理返回结果
if (result.get("errcode").getAsInt() == 0) {
System.out.println("企微获取用户列表成功:");
JsonArray userList = result.getAsJsonArray("userlist");
for (int i = 0; i < userList.size(); i++) {
JsonObject user = userList.get(i).getAsJsonObject();
System.out.println("姓名: " + user.get("name").getAsString() +
", UserID: " + user.get("userid").getAsString());
}
// 2. 转换为WeComUser集合
List<WeComUser> users = convertJsonToWeComUsers(result);
// 3. 检查重名并设置标志
markDuplicateNames(users);
// 4. 批量插入数据库
batchInsert(users);
}
}
@Override
public String getToken() {
//获取token
// String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ww1fd8778458e9f1e8&corpsecret=uFRq9Xi8-dVY90LydXYBhjc91JnnfkPUR6lHDdeJ_fo";
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET;
String accessToken = "";
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
JSONObject jsonResponse = new JSONObject(responseBody);
accessToken = jsonResponse.getString("access_token");
} else {
throw new RuntimeException("Failed to fetch token. HTTP Code: " + response.code());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return accessToken;
}
/**
* 获取企微部门列表
*
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public JsonObject getDepartmentList() throws Exception {
String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=" + getToken();
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
}
/**
* 根据企业微信部门ID获取用户列表
*
* @param departmentId 部门ID
* @return 包含用户列表的JsonObject
* @throws Exception
*/
public JsonObject getUserListByDepartment ( int departmentId) throws Exception {
String urlStr = BASE_URL + "?access_token=" + ACCESS_TOKEN + "&department_id=" + departmentId;
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 使用Gson解析JSON响应
Gson gson = new Gson();
return gson.fromJson(response.toString(), JsonObject.class);
} else {
throw new RuntimeException("HTTP GET请求失败,错误码: " + responseCode);
}
}
} }
// vo/LoginVO.java
package com.cmeeting.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
public class TencentMeetingVO {
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class TencentMeetingRequest{
private Long startTime; //开始结束时间戳
private Long endTime;
private Integer pageSize;
private Integer page;
private String operatorId;
private Integer operatorIdType;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class RecordFile{
private String meetingId;//如果是周期会议,这个id表示主会议
private String subMeetingId;//如果是周期会议,这个id表示子会议
private String recordFileId;
}
}
\ No newline at end of file
...@@ -28,4 +28,23 @@ mybatis.type-aliases-package=com.cmeeting.pojo\ ...@@ -28,4 +28,23 @@ mybatis.type-aliases-package=com.cmeeting.pojo\
# ?????? # ??????
mybatis.configuration.map-underscore-to-camel-case: true mybatis.configuration.map-underscore-to-camel-case: true
logging.level.com.zaxxer.hikari=INFO logging.level.com.zaxxer.hikari=INFO
\ No newline at end of file
# tencent meeting
# local
tencent.appId=211153201
tencent.sdkId=28370276340
tencent.secretId=BKOMDZVbvh0iT7k6UHsSizAWBCOVDtT6
tencent.secretKey=3Y1j0mzNp7KChKFJGyaEnZHLobFoAQ8eLwfaMx8nLbtXAerO
#tencent.admin.userId=woaJARCQAAJU1EsO73Ww5rn8YHMW6iYA
tencent.admin.userId=woaJARCQAAhkyWGuf8n9InhZsxQstjjA
# prod
#tencent.appId=210468336
#tencent.sdkId=28790143843
#tencent.secretId=0ks7u8cgQ8DGVtlYZeRA9TxZCjvUT3oL
#tencent.secretKey=gQU09rkJjiQfiGcUYdhiKq5Ol6LebXg4w7F7Ol0rwvvdv3Xy
#tencent.admin.userId=woaJARCQAAftcvU6GGoOn66rdSZ4IrOA
email.sender=cmeeting_assistant@cimc.com
email.sender.pwd=scyou@xih45g6@xih4
email.smtp.host=smtp.office365.com
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
#{user.isrepeatName} #{user.isrepeatName}
) )
</foreach> </foreach>
ON DUPLICATE KEY UPDATE
user_name = VALUES(user_name)
</insert> </insert>
<select id="getAlluser" resultType="com.cmeeting.pojo.TencentMeetingUser"> <select id="getAlluser" resultType="com.cmeeting.pojo.TencentMeetingUser">
......
...@@ -13,15 +13,18 @@ ...@@ -13,15 +13,18 @@
</insert> </insert>
<insert id="batchInsert" parameterType="list"> <insert id="batchInsert" parameterType="list">
INSERT INTO user_wecom ( user_name, user_id, isepeat_name) INSERT INTO user_wecom ( user_name, user_id, isrepeat_name,email)
VALUES VALUES
<foreach collection="list" item="user" separator=","> <foreach collection="list" item="user" separator=",">
( (
#{user.userName}, #{user.userName},
#{user.userId}, #{user.userId},
#{user.isRepeatName} #{user.isRepeatName},
#{user.email}
) )
</foreach> </foreach>
ON DUPLICATE KEY UPDATE
user_name = VALUES(user_name)
</insert> </insert>
<select id="getAlluser" resultType="com.cmeeting.pojo.WeComUser"> <select id="getAlluser" resultType="com.cmeeting.pojo.WeComUser">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论